动态菜单核心在于层级生成、权限过滤时机和高亮匹配:需用buildTree构建树形结构并逐层排序,权限应在数据库查询阶段过滤而非PHP遍历,高亮须基于路由名严格匹配并校验权限。

动态菜单不是把数组 foreach 一遍就完事,核心卡点在「层级结构生成」「权限过滤时机」和「高亮匹配逻辑」三处。做错任意一个,菜单就乱序、漏项或误亮。
怎么用 buildTree 把数据库菜单转成树形结构
数据库查出的菜单是一维扁平数据,直接渲染必然父子混排。TP6 没内置 buildTree 方法,得自己写或用扩展逻辑。
- 必须先加
where('status', 1)过滤禁用项,否则树里会混入不可见节点 - 推荐用引用方式组装,避免递归调用栈溢出:遍历一次,用
$map[$item['id']] = $item建映射,再按pid挂载子节点到父级children数组 - 排序不能只靠 SQL 的
ORDER BY sort,树形生成后要对每层children单独usort(),否则二级菜单顺序错乱 - url 字段建议存相对路径(如
'admin/user/list'),别带域名或入口文件index.php,否则多应用模式下跳转 404
为什么菜单高亮总不准确
用 $_SERVER['REQUEST_URI'] 或字符串 strpos() 匹配,是绝大多数人翻车的第一步。它含查询参数、入口文件,且无法区分层级语义。
- 正确做法是取当前路由名:
request()->rule()->getName()(TP6.1+),比如返回'admin/user/list',再跟菜单url字段严格比对 - 若菜单 url 是
'admin/user',而当前路由是'admin/user/detail',前缀匹配会误判——此时应统一用路由名,而非路径字符串 - 高亮前必须校验权限:即使路由名匹配,用户没该菜单权限,也得隐藏或不激活,否则存在越权风险
- 千万别在模板里写
in_array($menu['id'], $authMenuIds)做逐项判断,N² 复杂度,应提前用array_flip($authMenuIds)转键值映射
RBAC 权限怎么真正绑定到菜单渲染环节
权限不是“渲染完菜单再一个个 check”,而是“只查出用户能看的那些菜单”。否则性能崩、逻辑漏、缓存难维护。
立即学习“PHP免费学习笔记(深入)”;
- 菜单查询阶段就要 JOIN 或 IN 过滤:例如
Db::name('menu')->whereIn('id', $allowedMenuIds)->select(),而不是查全量再 PHP 过滤 - 缓存 key 必须含用户维度,不能只用角色 ID:原生
auth_rule_list_' . $roleId会导致同角色不同菜单配置的用户互相污染,应改为auth_rule_list_user_' . $userId - 菜单表字段需扩展
icon、sort、is_show,TP6 原生auth_rule表不提供这些,硬塞进 name/title 会破坏语义 - 菜单更新后必须主动清除缓存:
Cache::delete('menu_list_user_' . $uid),否则改了图标或状态,前端永远看不到
前端折叠状态怎么跨页面保持
后端不该管“哪个菜单展开”,那是前端职责。但后端输出的数据必须支持前端做状态映射,否则 localStorage 没法用。
- 菜单数据中必须有稳定唯一标识,如
id或模块前缀('admin-user'),不能只靠title或临时生成 hash - 后端只输出干净菜单树,所有
<ul class="submenu"></ul>初始设为style="display:none"或class="collapsed",不加open、active等状态类 - 前端用
localStorage.setItem('menu-opened-' . $key, 'true')存开关,key 推荐用模块+控制器组合(如admin-role),避免三级菜单时状态错乱 - 刷新闪动问题本质是 JS 加载延迟,解决方案是 CSS 层面初始隐藏:加
.submenu { display: none; },再由 JS 检查 localStorage 后显式show()
最易被忽略的是缓存键设计和权限过滤时机——菜单查出来再过滤,等于把压力全堆在 PHP 层;缓存不带用户 ID 或更新时间戳,等于开着门等数据过期。这两点一错,后续所有前端交互都建立在沙堆上。



















