基于权限表的动态菜单生成指南
权限表结构设计
CREATE TABLE permissions (
permission_id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_id BIGINT DEFAULT 0 COMMENT '父权限ID',
perm_name VARCHAR(50) NOT NULL COMMENT '权限名称',
perm_code VARCHAR(100) NOT NULL UNIQUE COMMENT '权限编码',
perm_type TINYINT DEFAULT 1 COMMENT '1:菜单,2:按钮,3:接口',
path VARCHAR(200) COMMENT '前端路由路径',
component VARCHAR(200) COMMENT '前端组件路径',
icon VARCHAR(50) COMMENT '图标',
sort_order INT DEFAULT 0 COMMENT '排序',
hidden TINYINT DEFAULT 0 COMMENT '是否隐藏 0:显示 1:隐藏',
status TINYINT DEFAULT 1 COMMENT '1:启用,0:禁用'
);
菜单数据示例
INSERT INTO permissions
(parent_id, perm_name, perm_code, perm_type, path, component, icon, sort_order)
VALUES
(0, '系统管理', 'system', 1, '/system', 'Layout', 'setting', 1),
(0, '用户中心', 'user', 1, '/user', 'Layout', 'user', 2),
(1, '用户管理', 'system:user', 1, '/system/user', 'system/user/index', 'user', 1),
(1, '角色管理', 'system:role', 1, '/system/role', 'system/role/index', 'peoples', 2),
(1, '菜单管理', 'system:menu', 1, '/system/menu', 'system/menu/index', 'tree-table', 3),
(2, '个人信息', 'user:profile', 1, '/user/profile', 'user/profile/index', 'user', 1),
(2, '修改密码', 'user:password', 1, '/user/password', 'user/password/index', 'password', 2);
后端实现
1. 查询用户菜单
public List<Menu> getUserMenus(Long userId) {
String sql = """
SELECT DISTINCT p.*
FROM permissions p
JOIN role_permissions rp ON p.permission_id = rp.permission_id
JOIN user_roles ur ON rp.role_id = ur.role_id
WHERE ur.user_id = ?
AND p.perm_type = 1
AND p.status = 1
ORDER BY p.sort_order
""";
List<Permission> permissions = jdbcTemplate.query(sql, new Object[]{userId}, permissionRowMapper);
return buildMenuTree(permissions);
}
private List<Menu> buildMenuTree(List<Permission> permissions) {
List<Menu> menus = new ArrayList<>();
Map<Long, Menu> menuMap = new HashMap<>();
for (Permission p : permissions) {
Menu menu = new Menu();
menu.setId(p.getPermissionId());
menu.setParentId(p.getParentId());
menu.setName(p.getPermName());
menu.setPath(p.getPath());
menu.setComponent(p.getComponent());
menu.setIcon(p.getIcon());
menuMap.put(menu.getId(), menu);
}
for (Menu menu : menuMap.values()) {
if (menu.getParentId() == 0) {
menus.add(menu);
} else {
Menu parentMenu = menuMap.get(menu.getParentId());
if (parentMenu != null) {
parentMenu.getChildren().add(menu);
}
}
}
return menus;
}
2. 菜单对象定义
public class Menu {
private Long id;
private Long parentId;
private String name;
private String path;
private String component;
private String icon;
private List<Menu> children = new ArrayList<>();
}
前端实现
1. Vue路由配置
function generateRoutes(menus) {
return menus.map(menu => {
const route = {
path: menu.path,
name: menu.name,
component: loadComponent(menu.component),
meta: {
title: menu.name,
icon: menu.icon
}
}
if (menu.children && menu.children.length > 0) {
route.children = generateRoutes(menu.children)
}
return route
})
}
function loadComponent(component) {
if (component === 'Layout') {
return Layout
}
return () => import(`@/views/${component}.vue`)
}
2. 菜单渲染组件
<!-- Menu.vue -->
<template>
<el-menu :default-active="activeMenu" :collapse="isCollapse">
<template v-for="menu in menus" :key="menu.id">
<!-- 有子菜单 -->
<el-sub-menu v-if="menu.children && menu.children.length" :index="menu.path">
<template #title>
<el-icon><component :is="menu.icon" /></el-icon>
<span>{{ menu.name }}</span>
</template>
<menu-item v-for="child in menu.children"
:key="child.id"
:menu="child" />
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item v-else :index="menu.path">
<el-icon><component :is="menu.icon" /></el-icon>
<span>{{ menu.name }}</span>
</el-menu-item>
</template>
</el-menu>
</template>
<script>
export default {
name: 'Menu',
props: {
menus: {
type: Array,
required: true
}
},
computed: {
activeMenu() {
return this.$route.path
},
isCollapse() {
return this.$store.state.app.sidebar.collapsed
}
}
}
</script>
使用示例
1. 获取菜单数据
const state = {
menus: []
}
const mutations = {
SET_MENUS: (state, menus) => {
state.menus = menus
}
}
const actions = {
async getUserMenus({ commit }) {
const response = await getUserMenus()
commit('SET_MENUS', response.data)
return response.data
}
}
2. 应用菜单
router.beforeEach(async (to, from, next) => {
if (hasToken) {
if (!store.state.user.menus.length) {
const menus = await store.dispatch('getUserMenus')
const routes = generateRoutes(menus)
routes.forEach(route => {
router.addRoute(route)
})
next({ ...to, replace: true })
} else {
next()
}
}
})
最佳实践建议
1. 性能优化
- 使用Redis缓存菜单树
- 按需加载组件
- 合理使用菜单层级
2. 安全考虑
- 前后端同时验证权限
- 定期清理无效菜单
- 记录菜单访问日志
3. 扩展功能
- 支持菜单拖拽排序
- 实现菜单搜索功能
- 添加菜单收藏功能
- 支持多主题切换