vue-element-admin 动态菜单改造
vue-element-admin 是一款优秀后台前端解决方案,它基于 vue 和 element-ui实现。开源后台管理系统解决方案项目 Boot-admin的前端模块就是基于vue-element-admin开发而来。
作为一款纯前端的后台界面解决方案,vue-element-admin是通过遍历路由进行渲染,从而得到菜单列表的,我们可以在 router.js 中看到相关代码,即是路由也是菜单。
改造思路:实现前后端分离要求,服务端控制菜单是否显示,前端控制路由信息定义。前端开发时不需要找服务端来新增路由信息,后端不需要关心前端路由的父/子关系、图标等定义信息。
第1步.定义路由
在 src/router/index.js 中将不需要后台控制的路由定义在 constantRoutes 中,如 /login /404 等;而需要后台控制是否显示的路由定义在 asyncRoutes 中。asyncRoutes 中每个节点都添加 srvName 属性,通过它来和服务端返回的菜单信息进行关联。
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/* 外部路由文件 */
import sysManageRouter from './modules/sysmanage.js'
import codeGeneratorRouter from './modules/codegenerator.js'
import myWorkRouter from './modules/mywork.js'
/**
* 同步路由
* 不需要后台权限控制的路由,所有角色均可操作
*/
export const constantRoutes = [{
path: '/redirect',
component: Layout,
hidden: true,
children: [{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index')
}]
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/auth-redirect',
component: () => import('@/views/login/auth-redirect'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error-page/401'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
component: () => import('@/views/dashboard/index'),
name: 'Dashboard',
meta: {
title: '仪表板',
icon: 'dashboard',
affix: true
}
}]
},
]
/**
* 异步路由
* 基于后台动态控制的路由
*/
export const asyncRoutes = [
/** 引入系统管理路由模块 **/
sysManageRouter,
/** 引入代码生成路由模块 **/
codeGeneratorRouter,
/** 引入工作流路由模块 **/
myWorkRouter,
// 404 page must be placed at the end !!!
{
path: '*',
redirect: '/404',
hidden: true
}
]
const createRouter = () => new Router({
scrollBehavior: () => ({
y: 0
}),
routes: constantRoutes
})
const router = createRouter()
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
在 src/router/modules 目录下,新建路由子模块文件
- 系统管理 sysmanage.js
- 代码生成 codegenerator.js
- 工作流 mywork.js
sysmanage.js内容如下:
import Layout from '@/layout'
const sysManageRouter = {
path: '/manage',
name: 'SysManage',
component: Layout,
redirect: '/manage/basemanage/dictionary',
srvName: '/api/system/auth/manage',
meta: {
title: '系统管理',
icon: 'example'
},
children: [{
path: 'basemanage',
name: 'BaseManage',
srvName: '/api/system/auth/manage/basemanage',
component: () => import('@/views/manage/basemanage/index'),
redirect: '/manage/basemanage/dictionary',
meta: {
title: '基础管理',
icon: 'tree'
},
children: [{
path: 'dictionary',
name: 'DicManage',
srvName: '/api/system/auth/manage/basemanage/dictionary',
component: () => import('@/views/manage/basemanage/dictionary/index'),
meta: {
title: '字典管理',
icon: 'tree'
}
},
{
path: 'region',
name: 'DivManage',
srvName: '/api/system/auth/manage/basemanage/region',
component: () => import('@/views/manage/basemanage/region/index'),
meta: {
title: '区域管理',
icon: 'tree'
}
},
{
path: 'organization',
name: 'OrgManage',
srvName: '/api/system/auth/manage/basemanage/organization',
component: () => import('@/views/manage/basemanage/organization/index'),
meta: {
title: '组织管理',
icon: 'tree'
}
},
{
path: 'employee',
name: 'EmpManage',
srvName: '/api/system/auth/manage/basemanage/employee',
component: () => import('@/views/manage/basemanage/employee/index'),
meta: {
title: '人员管理',
icon: 'tree'
}
}
]
},
{
path: 'authmanage',
name: 'AuthManage',
srvName: '/api/system/auth/manage/authmanage',
component: () => import('@/views/manage/authmanage/index'),
redirect: '/manage/authmanage/menu',
meta: {
title: '权限管理',
icon: 'tree'
},
children: [{
path: 'menu',
name: 'MenuManage',
srvName: '/api/system/auth/manage/authmanage/menu',
component: () => import('@/views/manage/authmanage/menu'),
meta: {
title: '菜单管理',
icon: 'table'
}
}, {
path: 'resource',
name: 'ResourceManage',
srvName: '/api/system/auth/manage/authmanage/resource',
component: () => import('@/views/manage/authmanage/resource'),
meta: {
title: '功能管理',
icon: 'table'
}
}, {
path: 'user',
name: 'UserManage',
srvName: '/api/system/auth/manage/authmanage/user',
component: () => import('@/views/manage/authmanage/user'),
meta: {
title: '用户管理',
icon: 'tree'
}
},
{
path: 'role',
name: 'RoleManage',
srvName: '/api/system/auth/manage/authmanage/role',
component: () => import('@/views/manage/authmanage/role'),
meta: {
title: '角色管理',
icon: 'tree'
}
}, {
path: 'userofrole',
name: 'UserOfRoleManage',
srvName: '/api/system/auth/manage/authmanagele/userofrole',
component: () => import('@/views/manage/authmanage/userofrole'),
meta: {
title: '角色-用户',
icon: 'tree'
}
}, {
path: 'resourceofrole',
name: 'ResourceOfRoleManage',
srvName: '/api/system/auth/manage/authmanage/resourceofrole',
component: () => import('@/views/manage/authmanage/resourceofrole/index'),
meta: {
title: '角色-功能',
icon: 'tree'
}
}
]
},
{
path: 'operationmanage',
name: 'OperationManage',
srvName: '/api/system/auth/manage/operationmanage',
component: () => import('@/views/manage/operationmanage/index'),
meta: {
title: '运行管理',
icon: 'tree'
},
children: [{
path: 'online',
name: 'OnlineManage',
srvName: '/api/system/auth/manage/operationmanage/online',
component: () => import('@/views/manage/operationmanage/online/index'),
meta: {
title: '在线用户',
icon: 'tree'
}
},
{
path: 'job',
name: 'JobManage',
srvName: '/api/system/auth/manage/operationmanage/job',
component: () => import('@/views/manage/operationmanage/job/index'),
meta: {
title: '定时任务',
icon: 'tree'
}
},
{
path: 'task',
name: 'TaskManage',
srvName: '/api/system/auth/manage/operationmanage/task',
component: () => import('@/views/manage/operationmanage/task/index'),
meta: {
title: '流程任务',
icon: 'tree'
}
},
{
path: 'histask',
name: 'HisTaskManage',
srvName: '/api/system/auth/manage/operationmanage/task/his',
component: () => import('@/views/manage/operationmanage/histask/index'),
meta: {
title: '历史任务',
icon: 'tree'
}
},
{
path: 'log',
name: 'LogManage',
srvName: '/api/system/auth/manage/operationmanage/log',
component: () => import('@/views/manage/operationmanage/log/index'),
meta: {
title: '系统日志',
icon: 'tree'
}
},
{
path: 'nacos',
name: 'nacos',
srvName: '/api/system/auth/manage/operationmanage/nacos',
component: () => import('@/views/manage/operationmanage/nacos/index'),
meta: {
title: 'Nacos',
icon: 'link'
}
},
{
path: 'admin',
name: 'admin',
srvName: '/api/system/auth/manage/operationmanage/admin',
component: () => import('@/views/manage/operationmanage/admin/index'),
meta: {
title: 'Admin',
icon: 'link'
}
}
]
},
{
path: 'definitionmanage',
name: 'DefManage',
srvName: '/api/system/auth/manage/definitionmanage',
component: () => import('@/views/manage/definitionmanage/index'),
meta: {
title: '定义管理',
icon: 'tree'
},
children: [
{
path: 'model',
name: 'ModelManage',
srvName: '/api/system/auth/manage/definitionmanage/model',
component: () => import('@/views/manage/definitionmanage/model/index'),
meta: {
title: '模型管理',
icon: 'tree'
}
},
{
path: 'process',
name: 'ProcessManage',
srvName: '/api/system/auth/manage/definitionmanage/process',
component: () => import('@/views/manage/definitionmanage/process/index'),
meta: {
title: '流程管理',
icon: 'tree'
}
},
{
path: 'drools',
name: 'DroolsManage',
srvName: '/api/system/auth/manage/definitionmanage/drools',
component: () => import('@/views/manage/definitionmanage/drools/index'),
meta: {
title: '规则管理',
icon: 'tree'
}
}
]
},
{
path: 'datamaintain',
name: 'DataMaintain',
srvName: '/api/system/auth/manage/datamaintain',
component: () => import('@/views/manage/datamaintain/index'),
meta: {
title: '数据处理',
icon: 'tree'
},
children: [{
path: 'sqlinput',
name: 'SqlInput',
srvName: '/api/system/auth/manage/datamaintain/sqlinput',
component: () => import('@/views/manage/datamaintain/sqlinput/index'),
meta: {
title: '提交',
icon: 'tree'
}
}, {
path: 'sqlexec',
name: 'sqlexec',
srvName: '/api/system/auth/manage/datamaintain/sqlexec',
component: () => import('@/views/manage/datamaintain/sqlexec/index'),
meta: {
title: '执行',
icon: 'tree'
}
}]
}
]
}
export default sysManageRouter
第2步.服务端接口定义
服务端接口返回数据格式如下:
@Data
public class MenuDTO {
private String id;
private String srvName;
private Boolean show;
private String accessControlStyle;
}
节点中 srvName 和前端的路由进行匹配,通过 show 属性来确定显示或隐藏。
服务端无需关心菜单的子/父级关系,只需要将所有的菜单信息输出一个数组即可。
@GetMapping("/auth/user/menu")
public List<MenuDTO> getMenus() throws Exception{
BaseUser baseUser = UserTool.getBaseUser();
List<MenuDTO> menuDTOList = resourceDataGetter.getMyselfMenuList(baseUser);
return menuDTOList;
}
第3步.定义 api 请求接口
在 src/api/ 目录下创建 menus.js
import request from '@/utils/request'
export function getMenus(token) {
return request({
url: '/api/system/auth/user/menu',
method: 'get',
params: { token }
})
}
第4步.配置 store 调用
新增文件 src/store/modules/menus.js
import {
Message
} from 'element-ui'
import {
getMenus
} from '@/api/menu'
import {
getToken
} from '@/utils/auth'
import {
asyncRoutes
} from '@/router/index'
const getDefaultState = () => {
return {
token: getToken(),
menuList: []
}
}
const state = getDefaultState()
const mutations = {
SET_MENUS: (state, menus) => {
state.menuList = menus
}
}
// 动态菜单定义在前端,后台只会返回有权限的菜单列表,通过遍历服务端的菜单数据,没有的将对于菜单进行隐藏,前端新增页面无需先通过服务端进行菜单添加,遵循了前后端分离原则
export function generaMenu(routes, srvMenus) {
for (let i = 0; i < routes.length; i++) {
const routeItem = routes[i]
var showItem = false
for (let j = 0; j < srvMenus.length; j++) {
const srvItem = srvMenus[j]
// 前后端数据通过 srvName 属性来匹配
if (routeItem.srvName !== undefined && routeItem.srvName === srvItem.srvName && srvItem.show === true) {
showItem = true
routes[i]['hidden'] = false
break
}
}
if (showItem === false) {
routes[i]['hidden'] = true
}
if (routeItem['children'] !== undefined && routeItem['children'].length > 0) {
generaMenu(routes[i]['children'], srvMenus)
}
}
}
const actions = {
getMenus({
commit
}) {
return new Promise((resolve, reject) => {
getMenus(state.token).then(response => {
if (response.code !== 100) {
Message({
message: response.message,
type: 'error',
duration: 5 * 1000
})
reject(response.message)
}
const {
data
} = response
if (!data) {
reject('Verification failed, please Login again.')
}
const srvMenus = data
var pushRouter = asyncRoutes
generaMenu(pushRouter, srvMenus)
commit('SET_MENUS', pushRouter)
resolve()
}).catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
第5步.修改路由钩子,渲染动态菜单
修改src/permission.js文件
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
} else {
// determine whether the user has obtained his permission roles through getInfo
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
const { roles } = await store.dispatch('user/getInfo')
// 获取菜单
await store.dispatch('menu/getMenus')
// 生成动态路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加动态路由
router.addRoutes(accessRoutes)
next({ ...to, replace: true })
} catch (error) {
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
关键代码:
// 在完成登录获取到用户信息后,开始从获取菜单
await store.dispatch('menu/getMenus')
// 生成动态路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加动态路由
router.addRoutes(accessRoutes)