Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_EleVue/zh/modules/menu.md
T
2026-10-08 17:50:29 +08:00

159 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 6.3 菜单管理
:::tip 概述
菜单管理负责维护系统的菜单树和权限节点,是 RBAC 权限体系的基础数据——**菜单即权限**:菜单表的 `permission` 字段即接口权限编码。`MenuService` 继承 `BaseService`(系统共享,不隔离)。
:::
## 模块特点
| 特性 | 说明 |
|------|------|
| 基类 | `BaseService`(系统共享,不做租户隔离) |
| 树形结构 | `treeParentField='parent_id'`、`treeLikeField='name'`,支持树形列表与子树搜索 |
| 菜单类型 | type: **0=菜单/目录(可导航) 1=权限节点**(`permission` 非空,参与接口鉴权) |
| 删除保护 | `beforeDelete` 检查子菜单与角色引用,任一存在则阻止删除 |
| 缓存联动 | 菜单变更时 `MenuObserver` 自动清除菜单/权限缓存 |
## API 接口
| 方法 | 路径 | 权限码 | 说明 |
|------|------|--------|------|
| GET | `/api/menu/list` | sys:menu:list | 树形列表 |
| GET | `/api/menu/detail/{id}` | sys:menu:detail | 详情 |
| POST | `/api/menu/add` | sys:menu:add | 新增 |
| PUT | `/api/menu/update` | sys:menu:update | 修改 |
| DELETE | `/api/menu/delete/{id}` | sys:menu:delete | 删除 |
:::tip 为何没有分页接口?
菜单是全局树形数据,体量小且前端需要完整树渲染,因此只提供 `list`(全量树形)而非 `page`(分页)。
:::
## Service 属性配置
```php
// app/Services/MenuService.php(来自项目实际代码)
class MenuService extends BaseService
{
use HasCrudOperations, HasQueryScopes;
protected string $modelClass = Menu::class;
protected array $pageLikeFields = ['name'];
protected array $pageEqFields = ['status', 'type'];
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
// 树形结构
protected string $treeParentField = 'parent_id';
protected string $treeLikeField = 'name'; // 树形搜索字段(命中节点保留父链)
}
```
## 菜单数据结构
```json
{
"id": 100,
"parentId": 0,
"name": "系统管理",
"type": 0,
"path": "/system",
"icon": "SettingOutlined",
"sort": 10,
"permission": "",
"component": "",
"target": 0,
"hide": false,
"children": [
{
"id": 101,
"parentId": 100,
"name": "用户管理",
"type": 0,
"path": "/user",
"component": "/system/user/index",
"children": [
{
"id": 102,
"parentId": 101,
"name": "查看用户",
"type": 1,
"permission": "sys:user:page"
},
{
"id": 103,
"parentId": 101,
"name": "添加用户",
"type": 1,
"permission": "sys:user:add"
}
]
}
]
}
```
(键名经驼峰自动转换;`children` 由树形组装生成。`GET /index/getMenus` 只返回 `type=0` 的可导航节点——`type=1` 权限节点不进菜单树,仅供鉴权查询。)
## 字段说明
| 字段 | 说明 |
|------|------|
| `type` | **0=菜单/目录(可导航) 1=权限节点**(鉴权专用,`permission` 必填) |
| `status` | 0=正常 1=停用(`getMenus` 只取 0) |
| `hide` | 0=可见 1=不可见(`getMenus` 只取 0) |
| `path` | 路由路径(type=0) |
| `component` | 前端组件路径(以 `/` 开头,type=0 页面必填,映射到 `views/**/index.vue`) |
| `permission` | 权限标识(type=1 必填且非空,格式 `sys:{模块}:{操作}`) |
| `target` | 打开方式:0=组件 1=内链 2=外链 |
## 数据流转
```text
菜单管理(本模块)
│ 写入 laravel_menu
▼
GET /index/getMenus(IndexController)
│ 按当前用户权限过滤(user_role → role_menu → menu)
▼
前端 asyncRouteStore
│ 转换为 Vue Router 动态路由 + 侧边栏菜单
▼
v-perm 指令按 permission 控制按钮显隐
```
## 删除保护(beforeDelete 钩子)
```php
// app/Services/MenuService.php(摘自实际代码)
protected function beforeDelete(int $id): void
{
// 存在未删除的子菜单时不允许删除(软删除由全局 Scope 过滤)
$hasChildren = Menu::where('parent_id', $id)->exists();
if ($hasChildren) {
throw new BizException('存在子菜单,不可删除');
}
// 已被角色引用时不允许删除,避免 role_menu 产生孤儿数据
$hasRoles = RoleMenu::where('menu_id', $id)->exists();
if ($hasRoles) {
throw new BizException('该菜单已被角色引用,不可删除');
}
}
```
:::warning 删除前校验
删除菜单时先检查子菜单(`存在子菜单,不可删除`),再检查角色引用(`该菜单已被角色引用,不可删除`),任一命中即抛出 `BizException` 阻止删除——需先解除引用/删除子菜单。菜单变更后相关角色的权限缓存由 `MenuObserver` 自动清除。
:::
## 最佳实践
| 实践 | 说明 |
|------|------|
| type=0 管导航 | 目录/页面用 type=0;页面必填 `component`(前端组件路径),目录留空 |
| type=1 管权限 | 权限节点用 type=1,`permission` 必填且非空(鉴权查询条件之一) |
| 权限节点挂页面下 | type=1 节点作为 type=0 页面的子级,便于角色树勾选 |
| 状态与隐藏 | `status=0` 启用、`hide=0` 可见才会出现在 `getMenus` 结果中 |
| 权限码规范 | `sys:{模块}:{操作}`,与控制器 `#[Permission]` 完全一致 |