引用博客:Vue + Spring Boot 项目实战(十五):动态加载后台菜单
不同用户登录后菜单显示不同的实现,需要同时结合前端和后端。
后端主要实现:1. 数据库设计用户可以访问的菜单列表。2. 接收url请求,查询数据库返回允许的菜单列表。
前端主要实现:1. Vuex Store添加菜单数组,保存允许访问的菜单项。2. 配置路由,包括Router入口、利用前置守卫添加菜单项路由。 3. 编写前端界面。
后端:
- 数据库设计用户可以访问的菜单列表:
访问控制采用RBAC,数据库涉及的表包括用户表user、角色表role、菜单表menu、用户-角色映射表user_role、角色-菜单映射表role_menu。
sql文件链接是:blog.sql
这5个表的表属性及内容截图:
- 后端接收url请求,查询数据库返回允许的菜单列表。
需要设计menuService
以及menuController
,当接收"api/menu"
请求后,从数据库中查询当前用户可以访问菜单列表,并返回给前端。
注意:本文使用的ORM框架为Mybatis-Plus,相关CRUD请访问官网:CRUD 接口、条件构造器
IMenuService:1
2
3public interface IMenuService extends IService<Menu> {
public List<Menu> getMenuByCurrentUser();
}
menuServiceImpl:
代码逻辑是:先根据当前用户查询对应的角色,再根据角色查询允许的菜单项。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
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
IUserService userService;
IUserRoleService userRoleService;
IRoleMenuService roleMenuService;
public List<Menu> getMenuByCurrentUser() {
// 获取当前用户
String username = SecurityUtils.getSubject().getPrincipal().toString();
User user = userService.getUser(username);
System.out.println("CurrentUser:" + username);
// 查询UserRole表, 找到用户对应的role id列表
LambdaQueryWrapper<UserRole> query = Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUid, user.getId());
List<Integer> rids = userRoleService.list(query).stream().map(UserRole::getRid).collect(Collectors.toList());
// 找出这些角色对应的菜单项
LambdaQueryWrapper<RoleMenu> query2 = Wrappers.<RoleMenu>lambdaQuery().in(RoleMenu::getRid, rids);
List<Integer> menuIds = roleMenuService.list(query2).stream().map(RoleMenu::getMid).collect(Collectors.toList());
List<Menu> menus = listByIds(menuIds).stream().distinct().collect(Collectors.toList());
//处理菜单项的结构
handleMenus(menus);
return menus;
}
public void handleMenus(List<Menu> menus) {
menus.forEach(menu -> {
// LambdaQueryWrapper<Menu> query = Wrappers.<Menu>lambdaQuery().eq(Menu::getParentId, menu.getId());
// List<Menu> children = list(query);
List<Menu> children = menus.stream().filter(m -> m.getParentId() == menu.getId()).collect(Collectors.toList());
menu.setChildren(children);
});
// 只是移除显示上的层次关系,但内部多级层次关系并没有删除
menus.removeIf(m -> m.getParentId() != 0);
}
}
menuController:1
2
3
4
5
6
7
8
9
10
11
public class MenuController {
IMenuService menuService;
public List<Menu> menu() {
return menuService.getMenuByCurrentUser();
}
}
前端:
- Vuex Store添加菜单数组,保存允许访问的菜单项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23export default new Vuex.Store({
state: {
user: {
username: window.localStorage.getItem('user' || '[]') == null ? '' : JSON.parse(window.localStorage.getItem('user' || '[]')).username
},
// 新增的用来保存可访问菜单项的数组
adminMenus: []
},
mutations: {
login (state, user) {
state.user = {username: user.username}
window.localStorage.setItem('user', JSON.stringify(user))
},
logout (state) {
state.user = []
window.localStorage.removeItem('user')
},
// 新增的菜单数组驱动
initMenu (state, menus) {
state.adminMenus = menus
}
}
}) - 配置路由,包括Router入口、利用前置守卫添加菜单项路由
- 首先配置
router
下的index.js
,新增'/admin'
路由,作为展示菜单界面的入口。
router/index.js:1
2
3
4
5
6
7
8{
path: '/admin',
name: 'Admin',
component: AdminIndex,
meta: {
requireAuth: true
}
}- 利用Vue-Router前置守卫,在真正发出url请求之前初始话菜单,包括1. 将后端返回的菜单项path添加到路由,2. 更新store的adminMenus。这部分代码是在main.js中书写。
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
75router.beforeEach((to, from, next) => {
if (store.state.user.username && to.path.startsWith('/admin')) {
// console.log('initMenu')
initMenu(router, store)
}
// 已登录状态下访问login直接跳转到后台首页
if (store.state.user.username && to.path.startsWith('/login')) {
next({
path: 'admin/dashboard'
})
}
// 登录部分
if (to.meta.requireAuth) {
// console.log(store.state.user.username)
if (store.state.user) {
axios.get('/authentication')
.then(resp => {
if (resp.data) next()
// resp.data为空代表后端拦截器判断是未认证、未RememberMe,但这时候依然有resp
else {
next({
path: 'login',
// path后缀, 以path?xxx=yyy附加拼接, redirect代表拼接字符xxx, 可以自定义
// 该URL=login?redirect=%2Findex
query: {redirect: to.fullPath}
})
}
})
} else {
next({
path: 'login',
query: {redirect: to.fullPath}
})
}
} else {
next()
}
})
//初始化菜单
const initMenu = (router, store) => {
if (store.state.adminMenus.length > 0) {
return
}
axios.get('/menu')
.then(resp => {
if (resp && resp.status === 200) {
// 把后端返回的菜单列表进行拼接
var fmtRoutes = formatRoutes(resp.data)
// 并添加到Router
router.addRoutes(fmtRoutes)
store.commit('initMenu', fmtRoutes)
}
})
}
//拼接菜单项路由
const formatRoutes = (routes) => {
let fmtRoutes = []
routes.forEach(route => {
if (route.children) {
route.children = formatRoutes(route.children)
}
let fmtRoute = {
path: route.path,
component: resolve => {
require(['./components/administration/' + route.component + '.vue'], resolve)
},
name: route.name,
nameZh: route.nameZh,
iconCls: route.iconCls,
children: route.children
}
fmtRoutes.push(fmtRoute)
})
return fmtRoutes
}
- 利用Vue-Router前置守卫,在真正发出url请求之前初始话菜单,包括1. 将后端返回的菜单项path添加到路由,2. 更新store的adminMenus。这部分代码是在main.js中书写。
- 首先配置
- 编写前端界面。
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<template>
<div>
<el-menu
:default-active="currentPath"
class="el-menu-admin"
router
mode="vertical"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
:collapse="isCollapse">
<div style="height: 80px;"></div>
<!--index 没有用但是必需字段且为 string -->
<el-submenu v-for="(item,i) in adminMenus" :key="i" :index="(i).toString()" style="text-align: left">
<span slot="title" style="font-size: 17px;">
<i :class="item.iconCls"></i>
{{ item.nameZh }}
</span>
<el-menu-item v-for="child in item.children" :key="child.path" :index="child.path">
<i :class="child.icon"></i>
{{ child.nameZh }}
</el-menu-item>
</el-submenu>
</el-menu>
</div>
</template>
<script>
export default {
name: 'AdminMenu',
data () {
return {
isCollapse: false
}
},
computed: {
adminMenus () {
return this.$store.state.adminMenus
},
currentPath () {
return this.$route.path
}
}
}
</script>
<style scoped>
.el-menu-admin {
border-radius: 5px;
height: 100%;
}
</style>
总结步骤:1. 后端设计数据库。2. 后端设计service和controller,接收请求返回当前用户允许的菜单列表。3. 前端设计Vuex.store,新添菜单数组。4. 前端设计Router,包括router/index.js
入口路由、main.js
中动态添加菜单项路由。 5. 编写Vue组件AdminMenu.vue
。