关于菜单


一、菜单系统架构:整体是怎么运转的?🔍

先用一句话概括:

菜单系统 = 菜单配置(PHP数组) + 菜单表 AdminRule + 权限控制 + 插件安装/卸载机制

核心角色可以按层次理解:

  1. 存储层(数据库)表模型:AdminRule实际存放“每一条菜单/按钮”记录(包括插件菜单)。字段里有:标题、路径、类型、所属插件、是否显示、排序等等。
  2. 配置层(PHP 配置数组)框架自身菜单:plugin/xbCode/config/menu.php各插件自己的菜单:plugin/{插件标识}/config/menu.php这些文件返回一个多维数组,描述菜单树结构(目录 → 菜单 → 按钮)。
  3. 服务层(菜单相关 API 类)Menus:安装菜单(写入 AdminRule)卸载菜单(从 AdminRule 删除)按管理员获取菜单树(带权限过滤)MenuChecked:把菜单数组“整理规范”:补字段、做树形/二维转换、生成 menu_key 等。MenuData:校验菜单配置数组是否缺必填字段。MenuOption:生成“选择父级菜单”的联级选项。MenuResponse:自动生成“添加/修改/删除/表格”等按钮菜单。
  4. 插件安装基类BasePlugin + InstallTrait / UnInstallTrait插件安装时自动调用:安装数据库、安装配置、安装菜单等;卸载时自动调用:卸载菜单、卸载配置、卸载字典等。
  5. 权限与角色层管理员表: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 即可,系统会在安装过程中给每个菜单自动补上 idpid 等。

2.2 树形关系如何落到数据库?

开发者不用自己管 idpid,两个步骤会自动做:

  1. 安装时(Menus::install()):按 children 结构递归插入数据库;在插入子菜单前,把父级菜单 ID 写入子菜单的 pid 字段。
  2. 数据加工时(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:是否显示
  • YesEnumSwitchEnum:通用开关类
  • 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() 会:

  1. AdminRule 取出所有菜单;
  2. 用内部工具 DataUtil::channelLevel 做出树形;
  3. 再整理成 label / value / children 结构的数组;
  4. 并在最前面加一个 '顶级权限菜单' 选项(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);
}

整个过程对插件开发者是“自动”的:

  1. 插件安装过程中调用 BasePlugin::install()
  2. 其中会调用 installMenus()
  3. 如果 config/menu.php 存在,会自动读取并交给 Menus::install()
  4. Menus::install() 会:

6.3 卸载时如何清理菜单?

卸载时 UnInstallTrait::unInstallMenus() 会调用:

Menus::uninstall($this->name);

Menus::uninstall($name) 的核心逻辑:

  1. 读取当前插件的菜单配置(通过 config("plugin.{$name}.menu", []) 获取);
  2. 把菜单树压平为二维数组;
  3. 分成“顶级菜单”和“子菜单”两类;
  4. 先删除所有子菜单,再尝试删除顶级菜单(前提是没有剩余子菜单依赖)。

重点注意 ⚠️:

  • 卸载时依赖“当前菜单配置文件的 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 最佳实践小结 💡

  1. 目录(type=10)只负责分组,不绑定接口通常不写 method,不直接访问,只当父节点。
  2. 菜单(type=20)对应页面path 写清楚 admin/控制器/方法;is_show=20,便于出现在侧边栏。
  3. 按钮(type=30)只做权限,不做导航is_show 一般为 10(不在侧边栏出现);通过角色授权控制页面上的按钮显示/隐藏。
  4. path 一旦上线,就尽量不要轻易改因为 path 同时用于卸载、权限、缓存等多个地方。
  5. 合理使用 sort 排序系统菜单通常会使用较大的排序值(如 8888, 9999),这样你自定义菜单可以插在中间,体验更好。
  6. 遇到安装报错时先看 MenuData 校验多半是 title/path/is_show/type 按要求没写。