feat(admin): fully align the ban page with the reference theme

- submenu page title becomes 处理举报及禁封申诉 (menu title 举报&禁封申诉)
- replicate the reference pagenavi: first/prev/numbered/next/last links
  with displaying-num (每页 20 共N) and paging-input (第X页共Y页)
- report process form is now status-only like the reference theme:
  被举报用户/举报信息/被举报用户禁封信息/处理说明 blocks, with a 点击此处
  link to the restrictions tab (prefilled user_id) instead of an
  enforcement select
- replicate reference row details (举报用户/举报内容, 正在处理 state,
  此用户已解封 auto-close for stale appeals, 当前用户已解封 notice) and
  the table-box/preview-item style block
- drop now-unused ReportCenter::get_report_row
This commit is contained in:
2026-09-19 22:09:24 +08:00
parent 902e71ee62
commit c0e0912eee
4 changed files with 100 additions and 98 deletions
+6 -23
View File
@@ -82,7 +82,7 @@ class UsersMenus {
);
add_submenu_page(
'users.php',
__('举报&禁封申诉', 'aksrt'),
__('处理举报及禁封申诉', 'aksrt'),
__('举报&禁封申诉', 'aksrt'),
self::CAPABILITY,
'aksrt_user_ban',
@@ -324,7 +324,7 @@ class UsersMenus {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_users_add_restriction" />';
wp_nonce_field('aksrt_users_add_restriction');
echo '<p><label>' . esc_html__('用户 ID', 'aksrt') . '<br><input type="number" name="user_id" min="1" required /></label></p>';
echo '<p><label>' . esc_html__('用户 ID', 'aksrt') . '<br><input type="number" name="user_id" min="1" required value="' . esc_attr((string) (isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0)) . '" /></label></p>';
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
foreach (['warn' => __('警告', 'aksrt'), 'mute' => __('禁言', 'aksrt'), 'suspend' => __('停权', 'aksrt'), 'ban' => __('封禁', 'aksrt')] as $key => $label) {
printf('<option value="%s">%s</option>', esc_attr($key), esc_html($label));
@@ -377,28 +377,11 @@ class UsersMenus {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$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';
$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'])) : '';
check_admin_referer('aksrt_users_update_report_' . $id);
$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'] ?? '')
);
}
}
}
$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' => 'tickets']);
}
+81 -50
View File
@@ -119,7 +119,8 @@ trait UsersMenusTickets {
$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');
$action_url = admin_url('users.php?page=aksrt_user_ban');
$process_action = isset($_GET['action']) ? sanitize_key((string) wp_unslash($_GET['action'])) : '';
$page_url = add_query_arg(array_filter([
'tab' => 'tickets',
'type' => $type,
@@ -128,6 +129,13 @@ trait UsersMenusTickets {
'send_user' => $send_user,
], static function ($v) { return $v !== '' && $v !== 0; }), $action_url);
// 对齐参考主题页面样式块
echo '<style>
.table-box>table{min-width:740px;}
.preview-item{position:relative;display:inline-block;width:160px;height:160px;border-radius:4px;background:#fff;margin:5px;vertical-align:top;}
.preview-item img{object-fit:contain;width:100%;height:100%;}
</style>';
$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));
@@ -175,6 +183,10 @@ trait UsersMenusTickets {
echo '<div class="mb10">"' . esc_html($s) . '" ' . esc_html__('的搜索结果', 'aksrt') . '</div>';
}
if ($process_action === 'process') {
echo '<a href="' . esc_url(remove_query_arg(['action', 'id'], $page_url)) . '">' . esc_html__('查看全部', 'aksrt') . '</a>';
}
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>';
@@ -205,8 +217,7 @@ trait UsersMenusTickets {
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']) : '';
$process_id = isset($_GET['id']) ? (string) wp_unslash($_GET['id']) : '';
foreach ($tickets as $row) {
$this->render_ticket_row($row, $page_url);
@@ -217,7 +228,28 @@ trait UsersMenusTickets {
echo '</tbody></table></div>';
$this->pagination($pages, $paged);
$this->ticket_pagenavi($total, self::TICKETS_PER_PAGE);
}
/** 分页(对齐参考主题 zibpay_admin_pagenavi:« ‹ 第X页共Y页 › » + 每页显示数) */
private function ticket_pagenavi(int $total_count, int $per_page): void {
$current_page = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
$total_pages = max(1, (int) ceil($total_count / max(1, $per_page)));
$first_page_url = add_query_arg('paged', 1);
$last_page_url = add_query_arg('paged', $total_pages);
$prev_page_url = $current_page > 1 ? add_query_arg('paged', $current_page - 1) : '#';
$next_page_url = $current_page < $total_pages ? add_query_arg('paged', $current_page + 1) : '#';
echo '<div class="tablenav bottom"><div class="tablenav-pages">';
echo '<span class="displaying-num">' . esc_html(sprintf(__('每页 %1$s 共%2$s', 'aksrt'), $per_page, $total_count)) . '</span>';
echo '<span class="pagination-links">';
echo '<a class="first-page button' . ($current_page <= 1 ? ' disabled' : '') . '" title="' . esc_attr__('前往第一页', 'aksrt') . '" href="' . esc_url($first_page_url) . '">«</a>';
echo '<a class="prev-page button' . ($current_page <= 1 ? ' disabled' : '') . '" title="' . esc_attr__('前往上一页', 'aksrt') . '" href="' . esc_url($prev_page_url) . '">‹</a>';
echo '<span class="paging-input">' . esc_html(sprintf(__('第 %1$s 页,共 ', 'aksrt'), $current_page)) . '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>' . esc_html__(' 页', 'aksrt') . '</span>';
echo '<a class="next-page button' . ($current_page >= $total_pages ? ' disabled' : '') . '" title="' . esc_attr__('前往下一页', 'aksrt') . '" href="' . esc_url($next_page_url) . '">›</a>';
echo '<a class="last-page button' . ($current_page >= $total_pages ? ' disabled' : '') . '" title="' . esc_attr__('前往最后一页', 'aksrt') . '" href="' . esc_url($last_page_url) . '">»</a>';
echo '</span></div><br class="clear"></div>';
}
private function render_ticket_row(array $row, string $page_url): void {
@@ -226,8 +258,12 @@ trait UsersMenusTickets {
$user_id = (int) $row['user_id'];
$url = add_query_arg(['action' => 'process', 'id' => $kind . '-' . $row['id']], $page_url);
// 状态列(对齐参考主题:待处理→立即处理按钮 / 已批准 / 处理完成 / 已拒绝)
if ($status === 'pending') {
// 状态列(对齐参考主题:待处理→立即处理按钮 / 已批准 / 处理完成 / 已拒绝 / 正在处理)
$is_processing = isset($_GET['action']) && sanitize_key((string) wp_unslash($_GET['action'])) === 'process'
&& isset($_GET['id']) && (string) wp_unslash($_GET['id']) === $kind . '-' . (string) $row['id'];
if ($is_processing) {
$status_html = '<span style="color:#fb4444;">' . esc_html__('正在处理', 'aksrt') . '</span>';
} elseif ($status === 'pending') {
$status_html = '<a class="button" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a>';
} elseif ($status === 'resolved') {
$status_html = $kind === 'appeal'
@@ -267,13 +303,17 @@ trait UsersMenusTickets {
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>';
if ($target_type === 'user') {
$html = '<div style="color:#fb4444;">' . esc_html__('举报用户:', 'aksrt') . ($link !== '' ? $link : esc_html('#' . $target_id)) . '</div>';
} else {
$html = '<div style="color:#fb4444;">' . esc_html__('举报内容:', 'aksrt') . ($link !== '' ? $link : esc_html($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>';
@@ -283,7 +323,7 @@ trait UsersMenusTickets {
/** 申诉行详情(对齐参考主题:禁封状态/原因/起止时间,或已解封) */
private function ticket_appeal_desc_html(array $payload, int $user_id): string {
$ban_html = $this->ticket_ban_info_html($user_id);
$ban_html = $this->ticket_ban_info_html($user_id, 'appeal_row');
$html = $ban_html;
if (!empty($payload['reason'])) {
$html .= '<div>' . esc_html(sprintf(__('申诉说明:%s', 'aksrt'), (string) $payload['reason'])) . '</div>';
@@ -291,13 +331,17 @@ trait UsersMenusTickets {
return $html;
}
/** 用户当前限制状态摘要(对齐参考主题禁封信息展示) */
private function ticket_ban_info_html(int $user_id): string {
/**
* 用户当前限制状态摘要(对齐参考主题禁封信息展示)。
*
* @param string $context appeal_row(申诉列表行,已解封时自动结案)| form(处理表单)
*/
private function ticket_ban_info_html(int $user_id, string $context = 'form'): string {
if ($user_id <= 0 || !class_exists('\AKSRT\User\Restriction')) {
return '';
}
$svc = \AKSRT\User\Restriction::get_instance();
$ban = $svc->get_active_ban($user_id);
$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');
@@ -315,6 +359,17 @@ trait UsersMenusTickets {
return '<div style="color:#fd6a11;">' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '</div>';
}
// 对齐参考主题:申诉列表行中用户已无禁封 → 显示已解封并自动结案
if ($context === 'appeal_row' && class_exists('\AKSRT\User\BanAppeal') && \AKSRT\User\BanAppeal::get_status($user_id) === 'pending') {
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_STATUS, 'approved');
return '<div style="color:#fd6a11;">' . esc_html__('此用户已解封', 'aksrt') . '</div>';
}
// 对齐参考主题:申诉处理表单中用户已解封的提示
if ($context === 'appeal_form') {
return '<div style="color:#16a60a;">' . esc_html__('当前用户已解封', 'aksrt') . '</div>';
}
return '<div style="color:#16a60a;">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
}
@@ -352,9 +407,10 @@ trait UsersMenusTickets {
$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 '<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 '<input type="hidden" name="new_status" value="resolved" />';
echo '</td></tr>';
echo '<tr><th>' . esc_html__('举报信息', 'aksrt') . '</th><td>';
@@ -366,44 +422,19 @@ trait UsersMenusTickets {
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 '<tr><th>' . esc_html__('被举报用户禁封信息', 'aksrt') . '</th><td>';
if (class_exists('\AKSRT\User\Restriction') && \AKSRT\User\Restriction::get_instance()->is_restricted($target_id)) {
echo $this->ticket_ban_info_html($target_id);
} else {
$ban_url = admin_url('users.php?page=aksrt_user_ban&tab=restrictions&user_id=' . $target_id);
echo '<div style="color:#16a60a;">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
echo '<p class="description">' . esc_html__('请查阅相关资料以及举证,如举报事实清楚,可以', 'aksrt');
echo ' <a target="_blank" href="' . esc_url($ban_url) . '">' . esc_html__('点击此处', 'aksrt') . '</a> ';
echo 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>';
@@ -424,7 +455,7 @@ trait UsersMenusTickets {
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>' . $this->ticket_ban_info_html($uid, 'appeal_form') . '</td></tr>';
echo '<tr><th>' . esc_html__('申诉信息', 'aksrt') . '</th><td>';
$payload = $row['payload'];
-14
View File
@@ -159,20 +159,6 @@ 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 与原生后台共用入口)。
*