初始化项目
开发环境
- IDE webstorm2019
- nodejs 8.15.1
- npm 6.4.1
- @vue/cli-3.11.1
package.json
{
"name": "webapp",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.19.0",
"core-js": "^2.6.5",
"element-ui": "^2.12.0",
"vue": "^2.6.10",
"vue-drag-verify": "^1.0.6",
"vue-fragment": "^1.5.1",
"vue-router": "^3.0.3",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-eslint": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"@vue/eslint-config-standard": "^4.0.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"vue-template-compiler": "^2.6.10"
}
}





目录结构整理

我在cli基础上做了修改,目录说明如下
- api目录封装axios http请求用的
- common文件夹是css js img文件
- component文件夹是通用的组件
- router文件夹是路由
- store文件夹是vuex
- view是page页面
- auth.js文件是做动态路由的文件


动态路由
和后端的约定
我和后端的约定是这样的,数据库中有一个表,假设叫menu,专门存放菜单的
接口返回数据机构如下
{
"code": 200,
"message": "SUCCESS",
"data": [
{
"menuId": "39acd7d439674d9687a115adc86e6785",
"parentId": "0",
"menuName": "基本信息",
"menuUrl": "basic-information",
"menuIcon": "el-icon-setting",
"menuSort": "9998",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": [
{
"menuId": "0b61404507d148c7a76695cf5da78c92",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "厂区设置",
"menuUrl": "factory-setting",
"menuIcon": null,
"menuSort": "99989",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "d42f1103ae1f4a33bc51c5b6cce42669",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "库区设置",
"menuUrl": "warehouse-setting",
"menuIcon": null,
"menuSort": "99988",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "f2adf1bb65c34da28b40f930e836542e",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "道口设置",
"menuUrl": "crossing-setting",
"menuIcon": null,
"menuSort": "99987",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "8c8bcf4974db4d6ea31b5862935e3db6",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "门岗设置",
"menuUrl": "gate-setting",
"menuIcon": null,
"menuSort": "99986",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "edac960fe25f45dfae18d9a3e96cae24",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "承运商管理",
"menuUrl": "carrier-management",
"menuIcon": null,
"menuSort": "99985",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "5a4bc0a658bd46d29f3d29323abaa974",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "车辆管理",
"menuUrl": "vehicle-management",
"menuIcon": null,
"menuSort": "99984",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "1746de6200ec47ee8f53fd214bb61dbc",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "司机管理",
"menuUrl": "driver-management",
"menuIcon": null,
"menuSort": "99983",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "22d38716ef6941a8bc1ba41338f92438",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "装车时长设置",
"menuUrl": "loading-time-setting",
"menuIcon": null,
"menuSort": "99982",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "cb8fd30191024a96b7d398f2a2ef2e6c",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "锁定时长设置",
"menuUrl": "lock-time-setting",
"menuIcon": null,
"menuSort": "99981",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "c8a063132e4a4ad6af8f1fca1f6bcce4",
"parentId": "39acd7d439674d9687a115adc86e6785",
"menuName": "系统设置",
"menuUrl": "system-seeting",
"menuIcon": null,
"menuSort": "99980",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
}
]
},
{
"menuId": "ec6ec25da6ec4818a6d6b5fda36e914e",
"parentId": "0",
"menuName": "作业管理",
"menuUrl": "job-management",
"menuIcon": "el-icon-s-flag",
"menuSort": "9997",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": [
{
"menuId": "00f412e738ed45bc88e8379d11b5e446",
"parentId": "ec6ec25da6ec4818a6d6b5fda36e914e",
"menuName": "预约信息列表",
"menuUrl": "appointment-information-list",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "4e1a2bd2302543278c24b2434cfd7a76",
"parentId": "ec6ec25da6ec4818a6d6b5fda36e914e",
"menuName": "整车监控管理",
"menuUrl": "vehicle-monitoring",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "c59e4e1f536648929274a97b80b55270",
"parentId": "ec6ec25da6ec4818a6d6b5fda36e914e",
"menuName": "门岗登记管理",
"menuUrl": "gate-registration",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
}
]
},
{
"menuId": "9ddcea2cd7a6494f818ce19801a512ed",
"parentId": "0",
"menuName": "预约管理",
"menuUrl": "ppointment-management",
"menuIcon": "el-icon-s-promotion",
"menuSort": "9996",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": [
{
"menuId": "b3efa8b104964cd1934e33ecab448b28",
"parentId": "9ddcea2cd7a6494f818ce19801a512ed",
"menuName": "新建预约",
"menuUrl": "new-appointment",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
}
]
},
{
"menuId": "efc659d4a8544b089c05da0bd8a90077",
"parentId": "0",
"menuName": "统计分析",
"menuUrl": "statistic-analysis",
"menuIcon": "el-icon-s-data",
"menuSort": "9995",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": [
{
"menuId": "346d07d74b644bfbb7c09638f485198a",
"parentId": "efc659d4a8544b089c05da0bd8a90077",
"menuName": "承运商迟到分析",
"menuUrl": "carrier-company-arrive-late",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
}
]
},
{
"menuId": "b6b7a4d5e27c4b949a9f4850cfd6340d",
"parentId": "0",
"menuName": "权限管理",
"menuUrl": "auth-management",
"menuIcon": "el-icon-lock",
"menuSort": "9994",
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": [
{
"menuId": "3a51229dc739487a9c694794e82369e3",
"parentId": "b6b7a4d5e27c4b949a9f4850cfd6340d",
"menuName": "角色管理",
"menuUrl": "role-management",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "67fb6c43069449158b1874090028222d",
"parentId": "b6b7a4d5e27c4b949a9f4850cfd6340d",
"menuName": "菜单管理",
"menuUrl": "menu-management",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
},
{
"menuId": "a3e9c552227745e0a04af8d8f8b40146",
"parentId": "b6b7a4d5e27c4b949a9f4850cfd6340d",
"menuName": "用户管理",
"menuUrl": "user-management",
"menuIcon": null,
"menuSort": null,
"createDate": null,
"createPersonId": null,
"updateDate": null,
"updatePersonId": null,
"logicDelete": "0",
"subMenuList": []
}
]
}
]
}
根据接口返回的数据动态创建路由
代码如下
// auth.js
import router from '@/router'
import store from '@/store'
import { post } from '@/api/http'
import { Message } from 'element-ui'
let flag = false
/**
* 路由守卫
* api /menu/getAllMenuInfo 查询所有菜单
*/
router.beforeEach((to, from, next) => {
// 如果store中没有token,则证明当前用户没有登录过
if (!store.state.token.length) {
// 如果路由刚好在登录页面上,则需要next掉,不然会死循环
if (to.path === '/login') {
return next()
}
// 跳转到登录页
return next('/login')
}
// 用户登录成功
// 如果路由刚好在登录页上,则跳转到 / 路径上
if (to.path === '/login') {
return next('/')
}
// 创建动态路由
// 不加这个判断页面会死循环
if (!flag) {
// flag变成true
flag = true
// 如果sessionStorage没有router字段
if (!sessionStorage.getItem('router')) {
// 向接口中拿menu list
post('/menu/getAllMenuInfo', {}).then(res => {
// 1.将接口数据转成vue-router数据格式
const data = transformHttpDataToVueRouterData(res.data)
// 2.将处理过的数据存到sessionStore中去
sessionStorage.setItem('router', JSON.stringify(data))
// 3.将数据派发store中
store.dispatch('menuList', {
menuList: data
})
// 4.创建动态路由
routerGo(to, next)
})
} else {
// 如果sessionStorage有router字段,则直接创建路由
routerGo(to, next)
}
} else {
next()
}
})
/**
* 添加路由
* @param to
* @param next
*/
function routerGo (to, next) {
// 获取sessionStorage中的router字段,并序列化
const routerData = JSON.parse(sessionStorage.getItem('router'))
// 如果前线列表是空的,返回提示
if (!routerData.length) {
return Message.error('当前权限列表为空!')
}
// layout组件是主页布局文件,需要手动引入
const asyncRouter = [
{
name: 'Layout',
path: '/layout',
component: () => import('@/component/layout/Layout.vue'),
children: []
}
]
// 通过componentPath创建component组件
asyncRouter[0].children = transformVueRouterDataToVueRouterComponent(routerData)
// 在路由末尾添加404
asyncRouter.push({
path: '*',
name: 'notFount',
component: () => import('@/view/not-fount/NotFound.vue')
})
// 添加动态路由
router.addRoutes(asyncRouter)
next({ ...to, replace: true })
}
/**
* 接口列表格式转换成满足vue-router的对应字段
* @param data
* @param array
* @param str
* @returns {Array}
*/
function transformHttpDataToVueRouterData (data, array = [], str = '/') {
data.forEach((item, index) => {
array.push({
menuId: item.menuId,
label: item.menuName,
path: item.menuUrl,
name: toCamel(item.menuUrl),
icon: item.menuIcon,
// 这个字段是用来做浏览器地址链接有用的
componentPath: `${str}${item.menuUrl}`,
children: []
})
if (item.subMenuList && item.subMenuList.length) {
array[index].redirect = {
name: toCamel(item.subMenuList[0].menuUrl)
}
transformHttpDataToVueRouterData(item.subMenuList, array[index].children, `${array[index].componentPath}/`)
} else {
array[index].redirect = null
}
})
return array
}
/**
* 将component字段转成component组件
* @param root
* @returns {*}
*/
function transformVueRouterDataToVueRouterComponent (root) {
root.forEach((item) => {
let path = item.componentPath
path = path + '/' + toCamel(path.substring(path.lastIndexOf('/') + 1))
// 因为webpack引入import机制的问题。全部转成变量不能解析
item.component = () => import(`./view${path}.vue`)
if (item.children && item.children.length) {
transformVueRouterDataToVueRouterComponent(item.children)
}
})
return root
}
/**
* 中划线命名转大驼峰命名
* @param str
* @returns {*}
*/
function toCamel (str) {
str = str.replace(/(\w)/, (match, $1) => `${$1.toUpperCase()}`)
while (str.match(/\w-\w/)) {
str = str.replace(/(\w)(-)(\w)/, (match, $1, $2, $3) => `${$1}${$3.toUpperCase()}`)
}
return str
}
创建之后来看一下路由
- 静态路由
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const constantRouter = [
{
path: '/',
redirect: 'layout'
},
{
path: '/login',
name: 'Login',
component: () => import('@/view/login/Login.vue')
},
]
const router = new VueRouter({
routes: constantRouter
})
export default router
-
动态路由
动态路由.png
最后看一下代码截图

