feat(admin): host the ban/report settings page in the SPA schema

The users-interaction sidebar entry 禁封/举报 was a redirect stub; it is
now a schema-driven settings page replicating the reference theme
field-for-field:

- user_ban_s / user_report_s switches, ban_preset_reason and
  ban_appeal_keys as repeatable group rows ({t}), ban_appeal_desc and
  user_report_desc HTML textareas with the reference default copy, and
  user_report_img_s — keys, labels, help texts, defaults and ordering
  all mirrored in SchemaUsers.php and the SPA report.js schema
- UserRuntime::ban_settings()/report_settings() and
  BanAppeal::settings() read the new group-array values with fallback
  to the previous line/comma formats
- delete the self-built SPA queue pages (UsersBan, UsersReport,
  ReportDetailDrawer, UsersMessages) that the reference theme does not
  have; legacy /users/report bookmarks still redirect to the native page
- contract tests: lock the schema keys/labels, the schemaPage mount and
  the deleted-file state; drop reads of removed components
This commit is contained in:
2026-09-19 22:24:55 +08:00
parent c0e0912eee
commit 381256ed59
12 changed files with 191 additions and 1236 deletions
@@ -103,8 +103,7 @@ test('asset notifications are absent and interaction notification hooks are pres
const notifications = read('inc/modules/user-notifications.php');
const settings = read('src/Settings/Integration.php');
const adminSettings = read('assets/admin/src/pages/content/NotificationCenterSettings.vue');
const messageAdmin = read('assets/admin/src/pages/users/UsersMessages.vue');
const combined = [notifications, settings, adminSettings, messageAdmin].join('\n');
const combined = [notifications, settings, adminSettings].join('\n');
for (const retired of [
'asset_points_increase',
@@ -15,7 +15,6 @@ const usersMenus = read('src/Admin/Native/UsersMenus.php')
const mailNotifications = read('src/Settings/MailNotifications.php')
const home = read('src/User/Home.php')
const commentItem = read('inc/helpers/comments/item.php')
const usersBan = read('assets/admin/src/pages/users/UsersBan.vue')
const functions = read('functions.php')
const uninstall = read('uninstall.php')
@@ -71,15 +70,44 @@ test('restriction class supports lifting and kicks sessions on ban', () => {
test('ban admin REST endpoints stay in sync with the SPA client', () => {
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrictions'/)
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrict'/)
assert.match(usersBan, /\/users\/\$\{addForm\.user_id\}\/restrict`/)
assert.ok(!usersBan.includes('/restrictions`, {'), 'SPA must not POST to the unregistered /restrictions endpoint')
assert.match(usersBan, /expires_at: durationToExpiresAt\(addForm\.duration\)/)
for (const t of ["value: 'warn'", "value: 'mute'", "value: 'suspend'", "value: 'ban'"]) {
assert.ok(usersBan.includes(t), `SPA restriction type missing: ${t}`)
// The self-built SPA ban-manager page is removed: ban enforcement lives on
// the native reference-theme page, and the SPA hosts the settings schema.
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersBan.vue')), 'self-built SPA ban page must stay deleted')
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersReport.vue')), 'self-built SPA report queue must stay deleted')
})
test('ban/report settings page mirrors the reference theme schema', () => {
const schemaPhp = read('src/Admin/Schema/SchemaUsers.php')
const schemaJs = read('assets/admin/src/schema/sections/users/report.js')
const sections = read('assets/admin/src/constants/sections/users.js')
// SPA 侧边栏“用户互动 → 禁封/举报”挂载为 schema 设置页(非跳转页)
assert.match(sections, /schemaPage\(\{\s*\n\s*key: 'users-ban',/)
assert.match(sections, /label: '禁封\/举报'/)
assert.match(sections, /section: 'report'/)
assert.ok(!sections.includes("component: 'NativeOpsRedirect'"), 'ban page must not be a redirect stub')
// 字段键 1:1 对齐参考主题:禁封/举报开关、预置原因、申诉说明、申诉资料、图片举报、举报说明
for (const key of ['user_ban_s', 'user_report_s', 'ban_preset_reason', 'ban_appeal_desc', 'ban_appeal_keys', 'user_report_img_s', 'user_report_desc']) {
assert.ok(schemaPhp.includes(`'${key}'`), `PHP schema missing reference key: ${key}`)
assert.ok(schemaJs.includes(key), `SPA schema missing reference key: ${key}`)
}
for (const dead of ["value: 'muted'", "value: 'restricted'", "value: 'banned'"]) {
assert.ok(!usersBan.includes(dead), `invalid SPA restriction type: ${dead}`)
// 参考主题文案与 group 结构
for (const label of ['禁封功能', '举报功能', '预置原因', '申诉说明', '申诉所需资料', '举报可上传图片', '举报说明']) {
assert.ok(schemaJs.includes(`label: '${label}'`), `SPA schema missing reference label: ${label}`)
}
assert.match(schemaJs, /type: 'group'/)
assert.match(schemaJs, /添加原因/)
assert.match(schemaJs, /添加申诉资料/)
// 运行时读取兼容 group 数组 [{t}] 并保留旧格式回退
const runtime = read('src/Settings/UserRuntime.php')
assert.match(runtime, /ban_settings\(\): array/)
assert.match(runtime, /report_settings\(\): array/)
assert.match(runtime, /user_report_s/)
const appeal = read('src/User/BanAppeal.php')
assert.match(appeal, /ban_appeal_keys/)
assert.match(appeal, /ban_appeal_desc/)
})
test('ban appeal approval actually lifts restrictions', () => {