版本 1.0.0 | 2026-09-27 | 依据
docs/插件开发规范.md方案文档:docs/最新会员插件开发方案_2026-09-27.md
一、功能
右栏(PC 右栏 + 移动侧栏)在排行榜面板正下方新增一块 .mn-panel:
- 标题「最新会员」(图标 =
users) - 内容:4 列 × 2 行 = 8 个最新注册会员
- 每格:上圆形头像(40px)、下用户名(11px,超长截断,hover 看全名)
- 整格是一个
<a href="/u/{id}">—— 点头像或名字都能跳个人页 - 无头像 → 首字占位(视觉对齐核心
.mn-user-avatar-placeholder) - 不足 8 个有几条渲染几条;0 条整块不渲染(无数据不渲染通例)
二、挂载点与顺序
| 项 | 值 |
|---|---|
| 钩子 | sidebar_above_friend_links(双触发) |
| PC 触发点 | app/Views/_components/right_sidebar.php:262 |
| 移动触发点 | app/Views/_components/mobile_sidebar.php:37(仅非 /tags、/tag/* 页) |
| 样式钩子 | layout_head_end(app/Views/layouts/main.php:40) |
与排行榜的先后顺序由 Plugin::sortPlugins()(app/Helpers/Plugin.php:445)决定:
排序键 = sort_order → seq(缓存键序)。本插件 plugin.json 显式声明
"sort_order": 1,排行榜为 0 → 排行榜先渲染、本面板紧随其后,不依赖目录名字母序。
三、数据
SELECT id, username, avatar FROM users
WHERE status = 'active'
ORDER BY created_at DESC, id DESC
LIMIT 8
- 表在
data/meta/business.sqlite;status∈ {active,banned} - 走既有索引
idx_users_created ON users (created_at)+ 固定LIMIT 8→ 不排序全表,单次 < 1ms,不随站点增长而变重 - 不建缓存表:与排行榜(全表 GROUP BY,需 TTL 缓存)不同,本查询天然轻量
- 仅用请求内
static缓存挡掉双触发的第二次查询 → 每请求查询数 = 1(规范红线 ≤5) - 站长不排除(与排行榜口径一致)
四、文件
| 文件 | 体积 | sha256(16) |
|---|---|---|
plugin.json |
486 B | 524a79a8fcd60a23 |
Plugin.php |
3878 B | 1934ff2721ea80b7 |
hook/sidebar_above_friend_links.php |
2821 B | c81638f8251fd820 |
hook/layout_head_end.php |
933 B | c4f88b9d93106fcc |
assets/style.css |
2455 B | 835a7f4bb53e582b |
lang/zh.php |
216 B | 103fa1c56c64e8bf |
lang/zh_tw.php |
219 | 43a05f1a81e6b5a6 |
lang/en.php |
217 B | 7f71d44534a13ad2 |
无 data/ 目录(无独立库)→ 无 init_after 建表钩子、无 route_after_dispatch、无卸载清理负担。
五、两个关键实现点
1. class_alias 必须写(目录名含下划线)
目录名 new_members 含下划线 → 核心按目录名硬拼的 \Plugin\new_members\Plugin
与真实命名空间 Plugin\NewMembers 不是「大小写差异」而是不同的段名 → 必然落空
(规范 §4.1)。落空时 activate()/deactivate()/uninstall() 静默跳过。
故 Plugin.php 末尾(类定义之外)必须有:
if (!\class_exists('Plugin\\new_members\\Plugin', false)) {
\class_alias(Plugin::class, 'Plugin\\new_members\\Plugin', false);
}
2. grid-template-columns 必须 minmax(0, 1fr)
不能写 1fr。1fr 的最小尺寸是 auto(= 内容最小宽),长用户名会把格子撑破
而不是省略;配合 .nm-cell { min-width: 0 } 才稳。这与「flex-wrap:wrap 容器
min-content = 单个子项宽」是同源陷阱。
六、红线遵守
| 红线 | 落实 |
|---|---|
| 零内联(§7.3 P0) | 无 <style> / <script> / on* / style="";图标用核心类 mn-fs-12 而非 $view->icon()(后者带内联 style) |
| 钩子文件不声明顶层符号(§9.0) | 两个钩子文件均只用「赋值给变量的闭包」 |
渲染钩子 $this 不可用(§11.24) |
用 htmlspecialchars() / Upload::url() / I18n::get() |
CSS 只用既有 --mn-*(§11.7) |
仅用 --mn-primary / --mn-primary-light / --mn-text-secondary,已对 10 个主题源逐条核对 |
| 选择器提权 | 一律带 .nm-panel 前缀,避免被核心同特异性后置声明压掉 |
| i18n 三语齐备 | 键集合零差异(实测三语各 2948 键、差集 0) |
七、验证记录(2026-09-27)
CLI 探针(.workbuddy-ai/tmp/nm_probe.php)— 28 PASS / 0 FAIL
- 插件注册且
activated=true;class_alias两条路径均可解析(method_exists('uninstall')为真) fetchLatest()与手写 SQL 逐行一致(8 行,首行 = 小助手)- 第二次调用
getTotalSqlCount()无增量 → 请求内缓存生效 plugins_cache.json:leaderboard(15) <new_members(37),sort_order1 / 0 正确- 两个钩子文件均未声明顶层符号
- 真实
include两次输出完全一致(幂等),8 个.nm-cell、8 个/u/{id}、8 个 40×40 头像 - 零内联(无
onclick/<style/<script/style=") - 空数据分支不输出任何 HTML
HTTP 冒烟(php -S 127.0.0.1:8899)— 29 PASS / 0 FAIL
- 首页 200 / 94120 B;登录页 200 / 46194 B
mn-panel nm-panel× 2(PC + 移动),.nm-cell× 16,.nm-avatar× 16- DOM 顺序:PC 右栏内
lb-panel(pos 5031) <nm-panel(pos 13193); 移动侧栏内lb-panel(3642) <nm-panel(11804) → 排行榜在前 ✓ - 样式 link 已注入,且排行榜的 link 未被挤掉
- CSS 200 / 2455 B,含
minmax(0, 1fr)/repeat(4/text-overflow: ellipsis - 登录页同样有 2 份面板(非列表页也渲染)
protected/error.log无new_members相关记录、无Plugin hook error(渲染钩子无$this报错)
i18n 三语核对 — 9 PASS / 0 FAIL
zh / zh_tw / en 各 2948 键,键集合零差异;本插件 2 个键三语齐备且值正确。
CSS 变量核对 — PASS
--mn-primary / --mn-primary-light / --mn-text-secondary 在
modern.css + 9 套 themes/modern-*.css 共 10 源中全部定义;var() 之外无字面色值。
八、部署注意
- 新增插件后
plugins_cache.json由cacheFresh()自动重建(检测到新目录),无需手工 refresh。 - ⚠️ 访客态列表页(首页/论坛/版块)走永久页面缓存
data/runtime/pages/*.html→ 上线后必须走后台「维护 → 清理缓存」,否则游客仍看旧右栏。 (已登录用户走完整管线,立即可见。) - 无数据库变更、无
data/目录 → 发布包只含plugins/new_members/**。