Layout组件布局
效果图如下

Menu菜单实现
我把接口的数据转换成vue-router支持的数据格式后存到的store中数据格式如下
[
{
"menuId": "39acd7d439674d9687a115adc86e6785",
"label": "基本信息",
"path": "basic-information",
"name": "BasicInformation",
"icon": "el-icon-setting",
"componentPath": "/basic-information",
"children": [
{
"menuId": "0b61404507d148c7a76695cf5da78c92",
"label": "厂区设置",
"path": "factory-setting",
"name": "FactorySetting",
"icon": null,
"componentPath": "/basic-information/factory-setting",
"children": [],
"redirect": null
},
{
"menuId": "d42f1103ae1f4a33bc51c5b6cce42669",
"label": "库区设置",
"path": "warehouse-setting",
"name": "WarehouseSetting",
"icon": null,
"componentPath": "/basic-information/warehouse-setting",
"children": [],
"redirect": null
},
{
"menuId": "f2adf1bb65c34da28b40f930e836542e",
"label": "道口设置",
"path": "crossing-setting",
"name": "CrossingSetting",
"icon": null,
"componentPath": "/basic-information/crossing-setting",
"children": [],
"redirect": null
},
{
"menuId": "8c8bcf4974db4d6ea31b5862935e3db6",
"label": "门岗设置",
"path": "gate-setting",
"name": "GateSetting",
"icon": null,
"componentPath": "/basic-information/gate-setting",
"children": [],
"redirect": null
},
{
"menuId": "edac960fe25f45dfae18d9a3e96cae24",
"label": "承运商管理",
"path": "carrier-management",
"name": "CarrierManagement",
"icon": null,
"componentPath": "/basic-information/carrier-management",
"children": [],
"redirect": null
},
{
"menuId": "5a4bc0a658bd46d29f3d29323abaa974",
"label": "车辆管理",
"path": "vehicle-management",
"name": "VehicleManagement",
"icon": null,
"componentPath": "/basic-information/vehicle-management",
"children": [],
"redirect": null
},
{
"menuId": "1746de6200ec47ee8f53fd214bb61dbc",
"label": "司机管理",
"path": "driver-management",
"name": "DriverManagement",
"icon": null,
"componentPath": "/basic-information/driver-management",
"children": [],
"redirect": null
},
{
"menuId": "22d38716ef6941a8bc1ba41338f92438",
"label": "装车时长设置",
"path": "loading-time-setting",
"name": "LoadingTimeSetting",
"icon": null,
"componentPath": "/basic-information/loading-time-setting",
"children": [],
"redirect": null
},
{
"menuId": "cb8fd30191024a96b7d398f2a2ef2e6c",
"label": "锁定时长设置",
"path": "lock-time-setting",
"name": "LockTimeSetting",
"icon": null,
"componentPath": "/basic-information/lock-time-setting",
"children": [],
"redirect": null
},
{
"menuId": "c8a063132e4a4ad6af8f1fca1f6bcce4",
"label": "系统设置",
"path": "system-seeting",
"name": "SystemSeeting",
"icon": null,
"componentPath": "/basic-information/system-seeting",
"children": [],
"redirect": null
}
],
"redirect": {
"name": "FactorySetting"
}
},
{
"menuId": "ec6ec25da6ec4818a6d6b5fda36e914e",
"label": "作业管理",
"path": "job-management",
"name": "JobManagement",
"icon": "el-icon-s-flag",
"componentPath": "/job-management",
"children": [
{
"menuId": "00f412e738ed45bc88e8379d11b5e446",
"label": "预约信息列表",
"path": "appointment-information-list",
"name": "AppointmentInformationList",
"icon": null,
"componentPath": "/job-management/appointment-information-list",
"children": [],
"redirect": null
},
{
"menuId": "4e1a2bd2302543278c24b2434cfd7a76",
"label": "整车监控管理",
"path": "vehicle-monitoring",
"name": "VehicleMonitoring",
"icon": null,
"componentPath": "/job-management/vehicle-monitoring",
"children": [],
"redirect": null
},
{
"menuId": "c59e4e1f536648929274a97b80b55270",
"label": "门岗登记管理",
"path": "gate-registration",
"name": "GateRegistration",
"icon": null,
"componentPath": "/job-management/gate-registration",
"children": [],
"redirect": null
}
],
"redirect": {
"name": "AppointmentInformationList"
}
},
{
"menuId": "9ddcea2cd7a6494f818ce19801a512ed",
"label": "预约管理",
"path": "ppointment-management",
"name": "PpointmentManagement",
"icon": "el-icon-s-promotion",
"componentPath": "/ppointment-management",
"children": [
{
"menuId": "b3efa8b104964cd1934e33ecab448b28",
"label": "新建预约",
"path": "new-appointment",
"name": "NewAppointment",
"icon": null,
"componentPath": "/ppointment-management/new-appointment",
"children": [],
"redirect": null
}
],
"redirect": {
"name": "NewAppointment"
}
},
{
"menuId": "efc659d4a8544b089c05da0bd8a90077",
"label": "统计分析",
"path": "statistic-analysis",
"name": "StatisticAnalysis",
"icon": "el-icon-s-data",
"componentPath": "/statistic-analysis",
"children": [
{
"menuId": "346d07d74b644bfbb7c09638f485198a",
"label": "承运商迟到分析",
"path": "carrier-company-arrive-late",
"name": "CarrierCompanyArriveLate",
"icon": null,
"componentPath": "/statistic-analysis/carrier-company-arrive-late",
"children": [],
"redirect": null
}
],
"redirect": {
"name": "CarrierCompanyArriveLate"
}
},
{
"menuId": "b6b7a4d5e27c4b949a9f4850cfd6340d",
"label": "权限管理",
"path": "auth-management",
"name": "AuthManagement",
"icon": "el-icon-lock",
"componentPath": "/auth-management",
"children": [
{
"menuId": "3a51229dc739487a9c694794e82369e3",
"label": "角色管理",
"path": "role-management",
"name": "RoleManagement",
"icon": null,
"componentPath": "/auth-management/role-management",
"children": [],
"redirect": null
},
{
"menuId": "67fb6c43069449158b1874090028222d",
"label": "菜单管理",
"path": "menu-management",
"name": "MenuManagement",
"icon": null,
"componentPath": "/auth-management/menu-management",
"children": [],
"redirect": null
},
{
"menuId": "a3e9c552227745e0a04af8d8f8b40146",
"label": "用户管理",
"path": "user-management",
"name": "UserManagement",
"icon": null,
"componentPath": "/auth-management/user-management",
"children": [],
"redirect": null
}
],
"redirect": {
"name": "RoleManagement"
}
}
]
递归组件
直接上代码

