[开发] 版块强制 Tag 插件 —— 胶囊徽章与列表布局改造方案

👑Lv.11 元老 🌞 骨干会员
2026-10-30 15:48:55
  • 日期:2026-10-30(v2,已并入 4 项决策与「历史前缀兜底渲染」需求)
  • 针对版本:forum_required_tag v1.4.1(plugins/forum_required_tag)
  • 范围:标题前缀徽章的视觉形态;后台「标题前缀管理」表格的列宽与操作区;前缀改名后的历史帖兜底渲染;别名映射机制
  • 状态:规划稿,未改动任何文件

决策记录(本版已锁定)

#事项结论
1胶囊墨色取色方案 C:color-mix(in srgb, 可选色 42%, var(--mn-text)),描边与文字共用该值
2--frt-fg 相关代码保留但不引用,待后续版本再删(contrastText() / pickTextColor / 两个文字色常量一并留用不动)
3改名策略方案 B:允许改名,保存时明确提示历史帖退化
4P2 移动端堆叠并入本次,随胶囊改造一起交付
5历史前缀兜底(新增需求)① 映射未命中 → 用中性灰透明胶囊渲染,不跳过;② 预留 aliases 字段,改名自动记旧名并映射到新色

一、核查结论

1.1 插件现状

项实际情况
版本 / 入口v1.4.1,plugin.json → admin_url: /admin/forum-required-tag,activated: true
代码规模Plugin.php 1191 行、Tokenizer.php 626 行、AdminController 207 行、FrontController 98 行、assets/style.css 271 行、assets/script.js 477 行、views/admin.php 379 行
前台徽章相关hook/layout_head_end.php(直出映射)、assets/script.js L59-111(渲染)、assets/style.css L162-179(样式)
后台前缀表格views/admin.php L133-242,Tab 2「标题前缀管理」
数据表frt_forum_prefixes(id, forum_id, prefix, color, sort_order, enabled, created_at),UNIQUE(forum_id, prefix)

1.2 徽章渲染链路(四段,全部在插件内)

① 服务端  hook/layout_head_end.php L35-40
   前台非 admin 模板 → 输出 <script type="application/json" id="frt-badge-map">
   {"求助":"#1d4ed8", "公告":"#047857", ...}   ← Plugin::getBadgeMap(),文件缓存 data/frt_badge_map.json

② 浏览器  assets/script.js applyTitleBadges() L63-88
   扫描 a[href*="/thread/"] / .thread-title / .mn-thread-card-title
   把标题首个非空文本节点开头的 [前缀] 替换为 <span class="frt-title-badge">
   仅写 CSS 自定义属性:--frt-bg(底色)、--frt-fg(当前为"对比色文字")

③ 样式    assets/style.css L166-179 .frt-title-badge
   底色 = var(--frt-bg),文字 = var(--frt-fg),padding 1px 7px,border-radius 4px

④ 后台预览 views/admin.php L180,复用同一个类 .frt-title-badge(附加 .frt-badge-preview),
   script.js initAdminColors() L125-130 用同一算法重算 --frt-fg

结论:徽章的全部形态只由 style.css 一个类控制,服务端与 JS 只负责注入两个自定义属性。胶囊化不需要改渲染链路,只需改样式 + 调整属性语义,改动面很小。

