签入版本
This commit is contained in:
@@ -1,26 +1,53 @@
|
||||
# 6.3 菜单管理
|
||||
|
||||
:::tip 概述
|
||||
菜单管理负责维护系统的菜单树和权限节点,是 RBAC 权限体系的基础数据。
|
||||
菜单管理负责维护系统的菜单树和权限节点,是 RBAC 权限体系的基础数据——**菜单即权限**:菜单表的 `permission` 字段即接口权限编码。`MenuService` 继承 `BaseService`(系统共享,不隔离)。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 树形结构 | treeParentField='parent_id',支持树形列表 |
|
||||
| 菜单类型 | type: 0=目录/页面(可导航) 1=权限按钮(仅权限校验,不显示在菜单树) |
|
||||
| 基类 | `BaseService`(系统共享,不做租户隔离) |
|
||||
| 树形结构 | `treeParentField='parent_id'`、`treeLikeField='name'`,支持树形列表与子树搜索 |
|
||||
| 菜单类型 | type: **0=菜单/目录(可导航) 1=权限节点**(`permission` 非空,参与接口鉴权) |
|
||||
| 删除保护 | `beforeDelete` 检查子菜单与角色引用,任一存在则阻止删除 |
|
||||
| 缓存联动 | 菜单变更时 `MenuObserver` 自动清除菜单/权限缓存 |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/menu/list` | sys:menu:page | 树形列表 |
|
||||
| GET | `/api/menu/detail/:id` | sys:menu:detail | 详情 |
|
||||
| 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 | 删除 |
|
||||
| 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'; // 树形搜索字段(命中节点保留父链)
|
||||
}
|
||||
```
|
||||
|
||||
## 菜单数据结构
|
||||
|
||||
@@ -42,16 +69,22 @@
|
||||
"id": 101,
|
||||
"parentId": 100,
|
||||
"name": "用户管理",
|
||||
"type": 1,
|
||||
"type": 0,
|
||||
"path": "/user",
|
||||
"component": "system/user/index",
|
||||
"permission": "sys:user:page",
|
||||
"component": "/system/user/index",
|
||||
"children": [
|
||||
{
|
||||
"id": 102,
|
||||
"parentId": 101,
|
||||
"name": "查看用户",
|
||||
"type": 1,
|
||||
"permission": "sys:user:page"
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"parentId": 101,
|
||||
"name": "添加用户",
|
||||
"type": 2,
|
||||
"type": 1,
|
||||
"permission": "sys:user:add"
|
||||
}
|
||||
]
|
||||
@@ -60,76 +93,66 @@
|
||||
}
|
||||
```
|
||||
|
||||
(键名经驼峰自动转换;`children` 由树形组装生成。`GET /index/getMenus` 只返回 `type=0` 的可导航节点——`type=1` 权限节点不进菜单树,仅供鉴权查询。)
|
||||
|
||||
## 字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `type` | 0=目录 1=菜单 2=按钮 |
|
||||
| `path` | 路由路径 |
|
||||
| `component` | 前端组件路径(type=1 时必填) |
|
||||
| `permission` | 权限标识(type=1,2 时填写) |
|
||||
| `target` | 打开方式:0=内部 1=iframe 2=外链 |
|
||||
| `hide` | 是否隐藏 |
|
||||
| `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=外链 |
|
||||
|
||||
## 核心代码
|
||||
## 数据流转
|
||||
|
||||
### Controller
|
||||
```text
|
||||
菜单管理(本模块)
|
||||
│ 写入 laravel_menu
|
||||
▼
|
||||
GET /index/getMenus(IndexController)
|
||||
│ 按当前用户权限过滤(user_role → role_menu → menu)
|
||||
▼
|
||||
前端 asyncRouteStore
|
||||
│ 转换为 Vue Router 动态路由 + 侧边栏菜单
|
||||
▼
|
||||
v-perm 指令按 permission 控制按钮显隐
|
||||
```
|
||||
|
||||
## 删除保护(beforeDelete 钩子)
|
||||
|
||||
```php
|
||||
// app/controller/MenuController.php
|
||||
// app/Services/MenuService.php(摘自实际代码)
|
||||
|
||||
class MenuController extends BaseController
|
||||
protected function beforeDelete(int $id): void
|
||||
{
|
||||
protected MenuLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new MenuLogic();
|
||||
// 存在未删除的子菜单时不允许删除(软删除由全局 Scope 过滤)
|
||||
$hasChildren = Menu::where('parent_id', $id)->exists();
|
||||
if ($hasChildren) {
|
||||
throw new BizException('存在子菜单,不可删除');
|
||||
}
|
||||
|
||||
// 树形列表(支持 status 参数过滤)
|
||||
#[Log('菜单管理-查询树形列表', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:menu:page', '菜单列表')]
|
||||
public function list(): Json
|
||||
{
|
||||
$where = [];
|
||||
$status = $this->getParam('status');
|
||||
if ($status !== null && $status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
return parent::_list($this->logic, $where);
|
||||
}
|
||||
|
||||
#[Log('菜单管理-新增记录', Log::TYPE_ADD, '新增菜单:{name}')]
|
||||
#[Permission('sys:menu:add', '添加菜单')]
|
||||
public function add(): Json
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
}
|
||||
|
||||
#[Log('菜单管理-修改记录', Log::TYPE_UPDATE, '修改菜单ID:{id}')]
|
||||
#[Permission('sys:menu:update', '修改菜单')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) return $this->fail('缺少id参数');
|
||||
return parent::_edit($this->logic, $id, $data);
|
||||
}
|
||||
|
||||
#[Log('菜单管理-删除记录', Log::TYPE_DELETE, '删除菜单ID:{id}')]
|
||||
#[Permission('sys:menu:delete', '删除菜单')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
// 已被角色引用时不允许删除,避免 role_menu 产生孤儿数据
|
||||
$hasRoles = RoleMenu::where('menu_id', $id)->exists();
|
||||
if ($hasRoles) {
|
||||
throw new BizException('该菜单已被角色引用,不可删除');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 菜单类型
|
||||
菜单使用 `_list()` 而非 `_index()`,返回全量树形数据。`type` 字段区分:0=目录、1=菜单、2=按钮。前端只渲染 type=0 和 type=1 的节点。
|
||||
:::warning 删除前校验
|
||||
删除菜单时先检查子菜单(`存在子菜单,不可删除`),再检查角色引用(`该菜单已被角色引用,不可删除`),任一命中即抛出 `BizException` 阻止删除——需先解除引用/删除子菜单。菜单变更后相关角色的权限缓存由 `MenuObserver` 自动清除。
|
||||
:::
|
||||
|
||||
:::warning 删除前校验
|
||||
删除菜单时,`MenuLogic::beforeDelete` 会检查是否存在子菜单(`think_menu` 中 `parent_id` 等于当前 ID 且未删除的记录),如有子菜单则抛出异常阻止删除。需先删除子菜单再删除父菜单。
|
||||
:::
|
||||
## 最佳实践
|
||||
|
||||
| 实践 | 说明 |
|
||||
|------|------|
|
||||
| type=0 管导航 | 目录/页面用 type=0;页面必填 `component`(前端组件路径),目录留空 |
|
||||
| type=1 管权限 | 权限节点用 type=1,`permission` 必填且非空(鉴权查询条件之一) |
|
||||
| 权限节点挂页面下 | type=1 节点作为 type=0 页面的子级,便于角色树勾选 |
|
||||
| 状态与隐藏 | `status=0` 启用、`hide=0` 可见才会出现在 `getMenus` 结果中 |
|
||||
| 权限码规范 | `sys:{模块}:{操作}`,与控制器 `#[Permission]` 完全一致 |
|
||||
|
||||
Reference in New Issue
Block a user