vue-element-admin动态菜单改造
vue-element-admin (opens new window) 是一款优秀后台前端解决方案,它基于 vue 和 element-ui实现。Boot-admin (opens new window)的前端模块就是基于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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
在 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# 第2步.服务端接口定义
服务端接口返回数据格式如下:
@Data
public class MenuDTO {
private String id;
private String srvName;
private Boolean show;
private String accessControlStyle;
}
1
2
3
4
5
6
7
2
3
4
5
6
7
节点中 srvName 和前端的路由进行匹配,通过 show 属性来确定显示或隐藏。 服务端无需关心菜单的子/父级关系,只需要将所有的菜单信息输出一个数组即可。
@GetMapping("/auth/user/menu")
public List<MenuDTO> getMenus() throws Exception{
BaseUser baseUser = UserTool.getBaseUser();
List<MenuDTO> menuDTOList = resourceDataGetter.getMyselfMenuList(baseUser);
return menuDTOList;
}
1
2
3
4
5
6
2
3
4
5
6
# 第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 }
})
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 第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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 第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()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
关键代码:
// 在完成登录获取到用户信息后,开始从获取菜单
await store.dispatch('menu/getMenus')
// 动态路由生成
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加动态路由
router.addRoutes(accessRoutes)
1
2
3
4
5
6
2
3
4
5
6
# 改造完成,效果如下:
编辑 (opens new window)
上次更新: 2024/04/18