使用到的资源
vue官网: https://cn.vuejs.org/
element-ui : https://element.eleme.cn/#/zh-CN/component/icon
ESLint
TS
SCSS
源码地址: https://github.com/qifutian/learngit/tree/main/TSvue-DEMO/edu-boss-fed
使用vue-cli创建项目
加入git 版本管理
目录结构
main.ts — 整个系统的入口
public — 纯静态资源文件,放index.html,不被webpack打包的
App.vue — 项目中的根组件,router-view是根路由出口
shims.tsx.d.ts 和 shims-vue.d.ts — 都是整个项目中ts的配置
Home.vue — home组件,组件中是ts规范
store — vuex目录,存放vuex的配置
route — 放置路由表,存放对应的ts路由,严格描述
components — 放置公共的组件文件夹
assets — 放置静态资源
node_modules — 第三方包
.gitignore — 放置不会被git 提交的文件
babel.config.js — babel 的配置
tsconfig.json – ts的配置
调整初始目录结构
App.vue 删除样式,删除多余模板
route/index.ts 清空路由表
删除Home.vue 和 about.vue
删除Hello.vue,删除log.png
创建utils目录,存放一些功能模块,请求等
创建styles目录,全局样式
创建services,放接口部分
在Vue中如何启动 typescript 支持
两种方式: 1. 在使用vue-cli创建项目中选择typescript
2. 已有项目,添加vue官方配置的ts适配插件
vue add @vue/typescript
使用TS开发,编辑器推荐VS Code
如果是vue项目,推荐安装 vetur插件
使用Options APIs
定义组件的方式
要让 TypeScript 正确推断 Vue 组件选项中的类型,您需要使用 Vue.component 或 Vue.extend 定义组件:
import Vue from 'vue'
const Component = Vue.extend({
// 类型推断已启用
})
const Component = {
// 这里不会有类型推断,
// 因为 TypeScript 不能确认这是 Vue 组件的选项
}
基于类的 Vue 组件
import Vue from 'vue'
import Component from 'vue-class-component'
// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
// 所有的组件选项都可以放在这里
template: ''
})
export default class MyComponent extends Vue {
// 初始数据可以直接声明为实例的 property
message: string = 'Hello!'
// 组件方法也可以直接声明为实例的方法
onClick (): void {
window.alert(this.message)
}
}
装饰器语法
良好的代码格式有利于维护,多人协作,阅读
约束代码规范
默认阅读不可靠,需要工具强制执行
项目中的代码规范
module.exports = {
root: true,
env: {
node: true
},
// 使用插件的编码校验规则
extends: [
'plugin:vue/essential',
'@vue/standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
// 自定义编码校验规则
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 'semi': ['error', 'always']
'@typescript-eslint/member-delimiter-style': ['error', {
"multiline": {
"delimiter": "none",
"requireLast": true
}
}]
}
}
自定义的校验规则
// 自定义编码校验规则
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 'semi': ['error', 'always']
'@typescript-eslint/member-delimiter-style': ['error', {
"multiline": {
"delimiter": "none",
"requireLast": true
}
}]
}
安装: npm i element-ui -S
引入element-ui
两种方式: 1.全部引入 2. 按需引入
在 main.js 中写入以下内容:
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI);
new Vue({
el: '#app',
render: h => h(App)
});
在使用element,进行组件定制
在src/styles中新增
index.scss 全局样式,在入口模块被加载
mixin.scss 公共的mixin混入
reset.scss 重置默认样式
variables.scss 公共样式变量
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;
}
}
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;
在全局配置,节省组件加载,注入到全局
增加vue.config.js
// vue.config.js
module.exports = {
css: {
loaderOptions: {
// 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
// 因为 `scss` 语法在内部也是由 sass-loader 处理的
// 但是在配置 `prependData` 选项的时候
// `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
// 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
scss: {
prependData: `@import "~@/styles/variables.scss";`
}
}
},
}
使用proxy设置代理接口地址
在vue.config.js 在添加
devServer: {
proxy: {
'/boss': {
target: 'http://eduboss.lagou.com',
changeOrigin: true // 把请求头中的 host 配置为 target
},
'/front': {
target: 'http://edufront.lagou.com',
changeOrigin: true
}
}
}
安装 axios
npm i axios
创建请求模块,src/utils/request.js
import axios from 'axios'
const request = axios.create({
// 配置选项
// baseURL,
// timeout
})
// 请求拦截器
// 响应拦截器
export default request
view下新创建对应的文件
在route/index.ts 下配置 路由懒加载,进行lauout布局,路由嵌套
layout 布局,src下新建layout文件夹,将除了登录页和404页面外全部是layout的子路由
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
使用container布局容器
在app-aside.vue中使用el-menu标签,加入route进入导航资源
<template>
<div class="aside">
<el-menu
default-active="4"
@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-setting"></i>
<span slot="title">角色管理</span>
</el-menu-item>
<el-menu-item index="/menu">
<i class="el-icon-setting"></i>
<span slot="title">菜单管理</span>
</el-menu-item>
<el-menu-item index="/resource">
<i class="el-icon-setting"></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-setting"></i>
<span slot="title">广告列表</span>
</el-menu-item>
<el-menu-item index="/advert-space">
<i class="el-icon-setting"></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-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="40"
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 {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown-link {
display: flex;
align-items: center;
}
}
</style>
进行登录页面的布局,采用form表单组件
可以只使用样式部分,js逻辑会进行删除
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
<div class="title-container">
<h3 class="title">综合管理平台</h3>
</div>
<el-form-item prop="username">
<span class="svg-container">
<i class = "el-icon-user"></i>
</span>
<el-input
ref="username"
v-model="loginForm.username"
placeholder="请输入用户名"
name="username"
type="text"
tabindex="1"
auto-complete="on"
/>
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<i class="el-icon-view"></i>
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="请输入密码"
name="password"
tabindex="2"
auto-complete="on"
@keyup.enter.native="handleLogin"
/>
<span class="show-pwd" @click="showPwd">
<!-- <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> -->
</span>
</el-form-item>
<div class="box clearfix">
<div class="rf">
<el-checkbox v-model="checked" style="color:#a0a0a0; padding-bottom: 10px;">记住密码</el-checkbox>
</div>
</div>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
</el-form>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'LoginIndex',
data () {
const validateUsername = (rule, value, callback) => {
callback()
}
const validatePassword = (rule, value, callback) => {
if (value.length < 6) {
callback(new Error('密码不低于六位数!'))
} else {
callback()
}
}
return {
loginForm: {
username: '',
password: ''
},
loginRules: {
username: [{
required: true, trigger: 'blur', validator: validateUsername }],
password: [{
required: true, trigger: 'blur', validator: validatePassword }]
},
loading: false,
passwordType: 'password',
redirect: undefined,
checked: false
}
},
mounted () {
this.getCookie()
},
watch: {
$route: {
handler: function (route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
methods: {
showPwd () {
if (this.passwordType === 'password') {
this.passwordType = ''
} else {
this.passwordType = 'password'
}
this.$nextTick(() => {
this.$refs.password.focus()
})
},
handleLogin () {
console.log('登录')
// this.$refs.loginForm.validate(valid => {
// if (valid) {
// if (this.checked) {
// this.setCookie(this.loginForm.username, this.loginForm.password, 7)
// } else {
// this.clearCookie()
// }
// this.loading = true
// this.$store.dispatch('user/login', this.loginForm).then(() => {
// this.$router.push({ path: this.redirect || '/' })
// this.loading = false
// }).catch(() => {
// this.loading = false
// })
// } else {
// console.log('error submit!!')
// return false
// }
// })
},
setCookie (name, pwd, exdays) {
const exdate = new Date()
exdate.setTime(exdate.getTime() + 24 * 60 * 60 * 1000 * exdays)
window.document.cookie = 'userName' + '=' + name + ';path=/;expires=' + exdate.toGMTString()
window.document.cookie = 'password' + '=' + pwd + ';path=/;expires=' + exdate.toGMTString()
},
getCookie () {
if (document.cookie.length > 0) {
const arr = document.cookie.split('; ')
for (let i = 0; i < arr.length; i++) {
const arr2 = arr[i].split('=')
if (arr2[0] === 'userName') {
this.loginForm.username = arr2[1]
} else if (arr2[0] === 'password') {
this.loginForm.password = arr2[1]
}
}
this.checked = true
}
},
clearCookie () {
this.setCookie('', '', -1)
}
}
})
</script>
<style lang="scss">
$bg:#283443;
$light_gray:#fff;
$cursor: #fff;
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
.login-container .el-input input {
color: $cursor;
}
}
/* reset element-ui css */
.login-container {
.el-input {
display: inline-block;
height: 47px;
width: 85%;
input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: $light_gray;
height: 47px;
caret-color: $cursor;
&:-webkit-autofill {
box-shadow: 0 0 0px 1000px $bg inset !important;
-webkit-text-fill-color: $cursor !important;
}
}
.rf{
float: right;
}
.box{
min-width: 350px;
margin-left:50px;
width: 30%;
}
.clearfix:after {
content:".";
display:block;
height:0;
visibility:hidden;
clear:both;
}
.clearfix {
*zoom:1;
}
}
.el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
color: #454545;
}
}
</style>
<style lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;
.login-container {
min-height: 100%;
width: 100%;
background-color: $bg;
overflow: hidden;
.login-form {
position: relative;
width: 520px;
max-width: 100%;
padding: 160px 35px 0;
margin: 0 auto;
overflow: hidden;
}
.tips {
font-size: 14px;
color: #fff;
margin-bottom: 10px;
span {
&:first-of-type {
margin-right: 16px;
}
}
}
.svg-container {
padding: 6px 5px 6px 15px;
color: $dark_gray;
vertical-align: middle;
width: 30px;
display: inline-block;
}
.title-container {
position: relative;
.title {
font-size: 26px;
color: $light_gray;
margin: 0px auto 40px auto;
text-align: center;
font-weight: bold;
}
}
.show-pwd {
position: absolute;
right: 10px;
top: 7px;
font-size: 16px;
color: $dark_gray;
cursor: pointer;
user-select: none;
}
}
</style>
可以使用postman,专门用来测试接口的
通过表单处理传入的数据
封装请求方法
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 是普通对象,则 Content-Type 是 application/json
// 如果 data 是 qs.stringify(data) 转换之后的数据:key=value&key=value,则 Content-Type 会被设置为 application/x-www-form-urlencoded
// 如果 data 是 FormData 对象,则 Content-Type 是 multipart/form-data
data: qs.stringify(data) // axios 默认发送的是 application/json 格式的数据
})
}
export const getUserInfo = () => {
return request({
method: 'GET',
url: '/front/user/getInfo'
})
}
将用户状态存到vuex中
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: {
// 修改容器数据必须使用 mutation 函数
setUser (state, payload) {
state.user = JSON.parse(payload)
// 为了防止页面刷新数据丢失,我们需要把 user 数据持久化
// 注意:本地存储只能存字符串
window.localStorage.setItem('user', payload)
}
},
actions: {
},
modules: {
}
})
使用路由拦截器控制页面的访问权限,修改route.ts
或者·可以搭配使用路由元信息,定义meta字段
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,
meta: {
requiresAuth: true
},
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
})
// 全局前置守卫:任何页面的访问都要经过这里
// to:要去哪里的路由信息
// from:从哪里来的路由信息
// next:通行的标志
router.beforeEach((to, from, next) => {
// to.matched 是一个数组(匹配到是路由记录)
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!store.state.user) {
// 跳转到登录页面
next({
name: 'login',
query: {
// 通过 url 传递查询字符串参数
redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页面
}
})
} else {
next() // 允许通过
}
} else {
next() // 允许通过
}
// // 路由守卫中一定要调用 next,否则页码无法展示
// next()
// if (to.path !== '/login') {
// // 校验登录状态
// }
})
export default router
处理token过期,token的有效时间是接口负责,一般是根据项目需要,如果设置时间比较短,体验并不好,可以根据设置refresh_token获取新的token,主要是处理安全性的问题
解决方法
使用请求拦截器设置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',
url: '/front/user/refresh_token',
data: qs.stringify({
// refresh_token 只能使用1次
refreshtoken: store.state.user.refresh_token
})
})
}
// 请求拦截器
request.interceptors.request.use(function (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 isRfreshing = false // 控制刷新 token 的状态
let requests: any[] = [] // 存储刷新 token 期间过来的 401 请求
request.interceptors.response.use(function (response) {
// 状态码为 2xx 都会进入这里
// console.log('请求响应成功了 => ', response)
// 如果是自定义错误状态码,错误处理就写到这里
return response
}, async function (error) {
// 超出 2xx 状态码都都执行这里
// console.log('请求响应失败了 => ', error)
// 如果是使用的 HTTP 状态码,错误处理就写到这里
// console.dir(error)
if (error.response) {
// 请求发出去收到响应了,但是状态码超出了 2xx 范围
const {
status } = error.response
if (status === 400) {
Message.error('请求参数错误')
} else if (status === 401) {
// token 无效(没有提供 token、token 是无效的、token 过期了)
// 如果有 refresh_token 则尝试使用 refresh_token 获取新的 access_token
if (!store.state.user) {
redirectLogin()
return Promise.reject(error)
}
// 刷新 token
if (!isRfreshing) {
isRfreshing = true // 开启刷新状态
// 尝试刷新获取新的 token
return refreshToken().then(res => {
if (!res.data.success) {
throw new Error('刷新 Token 失败')
}
// 刷新 token 成功了
store.commit('setUser', res.data.content)
// 把 requests 队列中的请求重新发出去
requests.forEach(cb => cb())
// 重置 requests 数组
requests = []
return request(error.config)
}).catch(err => {
console.log(err)
store.commit('setUser', null)
redirectLogin()
return Promise.reject(error)
}).finally(() => {
isRfreshing = false // 重置刷新状态
})
}
// 刷新状态下,把请求挂起放到 requests 数组中
return new Promise(resolve => {
requests.push(() => {
resolve(request(error.config))
})
})
} else if (status === 403) {
Message.error('没有权限,请联系管理员')
} else if (status === 404) {
Message.error('请求资源不存在')
} else if (status >= 500) {
Message.error('服务端错误,请联系管理员')
}
} else if (error.request) {
// 请求发出去没有收到响应
Message.error('请求超时,请刷新重试')
} else {
// 在设置请求时发生了一些事情,触发了一个错误
Message.error(`请求失败:${
error.message}`)
}
// 把请求失败的错误对象继续抛出,扔给上一个调用者
return Promise.reject(error)
})
export default request
显示用户头像和退出,修改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="40"
:src="userInfo.portrait || require('@/assets/default-avatar.png')"
></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>
<el-dropdown-item
divided
@click.native="handleLogout"
>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script lang="ts">
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
console.log('loadUserInfo')
},
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: '已取消退出'
})
})
}
}
})
</script>
<style lang="scss" scoped>
.header {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown-link {
display: flex;
align-items: center;
}
}
</style>
修改后的login/index.vue
<template>
<div class="login">
<!--
1. :model="ruleForm"
2. :rules="rules"
3. ref="ruleForm"
4. el-form-item 绑定 prop 属性
-->
<el-form
class="login-form"
label-position="top"
ref="form"
:model="form"
:rules="rules"
label-width="80px"
>
<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
class="login-btn"
type="primary"
:loading="isLoginLoading"
@click="onSubmit"
>登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {
Form } from 'element-ui'
import {
login } from '@/services/user'
export default Vue.extend({
name: 'LoginIndex',
data () {
return {
form: {
phone: '18201288771',
password: '111111'
},
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' }
]
},
isLoginLoading: false
}
},
methods: {
async onSubmit () {
try {
// 1. 表单验证
await (this.$refs.form as Form).validate()
// 登录按钮 loading
this.isLoginLoading = true
// 2. 验证通过 -> 提交表单
const {
data } = await login(this.form)
// 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 格式的数据
// })
// 3. 处理请求结果
// 失败:给出提示
if (data.state !== 1) {
this.$message.error(data.message)
} else {
// 1. 登录成功,记录登录状态,状态需要能够全局访问(放到 Vuex 容器中)
this.$store.commit('setUser', data.content)
// 2. 然后在访问需要登录的页面的时候判断有没有登录状态(路由拦截器)
// 成功:跳转回原来页面或首页
this.$router.push(this.$route.query.redirect as string || '/')
// this.$router.push({
// name: 'home'
// })
this.$message.success('登录成功')
}
} catch (err) {
console.log('登录失败', err)
}
// 结束登录按钮的 loading
this.isLoginLoading = false
}
}
})
</script>
<style lang="scss" scoped>
.login {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.login-form {
width: 300px;
background: #fff;
padding: 20px;
border-radius: 5px;
}
.login-btn {
width: 100%;
}
}
</style>
不同的角色有不同的权限,可以在从角色入手,角色的菜单和权限
在views下的menu进行组件创建及修改
<template>
<div class="menu">
<el-card class="box-card">
<div slot="header" class="clearfix">
<el-button @click="$router.push({ name: 'menu-create' })">添加菜单</el-button>
</div>
<el-table
:data="menus"
style="width: 100%">
<el-table-column
label="编号"
min-width="150"
type="index">
</el-table-column>
<el-table-column
prop="name"
label="菜单名称"
min-width="150">
</el-table-column>
<el-table-column
prop="level"
label="菜单级数"
min-width="150">
</el-table-column>
<el-table-column
prop="icon"
label="前端图标"
min-width="150">
</el-table-column>
<el-table-column
prop="orderNum"
label="排序"
min-width="150">
</el-table-column>
<el-table-column
label="操作"
min-width="150">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.row)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {
getAllMenus, deleteMenu } from '@/services/menu'
export default Vue.extend({
name: 'MenuIndex',
data () {
return {
menus: [] // 菜单列表
}
},
created () {
this.loadAllMenus()
},
methods: {
async loadAllMenus () {
const {
data } = await getAllMenus()
if (data.code === '000000') {
this.menus = data.data
}
},
handleEdit (item: any) {
this.$router.push({
name: 'menu-edit',
params: {
id: item.id
}
})
},
handleDelete (item: any) {
this.$confirm('确认删除吗?', '删除提示', {
})
.then(async () => {
// 确认执行这里
// 请求删除操作
const {
data } = await deleteMenu(item.id)
if (data.code === '000000') {
this.$message.success('删除成功')
this.loadAllMenus() // 更新数据列表
}
})
.catch(err => {
// 取消执行这里
console.log(err)
this.$message.info('已取消删除')
})
}
}
})
</script>
<style lang="scss" scoped></style>
servicers下新增menu.ts
/**
* 菜单相关请求模块
*/
import request from '@/utils/request'
export const createOrUpdateMenu = (data: any) => {
return request({
method: 'POST',
url: '/boss/menu/saveOrUpdate',
data
})
}
export const getEditMenuInfo = (id: string | number = -1) => {
return request({
method: 'GET',
url: '/boss/menu/getEditMenuInfo',
params: {
id
}
})
}
export const getAllMenus = () => {
return request({
method: 'GET',
url: '/boss/menu/getAll'
})
}
export const deleteMenu = (id: number) => {
return request({
method: 'DELETE',
url: `/boss/menu/${
id}`
})
}
export const getMenuNodeList = () => {
return request({
method: 'GET',
url: '/boss/menu/getMenuNodeList'
})
}
export const allocateRoleMenus = (data: any) => {
return request({
method: 'POST',
url: '/boss/menu/allocateRoleMenus',
data
})
}
export const getRoleMenus = (roleId: string | number) => {
return request({
method: 'GET',
url: '/boss/menu/getRoleMenus',
params: {
// axios 会把 params 转换为 key=value&key=value 的数据格式放到 url 后面(以?分割)
roleId
}
})
}
菜单管理页面
create.vue
<template>
<div class="menu-create">
<create-or-edit />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import CreateOrEdit from './components/CreateOrEdit.vue'
export default Vue.extend({
name: 'MenuCreate',
components: {
CreateOrEdit
},
data () {
return {
}
}
})
</script>
<style lang="scss" scoped></style>
封装公共组件菜单,menu下新增components/CreateOrEdit.vue
<template>
<div class="menu-create">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>{
{
isEdit ? '编辑菜单' : '添加菜单' }}</span>
</div>
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="菜单名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="菜单路径">
<el-input v-model="form.href"></el-input>
</el-form-item>
<el-form-item label="上级菜单">
<el-select v-model="form.parentId" placeholder="请选择上级菜单">
<el-option :value="-1" label="无上级菜单"></el-option>
<el-option
:label="item.name"
:value="item.id"
v-for="item in parentMenuList"
:key="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description"></el-input>
</el-form-item>
<el-form-item label="前端图标">
<el-input v-model="form.icon"></el-input>
</el-form-item>
<el-form-item label="是否显示">
<el-radio-group v-model="form.shown">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.orderNum" :min="1" label="描述文字"></el-input-number>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button
v-if="!isEdit"
>重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {
createOrUpdateMenu, getEditMenuInfo } from '@/services/menu'
export default Vue.extend({
name: 'MenuCreateOrEdit',
props: {
isEdit: {
type: Boolean,
default: false
}
},
data () {
return {
form: {
parentId: -1, // -1 表示没有上级菜单
name: '123',
href: '123',
icon: '123',
orderNum: 0,
description: '123',
shown: false
},
parentMenuList: [] // 父级菜单列表
}
},
created () {
this.loadMenuInfo()
},
methods: {
async loadMenuInfo () {
const {
data } = await getEditMenuInfo(this.$route.params.id || -1)
if (data.data.menuInfo) {
this.form = data.data.menuInfo
}
if (data.code === '000000') {
this.parentMenuList = data.data.parentMenuList
}
},
async onSubmit () {
// 1. 表单验证
// 2. 验证通过,提交表单
const {
data } = await createOrUpdateMenu(this.form)
if (data.code === '000000') {
this.$message.success('提交成功')
this.$router.back()
}
}
}
})
</script>
<style lang="scss" scoped></style>
views下resource的index.vue
<template>
<div class="resource">
<resource-list />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import ResourceList from './components/List.vue'
export default Vue.extend({
name: 'ResourceIndex',
components: {
ResourceList
}
})
</script>
<style lang="scss" scoped></style>
ResourceList.vue
<template>
<div class="resource-list">
<el-card class="box-card">
<div slot="header" class="clearfix">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item prop="name" label="资源名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item prop="url" label="资源路径">
<el-input v-model="form.url"></el-input>
</el-form-item>
<el-form-item prop="categoryId" label="资源分类">
<el-select
v-model="form.categoryId"
placeholder="请选择资源分类"
clearable
>
<el-option
:label="item.name"
:value="item.id"
v-for="item in resourceCategories"
:key="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="onSubmit"
:disabled="isLoading"
>查询搜索</el-button>
<el-button
@click="onReset"
:disabled="isLoading"
>重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table
:data="resources"
style="width: 100%; margin-bottom: 20px"
v-loading="isLoading"
>
<el-table-column
type="index"
label="编号"
width="100">
</el-table-column>
<el-table-column
prop="name"
label="资源名称"
width="180">
</el-table-column>
<el-table-column
prop="url"
width="180"
label="资源路径">
</el-table-column>
<el-table-column
prop="description"
width="180"
label="描述">
</el-table-column>
<el-table-column
width="180"
prop="createdTime"
label="添加时间">
</el-table-column>
<el-table-column
width="180"
label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.row)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--
total 总记录数
page-size 每页大小
分页组件会自动根据 total 和 page-size 计算出一共分多少页
-->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:disabled="isLoading"
:current-page.sync="form.current"
:page-sizes="[5, 10, 20]"
:page-size="form.size"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount">
</el-pagination>
</el-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {
getResourcePages } from '@/services/resource'
import {
getResourceCategories } from '@/services/resource-category'
import {
Form } from 'element-ui'
export default Vue.extend({
name: 'ResourceList',
data () {
return {
resources: [], // 资源列表
form: {
name: '',
url: '',
current: 1, // 默认查询第1页数据
size: 5, // 每页大小
categoryId: null // 资源分类
},
totalCount: 0,
resourceCategories: [], // 资源分类列表
isLoading: true // 加载状态
}
},
created () {
this.loadResources()
this.loadResourceCategories()
},
methods: {
async loadResourceCategories () {
const {
data } = await getResourceCategories()
this.resourceCategories = data.data
},
async loadResources () {
this.isLoading = true // 展示加载中状态
const {
data } = await getResourcePages(this.form)
this.resources = data.data.records
this.totalCount = data.data.total
this.isLoading = false // 关闭加载中状态
},
onSubmit () {
this.form.current = 1 // 筛选查询从第 1 页开始
this.loadResources()
},
handleEdit (item: any) {
console.log('handleEdit', item)
},
handleDelete (item: any) {
console.log('handleDelete', item)
},
handleSizeChange (val: number) {
this.form.size = val
this.form.current = 1 // 每页大小改变重新查询第1页数据
this.loadResources()
},
handleCurrentChange (val: number) {
// 请求获取对应页码的数据
this.form.current = val // 修改要查询的页码
this.loadResources()
},
onReset () {
(this.$refs.form as Form).resetFields()
this.form.current = 1 // 重置回到第1页
this.loadResources()
}
}
})
</script>
<style lang="scss" scoped></style>
Role部分
index.vue
<template>
<div class="role">
<role-list />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import RoleList from './components/List.vue'
export default Vue.extend({
name: 'RoleIndex',
components: {
RoleList
}
})
</script>
<style lang="scss" scoped></style>
分配组件
<template>
<div class="alloc-menu">
<el-card>
<div slot="header">
<span>分配菜单</span>
</div>
<el-tree
ref="menu-tree"
:data="menus"
node-key="id"
:props="defaultProps"
:default-checked-keys="checkedKeys"
show-checkbox
default-expand-all
></el-tree>
<div style="text-align: center">
<el-button @click="resetChecked">清空</el-button>
<el-button type="primary" @click="onSave">保存</el-button>
</div>
</el-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {
getMenuNodeList,
allocateRoleMenus,
getRoleMenus
} from '@/services/menu'
import {
Tree } from 'element-ui'
import {
getRoleById } from '@/services/role'
export default Vue.extend({
name: 'AllocMenu',
props: {
roleId: {
type: [String, Number],
required: true
}
},
data () {
return {
menus: [],
defaultProps: {
children: 'subMenuList',
label: 'name'
},
checkedKeys: []
}
},
async created () {
await this.loadMenus()
this.loadRoleMenus()
},
methods: {
async loadRoleMenus () {
const {
data } = await getRoleMenus(this.roleId)
this.getCheckedKeys(data.data)
},
getCheckedKeys (menus: any) {
menus.forEach((menu: any) => {
if (menu.selected) {
// this.checkedKeys.push(menu.id as never)
this.checkedKeys = [...this.checkedKeys, menu.id] as any
}
if (menu.subMenuList) {
this.getCheckedKeys(menu.subMenuList)
}
})
},
async loadMenus () {
const {
data } = await getMenuNodeList()
this.menus = data.data
},
async onSave () {
const menuIdList = (this.$refs['menu-tree'] as Tree).getCheckedKeys()
// 拿到选中节点的数据 id 列表
// 请求提交保存
await allocateRoleMenus({
roleId: this.roleId,
menuIdList
})
this.$message.success('操作成功')
this.$router.back()
},
resetChecked () {
(this.$refs['menu-tree'] as Tree).setCheckedKeys([])
}
}
})
</script>
<style lang="scss" scoped></style>
分配资源
<template>
<div class="alloc-resource">
<el-card>
<div slot="header">
<span>分配资源</span>
</div>
<el-tree
ref="tree"
:data="resources"
node-key="id"
:props="defaultProps"
show-checkbox
:default-checked-keys="defaultCheckedKeys"
:default-expanded-keys="defaultCheckedKeys"
></el-tree>
<div style="text-align: center">
<el-button @click="resetChecked">清空</el-button>
<el-button type="primary" @click="onSave">保存</el-button>
</div>
</el-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {
getAllResources,
allocateRoleResources,
getRoleResources
} from '@/services/resource'
import {
getResourceCategories } from '@/services/resource-category'
import {
Tree } from 'element-ui'
export default Vue.extend({
name: 'AllocResource',
props: {
roleId: {
type: [String, Number],
required: true
}
},
data () {
return {
resources: [],
defaultProps: {
children: 'children',
label: 'name'
},
defaultCheckedKeys: []
}
},
created () {
this.loadResources()
this.loadRoleResources()
},
methods: {
async loadResources () {
const ret = await Promise.all([getAllResources(), getResourceCategories()])
const resources = ret[0].data.data
const resourceCategories = ret[1].data.data
resources.forEach((r: any) => {
const category = resourceCategories.find((c: any) => c.id === r.categoryId)
if (category) {
category.children = category.children || []
category.children.push(r)
}
})
// 修改顶层分类 ID:因为分类 ID 和资源 ID 冲突
resourceCategories.forEach((item: any) => {
item.id = Math.random()
})
this.resources = resourceCategories
},
async loadRoleResources () {
const {
data } = await getRoleResources(this.roleId)
this.getCheckedResources(data.data)
},
getCheckedResources (resources: any) {
resources.forEach((r: any) => {
r.resourceList && r.resourceList.forEach((c: any) => {
if (c.selected) {
this.defaultCheckedKeys = [...this.defaultCheckedKeys, c.id] as any
}
})
})
},
async onSave () {
const checkedNodes = (this.$refs.tree as Tree).getCheckedNodes()
const resourceIdList: number[] = []
checkedNodes.forEach(item => {
if (!item.children) {
resourceIdList.push(item.id)
}
})
await allocateRoleResources({
roleId: this.roleId,
resourceIdList
})
this.$message.success('保存成功')
this.$router.back()
},
resetChecked () {
(this.$refs.tree as Tree).setCheckedKeys([])
}
}
})
</script>
<style lang="scss" scoped></style>