[开发]FlintHub 列表与正文图展示改造方案(v1 待确认)

👑Lv.11 元老 🌞 骨干会员
2026-10-26 11:02:36

FlintHub 列表与正文图展示改造方案

版本 v1(待确认)· 2026-09-19 · 范围:用户提出的 5 项 UI / 交互问题 · 前置状态:附件体系改造 P0–P6 迁移已完成
一句话结论 5 项里 1 项是已定位根因的功能缺陷(灯箱关闭按钮,一行代码),2 项是同一件事的两半(摘要图改缩略图 + 摘要图网格规范化), 1 项是布局拆分(附件卡),1 项需要动净化层(正文图分组,风险最高)。 另有一个顺带发现:原 P6 待办里「清理正文图过时 thumb_」的说法要改口 —— 那批 thumb_ 正是列表摘要要用的档位。

0. 需要你先拍板的 5 个决策点

编号决策点选项我的建议
C-a列表摘要图最多显示几张?(第 5 项「多的就不显示了」)① 只显示 3 张(一行)
② 最多 6 张(两行)
③ 最多 9 张(3×3,微博式)
① 3 张。理由:列表行高稳定、首屏流量可控;落库本来就最多 9 张,其余可进详情页看
C-b摘要只有 1 张图时,是否也按「一格」大小?① 统一为一格(约 108×81)
② 保持现在的放大单图(180×120)
① 统一。你说的是「统一一下尺寸」,且混排时视觉更整齐
D-a详情页正文只有 1 张图时怎么排?① 整行但限宽(约 480px)
② 只占半行(与多图同宽)
① 整行限宽。单图占半行会显得小气
D-b正文多图是否限制张数?① 不限(全部按一行 2 个铺开)
② 超过 N 张折叠 / 显示「+N」
① 不限。正文是内容主体,不该截断
A-1附件区布局怎么改?① 拆成两个容器:图片走网格、文件走宽列表
② 不拆,文件卡跨 2 列
① 拆容器。两类卡片的宽度诉求本来就不同,语义最清晰

1. 附件下载框太窄 布局缺陷

现状与根因

图片附件卡和非图片附件卡共用同一个网格 .att-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}。 150px 的列宽对图片卡合适,对文件卡就是灾难 —— 按实际 CSS 算一遍:

列宽 148px
  − padding 20            = 128px 可用
  − 图标 36 + gap 10      =  82px
  − gap 10 + 下载按钮 62  =  10px   ← 留给文件名
文件名只有约 10px 宽,实际等于完全看不见

移动端(<480px)与回复区紧凑变体 .att-card--sm 还额外把按钮文字 .att-card__dl-text 隐藏, 按钮退化成约 28px 的纯图标方块 —— 这就是「下载框太窄」的观感来源。

现状:文件卡被挤在 150px 列里(文件名不可读)
📦
a…
1.9 MB
下载
📦
a…
15 KB
下载
建议:文件卡独立成 300px 起跳的宽列表
📦
flinthub-release-1.0.8.zip
1.9 MB · 已下载 12 次
下载
📦
patch-2026-09.zip
15 KB · 已下载 3 次
下载

改法

  1. 拆容器:thread/show.php 两处(主帖 160 行、回复 264 行)把附件按「是否图片」分成两组 —— 图片组沿用 .att-grid;非图片组改用新增的 .att-list。 .att-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:8px},移动端单列。
  2. 按钮定宽:.att-card__dl{min-width:64px;justify-content:center},让每张卡的按钮等宽、不再被压缩。
  3. 移动端不再隐藏文字:删掉 <480px 与 .att-card--sm 里的 .att-card__dl-text{display:none} (改由宽列表保证空间),或退一步改成「图标 + 下载」竖排。
  4. 消除扩展名清单漂移:把 attachment_card.php 里那份 $imageExts 提为 Upload::isImageName(?string): bool,视图分组与组件判定共用同一份清单。
为什么不用「文件卡跨 2 列」 改动确实更小,但图片卡与文件卡混排时,跨列会在网格里留下空洞,而且响应式断点下跨列行为很难预测。 拆容器只是视图里多一个 if,收益明显更大。

