本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/home_spotlight/说明.md。
首页精选区块插件(home_spotlight)
首页顶部精选区块:左侧图片轮播 + 右侧帖子 Tab(推荐 / 置顶 / 最新 / 最热 / 精华),
纯 CSS 切换 tab,与核心「论坛社区」区块并存(不替换),渲染顺序排在公告块之上。
「推荐」位支持积分上墙、到期自动下架。
版本:1.2.0 min_core:1.1.2 sort_order:-1
一、挂载位置(复用核心钩子,零核心改动)
复用核心通用钩子 announcements_display,零核心改动。
| 项 | 事实 |
|---|---|
| 触发点 | app/Views/home/index_default.php:17、app/Views/forum/index.php:22、app/Views/blog/index.php:20 —— 一次页面渲染共三处 |
| 因此 | 渲染钩子第一件事就是「仅首页」门控:$GLOBALS['__view']->currentPath() !== '/' 即 return(其余两页零查询返回) |
| 排序 | plugin.json 的 sort_order = -1;同钩子上 announcements 为 0、home_members 为 2 |
| 页面位置 | 首页中栏最上方:公告块之上、会员区块之上、论坛社区区块之上 |
★ sort_order 支持负数
app/Helpers/Plugin.php:514 走 (int)($plugin['sort_order'] ?? 0),:516 用
$a['order'] <=> $b['order'] ?: $a['seq'] <=> $b['seq'] 排序 → 负数合法,
故 -1 即可插到所有未声明 sort_order(缺省 0)的插件之前。
实测真实执行顺序(读框架 hookIndex(),非推测):
home_spotlight → announcements → home_members⚠️ 本机
announcements未配置公告数据 → 该块不输出 HTML,所以「精选在公告之上」只能靠钩子真实顺序证明,不能靠页面 offset 比对。
与核心「论坛社区」区块的关系
并存(站长拍板)。首页会出现两份帖子列表:上方 = 精选/推荐帖,下方 = 核心最新帖。
核心视图 index_default.php 一行未改。
二、五个 Tab 的口径
| tab | 数据源 | 口径 |
|---|---|---|
recommend 推荐 | hs_wall(插件独立库)+ 后台手动 tid | 见 §三,实时计算、不走缓存 |
pinned 置顶 | main_index.topic_index | is_pinned = 1,按 last_reply_time DESC, id DESC |
latest 最新 | main_index.topic_index | 按 create_time DESC, id DESC |
hot 最热 | main_index.topic_index | 按 view_count DESC, reply_count DESC, id DESC |
highlight 精华 | main_index.topic_index | 复用核心 is_highlighted 字段,按 last_reply_time DESC, id DESC |
- 已去掉「公告」tab(站长要求):
TAB_KEYS只有上述 5 项。 - 每榜条数后台可配(
list_n,默认 10,范围 1-50)。 - 权限过滤:所有取数一律先过
Permission::getAuthorizedCategoryIds()
(规范红线:自造权限判定 = 越权高危)。未登录视 group_id = 0。
- 软删过滤:
status = 0 AND deleted_at IS NULL,与app/Models/Thread::getLatestAllowed()完全一致。 - 作者名:拿到 uid 后统一走
User::batchGetUsers()(规范 §11.8.3:插件禁止直查users表取展示字段)。
用户名缺失(已注销)的行直接丢弃,避免死链。
- 排序 / WHERE 片段一律取自白名单常量(
BOARD_ORDER/BOARD_WHERE/BOARD_TIME),
绝不把外部输入拼进 SQL。
- ⚠️
topic_index禁 status 打头索引(既有索引:idx_topic_index_list (is_pinned DESC, last_reply_time DESC, create_time DESC))。
Tab 可配 + 可排序
后台可多选启用并给每榜一个序号(seq,小者在前):
- 启用的 tab 按
seq升序(seq相同则按TAB_KEYS声明顺序兜底)。 - 一个都不启用 →
panelEnabled()为 false,整个区块不输出。 - 序号经
Plugin::tabs()做白名单 + 类型收敛,非法行直接丢弃。
三、轮播图(左侧)
站内原本没有中栏轮播组件 → 本插件自建一套(新表 hs_slide + 后台传图/链接/排序/启用)。
| 项 | 说明 |
|---|---|
| 图片上传 | 走核心 Upload::file()(规范红线:唯一落盘口) |
| 命名空间 | ★ covers(见下方红线) |
| 缩略图档位 | md_(800×600);前台取 URL 走 Upload::thumbUrl($rel, 'md_'),缺失自动回退原图 → 零 404 |
| 数量上限 | SLIDE_MAX = 20 |
| 自动播放 | interval 后台可配(默认 5000ms,范围 2000-15000),assets/script.js 的 initRotator() 驱动 |
| 控件 | 前后箭头 + 圆点;只有 1 张时前后端都不渲染控件(点了没反应的控件不出现) |
| 交互 | 鼠标悬停暂停、移出恢复;visibilitychange 切后台暂停;点击箭头/圆点后重置计时器 |
| 跳转链接 | 白名单:http(s):// 外链 或 站内相对路径(/ 开头);javascript: / data: / 协议相对 // 一律丢弃 |
| 关掉轮播 | 后台 carousel_on = 0 或一张图都没有 → 左栏不渲染,右栏自动占满整宽 |
⚠️⚠️ 命名空间红线(本插件上线时踩过的坑)
Upload::REL_PATTERN(app/Helpers/Upload.php:28)只允许 seed_avatars / covers / pwa:
private const REL_PATTERN = '#^(?:(?:seed_avatars|covers|pwa)/)?(?:[0-9]{4}-[0-9]{2}/)?[A-Za-z0-9_.\-]+$#';makeRelPath() 对任意 namespace 都会生成该前缀,但 absPath() 会按正则拒绝它
→ Upload::file() 走到「文件保存失败」分支。
症状:后台点「新增轮播图」永远失败,protected/error.log 零告警
(因为 Upload::file() 是正常返回 ['success'=>false,'error'=>'文件保存失败'],不是抛异常)。
根因:最初写成 'namespace' => 'spotlight' → absPath() 返回 null。
修法:改用 'namespace' => 'covers'。
一条断言就能证伪:
Upload::absPath(Upload::makeRelPath('png','spotlight')) === null。
孤儿文件保护(不需要额外配置)
核心 Settings::cleanupOrphanAttachments() 会扫 assets/uploads/ 下的孤儿文件,
但它的第 5 步「插件库兜底扫描」会遍历 plugins//data/.sqlite 的全部文本列,
把「整值即路径」的字符串记为「在用」(markUsedFromText() 口径 ①)。
本插件把完整相对路径(covers/2026-10/<32hex>.png)存进 hs_slide.image 单列
→ 会被命中 → 不会被误删。(hs_slide 是新表、插件源码指纹变化 → 必然被重扫,无需清白名单。)
删除轮播图时 Plugin::purgeUpload() 会一并删掉原图 + md_ / thumb_ 派生图
(一律走 Upload::path() 白名单校验,拿不到真实路径就什么都不做 → 防路径穿越 / 误删别人文件)。
四、推荐位与「上墙」
坑位组成
推荐位(slots,默认 10,可配 1-50)
├─ ① 后台手动 tid(按填写顺序,去重)—— 永不过期、不显示剩余天数
└─ ② 上墙记录(未过期)—— 补足到 slots,按 expires_at 倒序;显示「剩 N 天」- 手动 tid 解析:英文逗号 / 中文逗号 / 顿号 / 空格 / 换行均可分隔,非纯数字片段丢弃。
- 已注销 / 已软删 / 无权限版块的帖子不占坑(用
LIMIT slots+10多取若干条补位)。 - ★ 「推荐」取数 = 后台指定 tid:
topic_index没有推荐标记字段,故不新增标记,直接由站长填 tid。
上墙购买
| 项 | 说明 |
|---|---|
| 入口 | 首页「推荐」Tab 底部的「我要上墙」按钮 → 跳到 /spotlight/wall 上墙页挑自己的帖子 |
| 独立实现 | ★ 不与 home_members 复用:不同路由、不同表、不同配置、不同 flash 键(站长要求) |
| 显示条件 | 登录 + 开放购买 + 坑位未满(任一不满足 → 首页按钮不渲染) |
| 提交前确认 | assets/script.js 读 form[data-hs-confirm] 弹 window.confirm,确认后置灰按钮 + 标记已提交(防双击重复扣分) |
| 扣分 | 复用核心 Points::deduct() —— 条件 UPDATE 原子防透支 + points_log 流水 + syncLevel() 等级同步 + 扣分通知。插件不写自己的扣分逻辑 |
| 单价 0 | 免费上墙,完全不走 Points::deduct()(不产生流水、不发通知);文案改显示「免费上墙」 |
| 时效 | 默认 7 天(1-365 可配),到期由查询侧过滤自动下架(无定时任务) |
| 重复上墙 | 已在墙上 → 拒绝,并提示剩余天数 |
| 本人限制 | 只能上自己的帖子(row['uid'] !== $uid → 拒绝) |
| 坑位已满 | 拒绝,前台自动隐藏入口 |
顺序:先占位、后扣分、扣分失败回滚占位
$db->beginTransaction();
// ① 条件 INSERT 原子占位(SQLite 无 FOR UPDATE → 用 WHERE NOT EXISTS 等价防重复)
INSERT INTO hs_wall (...) SELECT ... WHERE NOT EXISTS (
SELECT 1 FROM hs_wall WHERE tid = :tid2 AND expires_at > :now2
)
if ($stmt->rowCount() === 0) { $db->rollBack(); return already; } // 并发时只有一方能插入
// ② 扣分(核心库独立事务);返回 false = 余额不足 → 回滚占位
if ($price > 0 && !Points::deduct(...)) { $db->rollBack(); return no_points; }
$db->commit();points字段记录成交价快照(price之后被后台改动不影响历史记录)。- 余额不足的文案带具体数字:
积分不足:上墙需 {need} 积分,你当前只有 {have} 积分。 - 后台「下架」是管理动作,不退款(
Plugin::removeWall())。
结果提示(PRG + 一次性 flash)
POST /spotlight/wall/buy → 写 $_SESSION['hs_wall_flash'] → 302 回 /spotlight/wall(PRG)
GET /spotlight/wall → FrontController::wall() 读 flash、渲染 .mn-alert、**立即 unset**
GET / → announcements_display 钩子读 $_SESSION['hs_flash']、渲染、**立即 unset**- 首页那条 flash 渲染段刻意放在
panelEnabled()之前 → 即使管理员随后关掉了面板,用户也能看到自己刚操作的结果。 - 必须 unset:
Csrf::verify()不消费 token,不 unset 则 F5 会重复提示。 - ⚠️
Csrf::verifyOrDie在未登录时跳/login(302)、已登录但 token 过期时 403 ——
两条都是核心行为,不是本插件的分支。
五、缓存机制(性能)
首页每个访客请求都渲染,不能在渲染路径做跨版块聚合。
渲染钩子(仅首页)
└─ 读 hs_cache(1 次查询,全表 4 行)
├─ 数据新鲜 → 直接渲染
└─ 已过期 → 仍渲染旧数据(不阻塞),置 $GLOBALS['hs_need_refresh']
route_after_dispatch(index.php:441,响应产出之后)
└─ 标志为真 → 重算 4 榜 + 回写(4 次聚合 + 1 次 upsert)- 正常请求的 hook 查询数 = 1(hs_cache)+ 1(batchGetUsers)+ 1(推荐位实时 hs_wall)= 3 次
(规范 §11.4 第 24 条红线 ≤ 5)。
- 冷启动(首次启用 / 缓存被清):渲染钩子内同步算一次,保证首屏不留空面板。
TTL = 300秒。- 推荐位与上墙状态不走缓存,每次实时算(
hs_wall行数极少)。 - ⚠️ 游客走核心
PageCache(整页永久缓存) → 上墙成功 / 后台保存配置 / 轮播图增删改,
四处都必须调 Plugin::invalidatePageCache(),否则游客看到的还是旧首页。
Windows 下 PageCache::invalidate() 可能因文件占用静默失败 → 插件补了一层 glob() + @unlink()。
登录用户不走 PageCache。
六、tab 实现:纯 CSS,零 JS
- N 个
<input type="radio" class="hs-radio">前置为.hs-tabs/.hs-panes的兄弟节点。 - 显隐 / 激活态由
:checked ~ …控制;对应关系靠 **:nth-of-type(N)(radio,按input计数)
/ :nth-child(N)(label、pane)建立,不依赖 id 语义**。
- ⚠️ 因此 面板内必须零
<form>(否则隐藏域会打乱input的nth-of-type序号)。
上墙入口在面板里是 <a> 链接,不是表单。
- ⚠️ radio 的
name/id带渲染序号(Plugin::nextSeq())→ 同一页面若出现多份面板,
两份不会串成一个 radio group。
- 视觉隐藏用
clip: rect(0 0 0 0)而不是display:none→ 保留键盘聚焦与方向键切换。 - 服务端一次渲染全部 pane,切换零请求。
七、文件结构
plugins/home_spotlight/
├── plugin.json # hooks: init_after / announcements_display
│ # / route_after_dispatch / route_register
│ # / admin_route_register / layout_head_end
│ # sort_order = -1(排在公告之上)
├── Plugin.php # 独立库 / 建表 / 生命周期 / 配置 / 5 tab 取数
│ # / 缓存读写 / 推荐位 / 上墙 / 轮播图 CRUD
│ # + 末尾 class_alias(目录名含下划线,规范 §4.1)
├── FrontController.php # GET /spotlight/wall · POST /spotlight/wall/buy
├── AdminController.php # 继承 app\Controllers\Admin\BaseController(鉴权)
├── views/admin.php # 后台四分区页(基础设置 / 轮播图 / 推荐与上墙 / 上墙记录)
├── views/wall.php # 前台上墙页(挑自己的帖子)
├── hook/
│ ├── init_after.php # 惰性建表兜底(戳文件门控)
│ ├── announcements_display.php # 精选区块渲染 + flash 渲染(三触发钩子,仅首页)
│ ├── route_after_dispatch.php # 榜单 TTL 懒刷新
│ ├── route_register.php # 前台路由
│ ├── admin_route_register.php # 后台路由(不写 /admin 前缀)
│ └── layout_head_end.php # 注入 style.css + script.js(defer)
├── assets/
│ ├── style.css # 前台区块 + 上墙页 + 后台局部(颜色只用既有 --mn-* / --c-*)
│ └── script.js # 轮播 + 二次确认/防重复提交 + 后台分区切换
├── lang/{zh,zh_tw,en}.php # 90 键 × 3 语,键集合与键序完全一致
└── data/
├── home_spotlight.sqlite # 独立库(hs_slide / hs_wall / hs_cache)
├── schema.version # 数据层版本号戳文件(比对**内容**)
└── .htaccess / web.config # 自动生成,防 Web 直读可调常量(Plugin.php 顶部):LIST_N_DEFAULT/MIN/MAX = 10/1/50、TTL = 300、
SLOTS_MIN/MAX = 1/50、DAYS_MIN/MAX = 1/365、PRICE_MAX = 1000000、
INTERVAL_DEFAULT/MIN/MAX = 5000/2000/15000、SLIDE_MAX = 20。
改动 ddlStatements() 时必须同步递增 SCHEMA_VERSION(当前 hs-schema-1),否则老站点戳文件命中、新表建不出来(规范 §11.15)。
八、数据表(插件独立库)
hs_slide id / image / title / url / sort_order / enabled / created_at
idx_hs_slide_list(enabled, sort_order, id)
hs_wall id / tid / user_id / points / started_at / expires_at / created_at
idx_hs_wall_tid(tid, expires_at) · idx_hs_wall_exp(expires_at)
hs_cache ckey(PK) / cdata / updated_at -- ckey ∈ {pinned, latest, hot, highlight}hs_wall.tid指向核心main_index.topic_index.id(跨库引用,无外键)。points= 成交价快照;expires_at为Y-m-d H:i:s文本,与核心时间列口径一致。- 建表走
Plugin::ensureSchema()(=Plugin::db($name)->exec($ddl)),
索引 DDL 必须排在 CREATE TABLE 之后。
- ⚠️ 手工删表后必须一并删掉
data/schema.version,否则戳文件命中不会重建。
九、路由
| 方法 | 路径 | 控制器 | 门禁 |
|---|---|---|---|
| GET | /spotlight/wall | FrontController::wall | requireLogin()(未登录 302 跳 /login) |
| POST | /spotlight/wall/buy | FrontController::buy | 同上 + Csrf::verifyOrDie($_POST['_token']) + PRG |
| GET | /admin/home-spotlight | AdminController::index | 继承 Admin\BaseController(构造函数鉴权 + 二次验证) |
| POST | /admin/home-spotlight/save | AdminController::save | 同上 + CSRF |
| POST | /admin/home-spotlight/slide/add | AdminController::slideAdd | 同上 + CSRF |
| POST | /admin/home-spotlight/slide/save | AdminController::slideSave | 同上 + CSRF |
| POST | /admin/home-spotlight/slide/delete | AdminController::slideDelete | 同上 + CSRF |
| POST | /admin/home-spotlight/remove | AdminController::remove | 同上 + CSRF |
- 后台路由不写
/admin前缀(核心挂载点自动拼接,规范 §11.22)。 plugin.json权限:route:admin+system:settings(缺后者Settings::update()会被拒绝 +error_log)。- ⚠️ 视图里
form action一律写成<?php echo $bp; ?>/admin/home-spotlight/...字面量拼接,
不要抽成 $slideActions = $bp . '/admin/...' 变量 —— plugin_form_action_audit.php
无法回溯「两个变量相加」的赋值,会报疑似 404 假阳性。
十、设计约束(改动前必读)
- 零内联:面板输出无
style=""/on*=""/<script>。
图标写字面量实体 + 核心字号类 mn-fs-12,不用 $view->icon()(其返回值恒带内联 style)。
用到的 9 个码点(f164 f08d f017 f06d f005 f053 f054 f067 f03e)均已在子集字体中,
新增码点须重跑 build_font_subset.py(否则静默方块)。
- 颜色只能用既有变量:前台
--mn-(--mn-danger/--mn-spectrum-全站从未定义,已避开);
后台 --c-*。style.css 在 layout_head_end(main.php:49)注入 → **加载早于核心
admin.css(:55) / modern.css(:60),故覆盖核心属性必须提高特异性**
(例:.mn-section.hs-section 而非 .hs-section),不打 !important 补丁;
收窄输入框宽度用 max-width(不与核心 width:100% 正面冲突)。
- ⚠️⚠️ 窄屏改
flex-direction后必须同时改align-items:A 段基础规则
.mn-section.hs-section { align-items: flex-start } 是给桌面横排两栏写的;
竖排后交叉轴由「垂直」翻成「水平」,flex-start 的语义随之变成
「子项按自身内容宽度收缩 + 左对齐」→ .hs-left 根本拿不到整宽,
里面 .hs-rotator 的 width:100% 落空,再被 aspect-ratio: 3/2 + min-height: 180px
反向传递出固定 min-width = 180×3/2 = 270px → 轮播图贴不到右侧
(实测只占卡片 73.5%、右侧空 26.5%)。正解 = 窄屏把 align-items 改 stretch。
⚠️ 只改 .hs-left 的 padding 是无效的 —— 宽度压根没给到那一层(1.1.0 曾这样误修过一轮)。
- 渲染钩子
$this不可用(闭包作用域,规范 §11.24):用htmlspecialchars()、Upload::url()、
I18n::get()、$GLOBALS['__view']->timeAgo()。
- 多触发钩子禁止声明顶层函数/类(规范 §9.0):
announcements_display三触发,
一旦声明会被降级为 include_once,第二/三处入口静默消失。
- 目录名含下划线必须
class_alias(规范 §4.1):核心按目录名拼\Plugin\home_spotlight\Plugin,
与命名空间 Plugin\HomeSpotlight 不同标识符 → 生命周期方法会静默跳过、卸载后数据表残留。
- 后台 markup 必须留在
<main class="admin-content">内(放</main>后 = 无 UI、零日志)。 - HTML 不允许表单嵌套 → 设置表单包住「基础设置」+「推荐位与上墙」两个 pane;
「轮播图」与「上墙记录」各自独立成 pane 并放在设置表单之外
(否则浏览器丢弃内层 form,表现为「点了没反应」)。
- 轮播图列表不用
<table>:每张图是自成一体的<form>卡片(表单不能横跨<tr>)。 - 两处配置写入点:
AdminController::save()与Plugin::activate();
新增配置键后必须清一次 protected/settings_cache.php(该文件不自动补新键)。
Settings是 string 语义:布尔开关保存时必须归一成'0'/'1',绝不能存空串(空串会回落默认值)。- 改
hooks清单后必须\app\Helpers\Plugin::refresh()重建plugins/plugins_cache.json。
十一、验证记录(2026-10-14)
| 套件 | 结果 |
|---|---|
语法:php -l(8.2.9nts,13 个 PHP 文件)+ json 解析 + node --check script.js | 全绿 |
plugin_case_lint.py(Linux 大小写精确匹配) | 全绿(59 个插件目录) |
php_var_cjk_check.php(全角标点紧跟 $var) | 全绿(14 文件) |
fh_i18n_check.php | 三语一致 90 键;代码引用键缺 0;总 63 PASS / 0 FAIL |
plugin_form_action_audit.php | 5 个 POST 表单全部命中已注册 POST 路由(修正前 4 个假阳性) |
| 图标字体子集 | 9 个码点全部命中(FA 448 码点),无需重跑子集构建 |
数据层 / 渲染顺序探针 hs_data_probe.php | 56 PASS / 0 FAIL —— 含 ★钩子真实顺序 home_spotlight → announcements → home_members |
HTTP 端到端冒烟 hs_smoke.sh(真实 php -S) | 55 PASS / 0 FAIL —— 见下 |
| 守卫 | protected/error.log 零新增;测试后 hs_slide=0、hs_wall=1(站长原有,未动)、assets/uploads/covers/2026-10/ 已清空并 rmdir、冒烟会话按精确 SID 删除 |
HTTP 冒烟覆盖点:首页 200 含区块/tab/文案/样式/脚本 · 脚本真的调用 initRotator() ·
游客首页(PageCache 路径)也含区块 · /forum 与 /blog 不渲染区块 ·
后台 200 且 main 区无原始 i18n 键 · 未登录访问后台/上墙页 302 · 缺 CSRF 403 ·
★轮播图上传成功(DB 新增行 + 原图落盘 + md_ 缩略图生成 + 命名空间 covers/ + 标题/链接入库 + 首页引用到该图)。
未验证项(交给站长自测):双主题(默认浅色 + 暗夜星辰)下的区块观感、
与上方/下方区块的视觉协调度、轮播动画手感、移动端窄屏收敛效果。
1.2.0(2026-10-15)
| 套件 | 结果 |
|---|---|
| CSS 花括号深度解析(剥离注释后计深) | 深度 0、无负深度(16,930 B / 698 行) |
css_fontface_e2e.py(真解析 @font-face + 字体文件可达) | 全部通过(FA 448 / Tabler 447 码点,图标零方块) |
HTTP 端到端冒烟 hs_smoke.sh(真实 php -S) | 58 PASS / 0 FAIL(新增 3 条移动端样式回归断言) |
| HTTP 实测 | ?v= == 本地 filemtime(缓存已破);新写法断言全中、旧写法残留 0 |
| 守卫 | protected/error.log 零新增;covers/2026-10/ 回到 6 个文件(冒烟测试图零残留) |
本版改动:移动端三处修复,只动 assets/style.css,零逻辑改动 ——
- 轮播图通栏 —— 根因是
align-items换轴(详见 §十),窄屏改stretch修复; - 帖子标题回到单行省略号截断 —— 撤掉 1.1.0 引入的「独占一行 + 最多折两行」;
- 列表项保持「标签-标题-作者-时间」同一行,窄屏放不下时去掉「时间」(作者名保留)。
⚠️ 「标签」不是本插件的元素 —— 它由核心插件
forum_required_tag的前端脚本(
BADGE_SELECTOR = 'a[href*="/thread/"], …')把标题开头的[前缀]原地替换成
<span class="frt-title-badge">并insertBefore进.hs-item-title的内部首位。所以「标签 + 标题」天然共处同一个
<a>:标签永远可见,只有后面的标题文本被省略号截断—— 本插件不需要自己渲染标签,也不该把标题改成
-webkit-box(会与inline-block子元素打架)。
未验证项(交给站长自测):窄屏收敛后的实际观感(轮播图是否真的通栏、列表是否确为单行)。
十二、已知取舍
- 无定时任务:到期靠查询侧过滤(
expires_at > now),hs_wall里会留历史行
(后台「上墙记录」页可见,带「已过期」徽章)。这是刻意的:便于管理员追溯,且避免在 2 核 2G 上跑定时清理。
- 下架不退款:后台「下架」是管理动作,不退积分、不删
points_log。 - 与核心论坛区块并存 → 首页会有两份帖子列表。站长已确认接受(上方精选、下方最新)。
- 「推荐」= 后台填 tid,不做站内自动推荐算法(
topic_index无推荐标记字段)。 - 无独立「更多」页:区块只出现在首页,5 个 tab 各最多
list_n条。 - 轮播图不生成
thumb_:只生成md_(800×600)。轮播区域约 300px 宽,
md_ 足够且能复用正文图档位;原图不删,便于日后换大尺寸容器。