feat(admin): review reports and ban appeals in dialogs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-30 23:06:04 +08:00
1 parent 32ac2112b0
commit 59ddf677b6
13 files changed
+2122 -704

No files matched your search

@@ -145,10 +145,13 @@ test('ban/report settings page mirrors the reference theme schema', () => {
})
test('ban appeal approval actually lifts restrictions', () => {
assert.match(usersMenus, /lift_user_restriction\(\$user_id\)/)
assert.ok(!usersMenus.includes("SET status = 'lifted'"), 'restriction table has no status column')
assert.ok(!usersMenus.includes('clear_user_bans'))
assert.ok(!usersMenus.includes('lift_ban'))
const banActions = read('src/Admin/Native/UsersMenusBanActions.php')
assert.match(banActions, /lift_user_restriction\(\$user_id\)/)
for (const source of [usersMenus, banActions]) {
assert.ok(!source.includes("SET status = 'lifted'"), 'restriction table has no status column')
assert.ok(!source.includes('clear_user_bans'))
assert.ok(!source.includes('lift_ban'))
}
})
test('ban identity badge occupies the public profile meta row', () => {
@@ -327,71 +330,67 @@ test('admin report notification links to the real page', () => {
assert.match(reportCenter, /users\.php\?page=aksrt_user_ban&type=report&status=pending/)
})
test('native ban page mirrors the reference theme layout', () => {
const tickets = read('src/Admin/Native/UsersMenusTickets.php')
test('native ban page uses the scoped template-and-dialog admin flow', () => {
const banPage = read('src/Admin/Native/UsersMenusBan.php');
const banQuery = read('src/Admin/Native/UsersMenusBanQuery.php');
const banActions = read('src/Admin/Native/UsersMenusBanActions.php');
const banDialogs = read('src/Admin/Native/UsersMenusBanDialogs.php');
const banJs = read('assets/js/admin-user-ban.js');
const banCss = read('assets/css/admin/user-ban.css');
// 菜单与页面标题对齐参考主题(page title=处理举报及禁封申诉,menu title=举报&禁封申诉)
assert.match(usersMenus, /__\('处理举报及禁封申诉', 'aksrt'\),\s*__\('举报&禁封申诉', 'aksrt'\)/, 'submenu titles should match the reference theme')
assert.ok(usersMenus.includes('用户禁封申诉及用户举报管理'), 'page heading should match the reference theme')
assert.match(usersMenus, /use UsersMenusBan;/);
assert.match(usersMenus, /admin_post_aksrt_users_update_report/);
assert.match(usersMenus, /admin_post_aksrt_users_process_appeal/);
assert.doesNotMatch(usersMenus, /use UsersMenusTickets;/, 'ban review must not use the deleted ticket renderer');
assert.ok(!fs.existsSync(path.join(root, 'src/Admin/Native/UsersMenusTickets.php')));
// 类型筛选按钮组 + 状态 subsubsub + 搜索
for (const label of [`__('禁封申诉', 'aksrt')`, `__('用户举报', 'aksrt')`, `__('待处理', 'aksrt')`, `__('已批准', 'aksrt')`, `__('已拒绝', 'aksrt')`, `__('搜索记录', 'aksrt')`]) {
assert.ok(tickets.includes(label), `missing reference-theme label: ${label}`)
for (const key of ['type', 'status', 's', 'send_user', 'orderby', 'order']) {
assert.ok(banQuery.includes(`'${key}'`), `missing ban filter: ${key}`);
}
assert.match(banQuery, /private const BAN_PER_PAGE = 20/);
assert.match(banQuery, /number'\s*=>\s*-1/);
assert.match(banQuery, /array_slice\(\$filtered, \(\$page - 1\) \* self::BAN_PER_PAGE, self::BAN_PER_PAGE\)/);
assert.match(banQuery, /'counts'\s*=>\s*\$counts/);
// 表头列与参考主题一致:状态/类型/提交用户/详情/提交时间/更新时间(带排序)
for (const head of ['状态', '类型', '提交用户', '详情', '提交时间', '更新时间']) {
assert.ok(tickets.includes(`__('${head}', 'aksrt')`), `missing table head: ${head}`)
assert.match(banPage, /if \(\$hook !== 'users_page_aksrt_user_ban'\) return/);
assert.match(banPage, /wp_enqueue_style\('aksrt-admin-user-ban'.*NativeUi::STYLE_HANDLE/);
assert.match(banPage, /wp_enqueue_script\('aksrt-admin-user-ban'/);
assert.match(banPage, /<dialog id="aksrt-ban-dialog"/);
assert.match(banDialogs, /<template id="aksrt-ban-detail-/);
assert.match(banPage, /data-aksrt-ban-view=/);
assert.doesNotMatch(banPage, /update_user_meta|update_status|do_action\(/);
assert.doesNotMatch(banPage, /data-aksrt-ticket-toggle|data-aksrt-ticket-cancel|data-aksrt-ban-toggle/);
assert.match(banDialogs, /if \(\$pending\) \{[\s\S]*?<form method="post"/);
assert.match(banDialogs, /else \{[\s\S]*?<div class="aksrt-ban-dialog__frame">/);
assert.match(banDialogs, /'action'\s*=>\s*\$action/);
assert.match(banDialogs, /record_version/);
assert.match(banDialogs, /wp_nonce_field\(\$action \. '_' \. \(int\) \$row\['id'\]\)/);
for (const name of ['filter_type', 'filter_status', 's', 'send_user', 'orderby', 'order', 'paged']) {
assert.ok(banDialogs.includes(`'${name}'`), `review form must preserve ${name}`);
}
assert.match(tickets, /orderby-but/)
assert.match(tickets, /dashicons-arrow-up/)
assert.match(banDialogs, /esc_url\(\$part, \['http', 'https'\]\)/);
assert.match(banDialogs, /esc_html\(\$value !== '' \? \$value : '—'\)/);
assert.match(banDialogs, /target="_blank" rel="noopener noreferrer"/);
assert.match(banDialogs, /data-required-error/);
// 行渲染与处理表单对齐参考主题
for (const label of [`__('立即处理', 'aksrt')`, `__('正在处理', 'aksrt')`, `__('被举报用户', 'aksrt')`, `__('被举报用户禁封信息', 'aksrt')`, `__('申诉用户', 'aksrt')`, `__('禁封信息', 'aksrt')`, `__('申诉信息', 'aksrt')`, `__('处理方式', 'aksrt')`, `__('处理留言', 'aksrt')`, `__('申诉通过(解封该用户)', 'aksrt')`, `__('申诉拒绝(保持原禁封状态)', 'aksrt')`, `__('此处内容会反馈给举报人', 'aksrt')`, `__('确认提交', 'aksrt')`, `__('用户状态正常', 'aksrt')`, `__('当前用户已解封', 'aksrt')`, `__('此用户已解封', 'aksrt')`, `__('暂无申请记录', 'aksrt')`]) {
assert.ok(tickets.includes(label), `missing process form label: ${label}`)
for (const method of ['handle_update_report', 'handle_process_appeal']) {
assert.match(banActions, new RegExp(`public function ${method}\\(\\): void`));
}
// 处理表单必须携带隐藏 action 字段:admin-post.php 靠它触发
// admin_post_{action} 钩子,缺失时 WordPress 不输出任何内容(白屏)。
// value 按工单类型分发(report → update_report,appeal → process_appeal)。
assert.match(tickets, /name="action" value="' \. esc_attr\(\$kind === 'report' \? 'aksrt_users_update_report' : 'aksrt_users_process_appeal'\)/)
assert.match(usersMenus, /admin_post_aksrt_users_update_report/)
assert.match(usersMenus, /admin_post_aksrt_users_process_appeal/)
// 内联处理视图(参考主题方向):表单以合法的 colspan=6 单元格随待处理行
// 预渲染;展开/取消走 JS 行↔表单互换(无整页刷新),信息只出现一次,
// 不再裸嵌 tbody 被 foster-parent 错位。
assert.match(tickets, /<td colspan="6">';\s*\$this->render_ticket_process_form/)
assert.match(tickets, /data-aksrt-ticket-toggle="/)
assert.match(tickets, /data-aksrt-ticket-cancel="/)
assert.match(tickets, /data-aksrt-ticket-row="/)
assert.match(tickets, /e\.preventDefault\(\);/)
// 展开的处理表单提供“取消”按钮(确认提交右侧)。
assert.match(tickets, /class="button button-primary process-submit"/)
assert.match(tickets, /<a class="button" data-aksrt-ticket-cancel="' \. esc_attr\(\$row\['kind'\] \. '-' \. \(string\) \$row\['id'\]\) . '" href="' \. esc_url\(remove_query_arg\(\['action', 'id'\]\)\) . '">' \. esc_html__\('取消', 'aksrt'\)/)
// 分页对齐参考主题 zibpay_admin_pagenavi(每页 20 条)
assert.match(tickets, /TICKETS_PER_PAGE = 20/)
assert.match(tickets, /ticket_pagenavi\(int \$total_count, int \$per_page\)/)
for (const label of ['前往第一页', '前往上一页', '前往下一页', '前往最后一页', 'displaying-num', 'paging-input', 'total-pages']) {
assert.ok(tickets.includes(label), `missing pagenavi part: ${label}`)
}
// 旧的三 tab 结构已收敛;举报处理表单不包含处罚下拉(对齐参考主题)
assert.ok(!usersMenus.includes('tab=reports'), 'old reports tab is gone')
assert.ok(!usersMenus.includes('tab=appeals'), 'old appeals tab is gone')
// 禁封限制 Tab 已移除(参考主题无此后台 Tab):禁封改走前台封禁弹窗。
assert.ok(!usersMenus.includes('tab=restrictions'), 'restrictions tab is gone')
assert.ok(!usersMenus.includes('render_restrictions_tab'), 'restrictions tab renderer is gone')
assert.ok(!usersMenus.includes('handle_add_restriction'), 'backend ban form handler is gone')
assert.ok(!usersMenus.includes('handle_remove_restriction'), 'backend unban form handler is gone')
assert.ok(!usersMenus.includes('render_reports_tab'))
assert.ok(!usersMenus.includes('render_appeals_tab'))
assert.match(usersMenus, /use UsersMenusTickets;/)
assert.ok(!tickets.includes('name="action_taken"'), 'report process form should be status-only like the reference theme')
// 工单处理页「点击此处」跳转前台用户公开主页(封面三点菜单 → 封禁弹窗)。
assert.match(tickets, /aksrt_get_user_home_url\(\$target_id\)/)
assert.ok(!tickets.includes('tab=restrictions'), 'ticket ban link must not point at the removed backend tab')
})
assert.match(banActions, /check_admin_referer\('aksrt_users_update_report_'/);
assert.match(banActions, /check_admin_referer\('aksrt_users_process_appeal_'/);
assert.match(banActions, /hash_equals\(\$this->ban_record_version\(\$row\), \$submitted\)/);
assert.match(banActions, /\$this->ban_request_is_current\(\$row, 'pending'\)/);
assert.match(banActions, /\$new_status === 'dismissed' && \$note === ''/);
assert.match(banActions, /\$decision === 'reject' && \$note === ''/);
assert.match(banActions, /lift_user_restriction\(\$user_id\)/);
assert.match(banActions, /'paged'\s*=>/);
assert.match(banJs, /window\.addEventListener\('pageshow'/);
assert.match(banJs, /previousOpener\.focus\(\)/);
assert.match(banJs, /if \(!submitting\) dialog\.close\(\)/);
assert.match(banCss, /html\.aksrt-ban-modal-open \{ overflow: hidden/);
});
test('report modal mirrors the reference theme: preset reasons, image evidence, notice, geometry', () => {
const scripts = read('src/Core/ScriptsPart1c.php')
// 预置原因真实数据源是 ban_preset_reason(UserRuntime::ban_settings),