<!-- Layout -->
<el-menu
:collapse="isCollapse"
background-color="#20222A"
text-color="rgba(255,255,255,0.9)"
active-text-color="#409EFF"
:default-active="this.$route.path"
router
mode="vertical">
<Menus :menus="menuList" :collapse="isCollapse"></Menus>
</el-menu>
<!-- Menus.vue -->
<template>
<div>
<template v-for="menu in menus">
<el-submenu v-if="menu.children && menu.children.length > 0"
:index="'/layout'+menu.componentPath"
:key="menu.children.menuId">
<template slot="title">
<i :class="menu.icon"></i>
<span v-show="!collapse" slot="title">{{menu.label}}</span>
</template>
<Menus :menus="menu.children"></Menus>
</el-submenu>
<template v-else>
<el-menu-item :index="'/layout'+menu.componentPath" :key="menu.menuId">
<i :class="menu.icon"></i>
<span slot="title">{{menu.label}}</span>
</el-menu-item>
</template>
</template>
</div>
</template>
<script>
export default {
name: 'Menus',
props: {
menus: {
type: Array,
required: true
},
collapse: {
type: Boolean,
default: false
}
}
}
</script>
<style>
.el-scrollbar__view {
height: 100%;
}
.el-menu-item.is-active {
background-color: rgba(26, 27, 34) !important;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.sidebar-container .el-scrollbar__wrap {
overflow-y: scroll !important;
}
.sidebar-container .el-icon-arrow-right {
display: none;
}
</style>
来看一下效果图


问题点说明
这里有一个bug需要说明一下,在菜单收起的状态下,文字是隐藏的。原因是递归组件上多了一个div导致dom渲染的时候多无用的div,

而这个div还不能被删除。大家可以阅读这篇文章,讲的很清晰了
【Vue+Element】菜单导航折叠后文字不隐藏
最开始我也是使用的这种方案处理的这个bug。然而在测试的时候,我发现这个东西在IE11下会报错。所以我用了另外一方法。如下图

权限菜单分配
什么是权限菜单
一个后台管理系统,往往有多个角色,每个角色的操作的菜单也不尽相同。如下图



如果路由都是静态被创建出来的,用户知道知道路由地址,就可以操作原本没有权限的菜单,那样是非常不安全的。这也是为什么要做动态路由的原因。
实现流程
- api接口 /menu/getAllMenuInfo 查询所有菜单(返回值是一个tree,前文已有这个字段了)
- api接口 /menu/getMenuByUserId 根据用户ID查询该用户拥有权限的菜单
let menuKeys = JSON.parse(res.data.menuId).menuCheckedKeys
{
"code": 200,
"message": "SUCCESS",
"data": {
"roleId": "5e10028557c3d93af3997a3e2815bb87",
"menuId": "{\"menuCheckedKeys\":[\"39acd7d439674d9687a115adc86e6785\",\"edac960fe25f45dfae18d9a3e96cae24\",\"5a4bc0a658bd46d29f3d29323abaa974\",\"1746de6200ec47ee8f53fd214bb61dbc\",\"9ddcea2cd7a6494f818ce19801a512ed\",\"b3efa8b104964cd1934e33ecab448b28\"],\"checkedKeys\":[\"edac960fe25f45dfae18d9a3e96cae24\",\"5a4bc0a658bd46d29f3d29323abaa974\",\"1746de6200ec47ee8f53fd214bb61dbc\",\"b3efa8b104964cd1934e33ecab448b28\"]}",
"createDate": "2019-11-27 08:57:33",
"createPersonId": "b1e282ce10c4a8a8ff844704554ba524",
"updateDate": "2019-11-27 08:57:33",
"updatePersonId": null
}
}
重点说明一下menuId这个字段,值是一个字符串,前端拿到之后要序列化转成对象。
- res.menuId.menuCheckedKeys这个值就是当前user所拥有的菜单
- res.menuId.checkedKeys这个值是用来反选的,因为element-ui tree这个控件的问题,强制加上去。
- 这里需要说明一下。这块本来可以设计的更优雅。当时项目太赶,一直没来得及该,后期我会优化的
代码片段.png
对应代码片段54行.png
- 将allMenu这个tree转成一维数组
- 通过menuKeys跟这个一位数组比对
- 将比对结果转成tree
代码实现如下
import router from '@/router'
import store from '@/store'
import { post, formDataPost } from '@/api/http'
import { Message } from 'element-ui'
/**
* 路由守卫
* api /menu/getAllMenuInfo 查询所有菜单
*/
router.beforeEach((to, from, next) => {
// 如果store中没有token,则证明当前用户没有登录过
if (!store.state.token.length) {
// 如果路由刚好在登录页面上,则需要next掉,不然会死循环
if (to.path === '/login') {
next()
} else {
// 跳转到登录页
next('/login')
}
} else {
// 如果路由刚好在登录页上,则跳转到 / 路径上
if (to.path === '/login') {
return next('/')
} else {
// 创建动态路由
// 不加这个判断页面会死循环
if (!store.state.authorFlag) {
// flag变成true
store.dispatch('authorFlag', {
flag: true
})
// 如果sessionStorage没有router字段
if (!sessionStorage.getItem('router')) {
// 1.api接口 /menu/getAllMenuInfo 查询所有菜单
post('/menu/getAllMenuInfo', {}).then(res => {
// 获取所有菜单
const treeMenuList = res.data
// 校验菜单
if (!treeMenuList.length) {
return Message.error('菜单为空!')
}
// 2 api接口 /roleMenu/getRoleMenuByUserId 根据用户ID查询该用户有权限的菜单
formDataPost('/roleMenu/getRoleMenuByUserId', {
userId: store.state.userId
}).then(res => {
// 获取权限菜单id
const menuKeys = JSON.parse(res.data.menuId).menuCheckedKeys
// 3.将menuList转为一维数组
const arrayMenuList = treeToArray(treeMenuList)
// 4.通过主键id进行过滤
const filterArrayMenuList = filterArrayByMenuId(arrayMenuList, menuKeys)
// 5.将过滤好的一位数组转成tree
const filterTreeMenuList = arrayToTree(filterArrayMenuList, '0')
// 6.将处理好的tree转成vue-router数据格式
const resultData = transformHttpDataToVueRouterData(filterTreeMenuList)
// 7.将数据存到sessionStorage中
sessionStorage.setItem('router', JSON.stringify(resultData))
// 8.派发到store中
store.dispatch('menuList', {
menuList: resultData
})
// 9.创建动态路由
routerGo(to, next)
router.replace({ name: resultData[0].name })
})
})
} else {
// 如果sessionStorage有router字段,则直接创建路由
routerGo(to, next)
}
} else {
next()
}
}
}
})
/**
* 添加路由
* @param to
* @param next
*/
function routerGo (to, next) {
// 获取sessionStorage中的router字段,并序列化
const routerData = JSON.parse(sessionStorage.getItem('router'))
// layout组件是主页布局文件,需要手动引入
const asyncRouter = [
{
name: 'Layout',
path: '/layout',
component: () => import('@/component/layout/Layout.vue'),
children: []
}
]
// 通过componentPath创建component组件
asyncRouter[0].children = transformVueRouterDataToVueRouterComponent(routerData)
// 在路由末尾添加404
asyncRouter.push({
path: '*',
name: 'notFount',
component: () => import('@/view/not-fount/NotFound.vue')
})
// 添加动态路由
router.addRoutes(asyncRouter)
next({ ...to, replace: true })
}
/**
* 接口列表格式转换成满足vue-router的对应字段
* @param data
* @param array
* @param str
* @returns {Array}
*/
function transformHttpDataToVueRouterData (data, array = [], str = '/') {
data.forEach((item, index) => {
array.push({
menuId: item.menuId,
label: item.menuName,
path: item.menuUrl,
name: toCamel(item.menuUrl),
icon: item.menuIcon,
// 这个字段是用来做浏览器地址链接有用的
componentPath: `${str}${item.menuUrl}`,
children: []
})
if (item.subMenuList && item.subMenuList.length) {
array[index].redirect = {
name: toCamel(item.subMenuList[0].menuUrl)
}
transformHttpDataToVueRouterData(item.subMenuList, array[index].children, `${array[index].componentPath}/`)
} else {
array[index].redirect = null
}
})
return array
}
/**
* 将component字段转成component组件
* @param root
* @returns {*}
*/
function transformVueRouterDataToVueRouterComponent (root) {
root.forEach((item) => {
let path = item.componentPath
path = path + '/' + toCamel(path.substring(path.lastIndexOf('/') + 1))
// 因为webpack引入import机制的问题。全部转成变量不能解析
item.component = () => import(`./view${path}.vue`)
if (item.children && item.children.length) {
transformVueRouterDataToVueRouterComponent(item.children)
}
})
return root
}
/**
* 中划线命名转大驼峰命名
* @param str
* @returns {*}
*/
function toCamel (str) {
str = str.replace(/(\w)/, (match, $1) => `${$1.toUpperCase()}`)
while (str.match(/\w-\w/)) {
str = str.replace(/(\w)(-)(\w)/, (match, $1, $2, $3) => `${$1}${$3.toUpperCase()}`)
}
return str
}
/**
* 树转数组
* @param data
*/
function treeToArray (data) {
const result = []
data.forEach(item => {
function loop (data) {
result.push({
menuId: data.menuId,
parentId: data.parentId,
menuName: data.menuName,
menuIcon: data.menuIcon,
menuUrl: data.menuUrl,
menuSort: data.menuSort,
createDate: data.createDate,
createPersonId: data.createPersonId,
updateDate: data.updateDate,
updatePersonId: data.updatePersonId,
logicDelete: data.logicDelete
})
let child = data.subMenuList
if (child) {
for (let i = 0; i < child.length; i++) {
loop(child[i])
}
}
}
loop(item)
})
return result
}
/**
* 数组转树
* @param arrayList
* @param parentId
*/
function arrayToTree (arrayList, parentId) {
let len = arrayList.length
function loop (parentId) {
let res = []
for (let i = 0; i < len; i++) {
let item = arrayList[i]
if (item.parentId === parentId) {
item.subMenuList = loop(item.menuId)
res.push(item)
}
}
return res
}
return loop(parentId)
}
/**
* 过滤数组
* @param list
* @param keys
*/
function filterArrayByMenuId (list, keys) {
let arr = []
list.forEach(item => {
let id = item.menuId
keys.forEach(cItem => {
if (id === cItem) {
arr.push(item)
return false
}
})
})
return arr
}
问题点说明
先用超级管理员登录,然后退出登录。这个时候router已经被创建了一次
如果再用另外一个账号A登录,这个时候会提示rouer被创建过了

而且原来超级管理员的所属的菜单依然存在,这样是不安全的。而我的决绝方案如下

只要路由是在登录的页面,我就强制的刷新以此页面,由于是单页面应用。这个时候会 重新执行main.js中的代码,路由也就重新被刷新了。如果有更好的方案,请一定告知!谢谢!!
项目部分截图



网友评论