所有文件覆盖即可,优化了界面UI和修复了一些问题。
接管站点顶部导航菜单:自定义菜单项、图标、二级下拉、可见性控制。
- 后台入口:后台 → 导航管理(
/admin/nav-manager) - 数据:插件独立库
plugins/nav_manager/data/nav_manager.sqlite(表nm_items/nm_settings) - 前台渲染:核心
app/Views/layouts/main.php调用Plugin\NavManager\Plugin::renderTopNav() - 多语言:
plugins/nav_manager/lang/{zh,zh_tw,en}.php(由I18n::mergePluginLangs()自动合并,无需钩子)
⚠️ 运维硬依赖:改完菜单必须更新缓存
这是必须执行的步骤,不是可选优化。
站点对游客的 GET 列表页(首页 /、论坛 /forum、版块页)启用了永久页面缓存
(index.php 前置静态缓存,产物在 data/runtime/pages/,不按 TTL 过期)。
由此产生两个必然后果:
- 菜单增删改后,游客看到的仍是缓存里的旧导航(管理员因为走动态渲染,会立刻看到新菜单,容易误判"已经生效")。
- 插件新增/修改的 CSS 与 JS 也不会注入已缓存的游客页 → 即使导航 HTML 变了,下拉样式与点击脚本也会缺失。
正确操作
每次改动菜单 / 接管开关后,到「后台 → 维护工具 → 更新缓存」执行一次。
(该按钮的核心语言键为 admin.mt_clear_cache;本插件后台页顶部的提示会直接引用核心键名,
所以核心改了按钮叫法,这里也会跟着变,不会写死一个不存在的名字。)
部署新版本后同理(与项目既有约定一致)。
说明
- 登录用户不受影响(一律走完整管线,动态渲染)。
- 带未登录 Cookie 的请求也仍会命中前置缓存(核心设计如此),只有登录态才绕过。
- 该缓存只在发帖 / 回帖时由
PageCache::invalidate()主动失效,不会自动过期。
图标字段
后台「图标」一栏是系统内置的 20 个常用图标,点击即可选择,另有「无图标」一项可清空。
- 可选图标全部取自核心
TemplateCompiler::icon()的映射表,保证字体(woff2)里有对应字形。 - 数据库里存的仍是 HTML 数字实体(如
),所以历史数据与手工写入的值都能正常渲染。 - 非法值会被清空(写入时按
^&#x?[0-9a-fA-F]{1,6};?$白名单校验,渲染前再兜底一次)。 这是安全措施:该字段在导航里原样输出,允许任意 HTML 会造成全站存储型 XSS。 该白名单的唯一实现是Plugin::isValidIcon(),写入侧、前台渲染侧、后台列表三处共用。 - 留空 = 该菜单项不显示图标。
接管模式说明
- 接管开启时,核心默认导航项(首页 / 论坛 / 博客 / 搜索 / 主题 / 语言 / 布局)不再显示, 顶部菜单完全由本插件的列表决定;想保留哪个系统按钮,在「接管开关」里勾选。
- 接管关闭、插件被禁用、或渲染异常时,自动回退核心默认导航(不会白屏)。
- 自定义菜单项之后会继续触发核心的
nav_main_links钩子, 其他插件(如签到)通过该钩子挂的导航入口不会因接管而消失。
下拉交互:点击展开
- PC 与移动端行为一致:点击导航项展开 / 收起下拉(不再使用鼠标悬停展开)。
- 点击面板外、按
Esc、或点开另一个下拉时自动收起。 - ⚠️ 带下拉的导航项本身不跳转:点击它只会展开下拉。 如果需要它也能跳转,请把目标地址作为它的子项添加一条。
- 带下拉的导航项,其字号 / 字重 / 内边距 / 行高与普通主导航项完全一致(都对齐核心
.mn-nav-links a)。⚠️ 易漏的是行高:核心那条规则没有声明
line-height,靠继承body{line-height:1.6}生效。 插件若给下拉项写死line-height:1.4,盒子会矮 2.8px,悬停高亮比普通项「矮一截、上沿偏低」, 看起来就像「带下拉的菜单稍微低一些」——字号其实一直是同一个 14px。
拖拽排序
- 拖动行左侧的
⋮⋮手柄可调整顺序,松手自动保存。 - 排序只调整同组内的相对顺序,不改变层级——把子项拖到父项前面不会把它升级为一级菜单。 层级由「父级菜单」下拉框决定。
链接(URL)填写规则
| 输入 | 结果 |
|---|---|
/forum |
站内路径,原样使用 |
forum |
自动补前导 / → /forum |
https://example.com |
外链,原样使用 |
mailto:a@b.com |
邮件链接,原样使用 |
javascript:alert(1) / data:... / vbscript:... |
拒绝(保存时提示,不落库) |
//evil.com |
拒绝(协议相对地址会被浏览器当外部站点) |
- 归一化与拦截的唯一口径是
Plugin::normalizeUrl()(模型层),控制器不再自行拼前缀。 - ⚠️ 站内相对路径需对应真实存在的路由,否则前台会渲染成 404 死链(插件不做路由存在性校验,只在后台给出提示)。
性能约定
- 前台每次渲染
renderTopNav()只查 2 条(allSettings()1 条 +listAll()1 条), 设置与菜单项都带进程内缓存,同请求内重复调用不再查库。 - 所有读查询经过
Plugin::budget()闸门,单插件每请求查询上限 5 条; 超限时降级(返回空 → 前台回退核心默认导航),并写error_log。