文章目录
一、使用vueCli创建项目?
vue create my-project
cd edu-boss-fed
npm run serve
二、加入Git版本管理
可添加到gitHub 或者gitee上均可
- 创建本地仓库 git init
- 将⽂件添加到暂存区 git add ⽂件
- 提交历史记录 git commit “提交⽇志”
- 添加远端仓库地址 git remote add origin 你的远程仓库地址
- 推送提交 git push -u origin
三、初始化目录介绍
四、调整目录结构
主要内容:
- 删除初始化的默认⽂件
- 新增调整我们需要的⽬录结构
1.修改 App.vue
<template>
<div id="app">
<!-- 根路由的出口 -->
<router-view/>
</div>
</template>
<style lang="scss" scoped>
</style>
2.修改 router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
Vue.use(VueRouter)
// 路由配置规则
const routes: Array<RouteConfig> = [
]
const router = new VueRouter({
routes
})
export default router
3.删除默认示例⽂件:
- src/views/About.vue
- src/views/Home.vue
- src/components/HelloWorld.vue
- src/assets/logo.png
4.创建以下内容:
- src/services ⽬录,接⼝模块
- src/utils ⽬录,存储⼀些⼯具模块
- src/styles ⽬录,存储⼀些样式资源
5.修改后的目录结构
五、使用TS开发Vue
1.环境说明
在 Vue 项目中启用 TypeScript 支持
两种方式:
(1)全新项目:使用 Vue CLI 脚手架工具创建 Vue 项目
(2)已有项目:添加 Vue 官方配置的 TypeScript 适配插件
使用 @vue/cli 安装TypeScript 插件:
vueadd@vue/typescript
2.相关配置说明
项目根目录下:tsconfig.json
3.使用OptionsAPI定义Vue组件
4. 使用ClassAPIs定义Vue组件
5.关于装饰器语法
6.使用vuePropertyDecorator创建Vue组件
7.总结创建组件方式
六、基础处理
1.导入elementUI
Element,⼀套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌⾯端组件库。
- 官⽹:https://element.eleme.cn/
- 仓库:https://github.com/ElemeFE/element
1.安装 element
npm i element-ui
2.在 main.ts 中导⼊配置
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
3.测试使⽤
在app.vue中直接使用
<template>
<div id="app">
<h1>rjy</h1>
<!-- 根路由的出口 -->
<router-view/>
<el-row>
<el-button>默认按钮</el-button>
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="info">信息按钮</el-button>
<el-button type="warning">警告按钮</el-button>
<el-button type="danger">危险按钮</el-button>
</el-row>
</div>
</template>
<style lang="scss" scoped>
</style>
2.样式的处理
src/styles
|-- index.scss 全局样式(在入口模块加载生效)
|-- mixin.scss 公共的mixin混入(可以吧重复的样式封装为mixin混入到复用的地方)
|-- reset.scss 重置基础样式
|-- variables.scss 公共样式变量
variables.scss
$primary-color: #40586F;
$success-color: #51cf66;
$warning-color: #fcc419;
$danger-color: #ff6b6b;
$info-color: #868e96; // #22b8cf;
$body-bg: #E9EEF3; // #f5f5f9;
$sidebar-bg: #F8F9FB;
$navbar-bg: #F8F9FB;
$font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
index.scss
@import './variables.scss';
// globals
html {
font-family: $font-family;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
// better Font Rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
background-color: $body-bg;
}
// custom element theme
$--color-primary: $primary-color;
$--color-success: $success-color;
$--color-warning: $warning-color;
$--color-danger: $danger-color;
$--color-info: $info-color;
// change font path, required
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// import element default theme
@import '~element-ui/packages/theme-chalk/src/index';
// node_modules/element-ui/packages/theme-chalk/src/common/var.scss
// overrides
// .el-menu-item, .el-submenu__title {
// height: 50px;
// line-height: 50px;
// }
.el-pagination {
color: #868e96;
}
// components
.status {
display: inline-block;
cursor: pointer;
width: .875rem;
height: .875rem;
vertical-align: middle;
border-radius: 50%;
&-primary {
background: $--color-primary;
}
&-success {
background: $--color-success;
}
&-warning {
background: $--color-warning;
}
&-danger {
background: $--color-danger;
}
&-info {
background: $--color-info;
}
}
app.vue中加载
记得index引入时添加后缀index.scss
// 加载全局样式
import './styles/index.scss'
3.共享全局样式变量
1、app.vue使用variables中的变量
<template>
<div id="app">
<h1>rjy</h1>
<!-- 根路由的出口 -->
<router-view/>
<p class="text">hello world</p>
</div>
</template>
<style lang="scss" scoped>
@import "~@/styles/variables.scss";
.text{
color: $success-color
}
</style>
2、每个文件均想引用variables.scss中的样式,
全局注册css
1 项目根目录下创建vue.config.js
module.exports = {
css: {
loaderOptions: {
// 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
// 因为 `scss` 语法在内部也是由 sass-loader 处理的
// 但是在配置 `prependData` 选项的时候
// `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
// 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
scss: {
prependData: `@import "~@/styles/variables.scss";`
}
}
}
}
修改完后需要重启
4.配置接口
后台为我们提供了数据接口,分别是:
- https://eduboss.lagou.com
- http://edufront.lagou.com
这两个接口都没有提供 CORS 跨域请求,所以需要在客户端配置服务端代理处理跨域请求。
配置客户端层面的服务端代理跨域可以参考官方文档中的说明:
下面是具体的操作流程。
在项目根目录下添加vue.config.js配置文件。
module.exports = {
css: {
loaderOptions: {
// 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
// 因为 `scss` 语法在内部也是由 sass-loader 处理的
// 但是在配置 `prependData` 选项的时候
// `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
// 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
scss: {
prependData: `@import "~@/styles/variables.scss";`
}
}
},
devServer: {
proxy: {
'/boss': {
target: 'http://eduboss.lagou.com',
changeOrigin: true // 把请求头中的 host 配置为 target
},
'/front': {
target: 'http://edufront.lagou.com',
changeOrigin: true
}
}
}
}
5.封装请求模块
安装 axios:npm i axios
创建 src/utils/request.ts :
import axios from 'axios'
const request = axios.create({
//配置选项
//baseURL
//timeout
})
//请求拦截器
//响应拦截器
export default request
测试:
app.vue中添加测试代码:
<template>
<div id="app">
<h1>rjy</h1>
<!-- 根路由的出口 -->
<router-view/>
<p class="text">hello world</p>
</div>
</template>
<script lang='ts'>
import Vue from 'vue'
import request from '@/utils/request'
request({
method: 'GET',
url: '/boss'
}).then(res => {
console.log(res)
})
export default Vue.extend({
name: 'App'
})
</script>
<style lang="scss" scoped>
.text{
color: $primary-color
}
</style>
结果:
七 布局
1.初始化路由页面
我们这⾥先把这⼏个主要的⻚⾯配置出来,其它⻚⾯在随后的开发过程中配置。
路径 | 说明 |
---|---|
/ | 首页 |
/login | ⽤户登录 |
/role | ⻆⾊管理 |
/menu | 菜单管理 |
/resource | 资源管理 |
/course | 课程管理 |
/user | ⽤户管理 |
/advert | ⼴告管理 |
/advert-space | ⼴告位管理 |
views文件夹下添加对应的文件
router/index.ts 文件中添加
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
Vue.use(VueRouter)
// 路由配置规则
const routes: Array<RouteConfig> = [
{
path: '/',
name: 'home',
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
},
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/role',
name: 'role',
component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
},
{
path: '/menu',
name: 'menu',
component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
},
{
path: '/resource',
name: 'resource',
component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
},
{
path: '/course',
name: 'course',
component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
},
{
path: '/user',
name: 'user',
component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
},
{
path: '/advert',
name: 'advert',
component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
},
{
path: '/advert-space',
name: 'advert-space',
component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
const router = new VueRouter({
routes
})
export default router
2.Layout和嵌套路由
src目录下新建layout文件,index.vue文件
<template>
<div class="lauout">布局组件
<!-- 子路由页面 -->
<router-view/>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'LayoutIndex'
})
</script>
<style lang='scss'>
</style>
更改路由
router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
Vue.use(VueRouter)
// 路由配置规则
const routes: Array<RouteConfig> = [
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/',
component: Layout,
children: [
{
path: '', //默认子路由
name: 'home',
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
},
{
path: '/role',
name: 'role',
component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
},
{
path: '/menu',
name: 'menu',
component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
},
{
path: '/resource',
name: 'resource',
component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
},
{
path: '/course',
name: 'course',
component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
},
{
path: '/user',
name: 'user',
component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
},
{
path: '/advert',
name: 'advert',
component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
},
{
path: '/advert-space',
name: 'advert-space',
component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
}
]
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
const router = new VueRouter({
routes
})
export default router
3.Container布局容器
处理layout,借助element组件库
可参照Container 布局容器 布局
layout/index.vue
<template>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
<!-- 子路由页面 -->
<!-- <router-view/> -->
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'LayoutIndex'
})
</script>
<style lang='scss'>
.el-container {
min-height: 100vh;
min-width: 980px;
}
.el-aside {
background: #d3dce6;
}
.el-header {
background: #fff;
}
.el-main {
background: #e9eef3;
}
</style>
4.侧边栏菜单
参照element中的menu选项
因为侧边栏代码比较臃肿,且侧边栏单独管理
所以在layout文件夹下创建components文件夹下添app-aside.vue文件
<template>
<div class="aside">
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
router>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>权限管理</span>
</template>
<el-menu-item index="/role">
<i class="el-icon-menu"></i>
<span slot="title">角色管理</span>
</el-menu-item>
<el-menu-item index="/menu">
<i class="el-icon-menu"></i>
<span slot="title">菜单管理</span>
</el-menu-item>
<el-menu-item index="/resource">
<i class="el-icon-menu"></i>
<span slot="title">资源管理</span>
</el-menu-item>
</el-submenu>
<el-menu-item index="/course">
<i class="el-icon-menu"></i>
<span slot="title">课程管理</span>
</el-menu-item>
<el-menu-item index="/user">
<i class="el-icon-document"></i>
<span slot="title">用户管理</span>
</el-menu-item>
<el-submenu index="4">
<template slot="title">
<i class="el-icon-location"></i>
<span>广告管理</span>
</template>
<el-menu-item index="/advert">
<i class="el-icon-menu"></i>
<span slot="title">广告列表</span>
</el-menu-item>
<el-menu-item index="/advert-space">
<i class="el-icon-menu"></i>
<span slot="title">广告位列表</span>
</el-menu-item>
</el-submenu>
</el-menu>
</div>
</template>
<script lang='ts'>
import Vue from 'vue'
export default Vue.extend({
name: 'AppAside',
methods: {
handleOpen (key: string, keyPath: string): void{
console.log(key, keyPath)
},
handleClose (key: string, keyPath: string): void {
console.log(key, keyPath)
}
}
})
</script>
<style lang='scss' scoped>
.aside {
.el-menu {
min-height: 100vh;
}
}
</style>
App.vue中
<template>
<el-container>
<el-aside width="200px">
<app-aside />
</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>
<router-view/>
</el-main>
</el-container>
</el-container>
<!-- 子路由页面 -->
<!-- <router-view/> -->
</template>
<script lang="ts">
import Vue from 'vue'
import AppAside from './components/app-aside.vue'
export default Vue.extend({
name: 'LayoutIndex',
components: {
AppAside
}
})
</script>
<style lang='scss'>
.el-container {
min-height: 100vh;
min-width: 980px;
}
.el-aside {
background: #d3dce6;
}
.el-header {
background: #fff;
}
.el-main {
background: #e9eef3;
}
</style>
5.头部Header
面包屑和登录状态
-
面包屑可参照Breadcrumb 面包屑
-
登录状态
可参照Dropdown 下拉菜单
components文件夹下添app-header.vue文件
<template>
<div class="header">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>活动管理</el-breadcrumb-item>
<el-breadcrumb-item>活动列表</el-breadcrumb-item>
<el-breadcrumb-item>活动详情</el-breadcrumb-item>
</el-breadcrumb>
<el-dropdown>
<span class="el-dropdown-link">
<el-avatar shape="square" :size="30" src="https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png"></el-avatar>
<i class="el-icon-arrow-down el-icon--right"></i></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>当前用户id</el-dropdown-item>
<el-dropdown-item divided>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script lang='ts'>
import Vue from 'vue'
export default Vue.extend({
name: 'AppHeader'
})
</script>
<style lang="scss" scoped>
.header{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown-link{
display: flex;
align-items: center;
}
}
</style>
App.vue
<template>
<el-container>
<el-aside width="200px">
<app-aside />
</el-aside>
<el-container>
<el-header>
<app-header/>
</el-header>
<el-main>
<router-view/>
</el-main>
</el-container>
</el-container>
<!-- 子路由页面 -->
<!-- <router-view/> -->
</template>
<script lang="ts">
import Vue from 'vue'
import AppAside from './components/app-aside.vue'
import AppHeader from './components/app-header.vue'
export default Vue.extend({
name: 'LayoutIndex',
components: {
AppAside,
AppHeader
}
})
</script>
<style lang='scss'>
.el-container {
min-height: 100vh;
min-width: 980px;
}
.el-aside {
background: #d3dce6;
}
.el-header {
background: #fff;
}
.el-main {
background: #e9eef3;
}
</style>
八 登录
1.页面布局
Form表单组件
login/index.vue中
<template>
<div class="system">
<div class="title">Edu boss 管理系统</div>
<div class="login">
<el-form class = "login-form demo-ruleForm" label-position="top" ref="form" :model="form" label-width="80px" >
<div class="denglu">登录</div>
<el-form-item label="手机号">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" class="login-btn">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'LoginIndex',
data () {
return {
form: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: ''
}
}
},
methods: {
onSubmit () {
console.log('submit!')
}
}
})
</script>
<style lang="scss" scoped>
.system{
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 40px;
.title{
display: flex;
margin-bottom: 50px;
text-align: center;
font-size: 45px;
width: 300px;
}
.login{
.login-form{
background: #fff;
width: 300px;
padding: 20px;
border-radius: 5px
}
.login-btn{
width: 100%
}
.denglu{
font-weight: 1000;
font-size: 20px;
padding-bottom: 10px
}
}
}
</style>
2.接口测试
接口文档:
http://eduboss.lagou.com/boss/doc.html#/home
http://edufront.lagou.com/front/doc.html#/home
3.请求登录
总结:
根据接口要求初始化表单数据,利用postman测试下接口
转化param的格式的转化
<script lang="ts">
import Vue from 'vue'
import request from '@/utils/request'
import qs from 'qs'
export default Vue.extend({
name: 'LoginIndex',
data () {
return {
form: {
phone: '',
password: ''
}
}
},
methods: {
async onSubmit () {
// 1.表单验证
// 2 验证通过-提交表单
// 3 处理请求结果
// 成功-跳转到首页
// 失败:
const { data } = await request({
method: 'POST',
url: '/front/user/login',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(this.form) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
})
console.log(data)
}
}
})
</script>
4.处理请求结果
主要处理成功请求跳转首页,并添加消息提示
const { data } = await request({
method: 'POST',
url: '/front/user/login',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(this.form) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
})
console.log(data)
if (data.state !== 1) {
this.$message.error(data.message)
return
}
this.$router.push({
name: 'home'
})
this.$message.success('登录成功')
5. 表单验证
form 表单验证
具体验证规格:async-validator
注意的问题:
1、表单验证规则
2、表单验证失败,不可提交表单
3、this.$refs[formName].validate()
使用时没办法通过Ts校验,原因是this.$refs[formName]
没有确定类型。需要将this.$refs[formName]
转换类型
await (this.$refs.form as Form).validate()
相关代码:
<template>
<div class="system">
<div class="title">Edu boss 管理系统</div>
<div class="login">
<!--
:model= "ruleForm"
:rules="rules"
ref="ruleForm"
4 el-form-item 绑定prop属性
-->
<el-form class = "login-form demo-ruleForm" label-position="top" label-width="80px"
ref="form"
:model="form"
:rules="rules"
>
<div class="denglu">登录</div>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="form.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" class="login-btn" :loading="isLoginLoading">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import request from '@/utils/request'
import qs from 'qs'
import { Form } from 'element-ui'
export default Vue.extend({
name: 'LoginIndex',
data () {
return {
form: {
phone: '',
password: ''
},
isLoginLoading: false,
rules: {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
]
}
}
},
methods: {
async onSubmit () {
try {
await (this.$refs.form as Form).validate()
this.isLoginLoading = true
// 1.表单验证
// 2 验证通过-提交表单
// 3 处理请求结果
// 成功-跳转到首页
// 失败:
const { data } = await request({
method: 'POST',
url: '/front/user/login',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(this.form) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
})
console.log(data)
if (data.state !== 1) {
this.$message.error(data.message)
this.isLoginLoading = false
return
}
this.$router.push({
name: 'home'
})
this.$message.success('登录成功')
this.isLoginLoading = false
} catch (err) {
console.log('登录失败', err)
this.isLoginLoading = false
}
}
}
})
</script>
<style lang="scss" scoped>
.system{
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 40px;
.title{
display: flex;
margin-bottom: 50px;
text-align: center;
font-size: 45px;
width: 300px;
}
.login{
.login-form{
background: #fff;
width: 300px;
padding: 20px;
border-radius: 5px
}
.login-btn{
width: 100%
}
.denglu{
font-weight: 1000;
font-size: 20px;
padding-bottom: 10px
}
}
}
</style>
6. 请求期间禁用按钮点击
网速较慢的情况下,用户可多次点击登录按钮(用户并不知道后端正在登录请求)
可参照加载中
详情请看5中的代码
7. 封装请求方法
services/user.ts
/* 用户相关请求模块 */
import request from '@/utils/request'
import qs from 'qs'
interface User{
phone: string;
password: string;
}
export const login = (data: User) => {
return request({
method: 'POST',
url: '/front/user/login',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(data) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
})
}
login/index.vue 代码修改
const { data } = await login(this.form)
8.关于请求体data和Contentype的问题
- 如果 data是普通对象,则 content-type 是application/json
- 如果 data 是 qs.stringify()转换之后的数据 key=value,则content-type 会被设置为application/x-www-form-urlencoded,则不需要手动设置headers
- 如果data是FormData对象,则 content-type 是multipart/form-data对象
问题:登录判断,则执行不到this.isLoginLoading = false
部分
if (data.state !== 1) {
return this.$message.error(data.message)
}
九 身份认证
1.把登录状态缓存到Vuex容器中
store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// 容器实现了数据共享,在组件总共享方便,但是没有持久化的功能
state: {
user: JSON.parse(window.localStorage.getItem('user') || 'null')
// user: null // 当前登录用户状态
},
mutations: {
// 修改容器数据必须使用mutations函数
setUser (state, payload) {
state.user = JSON.parse(payload)
// 1为了防止页面刷新,数据丢失,需要把数据持久化
// 2本地存储只能存储字符串
window.localStorage.setItem('user', payload)
}
},
actions: {
},
modules: {
}
})
2.校验页面访问权限
路由拦截器的设置。可查看全局前置守卫
给路由配置路由元信息
route/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
import store from '@/store'
Vue.use(VueRouter)
// 路由配置规则
const routes: Array<RouteConfig> = [
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/',
component: Layout,
children: [
{
path: '', // 默认子路由
name: 'home',
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue'),
meta: {
requiresAuth: true // 自定义数据
} // 默认就是空对象
},
{
path: '/role',
name: 'role',
component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue'),
meta: {
requiresAuth: true // 自定义数据
} // 默认就是空对象
},
{
path: '/menu',
name: 'menu',
component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue'),
meta: {
requiresAuth: true // 自定义数据
} // 默认就是空对象
},
{
path: '/resource',
name: 'resource',
component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue'),
meta: {
requiresAuth: true // 自定义数据
} // 默认就是空对象
},
{
path: '/course',
name: 'course',
component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue'),
meta: {
requiresAuth: true // 自定义数据
} // 默认就是空对象
},
{
path: '/user',
name: 'user',
component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue'),
meta: {
requiresAuth: true // 自定义数据
} // 默认就是空对象
},
{
path: '/advert',
name: 'advert',
component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue'),
meta: {
requiresAuth: true // 自定义数据
} // 默认就是空对象
},
{
path: '/advert-space',
name: 'advert-space',
component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
}
]
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
const router = new VueRouter({
routes
})
// 全局前置首位,任何页面的访问都要经过这里
// to:去哪里的路由信息
// from:从哪里来的路由信息
// next:通行的标志
router.beforeEach((to, from, next) => {
console.log('come in beforeEach')
console.log('to =>', to)
console.log('from =>', from)
// 路由守卫中一定要调用next,否则页面无法访问
// next()
// if (to.path !== '/login') {
// // 校验登录状态
// }
// to.matched匹配到的路由记录,是一个数组
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
if (!store.state.user) {
// 跳转到登录页面
next({
name: 'login'
})
} else {
next()
}
} else {
next()
}
})
export default router
3.测试获取当前登录用户信息接口
4.登录成功跳转回原来页面
route/index.vue修改
if (!store.state.user) {
// 跳转到登录页面
next({
name: 'login',
query: { // 通过url传递查询字符串参数
redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页面
}
})
}
login/index.vue中修改
// 2然后在访问需要登录的页面的时候,判断有没有登录状态,(路由拦截器)
this.$router.push(this.$route.query.redirect as string || '/')
5. 展示当前登录用户信息
services/index.vue
export const getUserInfo = () => {
return request({
method: 'GET',
url: '/front/user/getInfo',
headers: {
Authorization: store.state.user.access_token
}
})
}
在header加载方法并调用,处理默认头像
import Vue from 'vue'
import { getUserInfo } from '@/services/user'
export default Vue.extend({
name: 'AppHeader',
data () {
return {
userInfo: {} // 当前用户登录信息
}
},
created () {
this.loadUserInfo()
},
methods: {
async loadUserInfo () {
const { data } = await getUserInfo()
this.userInfo = data.content
}
}
})
<span class="el-dropdown-link">
<el-avatar shape="square" :size="30" :src="userInfo.portrait || require('@/assets/default-avatar.jpg')"></el-avatar>
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>{{userInfo.userName}}</el-dropdown-item>
6. 使用请求拦截器统一设置Token
自动设置某些请求的token,不用每次请求添加header
请求拦截器
uitls/request.ts中添加
import axios from 'axios'
import store from '@/store'
const request = axios.create({
// 配置选项
// baseURL
// timeout
})
// 请求拦截器
request.interceptors.request.use(function (config) {
// console.log('come in interceptors', config)
// 我们就可以在这里通过改写config配置信息来实现业务功能的统一处理
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
// 注意:这里一定要返回config,否则请求就发不出去
return config
}, function (error) {
// Do something with request error
return Promise.reject(error)
})
// 响应拦截器
export default request
添加成功后,可删除
services/user.ts
export const getUserInfo = () => {
return request({
method: 'GET',
url: '/front/user/getInfo'
// headers: {
// Authorization: store.state.user.access_token
// }
})
}
7. 用户退出
app-header.vue
<el-dropdown-item divided
@click="handleLogout">退出</el-dropdown-item>
</el-dropdown-menu>
点击事件不可用:原因click是原生dom事件,而这个一个组件,首先看组件上的事件是否支持dom事件,组件上的事件都是自定义事件
思路:1组件是否有可用的事件
2 click 用native方式注册@click.native
app-header.vue
<el-dropdown-item divided
@click.native="handleLogout">退出</el-dropdown-item>
</el-dropdown-menu>
handleLogout () {
// 清除登录信息状态
this.$store.commit('setUser', null)
// 跳转到登录页面
this.$router.push({
name: 'login'
})
}
为了美观性,可手动添加退出提示 ,参照MessageBox 弹框
handleLogout () {
this.$confirm('确认退出吗?', '退出提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // 确认执行
// 清除登录信息状态
this.$store.commit('setUser', null)
// 跳转到登录页面
this.$router.push({
name: 'login'
})
this.$message({
type: 'success',
message: '退出成功!'
})
}).catch(() => {
this.$message({ // 取消执行
type: 'info',
message: '已取消退出'
})
})
}
十 用户登录和身份认证
概念介绍
Token 是后端设置的过期时间
为什么access_token需要有过期时间,以及为什么特写短? 为了安全
- access_token
作用:获取需要授权的接口数据 - expires_in
作用:设定access_token的过期时间 - refresh_token
作用:用来刷新获取新的access_token - 解决方法
-
方法一:
在请求发起前拦截每个请求,判断Token 的有效时间expires_in是否已经过期,若已经过期,则将请求挂起,先刷新Token后再继续请求- 优点:在请求前拦截,能节省请求,省流量
- 缺点:需要后端额外提供一个Token的过期时间的字段:使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败
-
方法二
不在请求前拦截,而是拦截返回后的数据,先发起请求,接口返回过期后,先刷新Token,在进行一次重试- 优点:不需要额外的Token过期时间,不需要判断时间
- 缺点:会消耗多一次请求,耗流量
-
总结
方法一和二有缺点时互补的,方法一游校验失败的风险(本地时间被篡改时,当然一般没有用户去主动修改本地时间),方法二简单粗暴,等知道服务器过期了在重试一次,只是会耗多一个请求
-
1. 处理过期Token-分析响应拦截器
utils/request.ts添加响应拦截器,添加打印可查看返回的数据
// 响应拦截器
request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
console.log('请求响应成功', response)
// 如果是自定义错误状态码,错误处理就写到这里
return response
}, function (error) { // 超出2xx,都执行这里
// 若使用的HTTP错误,则错误处理写到这里
console.log('请求响应失败', error)
return Promise.reject(error)
})
2. 处理过期Token-axios错误处理
// 响应拦截器
request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
console.log('请求响应成功', response)
// 如果是自定义错误状态码,错误处理就写到这里
return response
}, function (error) { // 超出2xx,都执行这里
// 若使用的HTTP错误,则错误处理写到这里
// console.log('请求响应失败', error)
if (error.response) { // 请求发出去,收到响应了,但是状态码超出了2范围
} else if (error.request) { // 请求发出去了,但是未收到响应
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
} else { // 在设置请求时发生了一些事情,触发了错误
// Something happened in setting up the request that triggered an Error
}
// 把请求失败的错误对象继续抛出,扔给下一个上一个调用者
return Promise.reject(error)
})
3. 处理过期Token-错误消息提示
4. 处理过期Token-实现基本流程逻辑
相关代码
import axios from 'axios'
import store from '@/store'
import { Message } from 'element-ui'
import router from '@/router'
import qs from 'qs'
const request = axios.create({
// 配置选项
// baseURL
// timeout
})
function redirectLogin () {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath
}
})
}
function refreshToken () {
return axios.create()({
method: 'POST',
// 同一个refresh_token,该接口只能使用一次!!!否者会报错
url: '/front/user/refresh_token',
data: qs.stringify({
refreshtoken: store.state.user.refresh_token
})
})
}
// 请求拦截器
request.interceptors.request.use(function (config) {
// console.log('come in interceptors', config)
// 我们就可以在这里通过改写config配置信息来实现业务功能的统一处理
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
// 注意:这里一定要返回config,否则请求就发不出去
return config
}, function (error) {
// Do something with request error
return Promise.reject(error)
})
// 响应拦截器
request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
console.log('请求响应成功', response)
// 如果是自定义错误状态码,错误处理就写到这里
return response
}, async function (error) { // 超出2xx,都执行这里
// 若使用的HTTP错误,则错误处理写到这里
// console.log('请求响应失败', error)
if (error.response) { // 请求发出去,收到响应了,但是状态码超出了2范围
const { status } = error.response
// 400
// 401
// 403
// 404
// 500
if (status === 400) {
Message.error('请求参数错误')
} else if (status === 401) {
// Token无效(Token过期,或者未提供Token)
// 如果有refresh_token,则尝试使用refresh_token更新access_token
if (!store.state.user) {
console.log('come in store.state.user redirectLogin')
redirectLogin()
return Promise.reject(error)
}
// 尝试刷新新的cookie
try {
console.log('come in store.state.user redirectLogin22')
// request({ 不是用的原因,refreshtoken刷新接口,也有可能会出现401的错误,这样会陷入循环中
const { data } = await axios.create()({ // 新的axios对象,未有拦截器
method: 'POST',
url: '/front/user/refresh_token',
data: qs.stringify({
refreshtoken: store.state.user.refresh_token
})
})
// 成功了=> 则把本次失败的请求重新发出去
// 把刷新拿到的新access_token,更新到容器中和本地存储
store.commit('setUser', data.content)
return request(error.config)
} catch (error) {
// 把当前登录用户状态清除
store.commit('setUser', null)
// 失败了=> 跳转登录页重新登录获取新的token
redirectLogin()
return Promise.reject(error)
}
// Message.error('')
} else if (status === 403) {
Message.error('没有权限,请联系管理员')
} else if (status === 404) {
Message.error('请求资源不存在')
} else if (status >= 500) {
Message.error('服务端错误,请联系管理员')
}
} else if (error.request) { // 请求发出去了,但是未收到响应
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
Message.error('请求超时,请刷新重试')
} else { // 在设置请求时发生了一些事情,触发了错误
// Something happened in setting up the request that triggered an Error
Message.error(`请求失败:${error.message}`)
}
// 把请求失败的错误对象继续抛出,扔给下一个上一个调用者
return Promise.reject(error)
})
export default request
5. 处理过期Token-过于多次请求的问题
同意时间多个token过期
6. 处理过期Token-解决多次请求刷新Token的问题
7. 处理过期Token-解决多次请求其他接口重试问题
思路:定义数组,将刷新期间失败的请求放到数组中,被挂起,返回promise(因为可控制完成状态)
在书信token成功之后,调用requests数组中的内容
import axios from 'axios'
import store from '@/store'
import { Message } from 'element-ui'
import router from '@/router'
import qs from 'qs'
const request = axios.create({
// 配置选项
// baseURL
// timeout
})
function redirectLogin () {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath
}
})
}
function refreshToken () {
return axios.create()({
method: 'POST',
// 同一个refresh_token,该接口只能使用一次!!!否者会报错
url: '/front/user/refresh_token',
data: qs.stringify({
refreshtoken: store.state.user.refresh_token
})
})
}
// 请求拦截器
request.interceptors.request.use(function (config) {
// console.log('come in interceptors', config)
// 我们就可以在这里通过改写config配置信息来实现业务功能的统一处理
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
// 注意:这里一定要返回config,否则请求就发不出去
return config
}, function (error) {
// Do something with request error
return Promise.reject(error)
})
// 响应拦截器
let isRefreshing = false // 刷新token状态
let requests: any [] = []// 刷新token状态期间被挂起的请求
request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
console.log('请求响应成功', response)
// 如果是自定义错误状态码,错误处理就写到这里
return response
}, async function (error) { // 超出2xx,都执行这里
// 若使用的HTTP错误,则错误处理写到这里
// console.log('请求响应失败', error)
if (error.response) { // 请求发出去,收到响应了,但是状态码超出了2范围
const { status } = error.response
// 400
// 401
// 403
// 404
// 500
if (status === 400) {
Message.error('请求参数错误')
} else if (status === 401) {
// Token无效(Token过期,或者未提供Token)
// 如果有refresh_token,则尝试使用refresh_token更新access_token
if (!store.state.user) {
console.log('come in store.state.user redirectLogin')
redirectLogin()
return Promise.reject(error)
}
if (!isRefreshing) {
isRefreshing = true
// 尝试刷新新的cookie
return refreshToken().then(res => {
if (!res.data.success) {
throw new Error('刷新 token 失败')
}
store.commit('setUser', res.data.content)
// 刷新 toekn 成功, 就讲所有挂起的请求执行掉
requests.forEach(cb => cb())
// 将执行过的 请求数组 清空
requests = []
return request(error.config)
}).catch(err => {
console.log(err)
// 把当前登录用户状态清除
store.commit('setUser', null)
// 失败了=> 跳转登录页重新登录获取新的token
redirectLogin()
return Promise.reject(error)
}).finally(() => {
isRefreshing = false
})
}
// 刷新状态下,把请求挂起,放到requests数组中
return new Promise(resolve => {
requests.push(() => {
resolve(request(error.config))
})
})
// Message.error('')
} else if (status === 403) {
Message.error('没有权限,请联系管理员')
} else if (status === 404) {
Message.error('请求资源不存在')
} else if (status >= 500) {
Message.error('服务端错误,请联系管理员')
}
} else if (error.request) { // 请求发出去了,但是未收到响应
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
Message.error('请求超时,请刷新重试')
} else { // 在设置请求时发生了一些事情,触发了错误
// Something happened in setting up the request that triggered an Error
Message.error(`请求失败:${error.message}`)
}
// 把请求失败的错误对象继续抛出,扔给下一个上一个调用者
return Promise.reject(error)
})
export default request
十一 用户权限
介绍
作用:管理用户的权限,
- 可配置不同的菜单展示
- 资源的限制
1. 菜单管理-添加菜单-布局
页面布局处理(可参照Card 卡片)
- 添加菜单
- 列表
十二 角色权限管理
十三 课程管理
富文本编辑器的使用:
- ckeditor5
老牌富文本编辑器,稳定性较高,内置的插件和扩展性较好 - quill
- MediumEditor
- wangEditor
- ueditor
百度,目前已经不再维护 - tinymce
本次使用wangEditor,具体可查看网页
十四 编辑课程
十五 发布部署
1. 发布部署-项目打包
- npm run build
- npm install -g serve
- serve -s dist
2. 发布部署-本地预览服务
解决本地调试,接口代理问题
- 创建目录:
test-serve 中,添加app.js - 安装express ,安装到开发依赖中
npm i -D express
中间件https://github.com/chimurai/http-proxy-middleware
- 安装npm install --save-dev http-proxy-middleware
文件内容
const express = require('express')
const path = require('path')
const app = express()
const { createProxyMiddleware } = require('http-proxy-middleware')
// 托管了 dist 目录,当访问 / 的时候,默认会返回托管目录中的 index.html 文件
app.use(express.static(path.join(__dirname, '../dist')))
app.use('/boss', createProxyMiddleware({
target: 'http://eduboss.lagou.com',
changeOrigin: true
}))
app.use('/front', createProxyMiddleware({
target: 'http://edufront.lagou.com',
changeOrigin: true
}))
app.use
app.listen(3000,()=>{
console.log('running')
})
3. 发布部署-注意事项
4. 发布部署-部署说明
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。