摘要:系统如何管理它自己的菜单?这是一个经典的"先有鸡还是先有蛋"的问题。本文将介绍如何通过数据库初始化解决冷启动问题,并深入讲解如何利用 Element Plus 的树形表格展示无限层级数据,以及如何实现"目录、菜单、按钮"三种类型的动态表单交互。
一、 引言:先有鸡还是先有蛋?
在前面的文章中,我们实现了动态路由,前端的菜单是根据数据库里的 sys_menus 表自动生成的。
现在问题来了:我们想要在页面上通过图形化界面来新增、修改菜单,但是数据库里如果没有"菜单管理"这个页面,我们就进不去这个功能。这就是典型的系统**自举(Bootstrapping)**问题。
本篇的核心任务:
- 数据初始化:手动 SQL 插入第一条数据,打破僵局。
- 树形表格:不用递归组件,利用 el-table 原生特性展示树数据。
- 动态表单:根据是"目录"还是"按钮",动态控制表单项的显示与校验。
二、 数据库与初始化:打破僵局
在开发任何 CRUD 功能之前,先确保数据库结构支持我们的业务需求(如缓存控制、显隐控制)。
. 表结构确认
除了基础的 path 和 component,我们需要重点关注 菜单类型 type:
- M (Directory) : 目录。有路由地址,无组件路径(通常是 Layout),不显示在内容区,只展开子菜单。
- C (Menu) : 菜单。有路由,有组件,是真正的页面。
- F (Button) : 按钮。无路由,无组件,只用于控制页面内的按钮权限(如 system:user:add)。
2. 初始化 SQL (种子数据)
我们需要手动插入"菜单管理"本身,并把它分配给超级管理员。
sql
-- 1. 插入菜单管理页面 (作为系统管理的子菜单)
-- 假设系统管理 ID=1
INSERT INTO `sys_menus`
(`parent_id`, `menu_name`, `path`, `component`, `perms`, `icon`, `type`, `hidden`, `sort`)
VALUES
(1, '菜单管理', 'menu', 'system/menu/index', 'system:menu:list', 'list', 'C', 0, 3);
-- 2. 给角色分配权限 (Role ID = 1)
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`)
VALUES (1, (SELECT id FROM sys_menus WHERE path='menu' LIMIT 1));
执行完这条 SQL,刷新页面,侧边栏就会神奇地出现"菜单管理"了。
三、 后端实现:逻辑校验与树形组装
后端除了常规的 CRUD,主要有两个逻辑点:列表转树 和 删除保护。
1. 列表接口 (GET /menu/list)
js
// routes/menu.js
router.get('/list', authMiddleware, async (req, res, next) => {
try {
const { menuName, status } = req.query
let sql = 'SELECT * FROM sys_menus WHERE 1=1'
let params = []
// ... 拼接查询条件 ...
const [rows] = await pool.query(sql, params)
// 【关键】将扁平数组转换为树形结构
// 这里的 buildTree 与之前动态路由使用的是同一个辅助函数
const list = buildTree(JSON.parse(JSON.stringify(rows)))
res.json({ code: 200, data: list })
} catch (err) { next(err) }
})
四、 前端实现:树形表格与动态表单
1. 树形表格 (el-table)
Element Plus 的 Table 组件自带树形展示功能,只需配置 row-key 和 tree-props。
vue
<el-table
:data="menuList"
row-key="id"
border
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="menu_name" label="菜单名称" width="160" />
<!-- 动态图标展示 -->
<el-table-column prop="icon" label="图标" align="center" width="60">
<template #default="{ row }">
<el-icon v-if="row.icon">
<component :is="row.icon" />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="perms" label="权限标识" />
<el-table-column prop="component" label="组件路径" />
<!-- 状态展示 -->
<el-table-column prop="hidden" label="状态" width="80">
<template #default="{ row }">
<el-tag v-if="row.hidden === 0" type="success">显示</el-tag>
<el-tag v-else type="info">隐藏</el-tag>
</template>
</el-table-column>
</el-table>
- row-key="id" : 告诉表格每行的唯一标识,这是折叠展开的关键。
- tree-props: 告诉表格子节点在哪个字段里(默认是 children)。
2. 动态表单交互 (核心亮点)
在"新增/修改"弹窗中,我们需要根据 type 的不同,动态控制表单项的显示/隐藏 和校验规则。
- 目录 (M) : 需要路由地址,不需要组件路径。
- 菜单 (C) : 需要路由地址 + 组件路径。
- 按钮 (F) : 只需要权限字符,不需要路由和组件。
vue
<el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
<!-- 上级菜单选择:使用 TreeSelect 组件 -->
<el-form-item label="上级菜单">
<el-tree-select
v-model="form.parentId"
:data="menuOptions"
:props="{ value: 'id', label: 'label', children: 'children' }"
check-strictly
/>
</el-form-item>
<!-- 类型切换 -->
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio value="M">目录</el-radio>
<el-radio value="C">菜单</el-radio>
<el-radio value="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
<!-- 动态逻辑 v-if -->
<el-form-item label="路由地址" prop="path" v-if="form.menuType !== 'F'">
<el-input v-model="form.path" />
</el-form-item>
<el-form-item label="组件路径" prop="component" v-if="form.menuType === 'C'">
<el-input v-model="form.component" placeholder="views下的路径" />
</el-form-item>
<el-form-item label="权限字符" v-if="form.menuType !== 'M'">
<el-input v-model="form.perms" placeholder="system:user:add" />
</el-form-item>
<!-- ... 其他通用项 ... -->
</el-form>

3. 构建上级菜单下拉树
在选择"上级菜单"时,我们需要展示一个树形下拉框。这里有个小技巧:我们需要在数据顶层手动添加一个"主类目"节点,否则用户无法将菜单创建为一级菜单。
js
const getTreeselect = async () => {
const res = await listMenu({}); // 复用列表接口
// 手动构造根节点
const menu = { id: 0, label: '主类目', children: [] };
// 简单的字段映射 (menu_name -> label)
menu.children = res.data.map(node => mapTreeSelect(node));
menuOptions.value = [menu];
};
通过本篇,我们完成了 RBAC 系统管理的最后一块拼图------菜单管理 。
现在,你可以直接在页面上配置新的路由,设置图标,定义权限,无需修改一行代码,刷新页面即可生效。这标志着我们的系统具备了动态扩展能力
但是,我们在菜单里配置了 system:user:add 这种权限标识,它到底有什么用?前端如何利用它来控制按钮的显示和隐藏?
下一篇:《全栈 RBAC 实战 (12):操作日志与按钮级别权限》 ,我们将深入 Vue 3 自定义指令的实现原理,实现细粒度的按钮级权限控制,并顺带搞定操作日志记录。
具备完整功能后台管理系统的代码仓库:
感谢大家的star
前端:前端
后端:后端