2. 列表摘要图改读缩略图 省流量

现状

四处摘要渲染点直接输出原图 URL:

文件行号
app/Views/forum/index.php81(置顶区)、170(普通区)
app/Views/home/index_default.php71
app/Views/forum/category.php91
<img src="<?php echo $this->e($imgUrl); ?>" alt="">

$imgUrl 来自 topic_index.excerpt_images(写帖时从正文抽的前 9 张 <img src>,已排除 emoji), 是原图 URL。实测 topic 88 的 4 张原图合计 1.69 MB,而它们对应的 thumb_ 合计约 200 KB —— 列表首屏差 8 倍。

关键结论:这批缩略图已经存在,不需要额外生成

实测盘点(.workbuddy-ai/tmp/p7_thumb_matrix.php):

用途该用哪一档现状结论
列表摘要图(单格约 108px)thumb_ 400×30016 / 16 已有零额外生成 ✓
详情页正文图(单格约 330–400px)md_ 800×6000 / 16需补生成 15 个
附件卡图片(单格约 148px)thumb_ 400×3002 / 2 已有已就绪 ✓
头像 / 封面不生成——

改法

在 TemplateCompiler 加一个助手,把「URL → 缩略图 URL」收敛成一处:

public function uploadThumbFromUrl(?string $url, string $prefix = 'thumb_'): string
{
    $rel = \app\Helpers\Upload::relFromUrl($url);
    return $rel === '' ? (string)$url : \app\Helpers\Upload::thumbUrl($rel, $prefix);
}

四处视图统一改为 <img src="<?php echo $this->e($this->uploadThumbFromUrl($imgUrl)); ?>" …>。语义:

  • 站内 uploads 图 → 输出 thumb_;缩略图不存在时自动回退原图(thumbUrl() 内建),零 404
  • 外链图 / 其它来源 → relFromUrl() 返回空 → 原样输出,行为不变
  • 分月路径(2026-09/xxx.png)由 relFromUrl() 纯字符串归一,不依赖文件存在性

3. 摘要图统一尺寸 · 每行 3 个 · 超出不显示 规范化

现状

  • 列数已经是 3(.mn-excerpt-thumbs{grid-template-columns:repeat(3,1fr);max-width:320px}),但格子尺寸不统一: 高度写死 96px,宽度随容器浮动;单图另有 -single 分支(max-width:180px、高 120px)
  • 渲染张数没有上限:foreach ($thumbs as $imgUrl) 全渲染,落库最多 9 张 → 最多铺 3 行
现状:单图被单独放大(180×120),与多图不同尺寸
单图 180×120
1
2
3
4
5
6
建议:统一 3 列、统一 4:3 格子、最多 3 张
1
2
3
单图也只占一格
单图

改法

.mn-excerpt-thumbs{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;max-width:330px}
.mn-excerpt-thumb{aspect-ratio:4/3;width:100%;height:auto;object-fit:cover;border-radius:6px;
                  background:var(--mn-bg-row)}   /* 高度由比例决定,不再写死 96px */
/* 删除 .mn-excerpt-thumbs-single 及其单图分支 */
  1. 四处视图的 foreach ($thumbs as $imgUrl) 改为 foreach (array_slice($thumbs, 0, N) as $imgUrl)(N 见决策点 C-a)
  2. 删掉四处 count($thumbs) === 1 ? ' mn-excerpt-thumbs-single' : '' 这段三元判断
  3. CSS 删除 .mn-excerpt-thumbs-single 与 .mn-excerpt-thumbs-single .mn-excerpt-thumb 两条规则

4. 详情页正文图:一行 2 个 + 容器包裹 风险最高

现状(实测正文结构)

编辑器上传的图片在正文里的真实形态很规整,两种:

topic 88(多图):<p>文字…</p><div><img><img><img><img><br></div>
                  └ 4 张图挤在同一个 div 里,彼此之间没有任何分隔

topic 89(单图):<img><div>文字…</div>
                  └ 裸 img 挂在顶层

目前 Content::sanitizeHtml() 的 img 分支只做两件事:src 换 md_、 原图写进 data-fullurl,完全不动结构。所以图片是自然流排布(一张一行),既没有容器也没有网格。

