feat(admin): rebuild ban page to mirror the reference theme

- rename the users.php submenu to 举报&禁封申诉 and the page heading to
  用户禁封申诉及用户举报管理
- merge report tickets and ban appeals into one Zibll-style queue:
  type filter buttons (全部/禁封申诉/用户举报), status subsubsub
  (全部/待处理/已批准/已拒绝), search box, sortable widefat table with
  the same column set (状态/类型/提交用户/详情/提交时间/更新时间),
  inline process forms and 20-per-page pagination
- report process form shows the reported target, restriction status and
  an enforcement action select; appeal form offers approve/reject radios
  with a message to the user
- move restriction enforcement into ReportCenter::apply_action so REST
  and the native page share one implementation; add get_report_row()
- drop the old three-tab rendering (reports/appeals tabs) and fix all
  notice/mail/SPA links to the merged page
This commit is contained in:
2026-09-19 22:00:17 +08:00
parent 3764d48755
commit 902e71ee62
8 changed files with 626 additions and 217 deletions
+1 -1
View File
@@ -141,7 +141,7 @@ export const usersSection = {
iconClass: 'fa fas fa-gavel',
canonicalDomain: 'users',
section: 'ban',
nativeAdminPath: 'users.php?page=aksrt_user_ban&tab=reports',
nativeAdminPath: 'users.php?page=aksrt_user_ban',
}),
],
}
+1 -1
View File
@@ -83,7 +83,7 @@ function createSectionRedirectRoutes() {
const NATIVE_OPS_REDIRECTS = [
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=aksrt_messages' },
{ path: '/users/referral-codes', title: '推广码运营', nativeAdminPath: 'users.php?page=aksrt_referral_codes' },
{ path: '/users/report', title: '禁封/举报', nativeAdminPath: 'users.php?page=aksrt_user_ban&tab=reports' },
{ path: '/users/report', title: '禁封/举报', nativeAdminPath: 'users.php?page=aksrt_user_ban' },
]
const routes = [
+34 -149
View File
@@ -12,6 +12,7 @@ class UsersMenus {
use UsersMenusVerify;
use UsersMenusVip;
use UsersMenusReferral;
use UsersMenusTickets;
public const CAPABILITY = 'manage_options';
@@ -81,8 +82,8 @@ class UsersMenus {
);
add_submenu_page(
'users.php',
__('举报&禁封', 'aksrt'),
__('举报&禁封', 'aksrt'),
__('举报&禁封申诉', 'aksrt'),
__('举报&禁封申诉', 'aksrt'),
self::CAPABILITY,
'aksrt_user_ban',
[$this, 'render_ban_page']
@@ -112,7 +113,7 @@ class UsersMenus {
if ($page !== 'aksrt_user_ban' && class_exists('\AKSRT\Messages\ReportCenter')) {
$pending_reports = (int) \AKSRT\Messages\ReportCenter::get_instance()->get_pending_count();
if ($pending_reports > 0) {
$url = admin_url('users.php?page=aksrt_user_ban&tab=reports&status=pending');
$url = admin_url('users.php?page=aksrt_user_ban&status=pending');
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('举报待处理', 'aksrt') . '</h3>';
echo '<p>' . esc_html(sprintf(
/* translators: %d count */
@@ -131,7 +132,7 @@ class UsersMenus {
'fields' => 'ID',
]));
if ($pending_appeals > 0) {
$url = admin_url('users.php?page=aksrt_user_ban&tab=appeals&status=pending');
$url = admin_url('users.php?page=aksrt_user_ban&type=appeal&status=pending');
echo '<div class="notice notice-warning is-dismissible"><h3>' . esc_html__('禁封申诉待处理', 'aksrt') . '</h3>';
echo '<p>' . esc_html(sprintf(__('您有 %d 条禁封申诉待处理', 'aksrt'), $pending_appeals)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
@@ -283,87 +284,27 @@ class UsersMenus {
public function render_ban_page(): void {
$this->ensure_cap();
echo '<div class="wrap"><h1>' . esc_html__('举报&禁封', 'aksrt') . '</h1>';
echo '<div class="wrap"><h2>' . esc_html__('用户禁封申诉及用户举报管理', 'aksrt') . '</h2>';
$this->flash();
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'reports';
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'tickets';
if (!in_array($tab, ['tickets', 'restrictions'], true)) {
$tab = 'tickets';
}
echo '<h2 class="nav-tab-wrapper">';
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'reports' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=reports')), esc_html__('举报', 'aksrt'));
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'tickets' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban')), esc_html__('申诉&举报', 'aksrt'));
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'restrictions' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=restrictions')), esc_html__('禁封限制', 'aksrt'));
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'appeals' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=appeals')), esc_html__('禁封申诉', 'aksrt'));
echo '</h2>';
if ($tab === 'restrictions') {
$this->render_restrictions_tab();
} elseif ($tab === 'appeals') {
$this->render_appeals_tab();
} else {
$this->render_reports_tab();
$this->render_tickets_tab();
}
echo '</div>';
}
private function render_reports_tab(): void {
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
echo '<div class="notice notice-error"><p>' . esc_html__('举报中心未初始化。', 'aksrt') . '</p></div>';
return;
}
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
$svc = \AKSRT\Messages\ReportCenter::get_instance();
$counts = $svc->get_status_counts();
echo '<p>' . esc_html(sprintf(
/* translators: 1 pending 2 total */
__('待处理 %1$d / 共 %2$d', 'aksrt'),
(int) ($counts['pending'] ?? 0),
(int) ($counts['total'] ?? 0)
)) . '</p>';
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="reports" />';
echo '<select name="status">';
foreach (['pending' => __('待处理', 'aksrt'), 'reviewing' => __('审核中', 'aksrt'), 'resolved' => __('已处理', 'aksrt'), 'dismissed' => __('已驳回', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$items = $svc->get_reports(['status' => $status, 'limit' => 50, 'offset' => 0]);
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('举报人', 'aksrt') . '</th><th>' . esc_html__('目标', 'aksrt') . '</th><th>' . esc_html__('原因', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无举报', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0);
$st = (string) ($row['status'] ?? '');
$rid = (int) ($row['reporter_id'] ?? 0);
$ru = $rid > 0 ? get_userdata($rid) : null;
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
echo '<td>' . ($ru ? esc_html($ru->display_name) : esc_html((string) $rid)) . '</td>';
echo '<td>' . esc_html((string) ($row['target_type'] ?? '') . ' #' . (string) ($row['target_id'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($row['reason'] ?? '')) . '</td>';
echo '<td>' . esc_html($st) . '</td>';
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
echo '<td><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_users_update_report" />';
echo '<input type="hidden" name="report_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_users_update_report_' . $id);
echo '<select name="new_status">';
foreach (['pending', 'reviewing', 'resolved', 'dismissed'] as $key) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($key));
}
echo '</select>';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('更新', 'aksrt'), 'small', '', false);
echo '</form></td></tr>';
}
echo '</tbody></table>';
}
private function render_restrictions_tab(): void {
if (!class_exists('\AKSRT\User\Restriction')) {
echo '<div class="notice notice-error"><p>' . esc_html__('限制模块未初始化。', 'aksrt') . '</p></div>';
@@ -439,9 +380,26 @@ class UsersMenus {
$id = isset($_POST['report_id']) ? (int) $_POST['report_id'] : 0;
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
$action_taken = isset($_POST['action_taken']) ? sanitize_key((string) wp_unslash($_POST['action_taken'])) : 'none';
check_admin_referer('aksrt_users_update_report_' . $id);
$ok = $id > 0 && class_exists('\AKSRT\Messages\ReportCenter') && \AKSRT\Messages\ReportCenter::get_instance()->update_status($id, $new_status, $note);
$this->redirect('aksrt_user_ban', $ok ? 'report_updated' : 'report_failed', ['tab' => 'reports']);
$ok = false;
if ($id > 0 && class_exists('\AKSRT\Messages\ReportCenter')) {
$svc = \AKSRT\Messages\ReportCenter::get_instance();
$ok = $svc->update_status($id, $new_status, $note);
if ($ok && $action_taken !== '' && $action_taken !== 'none') {
$row = $svc->get_report_row($id);
if ($row) {
$svc->apply_action(
sanitize_key((string) $row['target_type']),
(int) $row['target_id'],
$action_taken,
(string) ($row['reason'] ?? '')
);
}
}
}
$this->redirect('aksrt_user_ban', $ok ? 'report_updated' : 'report_failed', ['tab' => 'tickets']);
}
public function handle_add_restriction(): void {
@@ -471,79 +429,6 @@ class UsersMenus {
$this->redirect('aksrt_user_ban', $ok ? 'restriction_removed' : 'restriction_failed', ['tab' => 'restrictions']);
}
private function render_appeals_tab(): void {
if (!class_exists('\AKSRT\User\BanAppeal')) {
echo '<div class="notice notice-error"><p>' . esc_html__('申诉模块未初始化。', 'aksrt') . '</p></div>';
return;
}
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
if (!in_array($status, ['pending', 'approved', 'rejected', 'all'], true)) {
$status = 'pending';
}
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="appeals" />';
echo '<select name="status">';
foreach (['pending' => __('待处理', 'aksrt'), 'approved' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'all' => __('全部', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$users = get_users([
'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
'meta_compare' => 'EXISTS',
'number' => 100,
'fields' => ['ID', 'display_name', 'user_login', 'user_email'],
'orderby' => 'ID',
'order' => 'DESC',
]);
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>UID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('说明', 'aksrt') . '</th><th>' . esc_html__('提交时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
$shown = 0;
foreach ($users as $u) {
$uid = (int) $u->ID;
$st = \AKSRT\User\BanAppeal::get_status($uid);
if ($st === '') {
continue;
}
if ($status !== 'all' && $st !== $status) {
continue;
}
$payload = \AKSRT\User\BanAppeal::get_payload($uid);
$shown++;
echo '<tr>';
echo '<td>' . esc_html((string) $uid) . '</td>';
echo '<td>' . esc_html($u->display_name . ' (' . $u->user_login . ')') . '<br><span class="description">' . esc_html($u->user_email) . '</span></td>';
echo '<td>' . esc_html($st) . '</td>';
echo '<td>' . esc_html((string) ($payload['reason_key'] ?? '—')) . '</td>';
echo '<td>' . esc_html((string) ($payload['reason'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($payload['time'] ?? '—')) . '</td>';
echo '<td>';
if ($st === 'pending') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_users_process_appeal" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $uid) . '" />';
wp_nonce_field('aksrt_users_process_appeal_' . $uid);
echo '<input type="text" name="note" placeholder="' . esc_attr__('处理备注', 'aksrt') . '" class="regular-text" />';
echo '<button type="submit" name="decision" value="approve" class="button button-primary">' . esc_html__('通过并解封', 'aksrt') . '</button>';
echo '<button type="submit" name="decision" value="reject" class="button">' . esc_html__('驳回', 'aksrt') . '</button>';
echo '</form>';
} else {
echo '—';
}
echo '</td></tr>';
}
if ($shown < 1) {
echo '<tr><td colspan="7">' . esc_html__('暂无申诉', 'aksrt') . '</td></tr>';
}
echo '</tbody></table>';
}
public function handle_process_appeal(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
@@ -554,7 +439,7 @@ class UsersMenus {
check_admin_referer('aksrt_users_process_appeal_' . $user_id);
if ($user_id <= 0 || !in_array($decision, ['approve', 'reject'], true) || !class_exists('\AKSRT\User\BanAppeal')) {
$this->redirect('aksrt_user_ban', 'appeal_failed', ['tab' => 'appeals']);
$this->redirect('aksrt_user_ban', 'appeal_failed', ['tab' => 'tickets', 'type' => 'appeal']);
}
$status = $decision === 'approve' ? 'approved' : 'rejected';
@@ -598,7 +483,7 @@ class UsersMenus {
$this->redirect(
'aksrt_user_ban',
$decision === 'approve' ? 'appeal_approved' : 'appeal_rejected',
['tab' => 'appeals']
['tab' => 'tickets', 'type' => 'appeal']
);
}
+452
View File
@@ -0,0 +1,452 @@
<?php
/**
* 举报&禁封申诉工单列表 — 对齐参考主题(Zibll)的后台管理页:
* 类型筛选按钮组 + 状态 subsubsub + 搜索 + 排序表头 + 行内处理表单 + 分页。
*
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
trait UsersMenusTickets {
private const TICKETS_PER_PAGE = 20;
/**
* 统一行结构:kind=report|appeal, id, user_id, status(pending|resolved|dismissed),
* raw_status, time, updated, payload。
*/
private function collect_ticket_rows(string $type, string $status, string $s, int $send_user, string $orderby, string $order): array {
$rows = [];
if ($type !== 'appeal' && class_exists('\AKSRT\Messages\ReportCenter')) {
$reports = \AKSRT\Messages\ReportCenter::get_instance()->get_reports(['limit' => 500]);
foreach ((array) $reports as $r) {
$st = sanitize_key((string) ($r['status'] ?? ''));
$norm = $st === 'reviewing' ? 'pending' : $st;
if (!in_array($norm, ['pending', 'resolved', 'dismissed'], true)) {
continue;
}
if ($status !== '' && $norm !== $status) {
continue;
}
if ($send_user > 0 && (int) ($r['reporter_id'] ?? 0) !== $send_user) {
continue;
}
$hay = (string) ($r['reason'] ?? '') . ' ' . (string) ($r['category'] ?? '') . ' ' . (string) ($r['target_type'] ?? '') . ' #' . (string) ($r['target_id'] ?? '');
if ($s !== '' && mb_stripos($hay, $s) === false) {
continue;
}
$rows[] = [
'kind' => 'report',
'id' => (int) ($r['id'] ?? 0),
'user_id' => (int) ($r['reporter_id'] ?? 0),
'status' => $norm,
'raw_status' => $st,
'time' => (string) ($r['created_at'] ?? ''),
'updated' => (string) ($r['updated_at'] ?? ''),
'payload' => $r,
];
}
}
if ($type !== 'report' && class_exists('\AKSRT\User\BanAppeal')) {
$users = get_users([
'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
'meta_compare' => 'EXISTS',
'number' => 500,
'fields' => ['ID'],
'orderby' => 'ID',
'order' => 'DESC',
]);
foreach ($users as $u) {
$uid = (int) $u->ID;
$st = \AKSRT\User\BanAppeal::get_status($uid);
if ($st === '' || !in_array($st, ['pending', 'approved', 'rejected'], true)) {
continue;
}
$norm = $st === 'approved' ? 'resolved' : ($st === 'rejected' ? 'dismissed' : 'pending');
if ($status !== '' && $norm !== $status) {
continue;
}
if ($send_user > 0 && $uid !== $send_user) {
continue;
}
$payload = \AKSRT\User\BanAppeal::get_payload($uid);
$hay = (string) ($payload['reason'] ?? '') . ' ' . (string) ($payload['reason_key'] ?? '');
if ($s !== '' && mb_stripos($hay, $s) === false) {
continue;
}
$rows[] = [
'kind' => 'appeal',
'id' => $uid,
'user_id' => $uid,
'status' => $norm,
'raw_status' => $st,
'time' => (string) ($payload['time'] ?? ''),
'updated' => (string) ($payload['handled_at'] ?? ''),
'payload' => $payload,
];
}
}
$weights = ['pending' => 0, 'resolved' => 1, 'dismissed' => 2];
usort($rows, static function (array $a, array $b) use ($orderby, $order, $weights): int {
if ($orderby === 'status') {
$cmp = ($weights[$a['status']] ?? 9) <=> ($weights[$b['status']] ?? 9);
} elseif ($orderby === 'type') {
$cmp = strcmp($a['kind'], $b['kind']);
} else {
$cmp = strcmp($a['time'], $b['time']);
}
return $order === 'ASC' ? $cmp : -$cmp;
});
return $rows;
}
public function render_tickets_tab(): void {
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
$type = in_array($type, ['report', 'appeal'], true) ? $type : '';
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$status = in_array($status, ['pending', 'resolved', 'dismissed'], true) ? $status : '';
$s = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$send_user = isset($_GET['send_user']) ? (int) $_GET['send_user'] : 0;
$paged = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'time';
$orderby = in_array($orderby, ['time', 'status', 'type'], true) ? $orderby : 'time';
$order = (isset($_GET['order']) && strtoupper((string) $_GET['order']) === 'ASC') ? 'ASC' : 'DESC';
$action_url = admin_url('users.php?page=aksrt_user_ban');
$page_url = add_query_arg(array_filter([
'tab' => 'tickets',
'type' => $type,
'status' => $status,
's' => $s,
'send_user' => $send_user,
], static function ($v) { return $v !== '' && $v !== 0; }), $action_url);
$rows = $this->collect_ticket_rows($type, $status, $s, $send_user, $orderby, $order);
$total = count($rows);
$pages = max(1, (int) ceil($total / self::TICKETS_PER_PAGE));
$paged = min($paged, $pages);
$tickets = array_slice($rows, ($paged - 1) * self::TICKETS_PER_PAGE, self::TICKETS_PER_PAGE);
// 类型筛选按钮组(对齐参考主题:全部 / 禁封申诉 / 用户举报)
echo '<div style="margin:8px 0;">';
foreach (['' => __('全部', 'aksrt'), 'appeal' => __('禁封申诉', 'aksrt'), 'report' => __('用户举报', 'aksrt')] as $t => $label) {
$url = $t === ''
? remove_query_arg(['s', 'type', 'action', 'id'], $page_url)
: add_query_arg('type', $t, remove_query_arg(['s', 'action', 'id'], $page_url));
printf('<a class="button%s" href="%s">%s</a> ', $type === $t ? ' button-primary' : '', esc_url($url), esc_html($label));
}
echo '</div>';
// 状态 subsubsub(对齐参考主题:全部 | 待处理 | 已批准 | 已拒绝)
echo '<ul class="subsubsub">';
$status_links = [
'' => __('全部', 'aksrt'),
'pending' => __('待处理', 'aksrt'),
'resolved' => __('已批准', 'aksrt'),
'dismissed'=> __('已拒绝', 'aksrt'),
];
$i = 0;
foreach ($status_links as $key => $label) {
$url = $key === '' ? remove_query_arg(['s', 'status'], $page_url) : add_query_arg('status', $key, remove_query_arg('s', $page_url));
$sep = ++$i < count($status_links) ? ' |' : '';
printf('<li><a href="%s">%s</a>%s</li> ', esc_url($url), esc_html($label), esc_html($sep));
}
echo '</ul>';
// 搜索(对齐参考主题:右侧搜索记录表单)
echo '<div class="order-header" style="margin:6px 0 12px;float:right;">';
echo '<form class="form-inline" method="get">';
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="tickets" />';
if ($type !== '') {
echo '<input type="hidden" name="type" value="' . esc_attr($type) . '" />';
}
echo '<input type="text" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索记录', 'aksrt') . '" />';
submit_button(__('提交', 'aksrt'), 'primary', '', false);
echo '</form></div>';
if ($s !== '') {
echo '<div class="mb10">"' . esc_html($s) . '" ' . esc_html__('的搜索结果', 'aksrt') . '</div>';
}
echo '<div class="table-box" style="overflow-y:auto;width:100%;">';
echo '<table class="widefat fixed striped posts" style="min-width:1000px;">';
echo '<thead><tr>';
// 表头(对齐参考主题:状态/类型/提交用户/详情/提交时间/更新时间 + 排序箭头)
$theads = [
['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'aksrt')],
['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'aksrt')],
['width' => '5%', 'orderby' => '', 'name' => __('提交用户', 'aksrt')],
['width' => '18%', 'orderby' => '', 'name' => __('详情', 'aksrt')],
['width' => '6%', 'orderby' => 'time', 'name' => __('提交时间', 'aksrt')],
['width' => '6%', 'orderby' => 'updated','name' => __('更新时间', 'aksrt')],
];
foreach ($theads as $thead) {
$orderby_html = '';
if ($thead['orderby'] !== '') {
$orderby_url = add_query_arg('orderby', $thead['orderby'], $page_url);
$orderby_html = '<span class="orderby-but">';
$orderby_html .= '<a title="' . esc_attr__('降序', 'aksrt') . '" href="' . esc_url(add_query_arg('order', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
$orderby_html .= '<a title="' . esc_attr__('升序', 'aksrt') . '" href="' . esc_url(add_query_arg('order', 'DESC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-down"></span></a>';
$orderby_html .= '</span>';
}
echo '<th width="' . esc_attr($thead['width']) . '">' . esc_html($thead['name']) . $orderby_html . '</th>';
}
echo '</tr></thead><tbody>';
if (!$tickets) {
echo '<tr><td colspan="6" align="center"><strong>' . esc_html__('暂无申请记录', 'aksrt') . '</strong></td></tr>';
}
$process_action = isset($_GET['action']) ? sanitize_key((string) wp_unslash($_GET['action'])) : '';
$process_id = isset($_GET['id']) ? (string) wp_unslash($_GET['id']) : '';
foreach ($tickets as $row) {
$this->render_ticket_row($row, $page_url);
if ($process_action === 'process' && $process_id === (string) $row['kind'] . '-' . (string) $row['id']) {
$this->render_ticket_process_form($row, $action_url);
}
}
echo '</tbody></table></div>';
$this->pagination($pages, $paged);
}
private function render_ticket_row(array $row, string $page_url): void {
$kind = $row['kind'];
$status = $row['status'];
$user_id = (int) $row['user_id'];
$url = add_query_arg(['action' => 'process', 'id' => $kind . '-' . $row['id']], $page_url);
// 状态列(对齐参考主题:待处理→立即处理按钮 / 已批准 / 处理完成 / 已拒绝)
if ($status === 'pending') {
$status_html = '<a class="button" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a>';
} elseif ($status === 'resolved') {
$status_html = $kind === 'appeal'
? '<span style="color:#16a60a;">' . esc_html__('已批准', 'aksrt') . '</span>'
: '<span style="color:#0989fd;">' . esc_html__('处理完成', 'aksrt') . '</span>';
} else {
$status_html = '<span style="color:#fb4444;">' . esc_html__('已拒绝', 'aksrt') . '</span>';
}
// 类型列(对齐参考主题:不良举报红 / 禁封申诉绿)
$type_html = $kind === 'report'
? '<span style="color:#fb4444;">' . esc_html__('不良举报', 'aksrt') . '</span>'
: '<span style="color:#16a60a;">' . esc_html__('禁封申诉', 'aksrt') . '</span>';
// 提交用户列(显示名链接 + 管理|查看 row-actions)
$user = $user_id > 0 ? get_userdata($user_id) : null;
if ($user) {
$user_html = '<a href="' . esc_url(add_query_arg('send_user', $user_id, $page_url)) . '">' . esc_html($user->display_name) . '</a>';
$user_html .= '<div class="row-actions">';
$user_html .= '<a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html__('管理', 'aksrt') . '</a> | ';
$home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id);
$user_html .= '<a href="' . esc_url($home) . '">' . esc_html__('查看', 'aksrt') . '</a>';
$user_html .= '</div>';
} else {
$user_html = esc_html__('未登录用户', 'aksrt');
}
$desc_html = $kind === 'report' ? $this->ticket_report_desc_html($row['payload']) : $this->ticket_appeal_desc_html($row['payload'], (int) $row['id']);
echo '<tr>';
echo '<td>' . $status_html . '</td>';
echo '<td>' . $type_html . '</td>';
echo '<td>' . $user_html . '</td>';
echo '<td><div style="max-height:39px;overflow:hidden;">' . $desc_html . '</div></td>';
echo '<td>' . esc_html((string) $row['time']) . '</td>';
echo '<td>' . esc_html((string) ($row['updated'] !== '' ? $row['updated'] : '—')) . '</td>';
echo '</tr>';
}
/** 举报行详情(对齐参考主题:举报目标红字 + 原因 + 分类) */
private function ticket_report_desc_html(array $r): string {
$target_type = sanitize_key((string) ($r['target_type'] ?? ''));
$target_id = (int) ($r['target_id'] ?? 0);
$link = $this->ticket_target_link($target_type, $target_id);
$html = '<div style="color:#fb4444;">' . esc_html(sprintf(__('举报目标:%s', 'aksrt'), $link !== '' ? $link : $target_type . ' #' . $target_id)) . '</div>';
$html .= '<div>' . esc_html(sprintf(__('举报原因:%s', 'aksrt'), (string) ($r['reason'] ?? ''))) . '</div>';
if (!empty($r['category'])) {
$html .= '<div>' . esc_html(sprintf(__('分类:%s', 'aksrt'), (string) $r['category'])) . '</div>';
}
return $html;
}
/** 申诉行详情(对齐参考主题:禁封状态/原因/起止时间,或已解封) */
private function ticket_appeal_desc_html(array $payload, int $user_id): string {
$ban_html = $this->ticket_ban_info_html($user_id);
$html = $ban_html;
if (!empty($payload['reason'])) {
$html .= '<div>' . esc_html(sprintf(__('申诉说明:%s', 'aksrt'), (string) $payload['reason'])) . '</div>';
}
return $html;
}
/** 用户当前限制状态摘要(对齐参考主题禁封信息展示) */
private function ticket_ban_info_html(int $user_id): string {
if ($user_id <= 0 || !class_exists('\AKSRT\User\Restriction')) {
return '';
}
$svc = \AKSRT\User\Restriction::get_instance();
$ban = $svc->get_active_ban($user_id);
if ($ban) {
$end = !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt');
$html = '<div style="color:#fd6a11;">' . esc_html(sprintf(__('禁封状态:%s', 'aksrt'), __('封禁中(禁止登录)', 'aksrt'))) . '</div>';
$html .= '<div>' . esc_html(sprintf(__('禁封原因:%s', 'aksrt'), (string) ($ban['reason'] ?? ''))) . '</div>';
$html .= '<div>' . esc_html(sprintf(__('开始时间:%s', 'aksrt'), (string) ($ban['created_at'] ?? ''))) . '</div>';
$html .= '<div>' . esc_html(sprintf(__('结束时间:%s', 'aksrt'), $end)) . '</div>';
return $html;
}
if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_SUSPEND)) {
return '<div style="color:#fd6a11;">' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('停权中', 'aksrt'))) . '</div>';
}
if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_MUTE)) {
return '<div style="color:#fd6a11;">' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '</div>';
}
return '<div style="color:#16a60a;">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
}
private function ticket_target_link(string $target_type, int $target_id): string {
if ($target_type === 'post') {
$post = get_post($target_id);
if ($post instanceof \WP_Post) {
return '<a target="_blank" href="' . esc_url((string) get_permalink($post)) . '">' . esc_html(get_the_title($post)) . '</a>';
}
} elseif ($target_type === 'comment') {
$comment = get_comment($target_id);
if ($comment instanceof \WP_Comment) {
return '<a target="_blank" href="' . esc_url((string) get_comment_link($comment)) . '">#' . $target_id . '</a>';
}
} elseif ($target_type === 'user') {
$url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : '';
$user = get_userdata($target_id);
if ($url !== '' && $user) {
return '<a target="_blank" href="' . esc_url($url) . '">' . esc_html($user->display_name) . '</a>';
}
}
return '';
}
/** 行内处理表单(对齐参考主题 form-table 结构) */
private function render_ticket_process_form(array $row, string $action_url): void {
$kind = $row['kind'];
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<table class="form-table"><tbody>';
if ($kind === 'report') {
$r = $row['payload'];
$target_type = sanitize_key((string) ($r['target_type'] ?? ''));
$target_id = (int) ($r['target_id'] ?? 0);
$link = $this->ticket_target_link($target_type, $target_id);
echo '<tr><th>' . esc_html__('被举报目标', 'aksrt') . '</th><td>';
echo $link !== '' ? $link : esc_html($target_type . ' #' . $target_id);
echo '<input type="hidden" name="report_id" value="' . esc_attr((string) $row['id']) . '" />';
echo '</td></tr>';
echo '<tr><th>' . esc_html__('举报信息', 'aksrt') . '</th><td>';
echo esc_html(sprintf(__('举报原因:%s', 'aksrt'), (string) ($r['reason'] ?? ''))) . '<br>';
if (!empty($r['category'])) {
echo esc_html(sprintf(__('分类:%s', 'aksrt'), (string) $r['category'])) . '<br>';
}
echo esc_html(sprintf(__('提交时间:%s', 'aksrt'), (string) $row['time']));
echo '</td></tr>';
if ($target_type === 'user') {
echo '<tr><th>' . esc_html__('被举报用户限制信息', 'aksrt') . '</th><td>';
echo $this->ticket_ban_info_html($target_id);
echo '<p class="description">' . esc_html__('如举报事实清楚,可对用户执行处罚动作,处理结果会反馈给举报人。', 'aksrt') . '</p>';
echo '</td></tr>';
echo '<tr><th>' . esc_html__('处罚动作', 'aksrt') . '</th><td><select name="action_taken">';
foreach ([
'none' => __('仅更新状态', 'aksrt'),
'warn' => __('发送警告', 'aksrt'),
'mute' => __('禁言', 'aksrt'),
'suspend' => __('停权', 'aksrt'),
'ban' => __('封禁账号', 'aksrt'),
'clear' => __('解除限制', 'aksrt'),
] as $value => $label) {
printf('<option value="%s">%s</option>', esc_attr($value), esc_html($label));
}
echo '</select></td></tr>';
} else {
echo '<tr><th>' . esc_html__('内容处理', 'aksrt') . '</th><td><select name="action_taken">';
foreach ([
'none' => __('仅更新状态', 'aksrt'),
'content_removed' => __('转为待审内容', 'aksrt'),
] as $value => $label) {
printf('<option value="%s">%s</option>', esc_attr($value), esc_html($label));
}
echo '</select></td></tr>';
}
echo '<tr><th>' . esc_html__('处理结果', 'aksrt') . '</th><td><select name="new_status">';
foreach ([
'resolved' => __('处理完成', 'aksrt'),
'dismissed' => __('已驳回', 'aksrt'),
'reviewing' => __('处理中', 'aksrt'),
] as $value => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($value), $value === 'resolved' ? ' selected="selected"' : '', esc_html($label));
}
echo '</select></td></tr>';
echo '<tr><th>' . esc_html__('处理说明', 'aksrt') . '</th><td>';
echo '<textarea style="width:95%;max-width:500px;" rows="2" cols="40" name="note" placeholder="' . esc_attr__('请输入本次处理的方式及说明', 'aksrt') . '"></textarea>';
echo '<p class="description">' . esc_html__('此处内容会反馈给举报人', 'aksrt') . '</p>';
echo '</td></tr>';
wp_nonce_field('aksrt_users_update_report_' . (int) $row['id']);
} else {
$uid = (int) $row['id'];
$user = get_userdata($uid);
echo '<tr><th>' . esc_html__('申诉用户', 'aksrt') . '</th><td>';
if ($user instanceof \WP_User) {
$url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($uid) : get_author_posts_url($uid);
echo '<a target="_blank" href="' . esc_url($url) . '">' . esc_html($user->display_name) . '</a>';
} else {
echo esc_html('#' . $uid);
}
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $uid) . '" />';
echo '</td></tr>';
echo '<tr><th>' . esc_html__('禁封信息', 'aksrt') . '</th><td>' . $this->ticket_ban_info_html($uid) . '</td></tr>';
echo '<tr><th>' . esc_html__('申诉信息', 'aksrt') . '</th><td>';
$payload = $row['payload'];
echo esc_html(sprintf(__('申诉类型:%s', 'aksrt'), (string) ($payload['reason_key'] ?? '—'))) . '<br>';
echo esc_html(sprintf(__('申诉说明:%s', 'aksrt'), (string) ($payload['reason'] ?? ''))) . '<br>';
echo esc_html(sprintf(__('提交时间:%s', 'aksrt'), (string) ($payload['time'] ?? '')));
echo '</td></tr>';
echo '<tr><th>' . esc_html__('处理方式', 'aksrt') . '</th><td>';
echo '<p><input type="radio" name="decision" id="aksrt-process-1-' . $uid . '" value="approve" checked="checked"><label for="aksrt-process-1-' . $uid . '" style="color:#036ee2;">' . esc_html__('申诉通过(解封该用户)', 'aksrt') . '</label></p>';
echo '<p><input type="radio" name="decision" id="aksrt-process-2-' . $uid . '" value="reject"><label for="aksrt-process-2-' . $uid . '" style="color:#eb1b65;">' . esc_html__('申诉拒绝(保持原禁封状态)', 'aksrt') . '</label></p>';
echo '</td></tr>';
echo '<tr><th>' . esc_html__('处理留言', 'aksrt') . '</th><td>';
echo '<textarea style="width:95%;max-width:500px;" rows="2" cols="40" name="note" placeholder="' . esc_attr__('给用户留言', 'aksrt') . '"></textarea>';
echo '<p class="description">' . esc_html__('如需给用户留言请填写此处,如果拒绝请填写拒绝原因', 'aksrt') . '</p>';
echo '</td></tr>';
wp_nonce_field('aksrt_users_process_appeal_' . $uid);
}
echo '<tr><th></th><td><p><button type="submit" class="button button-primary process-submit">' . esc_html__('确认提交', 'aksrt') . '</button></p></td></tr>';
echo '</tbody></table></form>';
}
}
+2 -53
View File
@@ -354,61 +354,10 @@ trait UserControllerReportOperations {
}
private function apply_report_action(string $target_type, int $target_id, string $action_taken, string $reason = ''): string {
if ($target_id <= 0 || $action_taken === 'none') {
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
return '';
}
if ($target_type === 'user') {
if (!class_exists('\AKSRT\User\Restriction')) {
return '';
}
$svc = \AKSRT\User\Restriction::get_instance();
if ($action_taken === 'clear') {
$removed = $svc->lift_user_restriction($target_id);
return $removed > 0
? sprintf(__('已解除 %d 条生效中的限制', 'aksrt'), $removed)
: __('该用户没有生效中的限制', 'aksrt');
}
if (in_array($action_taken, \AKSRT\User\Restriction::TYPES, true)) {
$labels = [
\AKSRT\User\Restriction::TYPE_WARN => __('警告', 'aksrt'),
\AKSRT\User\Restriction::TYPE_MUTE => __('禁言', 'aksrt'),
\AKSRT\User\Restriction::TYPE_SUSPEND => __('停权', 'aksrt'),
\AKSRT\User\Restriction::TYPE_BAN => __('封禁', 'aksrt'),
];
$result = $svc->restrict(
$target_id,
$action_taken,
$reason !== '' ? $reason : __('举报处理处罚', 'aksrt'),
\AKSRT\User\Restriction::SCOPE_GLOBAL,
'',
get_current_user_id()
);
if (!empty($result['success'])) {
return sprintf(__('已对用户执行限制:%s', 'aksrt'), $labels[$action_taken] ?? $action_taken);
}
return '';
}
return '';
}
if ($action_taken === 'content_removed' && $target_type === 'post' && get_post($target_id)) {
wp_update_post([
'ID' => $target_id,
'post_status' => 'pending',
]);
return __('已将内容改为待审状态', 'aksrt');
}
if ($action_taken === 'content_removed' && $target_type === 'comment' && get_comment($target_id)) {
wp_set_comment_status($target_id, 'hold', true);
return __('已将评论改为待审状态', 'aksrt');
}
return '';
return \AKSRT\Messages\ReportCenter::get_instance()->apply_action($target_type, $target_id, $action_taken, $reason);
}
private function get_report_status_options(): array {
+81
View File
@@ -159,6 +159,87 @@ class ReportCenter {
return $result !== false;
}
/**
* 获取单条举报原始行(ARRAY_A),不存在返回 null。
*/
public function get_report_row(int $report_id): ?array {
$table = $this->table();
if ($table === '' || $report_id <= 0) {
return null;
}
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A);
return $row ?: null;
}
/**
* 对举报目标执行处罚动作(REST 与原生后台共用入口)。
*
* @param string $target_type 目标类型 post/comment/user
* @param int $target_id 目标 ID
* @param string $action_taken none/warn/mute/suspend/ban/clear/content_removed
* @param string $reason 处罚原因(写入限制记录)
* @return string 执行结果描述,未执行返回空串
*/
public function apply_action(string $target_type, int $target_id, string $action_taken, string $reason = ''): string {
if ($target_id <= 0 || $action_taken === 'none') {
return '';
}
if ($target_type === 'user') {
if (!class_exists('\AKSRT\User\Restriction')) {
return '';
}
$svc = \AKSRT\User\Restriction::get_instance();
if ($action_taken === 'clear') {
$removed = $svc->lift_user_restriction($target_id);
return $removed > 0
? sprintf(__('已解除 %d 条生效中的限制', 'aksrt'), $removed)
: __('该用户没有生效中的限制', 'aksrt');
}
if (in_array($action_taken, \AKSRT\User\Restriction::TYPES, true)) {
$labels = [
\AKSRT\User\Restriction::TYPE_WARN => __('警告', 'aksrt'),
\AKSRT\User\Restriction::TYPE_MUTE => __('禁言', 'aksrt'),
\AKSRT\User\Restriction::TYPE_SUSPEND => __('停权', 'aksrt'),
\AKSRT\User\Restriction::TYPE_BAN => __('封禁', 'aksrt'),
];
$result = $svc->restrict(
$target_id,
$action_taken,
$reason !== '' ? $reason : __('举报处理处罚', 'aksrt'),
\AKSRT\User\Restriction::SCOPE_GLOBAL,
'',
get_current_user_id()
);
if (!empty($result['success'])) {
return sprintf(__('已对用户执行限制:%s', 'aksrt'), $labels[$action_taken] ?? $action_taken);
}
return '';
}
return '';
}
if ($action_taken === 'content_removed' && $target_type === 'post' && get_post($target_id)) {
wp_update_post([
'ID' => $target_id,
'post_status' => 'pending',
]);
return __('已将内容改为待审状态', 'aksrt');
}
if ($action_taken === 'content_removed' && $target_type === 'comment' && get_comment($target_id)) {
wp_set_comment_status($target_id, 'hold', true);
return __('已将评论改为待审状态', 'aksrt');
}
return '';
}
/**
* Get status counts for the admin dashboard.
*
+1 -1
View File
@@ -176,7 +176,7 @@ final class MailNotifications {
public static function report_submitted(int $report_id, array $report): void {
$reporter = get_userdata((int) ($report['reporter_id'] ?? 0));
self::send_admin('report_admin', ['username' => $reporter instanceof \WP_User ? $reporter->display_name : __('用户', 'aksrt'), 'title' => (string) ($report['target_type'] ?? ''), 'url' => admin_url('users.php?page=aksrt_user_ban&tab=reports')]);
self::send_admin('report_admin', ['username' => $reporter instanceof \WP_User ? $reporter->display_name : __('用户', 'aksrt'), 'title' => (string) ($report['target_type'] ?? ''), 'url' => admin_url('users.php?page=aksrt_user_ban')]);
}
public static function report_status_changed(int $report_id, int $reporter_id, string $status): void {
@@ -43,11 +43,13 @@ test('report center has no legacy ajax handlers or option migration', () => {
assert.match(reportOps, /has_pending_report\(\$reporter_id, \$target_type, \$target_id\)/)
})
test('report processing writes real restrictions, not a display-only meta', () => {
assert.match(reportOps, /->restrict\(\s*\$target_id,\s*\$action_taken,/)
assert.match(reportOps, /lift_user_restriction\(\$target_id\)/)
assert.match(reportOps, /in_array\(\$action_taken, \\AKSRT\\User\\Restriction::TYPES, true\)/)
assert.ok(!reportOps.includes('aksrt_user_ban_state'), 'dual-track ban state meta must not be written or read')
test('report processing delegates actions to the shared ReportCenter entry', () => {
assert.match(reportOps, /ReportCenter::get_instance\(\)->apply_action\(\$target_type, \$target_id, \$action_taken, \$reason\)/)
assert.match(reportCenter, /public function apply_action\(/)
assert.match(reportCenter, /->restrict\(\s*\$target_id,\s*\$action_taken,/)
assert.match(reportCenter, /lift_user_restriction\(\$target_id\)/)
assert.match(reportCenter, /in_array\(\$action_taken, \\AKSRT\\User\\Restriction::TYPES, true\)/)
assert.ok(!reportCenter.includes('aksrt_user_ban_state'), 'dual-track ban state meta must not be written or read')
for (const deadAction of ["'value' => 'warned'", "'value' => 'muted'", "'value' => 'restricted'", "'value' => 'banned'"]) {
assert.ok(!reportOps.includes(deadAction), `action value must map to Restriction types: ${deadAction}`)
}
@@ -55,6 +57,8 @@ test('report processing writes real restrictions, not a display-only meta', () =
assert.ok(reportOps.includes(action), `missing action option: ${action}`)
}
assert.match(reportOps, /get_user_restriction_state/)
assert.match(reportCenter, /public function get_report_row\(/)
assert.match(usersMenus, /\$svc->apply_action\(/)
})
test('restriction class supports lifting and kicks sessions on ban', () => {
@@ -93,8 +97,46 @@ test('profile governance notice reads from the restriction table', () => {
})
test('admin report notification links to the real page', () => {
assert.match(mailNotifications, /users\.php\?page=aksrt_user_ban&tab=reports/)
assert.match(mailNotifications, /users\.php\?page=aksrt_user_ban'/)
assert.ok(!mailNotifications.includes('page=aksrt_reports&'), 'dead admin page link')
assert.ok(!mailNotifications.includes('tab=reports'), 'merged ticket page has no reports tab')
})
test('native ban page mirrors the reference theme layout', () => {
const tickets = read('src/Admin/Native/UsersMenusTickets.php')
// 菜单与页面标题对齐参考主题
assert.ok(usersMenus.includes(`__('举报&禁封申诉', 'aksrt'),\n __('举报&禁封申诉', 'aksrt')`), 'submenu title should be 举报&禁封申诉')
assert.ok(usersMenus.includes('用户禁封申诉及用户举报管理'), 'page heading should match the reference theme')
// 类型筛选按钮组 + 状态 subsubsub + 搜索
for (const label of [`__('禁封申诉', 'aksrt')`, `__('用户举报', 'aksrt')`, `__('待处理', 'aksrt')`, `__('已批准', 'aksrt')`, `__('已拒绝', 'aksrt')`, `__('搜索记录', 'aksrt')`]) {
assert.ok(tickets.includes(label), `missing reference-theme label: ${label}`)
}
// 表头列与参考主题一致:状态/类型/提交用户/详情/提交时间/更新时间(带排序)
for (const head of ['状态', '类型', '提交用户', '详情', '提交时间', '更新时间']) {
assert.ok(tickets.includes(`__('${head}', 'aksrt')`), `missing table head: ${head}`)
}
assert.match(tickets, /orderby-but/)
assert.match(tickets, /dashicons-arrow-up/)
// 行内处理表单对齐参考主题
for (const label of [`__('立即处理', 'aksrt')`, `__('申诉通过(解封该用户)', 'aksrt')`, `__('申诉拒绝(保持原禁封状态)', 'aksrt')`, `__('此处内容会反馈给举报人', 'aksrt')`, `__('确认提交', 'aksrt')`, `__('用户状态正常', 'aksrt')`, `__('暂无申请记录', 'aksrt')`]) {
assert.ok(tickets.includes(label), `missing process form label: ${label}`)
}
// 分页(每页 20 条,与参考主题一致)
assert.match(tickets, /TICKETS_PER_PAGE = 20/)
assert.match(usersMenus, /private function pagination\(int \$pages, int \$page_num\): void/)
assert.match(usersMenus, /paginate_links\(/)
// 旧的三 tab 结构已收敛
assert.ok(!usersMenus.includes('tab=reports'), 'old reports tab is gone')
assert.ok(!usersMenus.includes('tab=appeals'), 'old appeals tab is gone')
assert.ok(!usersMenus.includes('render_reports_tab'))
assert.ok(!usersMenus.includes('render_appeals_tab'))
assert.match(usersMenus, /use UsersMenusTickets;/)
})
test('comments expose a report entry for other users when reporting is enabled', () => {