1.3 颜色链路

  • 可选色:Plugin::PRESET_BADGE_COLORS 固定 8 色(#1d4ed8 蓝 / #047857 绿 / #b45309 橙 / #b91c1c 红 / #6d28d9 紫 / #0e7490 青 / #be185d 玫红 / #111827 深灰黑),默认 #1d4ed8。
  • 文字色:Plugin::contrastText()(PHP)与 script.js pickTextColor()(JS)用同一 WCAG 算法,在 #ffffff / #1f2937 中取对比度高者。因 8 色全为深色阶,实际恒为白字。
  • 存储:color 字段只存描边/底色色值,无第二字段。

1.4 列表页结构与宽度瓶颈

Tab 2 表格 4 列,views/admin.php L158-221:

列内容当前实际宽度需求
1 标题前缀1 个徽章预览窄
2 徽章颜色8 个 22px 圆形色块(间隔 5px,约 216px)+ 保存按钮最宽
3 状态「图标 + 启用/禁用」按钮,约 64px中
4 操作仅 1 个删除图标按钮最窄

关键发现(根因):表格外层容器是 <div class="admin-section admin-form-lg">(L42),而 admin.css L388 定义 .admin-form-lg { max-width: 700px; }。整个 Tab 被锁在 700px 内,4 列按内容挤占,色块列吃掉一半,操作列只剩一个图标的位置。因此"状态区太宽、操作区太窄"不是列宽设置问题,而是容器上限 + 色块列过宽共同造成。只调列宽、不放宽容器,编辑按钮加进去仍会挤压前缀列。

1.5 编辑能力的缺口

插件当前完全没有"编辑前缀"能力,操作区加编辑按钮不是纯 UI 改动:

层现状
路由 hook/admin_route_register.php只有 add-tag / add-prefix / prefix-color / delete / toggle / copy / delete-record / save-autotag / save-stopwords,无 edit/update
控制器 AdminController.php有 addTag / addPrefix / setPrefixColor / delete / toggle,无 update
数据层 Plugin.php有 addPrefix / setPrefixColor / deleteItem / toggleItem,无改名或整体更新方法
视图无编辑表单;改色只能点色块 + 单独保存
语言包zh / en / zh_tw 各 60 键,无 edit 相关键

即:编辑按钮需要「路由 + 控制器方法 + 数据层方法 + 视图 + 三语键 + 缓存刷新」六件套,属于新增功能而非改版式。

1.6 顺带发现的三个问题

  1. 核心 CSS 已耦合本插件类:assets/css/modern.css L2813 有 .thread-title .frt-title-badge { font-size: 13px; }。胶囊化后该规则仍然成立(只影响字号),不必改核心文件;但需在验收时确认详情页字号与列表页一致。
  2. 插件表格在移动端会横向溢出:admin.css 的移动端堆叠规则只作用于 .admin-table,而插件表格用的是自定义 .frt-table,无对应规则。4 列 + 8 色块在 360px 下必然溢出。(对应决策 4,已并入本次范围。)
  3. 暗色主题下徽章不可读:见 §3.2,这是胶囊化必须一并解决的既有缺陷——底色填充时白字尚可,改成"透明底 + 同色文字"后,深色阶颜色在暗色背景上会直接糊掉。

1.7 别名机制的落地前置:补列能力已具备

Plugin::ensureColumn()(Plugin.php L194-208)已经是一套经过验证的幂等补列机制:PRAGMA table_info 探测缺列 → ALTER TABLE ... ADD COLUMN,列名白名单校验 ^[a-z_]{1,32}$(aliases 可通过),SQLite 要求"恒定默认值"也满足(TEXT NOT NULL DEFAULT '[]')。

现有 v1.0.0 → v1.1.0 补 color 列走的就是这条路(L179 一行调用)。新增 aliases 列可完全复用,无需新写迁移框架。同时 ddlPrefixes() 需为全新安装补上该列,两条路径保持一致。


二、目标与红线

2.1 目标

  1. 标题前缀由"实心彩色底 + 白字"改为透明底胶囊:border-radius 取半高、1px 描边、无填充,描边色与文字色由同一个 42% 混色表达式得出。
  2. 收窄状态列,加宽操作列,使操作列能并排容纳「编辑 + 删除」两个按钮。
  3. 操作区新增编辑按钮,可修改前缀名与描边色;改名自动记入 aliases。
  4. 历史前缀不出现视觉突变:映射未命中的 [xxx] 用中性灰透明胶囊渲染;改名后的旧名经 aliases 继续显示为原彩色胶囊。
  5. 前缀表格在移动端堆叠显示,不横向溢出。

2.2 红线

  • 不改 app/ 与 assets/css/ 下的核心文件(含 modern.css L2813 那行,保持原样)。
  • 不做破坏性表结构变更:aliases 只做增量补列(恒定默认值 '[]'),不重建表、不改既有列、不回填改写历史数据。
  • 零内联样式:视图内只允许写 CSS 自定义属性(--frt-bg),不写 background / color / width 等 CSS 属性(沿用插件既有约定)。
  • 三语语言包键集与键序逐行一致(zh / en / zh_tw),不出现裸键。
  • aliases 只影响渲染,不影响提交校验:见 §5.6。
  • 不因编辑功能破坏 UNIQUE(forum_id, prefix) 约束与既有提交校验链路。

三、胶囊徽章方案

3.1 形态参数

style.css 中 .frt-title-badge 的目标形态(示意,非最终代码):

属性现值目标值说明
backgroundvar(--frt-bg) 实心transparent用户诉求:透明
border无1px solid <墨色>外框用可选色
colorvar(--frt-fg)(白字)同 border 墨色文字与描边同色
border-radius4px999px胶囊形,圆角恒等于半高
padding1px 7px1px 8px描边占 1px,横向补 1px 保持文字不贴边
font-size.8em不变详情页由核心 13px 覆盖,保持现状
line-height1.51.4胶囊在行内不撑高,避免标题行距变化

描边与文字使用同一个颜色表达式,因此"文字与胶囊外框颜色一致"在实现上是由同一个值保证的,不存在两处色值漂移。

3.2 同色取色:方案 C(已定)

"描边与文字同色"意味着颜色同时承担两种职责。用现有 8 色原色直接做文字色,实测对比度:

色色值白底对比暗色卡片底 #1e293b 对比判定
蓝#1d4ed86.702.18暗色不足
绿#0478575.482.67暗色不足
橙#b453095.022.91暗色不足
红#b91c1c6.472.26暗色不足
紫#6d28d97.102.06暗色不足
青#0e74905.362.73暗色不足
玫红#be185d6.042.42暗色不足
深灰黑#11182717.741.21暗色基本不可见

站内实际存在暗色主题(modern-night.css,卡片底 #1e293b,且支持"夜间自动切换"强制启用)。可行的对比度边界:

  • 若要满足 AA 正文级 4.5:浅底要求相对亮度 ≤ 0.183,暗底要求 ≥ 0.273 —— 区间为空,单色无解。
  • 若放宽到 AA 大字号级 3.0:浅底 ≤ 0.300、暗底 ≥ 0.165 —— 有解,但只剩很窄的中间调,会牺牲 8 色的辨识差异。

采用方案 C:墨色 = color-mix(in srgb, <可选色> 42%, var(--mn-text)),border-color 与 color 共用该表达式。实测(--mn-text 浅色 #1a1a2e、暗色 #e2e8f0):

色浅色主题墨色白底对比暗色主题墨色卡片底对比
蓝#1b307512.17#8fa7e66.16
绿#11413f11.34#85b9b06.66
橙#5b321e10.94#cfa98f6.77
红#5d1b2612.73#d192975.76
紫#3d207612.60#b197e65.88
青#15405711.04#89b7c86.74
玫红#5f194212.32#d391b25.90
深灰黑#16192b17.38#8a919c4.61

8 色两端全部 ≥ 4.5,最差为深灰黑 4.61。42% 是实测边界(40% 时深灰黑 4.85,再往下色相偏移过大)。

color-mix 在项目已有先例(assets/css/portal.css L32、L56、L635 三处使用),不构成新增的浏览器基线风险。

3.3 --frt-fg 处理:保留但不引用(已定)

变量语义原是"按底色亮度二选一的白字/深字"。胶囊化后文字色由 CSS 的 color-mix 直接算出,该变量失去用途。按决策 2:

  • style.css 不再引用 --frt-fg(color 改为 color-mix 表达式)。
  • views/admin.php L180、script.js 的 applyTitleBadges() / setBadgeColors() 继续写该属性,不删代码。
  • Plugin::contrastText()、BADGE_TEXT_LIGHT / BADGE_TEXT_DARK、JS 的 pickTextColor() 及亮度工具函数全部保留不动。

这样做的收益是本次改动面最小、可随时回退;代价是留有一段"写入但无人读取"的死数据。须在文件内注释标明"保留待删",并在 说明.md / CHANGELOG 记一条待清理项,避免下一个版本又当成有效逻辑去维护。

3.4 后台预览

views/admin.php L180 的 .frt-title-badge.frt-badge-preview 复用同类,胶囊化后自动跟随。initAdminColors() 仍会重算并写入 --frt-fg,但因 CSS 不再引用,不影响显示,也不需要为它改 JS(与决策 2 一致)。

色块选择器 .frt-swatch(22px 实心圆,background: var(--frt-bg))保持实心:它的语义是"选哪个颜色",实心圆比空心圆环更易辨识,且与胶囊的描边色一一对应。

3.5 影响面

徽章出现在 8 类前台位置(说明.md §3.6 记录:forum/index、forum/category、home/index_default、thread/show、search/index、profile/index、tags/show、_components/right_sidebar),全部走同一个 .frt-title-badge 类和同一段 applyTitleBadges()。改一处即可全站生效,无需逐视图处理。


四、列表布局与编辑按钮

4.1 宽度瓶颈

如 §1.4 所述,700px 容器上限是主因。目标:

  1. Tab 2 表格容器放宽。建议拆出修饰类,仅作用于该表格外层,避免影响 Tab 1 / 3 / 4 的既有视觉。
  2. 色块列收窄:8 个色块 22px → 18px、间隔 5px → 4px,可省约 40px。
  3. 状态列收窄(见 §4.2)。
  4. 操作列以 width: 1%; white-space: nowrap 让其按内容自适应,剩余空间回流给前缀列与颜色列。

4.2 列宽分配建议

列建议具体做法
1 标题前缀自适应,吃掉剩余宽度徽章 white-space: nowrap,过长前缀由 20 字上限兜底
2 徽章颜色固定不换行色块缩至 18px;保存按钮改 btn-sm;整体 white-space: nowrap
3 状态width: 64px按钮紧凑化:padding: 2px 6px、图标 12px、保留"启用/禁用"二字;white-space: nowrap
4 操作width: 1%; white-space: nowrap并排「编辑 + 删除」,间距 6px

预计操作列从"1 个图标 ≈ 32px"扩到"编辑 + 删除 ≈ 96px"。增宽来源主要是容器放宽 + 色块收窄,不需要靠压缩状态列硬挤。

4.3 编辑入口的交互形态

方案做法评估
A 行内展开(推荐)点击编辑 → 该行下方展开一行编辑表单(前缀名输入框 + 色板 + 保存/取消),原生 JS 切换 hidden字段只有 2 个,行内最省空间;零新页面、零弹窗样式依赖;与现有 .frt-inline-form 一致
B <dialog> 弹窗复用 orange_city 的 <dialog class="oc-admin-dialog"> 先例视觉更规整,但要引入弹窗样式与焦点管理,收益不抵成本
C 独立编辑页新增 /admin/forum-required-tag/edit?id=N最重,与"少点一次"的诉求相反

采用 A。编辑行复用新增表单结构(prefix 输入 + frt-swatch-picker + 隐藏 id),提交到新增的 update 路由。

4.4 服务端需新增的部分

层新增项要点
路由POST /forum-required-tag/update-prefix → AdminController::updatePrefix注册在 hook/admin_route_register.php,与现有 10 条同风格
控制器updatePrefix()Csrf::verifyOrDie → 取 id / forum_id / prefix / color → 调数据层 → 写 $_SESSION['frt_msg'](含"历史帖提示"文案)→ 重定向回 ?tab=prefixes&forum_id=N
数据层Plugin::updatePrefix(int $id, string $prefix, string $color): bool校验 id > 0、名称非空、≤20 字、剥方括号(与 addPrefix 同口径);color 走 normalizeHex,为空落默认色;捕获 UNIQUE 冲突返回 false;改名时把旧名写入 aliases;成功后必须 flushCaches()
视图每行追加编辑行(默认 hidden)+ 操作列编辑按钮(data-frt-edit + 行 id)零内联 CSS 属性,色值只写 --frt-bg
JSinitAdminEdit():按 data-frt-edit 展开/收起、同步色板选中态、取消时还原色板绑定走 .frt-swatch-picker 全局扫描,编辑行追加后自动生效
语言包edit_btn / edit_save / edit_cancel / msg_edit_ok / msg_edit_dup / edit_rename_tipzh / en / zh_tw 三份同步,键序一致
文档说明.md 功能清单、§3.6 颜色章节、CHANGELOG版本号升 v1.5.0(新增编辑 + 别名 + 兜底渲染)

flushCaches() 已包含 invalidateBadgeMap() 与 PageCache::invalidate()(Plugin.php L946-954),改名或改色后前台立即生效。


五、历史前缀兜底渲染(新增需求)

5.1 需求与要解决的问题

改名后,历史帖标题里写的是旧前缀。若映射按前缀名精确匹配,旧名不再命中 → 徽章消失、退化为裸文本 [旧名]。用户明确要求不允许出现这种视觉突变,因此加两层保险:

第 1 层(兜底,立即生效):映射未命中 → 中性灰透明胶囊,仍然渲染,不跳过
第 2 层(精确,逐步覆盖):aliases 字段记录旧名 → getBadgeMap() 把旧名指向新前缀的色

两层是包含关系:第 2 层命中则用原彩色;第 2 层不命中则第 1 层用中性灰兜住。所以升级瞬间(aliases 尚空)也不会出现裸文本。

5.2 第 1 层:中性灰兜底胶囊

前端判定改动(script.js applyTitleBadges() L74-77):当前逻辑是"映射里没有 → 打标记后 return,保持裸文本"。改为:映射命中走原彩色;未命中仍创建徽章,只把颜色换成中性灰。

中性灰的取值同样用 color-mix,基准取主题变量而非硬编码,保证浅色/暗色主题都成立:

墨色 = color-mix(in srgb, var(--mn-text) 65%, var(--mn-bg-card))

实测(--mn-text 浅 #1a1a2e / 暗 #e2e8f0,--mn-bg-card 浅 #ffffff / 暗 #1e293b):

混合比例浅色主题灰白底对比暗色主题灰卡片底对比最差
55%#81818c3.85#8a929f4.663.85
60%#7676824.48#949ca85.284.48
65%(选)#6a6a775.33#9da5b15.895.33
70%#5f5f6d6.28#a7afba6.616.28

取 65%:两端均 ≥5.3,灰阶明显但不抢眼,与彩色胶囊在视觉上可区分(灰 = "这个前缀已无对应配置")。

实现上用独立类而不是给 .frt-title-badge 加内联色:新增 .frt-title-badge--muted(或由 JS 打 data-frt-muted="1" 属性,CSS 用属性选择器)。理由:零内联、颜色仍全在样式表、且便于后台预览复用。

5.3 第 1 层的副作用(必须知情)

现有逻辑刻意"只处理映射命中的前缀,用户手写方括号一律不动"(script.js L77 注释)。改成兜底后,用户在标题开头手写的 [注意]、[已解决] 这类非配置前缀,也会变成灰色胶囊。

影响可控的部分:正则锚定在标题首个文本节点开头(^\s*\[([^\]\r\n]{1,20})\]\s*),标题中间的方括号不受影响;长度上限 20 字也沿用。

建议加一个开关兜住争议:复用已有 KV 表 frt_settings(K_AUTO_TAG_ENABLED 同款模式),键名 fallback_unknown_badge,默认开启(符合本次需求),关闭后回到"未命中即跳过"的旧行为。成本仅一个 KV 键 + 后台 Tab 2 一个复选框,无表结构变化。

5.4 第 2 层:aliases 别名映射

表结构(增量补列,不动既有列)

aliases TEXT NOT NULL DEFAULT '[]'      -- 存 JSON 数组,如 ["求助","帮忙"]

两条路径同时补:

  • 全新安装:ddlPrefixes() 的 CREATE TABLE 里带上该列。
  • 存量升级:ensureSchema() 里加一行 self::ensureColumn('frt_forum_prefixes', 'aliases', "TEXT NOT NULL DEFAULT '[]'")(紧跟现有 color 补列之后,L179)。ensureColumn 已有白名单校验(^[a-z_]{1,32}$,aliases 通过)与 PRAGMA 探测,幂等安全。

写入:改名时自动追加旧名

Plugin::updatePrefix() 内,当 新名 !== 旧名 时:

  1. 读取该行现有 aliases,json_decode 后过滤(只留字符串、trim、剥方括号、≤20 字、去重)。
  2. 把旧名追加进去;若旧名已存在则移到末尾(保持"最近用过的旧名优先")。
  3. 移除与新名相同的条目(防自引用)。
  4. 上限 20 条 / 400 字符,超出丢最旧的——防长期反复改名把字段撑爆。
  5. json_encode(..., JSON_UNESCAPED_UNICODE) 写回。

读取:rebuildBadgeMap() 展开别名

rebuildBadgeMap()(Plugin.php L905-932)当前 SELECT prefix, color ... ORDER BY id ASC,改为带 aliases,并调整优先序:

第一轮:先按现有逻辑灌入"活前缀名"(id ASC,同名前缀取 id 最小者先配先生效)
第二轮:再遍历每行的 aliases,仅当该别名在 map 里"尚不存在"时才写入,值 = 本行 color

顺序不能反:活前缀名优先级必须高于别名。否则 A 前缀改名留下的别名,可能把另一个同名的新前缀颜色顶掉。

边界处理

场景行为
别名与某活前缀同名活前缀胜出(第二轮 array_key_exists 判断已保证)
同一别名出现在多行 aliases按 id ASC 先到先得(与现有同名前缀规则一致)
前缀被删除整行(含 aliases)消失 → 旧帖回落到 §5.2 灰色胶囊,这正好是"该前缀已下线"的合理信号
前缀被禁用(enabled=0)现状是禁用的不参与映射 → 旧帖回落灰色。可选优化:禁用仍保留别名映射;本版不做,保持与现有 WHERE enabled = 1 口径一致
一键复制配置 copyConfig()不复制 aliases(见 §5.5)
aliases 是脏数据(非 JSON / 非数组)json_decode 失败即视为空数组,不抛错、不影响其他行

5.5 别名的作用域问题(复制配置)

copyConfig()(Plugin.php L1110-1142)用 INSERT OR IGNORE ... SELECT 复制 prefix / color / sort_order / enabled。建议不复制 aliases:

徽章映射是按前缀名全局化的(无版块上下文),别名一旦跟着复制到别的版块,等于把 A 版块的历史名硬塞进全局映射,可能顶掉 B 版块某个正常前缀的渲染。别名本质是"某个版块历史帖子的痕迹",不具备跨版块可移植性。复制后目标行的 aliases 取默认 '[]'。

5.6 提交校验不受别名影响(红线)

hook/route_before_dispatch.php → Plugin::validateSubmit() L985-992 用 str_starts_with($title, '[' . $p['prefix'] . ']') 逐条匹配 getForumPrefixes() 的 prefix 字段。该处不改:

  • 别名只用于渲染历史帖,不作为新提交的合法前缀。
  • 否则改名形同虚设(用户继续用旧名也能过校验),且 aliases 会变成绕过强制前缀的后门。
  • 因此改名后新发帖必须用新名,旧名会被拒——这是预期行为,须写进后台改名提示文案里。

5.7 升级瞬间的行为(无 aliases 数据的存量站)

升级到 v1.5.0 时,所有存量前缀的 aliases 都是 '[]'。此前若已发生过改名(历史上只能"删了重建",旧帖里的旧名已不在表内),这些旧名永远拿不到别名记录 → 由 §5.2 的灰色胶囊兜住,不会出现裸文本。这是两层设计的分工:别名负责"从现在起"的改名,灰色兜底负责"历史遗留"。

可选的补录入口(P2,本版不做):后台 Tab 2 提供"手动补录别名"输入框,让站长给历史上改过名的前缀补几个旧名。因为需要人工回忆旧名,自动化价值低,暂不纳入。


六、改动清单

6.1 P1(本次交付,共 10 项)

#文件改动
1assets/style.css① .frt-title-badge 改胶囊:background: transparent + 1px solid 描边 + border-radius: 999px,border-color 与 color 共用 color-mix(in srgb, var(--frt-bg) 42%, var(--mn-text)),line-height 1.5 → 1.4;② 新增 .frt-title-badge--muted(灰胶囊,墨色 color-mix(in srgb, var(--mn-text) 65%, var(--mn-bg-card)));③ .frt-table 列宽/状态按钮紧凑化 + 编辑行样式;④ 移动端堆叠规则(决策 4)
2views/admin.php① 前缀表格外层加宽度修饰类,突破 700px;② 操作列加编辑按钮 + 删除并排;③ 每行追加编辑行(默认 hidden);④ 前缀列下方展示别名(有则显示,mn-fs-12 c-999);⑤ Tab 2 增加"未知前缀兜底"复选框;⑥ --frt-fg 写入保留(决策 2)
3assets/script.js① applyTitleBadges() 未命中改为渲染灰胶囊(读开关);② 新增 initAdminEdit();③ --frt-fg 相关代码保留不删(决策 2)
4Plugin.php① ddlPrefixes() 加 aliases 列;② ensureSchema() 加一行 ensureColumn('frt_forum_prefixes','aliases',"TEXT NOT NULL DEFAULT '[]'");③ rebuildBadgeMap() 两轮写入(活前缀优先,别名次之);④ 新增 updatePrefix()(含别名追加、20 条/400 字符上限);⑤ 新增 KV 键 K_FALLBACK_UNKNOWN_BADGE + getFallbackBadgeEnabled() / 保存方法;⑥ copyConfig() 明确不复制 aliases
5AdminController.php新增 updatePrefix();新增兜底开关的保存方法(或并入现有 saveAutoTag 风格)
6hook/admin_route_register.php新增 1–2 条路由(update-prefix、兜底开关)
7hook/layout_head_end.php把兜底开关并入 #frt-badge-map 的输出条件与内容,详见下方修正说明
8lang/zh.php、en.php、zh_tw.php各新增 9 个键,键集与键序逐行一致(清单见 §6.4)
9plugin.jsonversion → v1.5.0;description 补胶囊化 + 编辑 + 别名 + 兜底一段
10说明.md、CHANGELOG.md§3.6 颜色章节改写(胶囊 + 42% 混色 + 灰兜底 + 别名);功能清单加编辑;记 --frt-fg 待清理项

改动顺序建议(降低联调成本):

① 表结构(aliases 补列 + ddlPrefixes)→ 可单独跑,幂等
② getBadgeMap/rebuildBadgeMap(别名展开)→ 用 CLI 脚本直接 dump 映射验证
③ 灰兜底(CSS 类 + JS 未命中分支 + 开关)→ 前台独立可见,不依赖编辑功能
④ 编辑功能(路由 + 控制器 + updatePrefix + 视图 + JS)
⑤ 列表布局与移动端堆叠
⑥ 三语包 + 文档 + 版本号

6.2 修正说明:layout_head_end.php 的输出条件(重要)

这是本次方案里最容易踩的一处坑,单独说明。

现状(hook/layout_head_end.php):

  • L35:if (!empty($badgeMap)) 才输出 <script id="frt-badge-map">。
  • L100:if ($isPostPage || !empty($badgeMap)) 才输出 style.css + script.js。

问题:灰兜底开启时,若站点一个前缀都没配($badgeMap 为空),映射节点与两个资源都不会输出 → initTitleBadges() 直接 return(script.js L91-92 首行就是找节点)→ 兜底逻辑根本不执行。同理,若站长把所有前缀都删了,灰兜底也随之静默失效。

修正(三处联动,缺一不可):

位置改为
L35 输出条件增加"或兜底开关开启"的判断,映射为空时输出空对象 {}(仍走 JSON_HEX_TAG / JSON_HEX_AMP 转义)
映射内容开关状态用 data-fallback="1" 属性挂在 <script> 上,扁平映射结构不动(推荐);或改为 {"map":{...},"fallback":true} 结构,但需同步改 script.js 的解析
L100 资源条件增加"或兜底开关开启"的判断,保证无前缀配置时也加载 CSS/JS

三处条件的具体形态:

// L35
if (!empty($badgeMap) || $fallbackOn) { ... }

// L100
if ($isPostPage || !empty($badgeMap) || $fallbackOn) { ... }

取舍:data-fallback="1" 方案只改 hook 两行 + JS 一行读属性,不碰映射结构,也不影响 getBadgeMap() 的既有缓存格式,推荐。代价是开关状态随 HTML 直出,被 PageCache 缓存——但开关变更时会走 flushCaches()(含 PageCache::invalidate()),与改色的处理一致,不会出现"关了还在生效"。

6.3 P2(本版不做,留档)

#内容不做理由
11--frt-fg 及 contrastText / pickTextColor 清理决策 2 明确留待后续版本
12别名手动补录输入框需人工回忆旧名,自动化价值低(§5.7)
13禁用前缀仍保留别名映射与现有 WHERE enabled = 1 口径冲突,本版保持一致
14色块列折叠为"当前色 + 编辑入口"会失去"点色块即改色"的快捷路径,需另行确认

6.4 三语新增键(各 9 个,键序一致)

键zhenzh_tw
edit_btn编辑Edit編輯
edit_save保存修改Save儲存修改
edit_cancel取消Cancel取消
msg_edit_ok修改已保存。Changes saved.修改已儲存。
msg_edit_dup修改失败:该版块已存在同名前缀。Failed: a prefix with this name already exists in this forum.修改失敗:該版塊已存在同名前綴。
edit_rename_tip改名后,历史帖子标题里的旧前缀不会被自动替换;旧名会作为别名继续以原颜色显示。After renaming, the old prefix in existing thread titles is not rewritten; it keeps its original color via an alias.改名後,歷史帖子標題裡的舊前綴不會被自動替換;舊名會作為別名繼續以原顏色顯示。
aliases_label别名Aliases別名
set_fallback_badge未知前缀也渲染为灰色胶囊Render unknown prefixes as muted pills未知前綴也渲染為灰色膠囊
set_fallback_tip开启后,标题开头未被任何前缀配置命中的 [xxx] 也会渲染为灰色胶囊(含用户手写的方括号);关闭则保持原样不渲染。When on, a leading [xxx] not matching any configured prefix is rendered as a muted pill (including hand-written brackets). When off, it is left as-is.開啟後,標題開頭未被任何前綴配置命中的 [xxx] 也會渲染為灰色膠囊(含使用者手寫的方括號);關閉則保持原樣不渲染。

七、验收与回归

7.1 功能验收

胶囊形态

  1. 前台 8 类标题位置全部显示为透明胶囊,无实心底残留,方括号不显示。
  2. 描边色与文字色肉眼一致(同一表达式,理论上必然一致)。
  3. 后台 Tab 2 预览徽章与前台形态、颜色完全一致(含 modern-night)。
  4. 8 色在浅色主题与 modern-night 下均清晰可读,按 §3.2 表逐色核对(最差深灰黑 4.61)。

历史前缀兜底

  1. 手工往标题写一个未配置的 [测试前缀] → 开关开启时渲染为灰色胶囊;关闭时保持裸文本。
  2. 标题中间出现 [xxx] → 不渲染(正则锚定开头)。
  3. 前缀名超过 20 字 → 不渲染(沿用现有上限)。
  4. 改名 A → B:A 立即成为 B 的别名,历史帖 [A] 仍显示原彩色(不是灰色)。
  5. 把别名记录手动清空(模拟升级瞬间)→ 历史帖 [A] 显示灰色胶囊,不是裸文本。
  6. 删除前缀 B → 历史帖 [B] 与 [A] 都回落灰色。

编辑功能

  1. 编辑按钮 → 行内展开 → 改名 + 改色 → 保存 → 列表与前台徽章同步更新。
  2. 改名为已存在的前缀名 → 提示"已存在同名",不产生重复行、不 500。
  3. 改名保存后页面出现 edit_rename_tip 提示文案。
  4. 编辑行取消 → 表单还原,不改数据、不误提交。

布局

  1. 前缀表格不再被限制在 700px,操作列能并排显示「编辑 + 删除」且不换行。
  2. 360px 视口下表格堆叠为多行,无横向滚动条。
  3. Tab 1 / 3 / 4 的既有视觉未变。

7.2 回归点

项检查
提交校验(红线)改名后新发帖用旧名必须被拒(validateSubmit 不认别名);用新名通过
徽章映射缓存data/frt_badge_map.json 在改名/改色/开关变更后被删除并重建
别名优先级构造"A 的别名 = B,且 B 是另一个活前缀" → B 显示的是活前缀自己的颜色,不是 A 的
多行同别名两行的 aliases 含同名 → 按 id 最小者生效(与同名前缀规则一致)
脏数据容错手工把 aliases 写成 not-json → 该行被跳过,其他行正常,无 500
copyConfig()复制后目标版块 aliases 为空数组,不继承源版块别名
三语包三份键集与键序逐行一致,无裸键
零内联视图内无 style="background/color/width...",仅 --frt-bg
核心未动git status 确认 app/、assets/css/ 无改动(含 modern.css L2813)
JS 缺席降级禁用 JS → 标题退化为 [前缀] 纯文本,不出现空胶囊或错位
存储压力反复改名 30 次 → aliases 稳定在 20 条 / ≤400 字符,不再增长

7.3 视觉核对清单

  • 胶囊高度不撑开标题行距(对比改造前后行高、列表行高)
  • 相邻两个前缀徽章之间有间距,不粘连
  • 详情页 h1 中徽章与分类/置顶标签的间距保持(.thread-title .frt-title-badge { margin-left: 6px })
  • 长前缀(20 字上限)不换行、不溢出
  • 彩色胶囊与灰色兜底胶囊并排时,色相差异清晰但灰的不抢眼

八、风险与未决

风险影响应对
灰兜底扩大到用户手写方括号用户习惯用 [注意] 开头会被加胶囊提供 fallback_unknown_badge 开关(默认开),关闭即回旧行为;文案写清范围
别名污染全局映射A 版块的旧名顶掉 B 版块同名的活前缀两轮写入严格"活前缀优先";copyConfig() 不复制别名
别名无限增长aliases 字段撑大,映射文件变大20 条 / 400 字符上限,超出丢最旧
改名后旧名被当合法前缀违背强制前缀约束validateSubmit 不认别名(§5.6 红线),并在提示文案里说明
700px 容器放宽影响其他 Tab视觉回退只加作用于前缀表格的修饰类,不改 .admin-form-lg 本身
color-mix 不支持颜色表达式失效 → 文字继承正文色项目已有 3 处先例,基线一致;可先声明 color 后备值
42% 混色观感偏暗用户觉得"不是我要的那个蓝"已锁定决策 1;如后续反馈强烈,只需调 CSS 里一个百分比,不动数据与代码结构

已全部定案,无遗留待确认项(决策 1–5 见文首)。实施时若 §5.3 的兜底开关默认值需要改变,属产品取向调整,不影响本方案结构。


九、明确不做

  1. 不改 app/ 与 assets/css/ 任何文件(含 modern.css 对 .frt-title-badge 的字号覆盖)。
  2. 不做破坏性表变更:aliases 只增量补列,不重建表、不改既有列、不回填历史数据。
  3. 不改徽章的服务端直出格式与 BADGE_SELECTOR 扫描范围。
  4. 不改 validateSubmit() 的提交校验逻辑(别名不参与校验)。
  5. 不做历史帖子标题的批量前缀改写。
  6. 不删除 --frt-fg / contrastText() / pickTextColor()(决策 2,留待后续版本)。
  7. 不引入前端框架或第三方库;编辑行与移动端堆叠均用原生 JS + CSS。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 8 | Replies 0

All Replies (0)

No replies yet

Please Log in