改法(推荐:渲染期分组,服务端出 HTML)

在 sanitizeHtml() 现有 img 处理之后追加一个「分组」步骤:

  1. 收集所有已识别的正文图($isUpload || $isExternal)
  2. 对每张图,向上找最近的块级祖先(div / p / 根节点)
  3. 该祖先的子节点只含图片 / <br> / 空白,且至少 1 张图 → 给这个祖先加 class="post-img-grid"(单图再加 --single),并把每张 img 包一层 <span>
  4. 否则(图片混在文字里,如裸 img 挂顶层)→ 就地新建 <div> 包住该图
.post-img-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:12px 0}
.post-img-grid--single{grid-template-columns:1fr;max-width:480px}   /* 单图整行限宽 */
.post-img-cell{display:block;overflow:hidden;border-radius:8px;cursor:zoom-in;
               background:var(--mn-bg-row-hover)}
.post-img-cell img{display:block;width:100%;height:100%;object-fit:cover}
建议效果:正文多图 = 一行 2 个,单图 = 整行限宽
图 1
图 2
图 3
图 4
单图(整行,最大 480px)

点击看高清图:不需要改 JS

lightbox.js 已有两条现成通路:① data-fullurl(净化期写入);② 兜底选择器 .post-text-content img。图片进网格后依然命中,且灯箱「全页连读」会按 DOM 顺序把 4 张图收进同一阅读序列,左右箭头直接可用。

幂等是这里唯一的硬要求 净化在渲染期可重复执行(Content 的 md_ 逻辑当初就是为此改成幂等的)。 分组必须能识别「已经分过组的结构」并跳过:判据用结构自检而非 class 名 —— 父链上存在 div.post-img-grid 且其子节点全为 cell / br / 空白,即视为已处理。 这样即使有人粘贴带同名 class 的内容也不会被误判(它本来就是图片网格)。
这是本轮唯一动净化层的改动 → 必须回归 重跑 .workbuddy-ai/tmp/test_p5_content_img.php(含 S-1 CDATA 那一组,因为 thread/show.php 的预渲染槽位故意不转义,净化层是唯一防线); 另加「同一份正文连跑两遍净化,输出必须逐字节一致」的幂等用例。

备选(不推荐):前端 JS 分组

DOM ready 后扫描 .post-text-content img 就地包容器。改动小、不动净化层,但会首屏布局跳动(FOUC)、 无 JS 环境退化为原样,且与项目「服务端渲染」的既有约定不符。仅在净化层改动被否决时才考虑。

5. 灯箱右上角关闭按钮点击无反应 已定位根因

根因(实测确认)

容器里的关闭按钮长这样(_components/image_lightbox.php:23):

<button>
    <i>&#xf00d;</i>   ← $this->icon('close',20) 的输出
</button>

点击落在里层的 <i> 上,于是 e.target 是这个 <i>。 而 lightbox.js:152-166 的关闭函数用 e.target 做白名单判定:

function closeLightbox(e) {
    if (e && e.type === 'click' && justDragged) { justDragged = false; return; }
    if (!ensure()) return;
    if (e && e.target) {
        var t = e.target;
        var ok = (t === lb) || (t === lbImg) ||
                 (t.classList && (t.classList.contains('lb-close') || t.classList.contains('lb-nav')));
        if (!ok) return;        // ← <i> 三项都不满足 → 静默返回,什么都不做
    }
    …真正关闭的代码在这里,永远走不到…
}

而调用方(lightbox.js:322)明明已经用 closest() 正确找到了按钮:

var closeLink = target.closest('[data-action="close-lightbox"]');
if (closeLink) { e.preventDefault(); closeLightbox(e); return; }   // ← 传了原始 event

结论:按钮找对了,但传进去的 event 让函数二次自检失败 → 关闭被静默丢弃。 Esc / 点遮罩 / 点图片关闭都正常(那些路径 e.target 恰好命中白名单),所以只有「点 ×」失灵 —— 与你的现象完全吻合。

附带说明:左右箭头同样是 <button><i></i></button>,但它们不走 closeLightbox(), 所以一直是好的。这也是这个 bug 藏得住的原因。

