一、菜单系统架构:整体是怎么运转的?🔍
先用一句话概括:
菜单系统 = 菜单配置(PHP数组) + 菜单表 AdminRule + 权限控制 + 插件安装/卸载机制
核心角色可以按层次理解:
- 存储层(数据库)表模型:AdminRule实际存放“每一条菜单/按钮”记录(包括插件菜单)。字段里有:标题、路径、类型、所属插件、是否显示、排序等等。
- 配置层(PHP 配置数组)框架自身菜单:plugin/xbCode/config/menu.php各插件自己的菜单:plugin/{插件标识}/config/menu.php这些文件返回一个多维数组,描述菜单树结构(目录 → 菜单 → 按钮)。
- 服务层(菜单相关 API 类)Menus:安装菜单(写入 AdminRule)卸载菜单(从 AdminRule 删除)按管理员获取菜单树(带权限过滤)MenuChecked:把菜单数组“整理规范”:补字段、做树形/二维转换、生成 menu_key 等。MenuData:校验菜单配置数组是否缺必填字段。MenuOption:生成“选择父级菜单”的联级选项。MenuResponse:自动生成“添加/修改/删除/表格”等按钮菜单。
- 插件安装基类BasePlugin + InstallTrait / UnInstallTrait插件安装时自动调用:安装数据库、安装配置、安装菜单等;卸载时自动调用:卸载菜单、卸载配置、卸载字典等。
- 权限与角色层管理员表:Admin角色表:AdminRole(保存该角色拥有哪些菜单路径 rule)菜单表:AdminRuleMenus::get() 会结合管理员的角色,把“无权限的菜单”过滤掉,只返回当前管理员可见的菜单树。
二、菜单配置结构:菜单数组长什么样?🌳
2.1 整体是“树形数组”
在配置文件里(比如 plugin/xbCode/config/menu.php),菜单是一个多维数组,最外层是若干个“顶级菜单项”,每个菜单项可以有 children 子菜单,再往下嵌套。
典型结构可以抽象理解为:
return [
[
'title' => '一级目录A',
'path' => 'ModuleA',
'type' => '10', // 目录
'children' => [
[
'title' => '页面菜单A1',
'path' => 'admin/Controller/action',
'type' => '20', // 菜单
'children' => [
[
'title' => '按钮权限A1-添加',
'path' => 'admin/Controller/add',
'type' => '30', // 按钮
],
// 其他按钮...
],
],
// 其他页面菜单...
],
],
// 其他一级目录...
];你只需要正确写好 title / path / type / children 即可,系统会在安装过程中给每个菜单自动补上 id、pid 等。
2.2 树形关系如何落到数据库?
开发者不用自己管 id 和 pid,两个步骤会自动做:
- 安装时(Menus::install()):按 children 结构递归插入数据库;在插入子菜单前,把父级菜单 ID 写入子菜单的 pid 字段。
- 数据加工时(MenuChecked):有一套内部算法会给菜单树打编号(menusNo、menuTreeTo2D 等),确保最终形成一个“带 id/pid 的清晰层级结构”。
总结一句话:
你只要保证 children 嵌套关系正确,其它层级 ID 的事情系统会帮你搞定 ✅
三、字段含义详解:常见配置项说明 📋
下面按“最常见”的字段,逐个拆解,配合一些简单示例讲用途。
3.1 标题与所属插件
1)title
- 类型:字符串
- 作用:菜单显示名称(侧边栏上看到的文字)。
- 必填,不能为空。
示例:"title" => "权限管理"
2)plugin
- 类型:字符串(插件标识,比如
xbCode/xbNavs等) - 作用:记录这条菜单属于哪个插件。
默认行为:
- 如果你在配置里没写
plugin,在处理时会自动设为:
场景意义:
- 后台可以根据 plugin 字段区分“哪个菜单是哪个插件带来的”;
- 卸载某个插件时,可以根据 plugin 来删除对应菜单。
3.2 路由与请求方式
3)path
- 类型:字符串
- 作用:菜单对应的路由路径,也是权限控制的关键字段。
- 典型格式:
"admin/控制器/方法",例如"admin/Admin/index"。
⚠️ 重要逻辑:
在 MenuChecked::parseMenuData() 中,如果 path 不包含 workbench,会自动加前缀:
最终实际使用的是:app/{plugin}/{path}
也就是说,如果你写:
'plugin' => 'xbCode',
'path' => 'admin/Admin/index',处理后会变成:
app/xbCode/admin/Admin/index这条路径会用于:
- 菜单返回给前端;
- 角色授权时关联;
- 其他功能查询菜单信息时作为“键”。
4)method
- 类型:字符串或数组,如
"GET"/"POST"/"GET,PUT"或['GET', 'PUT'] - 作用:接口请求方式,主要用于:
如果你没写,系统会默认当成 GET。
3.3 类型与图标
5)type
- 类型:字符串
"10"/"20"/"30" - 作用:菜单类型,具体含义见后面的枚举说明:
这项非常关键,因为:
- 前端会根据 type 来决定是否当“页面菜单”渲染;
- 权限系统会用 type=30 的菜单作为“按钮级权限”。
6)icon
- 类型:字符串(一般匹配前端图标库名称)
- 作用:菜单前显示的小图标,如
"DashboardOutlined"、"AppstoreOutlined"等。 - 可以为空,空的话前端通常显示默认样式或不显示图标。
3.4 显示控制与系统标识
7)is_show
- 类型:
10/20 - 含义:
安装时会进行校验:
如果指定了 is_show,必须是 10 或 20,否则直接抛异常。
8)is_system
- 类型:
10/20 - 含义:
一般用来标记“框架必须保留的菜单”,避免被随意删除或隐藏。
9)is_default
- 类型:
10/20 - 含义:
没有特别需求时,可以不写,系统会统一默认 10。
10)state
- 类型:
10/20 - 含义:
Menus::get() 在查询管理员菜单时,会加条件:
['state', '=', '20']
也就是只取启用状态的菜单。
3.5 排序与层级
11)sort
- 类型:整数
- 含义:排序号,数字越小显示越靠前。
- 例如:
12)pid
- 类型:整数
- 含义:父级菜单 ID。
在“配置文件”里可以不写 pid,推荐只使用 children 嵌套;
写入数据库时,框架会自动根据嵌套关系计算并填充 pid。
四、枚举类型说明:MenuTypeEnum 等 🧾
4.1 MenuTypeEnum:菜单类型枚举
位置:plugin/xbCode/enum/MenuTypeEnum.php
它把 type 字段的三个取值做了更清晰的人类标注:
- 10 – 目录(STATE10) 📂label:目录一般用作一级菜单/分组,通常不直连具体接口。
- 20 – 菜单(STATE20) 📄label:菜单有对应页面,左侧菜单点击后会切换到某个内容区域。
- 30 – 按钮(MENU_TO_30) 🔘label:按钮不显示在侧边栏,常用于页面内按钮操作的权限控制,如“添加”、“编辑”、“删除”。
在配置中直接用字符串数字:
'type' => '10', // 目录
'type' => '20', // 菜单
'type' => '30', // 按钮
枚举类更多用于后台 UI 渲染、过滤条件、字典显示等,让数字更易读。
4.2 其他枚举(了解即可)
在 plugin/xbCode/enum 目录下还有:
StateEnum:状态(启用/禁用)ShowEnum:是否显示YesEnum、SwitchEnum:通用开关类BanEnum等与具体业务相关
这些枚举的共同作用:
把“10/20 这种数字”翻译成“直观标签”,在界面上更友好显示。
五、辅助类功能:谁在幕后帮你处理菜单?🔧
5.1 MenuData:配置校验员
位置:plugin/xbCode/api/MenuData.php
职责:在加载菜单文件时,提前帮你把低级错误拦截掉,比如:
- 标题为空 ➜ 抛异常 “菜单标题不能为空”
- 路径为空 ➜ 抛异常 “菜单路径不能为空”
- is_show 为空 ➜ 抛异常 “菜单是否显示不能为空”
- type 未设置 ➜ 抛异常 “菜单类型不能为空”
如果你的菜单配置写错了(比如漏填 title 或 path),安装阶段就会直接 fail,让你立刻发现问题,而不是到运行时再踩坑 ⚠️
5.2 MenuChecked:菜单格式整理总管
位置:plugin/xbCode/api/MenuChecked.php
主要做三类工作:
① 格式补全与路径规范
parseMenuData()会对每一条菜单做统一处理:
② 树形结构与二维数组之间转换
menuTreeTo2D():
把树形菜单压平为一个列表(每条都有 id/pid)。menu2DToTree():
把一维列表重新还原成树形。menusNo():
为菜单树自动分配 id / pid 编号。
这一套保证了“无论是从数据库查出的二维数组,还是配置里写的树形数组”,最终都能转换为统一、规范的树形菜单结构。
③ 辅助操作
getMenuKey():给每个菜单生成menu_key,比如"1-3-5"表示层级路径;resetKeys():清理数组下标,让 children 总是从 0 开始;resetField():对整棵菜单树批量设置某个字段值(如 plugin)。
5.3 MenuOption:父级菜单选择器
位置:plugin/xbCode/api/MenuOption.php
当你在后台管理“编辑菜单”时,需要选择“该菜单的上级菜单”,页面上通常会用一个“级联选择框”。
MenuOption::getCascaderOptions() 会:
- 从
AdminRule取出所有菜单; - 用内部工具
DataUtil::channelLevel做出树形; - 再整理成
label/value/children结构的数组; - 并在最前面加一个
'顶级权限菜单'选项(value=0)。
这样前端就能直接使用这个数组来生成“选择父菜单”的联级下拉,不用你手写树形结构。
5.4 MenuResponse:自动生成“资源按钮菜单”
位置:plugin/xbCode/api/MenuResponse.php
应用场景:
你已经有一个“列表菜单”,例如 admin/Admin/index,希望快速生成:
- 添加按钮:
admin/Admin/add - 修改按钮:
admin/Admin/edit - 删除按钮:
admin/Admin/del - 修改列按钮:
admin/Admin/rowEdit - 表格按钮:
admin/Admin/Table
而不是在菜单配置里一条条手写。
MenuResponse 内部有一个选项表:
- 添加(add)
- 修改(edit)
- 删除(del)
- 修改列(rowEdit)
- 表格(Table)
你只要告诉它:父级菜单信息 + 想生成哪些资源,它就会按规则补全:
- 标题:
父标题-添加/修改/... - 路径:
模块/控制器/资源方法 - pid:挂在父菜单下面
- type:30(按钮)
- is_show:10(一般不在侧边栏显示)
大大减轻重复劳动 ✨
5.5 AdminRule::getMenuDict:菜单字典缓存
位置:plugin/xbCode/app/model/AdminRule.php
作用:提供一个“按 path 快速查菜单信息”的字典。
- key:完整 path(如
app/xbCode/admin/Admin/index) - value:菜单记录
并使用缓存(默认 600 秒)提高性能。
在系统其他地方,如果只知道路由 path,也能通过这个字典快速找到对应菜单信息。
六、插件菜单集成机制:插件是怎么把菜单挂载进来的?🔌
这一部分是“写插件的人”最关心的内容。
6.1 插件文件结构里的菜单配置位置
以插件 xbNavs 为例:
- 菜单配置:
plugin/xbNavs/config/menu.php - 安装脚本:
plugin/xbNavs/api/Install.php(继承BasePlugin)
你的插件只要提供类似结构:
config/menu.php:描述菜单树api/Install.php:继承BasePlugin
就能自动参与到菜单系统里。
6.2 安装流程中菜单是怎么写入的?
BasePlugin 使用了 InstallTrait,其中有:
protected function installMenus()
{
// 获取菜单文件
$file = base_path() . "/plugin/{$this->name}/config/menu.php";
if (!file_exists($file)) {
return;
}
// 获取菜单数据
$data = include $file;
if (empty($data)) {
return;
}
// 开始安装菜单
Menus::install($data, $this->name);
}整个过程对插件开发者是“自动”的:
- 插件安装过程中调用
BasePlugin::install(); - 其中会调用
installMenus(); - 如果
config/menu.php存在,会自动读取并交给Menus::install(); Menus::install()会:
6.3 卸载时如何清理菜单?
卸载时 UnInstallTrait::unInstallMenus() 会调用:
Menus::uninstall($this->name);Menus::uninstall($name) 的核心逻辑:
- 读取当前插件的菜单配置(通过
config("plugin.{$name}.menu", [])获取); - 把菜单树压平为二维数组;
- 分成“顶级菜单”和“子菜单”两类;
- 先删除所有子菜单,再尝试删除顶级菜单(前提是没有剩余子菜单依赖)。
重点注意 ⚠️:
- 卸载时依赖“当前菜单配置文件的 path/结构”来查找要删除的菜单;
- 如果你后来改动了
menu.php中的 path,但数据库里还是老的路径,卸载可能删不干净; - 最佳做法:菜单一旦上线,尽量不要随意变更 path,确需变更时要同步处理数据库。
七、实际应用示例与最佳实践 💻📝
7.1 示例:给插件增加一个常规菜单模块
假设你做了一个 xbDemo 插件,希望:
- 在后台左侧出现一个“Demo管理”的目录;
- 下面有一个“Demo列表”菜单页面;
- 页面内部有“添加”“编辑”“删除”三个操作按钮(权限控制)。
你的 plugin/xbDemo/config/menu.php 可以概念上这样设计(简化展示):
return [
[
'title' => 'Demo管理',
'path' => 'Demo', // 顶级目录
'type' => '10', // 目录
'icon' => 'AppstoreOutlined',
'is_show' => 20,
'sort' => 100,
'children' => [
[
'title' => 'Demo列表',
'path' => 'admin/Demo/index',
'type' => '20', // 菜单
'is_show' => 20,
'children' => [
[
'title' => 'Demo列表-添加',
'path' => 'admin/Demo/add',
'type' => '30', // 按钮
'is_show' => 10,
],
[
'title' => 'Demo列表-编辑',
'path' => 'admin/Demo/edit',
'type' => '30',
'is_show' => 10,
],
[
'title' => 'Demo列表-删除',
'path' => 'admin/Demo/del',
'type' => '30',
'is_show' => 10,
],
],
],
],
],
];
安装插件后:
- AdminRule 中会多出多条记录,plugin=xbDemo;
- 菜单树中多出“Demo管理”目录;
- 根据角色授权情况,用户登录后能看到对应菜单/按钮。
7.2 示例:快速生成资源按钮(减少手写)
如果你已经有一个菜单:
- 标题:
商品管理 - 路径:
admin/Goods/index
可以使用 MenuResponse 来自动生成:
- 商品管理-添加(admin/Goods/add)
- 商品管理-修改(admin/Goods/edit)
- 商品管理-删除(admin/Goods/del)
- 商品管理-表格(admin/Goods/Table)
这样你无须逐条在 menu.php 中书写所有按钮项,只需要告诉系统“哪些资源按钮需要生成”,即可批量完成,减少出错率 ✨
7.3 最佳实践小结 💡
- 目录(type=10)只负责分组,不绑定接口通常不写 method,不直接访问,只当父节点。
- 菜单(type=20)对应页面path 写清楚 admin/控制器/方法;is_show=20,便于出现在侧边栏。
- 按钮(type=30)只做权限,不做导航is_show 一般为 10(不在侧边栏出现);通过角色授权控制页面上的按钮显示/隐藏。
- path 一旦上线,就尽量不要轻易改因为 path 同时用于卸载、权限、缓存等多个地方。
- 合理使用 sort 排序系统菜单通常会使用较大的排序值(如 8888, 9999),这样你自定义菜单可以插在中间,体验更好。
- 遇到安装报错时先看 MenuData 校验多半是 title/path/is_show/type 按要求没写。