本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/Xenforo主题对齐与改造报告_2026-09-28.md。
Xenforo 主题对齐与改造报告(2026-09-28)
范围:
assets/themes/Xenforo/全部改动,共 7 轮(功能对齐 → 视觉改造 → 回归修复 → 细节打磨)。当前 sha256:
xenforo.css=b0353dd9…82ba8bca、thread/show.php=43c575b8…513cab45
0. 概览
| 项 | 数据 |
|---|---|
| 起因 | assets/themes/Xenforo 是独立覆盖副本(不是继承),核心后来加的收敛层/组件它全错过了 → 功能脱节 |
| 改动的模板 | 30 个里 12 个(+149 行 / −110 行) |
| 改动的样式 | xenforo.css(第 2~6 轮主战场) |
| 新增文件 | _components/attachment_card.php(主题版附件卡) |
| 刻意未改 | 判定为「设计差异」的 4 项(见 §3) |
一句话:前 4 轮解决「功能脱节」(云存储失效、附件/灯箱缺组件、htmx 契约不成立、hx-select 继承导致点赞/楼中楼全废),后 3 轮解决「视觉与落位」(用户栏、等级块、楼中楼位置与底色)。
1. 先讲机制(决定改法的三条)
TemplateCompiler::resolveTemplate()对$this->include()同样生效 → 主题内放同名_components/xxx.php即可「复用核心调用 + 保持主题视觉」,不必改调用点。本次即用此法做 xen 版attachment_card。- 覆盖层目录名白名单
^[A-Za-z0-9_-]+$;后台请求(/admin)不走覆盖层。 - 主题找不到的模板自动回落
app/Views→ 无视觉差异的组件(如image_lightbox)别另造,直接 include 走核心回落。
2. 按方面说明
A. 功能对齐(第 1 轮)
A1. 上传 URL 收敛 —— 本次最致命的一项
- 25 处
\UPLOAD_URL . $x→$this->uploadUrl($x)(涉及 11 个文件)。 - 根因:
Upload::url()内含upload_url钩子(云存储/CDN 改写),直拼会绕过钩子 →bitiful_storage(缤纷云存储)在 Xenforo 主题下静默失效;同时丢掉 http(s) 直通 / null 归一 / 反斜杠归一。 - 另有 2 处
style="…url('<?php echo $this->e(…) ?>')"→cssValue($this->uploadUrl(…))(CSS 上下文不能用 HTML 转义)。
A2. 附件组件化 + 灯箱
- 新建
assets/themes/Xenforo/_components/attachment_card.php(xen 风格):Upload::isImageAttachment()判图 +?s=thumb缩略图 +data-fullurl+data-action="view-image"+ 下载按钮/次数 + 图片文件分组。 thread/show.php:188 / 193 / 317 / 322改组件调用;:438灯箱改include('_components/image_lightbox')。
A3. htmx 容器 / 空态 / 钩子
- 补
#replieshtmx 容器(thread/show.php:241)、thread.no_replies空态、thread.reply_all标题。 - 补
thread_show_top插件钩子(:57)—— 「跳转后一次性提示」的唯一正解(核心没有 flash 机制)。 - 引用按钮门禁改
$hasReplyForm && $threadQuotable(主帖)/$post['_quotable'](楼层);data-quote-content由content改_content_raw。
A4. 其它功能补齐
tags/index.php:补分页(原本超一页看不到)+tagMax改全站基准(否则同标签跨页字号不一致)。blog/edit.php:补封面移除(remove_cover+blog.cover_remove)。layouts/main.php:补nav.language(语言按钮 title)+ 中栏接入_components/mobile_sidebar。
B. 帖子详情页「布局 + 配色」改造(第 2 轮,14 处,仅 xenforo.css)
目标:对齐 XenForo 2 默认风格(白卡 + 浅灰用户栏 + 归属行无底色)。
- 配色:
--mn-primary#1f4e79 → #185886(XenForo paletteColor1)、--mn-bg-body#eef0f5 → #f5f5f5、--mn-text-muted#8a94a6 → #8c8c8c、新增--xen-user-bg。 - 布局:用户栏白底 → 浅灰、去
border-right、加指向三角::after;归属行去底色去下边框;dateorder:1 + margin-right:auto靠左,anchororder:2,actionsorder:3。 - ⚠️ 关键坑(头部里的插件注入元素):
post_operation_after(楼中楼按钮、屏蔽按钮)输出的是无order、无 auto margin 的直接子元素 → 只给anchor加margin-left:auto不够,插件元素会留在最左。正解 = **.xen-message-header > * { order:2 }兜底归组**。 - ⚠️ 且只能有一个 auto margin:
anchor与actions同时带会平分剩余空间,把日期挤到中间。 - ⚠️
--mn-bg-nav仍是#1f4e79,未在方案内,待定。
C. 回复贴右侧对齐核心 + 楼中楼按钮归位(第 3 轮)
用户诉求:用户栏 120 → 200px;回复贴右侧改成跟默认模板一样;底栏靠底;楼中楼按钮挪进底栏;签名档改 UI。
★★★ 核心根因:floor_reply 的 DOM 契约在主题里不成立
plugins/floor_reply/assets/script.js::init() 依赖核心类名 + 核心嵌套:
var bar = anchor.closest('.thread-post-user-bar');
var floor = bar.parentElement; // ← 必须正好是楼层容器
var voteActions = floor.querySelector('.thread-vote-actions');
quote.insertAdjacentElement('afterend', btn); // 主路径
voteActions.insertBefore(btn, voteActions.querySelector('.mn-ml-auto')); // 兜底
floor.appendChild(anchor); // 锚点 → 楼层末尾主题用 .xen-message-* → closest() 返 null → if (!floor) return; 整楼早退(按钮不挪、锚点不挪、自动展开失效)。
修法(主题侧补契约,不动插件)
| 主题元素 | 追加的标记类 | 作用 |
|---|---|---|
.xen-message-inner | thread-post-user-bar | 令 parentElement = article.xen-message(楼层容器) |
.xen-message-footer | thread-vote-actions | 插件插入目标 |
.xen-message-report | mn-ml-auto | 兜底分支的插入锚点 |
- ⚠️⚠️ 最容易漏:
.mn-ml-auto必须是.thread-vote-actions的直接子元素,否则insertBefore抛NotFoundError(DOMException)→ 整个init()中断,连后续楼层一起废。 - 底栏「靠不到底」真因:回复贴
.xen-message-footer嵌在.xen-message-body内部,而flex:1撑高的是 body 本身 → footer 浮在中间。提到.xen-message-content直接子级(与主帖一致)。 - ⚠️ 补了核心类就要提权抵消 modern.css 的同名规则:
.thread-post-user-bar{padding:12px 20px}/.thread-vote-actions{margin-top:10px}。
D. 三个回归修复(第 4 轮)
症状:① 点赞后按钮不见 ② 楼中楼点不开 ③ 回复贴之间没了间隔。
①+② 同一个真因:#replies 的 hx-select="#replies" 被后代继承
htmx 里 hx-select 是继承属性 → 楼层内所有请求都去响应体里找 #replies:点赞 hx-post="/api/vote"(响应是 <span class="vote-buttons">…</span>)、插件 htmx.ajax({source: btn}) → 都找不到。htmx 的 no-match 分支 return [be] 返回空占位节点 → swap 把目标整个换掉 → 按钮/锚点从 DOM 消失(HTTP 200、控制台无报错)。
→ 正解 = 抄核心,不是自己发明:核心 app/Views/thread/show.php:269 每个楼层容器上就有 hx-disinherit="*",主题抄结构时漏了这一层。落到 thread/show.php:257。
- ⚠️
hx-disinherit必须挂在触发元素的祖先上(htmx 走查跳过元素自身)。 - ⚠️ 别只挂
.xen-message-footer:只覆盖主分支,插件兜底分支会漏。
③ 真因:换了父容器,丢了父容器的 gap
第 1 轮把回复贴从 .xen-messageList(gap:10px)挪进新容器 #replies.xen-replyList(原本只有 margin-top)→ 楼层变普通块相邻。→ 补 display:flex; flex-direction:column; gap:10px(xenforo.css:456)。
E. 楼中楼只落右侧 + 等级块改 2 行(第 5 轮)
- 楼中楼:插件把锚点
append到楼层容器,锚点是块级元素 → 从卡片左缘起排、横跨整卡 → 压在 200px 用户栏下面。→ 主题推到内容列内缘(xenforo.css:404-405,margin-left: calc(var(--xen-user-w) + 16px)= 216px)。 - ⚠️ 必须提权到 (0,2,0):插件那条是 (0,1,0) 且用
margin简写,同权重会连margin-left一起抢走。 - 顺手把用户栏宽度抽成
--xen-user-w(:86),以后改宽度一处生效。 - 等级块:「级别徽章 + 信任等级徽章」原本挤在同一行 →
.xen-message-badges改flex-direction: column(:377),各占一行。该块主帖(show.php:122)+ 楼层(:278)共用,一处改动两处生效。 - 窄屏(≤900px,用户栏折成满宽顶栏)还原:
margin-left: 16px、徽章回row。
F. 楼中楼间距 + 用户栏底色贯通(第 6 轮)
- 间距:插件给的是
margin-top: calc(-1 * var(--mn-pad-flow, 20px) + 12px / 8px)= -8px / -12px —— 这是为核心.mn-p-20的 20px 底部内边距做的抵消。主题的底栏只有 10px 内边距 → 负值把楼中楼往上压进底栏按钮行约 2px。
→ 主题接管 margin-top: 6px(净间距 = 10 + 6 = 16px,与 .xen-signature 同节奏)。
- 底色:
.xen-message-inner { align-items: stretch }只把用户栏撑到内容列高度;楼中楼块在 inner 之外 → 那一横条里左侧 200px 露的是卡片白底,与上方#f5f5f5断层。→ 给「含楼中楼的卡片」铺左列渐变(xenforo.css:412-416)。 - ⚠️ 必须用
:has()精确限定,不能写成「所有.xen-message」:博客评论同样是.xen-message(blog/detail.php:76),但桌面端用户栏只有 120px,渐变会铺错宽度。 - ✅ 已确认博客评论没有楼中楼(走
blog_comment_operation_after,不是post_operation_after)→:has()天然不命中,零误伤。 :has()不是新依赖(modern-spectrum.css:570已在用);不支持时整条规则被丢弃 → 只是没这条底色,布局不受影响(优雅降级)。- ⚠️ 窄屏必须在媒体查询里
background-image: none,否则左半边整条变灰。
3. 刻意未改(判定为「设计差异」)
两栏 vs 核心三栏、无 left_sidebar(分类在移动抽屉)、forum 浮动栏(面包屑已可返回)、forum.pinned 用 common.pinned 替代。
理由:用户要求「界面不要有太大改动」,为了「对齐 i18n 键」去改文案 = 无意义的界面变动。
4. 验证方式(本次最有价值的部分)
- 主题冒烟必须带
?preview_theme=Xenforo:匿名访客走settings.default_theme,不带参数会渲染出别的主题却返回 200 → 「绿了但根本没测到目标主题」的静默假通过。 - jsdom 跑真实插件脚本:
runScripts:'outside-only'+ 强制readyState='complete'(否则 IIFE 挂在 DOMContentLoaded 上永不执行)+window.eval(真实 script.js)。插件 DOM 契约类问题静态扫描必漏(PHP 语法对、页面 200,脚本运行时早退或抛异常)。 - 喂真实渲染页(
curl '…?preview_theme=Xenforo')+ 真实assets/lib/fhajax.js;XHR 桩按 URL 精确分派,不认识的一律回空注释。 - A/B 反证:把修复剥离再跑一遍对比 PASS 数(第 4 轮实测 32/32 vs 16/32),否则无法证明修的是真因。
- 两条分支都要测:游客(无引用按钮 → 兜底分支)、登录(有引用按钮 → 主分支)。登录态用临时
sessions行,测完按 id 精确 DELETE。 - CSS 用花括号深度解析(不是
count('{')==count('}'))。 - 每轮收尾核对:
protected/error.log83192 字节无新增、临时会话行数还原、临时产物按精确文件名清理。 - ⚠️ jsdom 的
getComputedStyle不能当级联证据:解析不了var()/calc()(含calc()的margin简写会整条丢掉);普通 px 值可信。
5. 遗留待定
| 项 | 现状 |
|---|---|
.xen-comment .xen-message-user(博客评论) | 仍固定 120px,未跟随 200px |
--mn-bg-nav | 仍 #1f4e79,是否改 #185886 待定 |
.fr-box 下外边距 | 仍是插件给的 4px(灰盒几乎贴卡片下边框) |
.xen-message--thread 主帖 | 未挂核心标记类(主帖无 post_operation_after,插件用不到) |
6. 备份与回滚
| 备份目录 | 内容 | 改动前 sha256(完整) |
|---|---|---|
.workbuddy-ai/tmp/xen_bak_20260928_121134/ | 第 1 轮前:30 个模板 + sha256_before.txt | 见目录内 sha256_before.txt(逐文件基线) |
.workbuddy-ai/tmp/xen_css_bak_20260928_123228/ | 第 2 轮前:xenforo.css | a3b9007d8540d6e152435046d28e1b1db25066a61f388bb723d69e1d32059bed |
.workbuddy-ai/tmp/xen_bak2_20260928_124438/ | 第 3 轮前:thread/show.php + xenforo.css | 51d0b79e15db5da610431138b03069f155d7df3c721440536e62955f4f26b28c / 36a4615e778359505c4290e4e282d393682535da214266029f3d327f9467d93e |
.workbuddy-ai/tmp/xen_bak4_20260928_162649/ | 第 5 轮前:xenforo.css | 2e893564e397fcdcd1f22bca1676d338b38cdf43bcf433ee9655d30fde11b710 |
.workbuddy-ai/tmp/xen_bak5_20260928_163753/ | 第 6 轮前:xenforo.css | 24b6a3abcc65a9db251807b1a9ab432436b34aaac6a3840f89eee745e0030562 |
当前值(改动后):xenforo.css = b0353dd93d397e6ec2a1a9405bd6ee6143de7e724c33edbedb251aac82ba8bca;thread/show.php = 43c575b81ad1db50107d28d9547fad7e0faa0698fb98c80165991c2e513cab45
回滚方式:按目录覆盖对应文件(模板按相对路径回填到 assets/themes/Xenforo/)。
⚠️ 若改动已进发布包,config.php 不在包内,无需同步;主题文件属纯代码,直接覆盖即可。