改法

// lightbox.js 的 closeLightbox():白名单判定改用 closest()
var ok = (t === lb) || (t === lbImg) ||
         (t.closest && (t.closest('.lb-close') || t.closest('.lb-nav')));
if (!ok) return;

并把点击分支里的 closeLightbox(e) 改成 closeLightbox() —— 调用方已经用 closest() 判定过,无需二次自检。顺带修好 window.closeLightbox(event) 兼容出口的同样隐患。

回归

  • Node DOM 桩(node .workbuddy-ai/tmp/test_lightbox_js.js):新增「点 <i> 应关闭」「点箭头里的 <i> 应切换」
  • HTTP 端到端:新增一条「触发关闭后 .image-lightbox 不再带 show 类」

6. 顺带修正:P6 剩余待办要改口

原待办写错了 P6 待办原文是「为正文图补生成 md_,并清理仅用于正文的过时 thumb_」。 按本次实测盘点,那句话的后半段是错的。

实测:正文图 16 个,全部已有 thumb_(400×300),而 md_ 一个都没有。 那批 thumb_ 是 P3 之前的旧默认档位产物,而它恰好就是列表摘要现在要用的档位。

动作原待办修正后
补生成 md_ 800×600(详情页正文图用)要做要做(15 个)
清理正文图的 thumb_要做不要做 —— 摘要图正要用它

注:盘点时正文图里有 1 个 md_(efec1066…png),那是我上一轮测试脚本遗留的 8 字节假探针,已删除。当前 md_ 实际为 0 个。

7. 施工顺序与验收

按「独立 → 耦合 → 高风险」排序,每步单独验收后再进下一步(沿用你要求的「一步一步来,完成一步做检查」)。

序步骤改动面验收
1灯箱关闭按钮(第 4 项)assets/js/lightbox.js 1 处Node 桩 + HTTP 端到端;顺带确认左右箭头未回归
2摘要图改读缩略图(第 2 项)TemplateCompiler +1 助手;4 处视图断言摘要 src 指向 thumb_;无缩略图时回退原图不 404
3摘要图网格规范化(第 5 项)modern.css + 4 处视图(array_slice + 去三元)渲染张数 = min(N, 实际);单图与多图格子等尺寸
4附件卡布局拆分(第 1 项)Upload +1 判定;组件 1 处;thread/show.php 2 处;modern.css文件卡文件名可读(≥180px);图片卡不变;移动端单列
5正文图分组(第 3 项)Content::sanitizeHtml();modern.css重跑 test_p5_content_img.php;新增幂等用例;4 图帖渲染为 2×2
6补生成正文图 md_(P6 收尾)新增 CLI;assets/uploads/15 个 md_ 生成且尺寸恰为 800×600;原图与 thumb_ 零改动
7发布(P7)i18n(如有新键)→ FLINTHUB_VERSION → update/N/ + 增量包三语字典零差异;包内结构与站点根镜像一致

8. 统一遵守的既有红线

  • CSS 只用既有 --mn-* 变量 —— 9 套主题各覆盖 103 个变量,新造变量不会被主题覆盖, 夜间主题会残留浅色默认值造成「浅底浅字」
  • 改视图 / CSS / 数据后必须清页面静态缓存(data/runtime/pages/*.html → PageCache::invalidate())
  • i18n 三语同步:lang/{zh,en,zh_tw}.php 当前各 1332 键、键序一致;本轮若新增文案键须三文件同改
  • 编码:UTF-8 无 BOM + LF(modern.css、lightbox.js、4 个视图均在 LF 组内)
  • 图标:新增 $this->icon() 名称前先跑 .workbuddy-ai/tmp/check_glyphs.py <hex> 核查字形
  • 关键改动后必须回读验证:本机 Edit 偶发「报成功但未落盘」(伴 EBUSY)
等你确认 请先对第 0 节的 5 个决策点(C-a / C-b / D-a / D-b / A-1)给个方向;确认后我按第 7 节的顺序从第 1 步开始,一步一步来,每步跑完检查再报。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 6 | Replies 0

All Replies (0)

No replies yet

Please Log in