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'); $process_action = isset($_GET['action']) ? sanitize_key((string) wp_unslash($_GET['action'])) : ''; $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); NativeUi::card_open(); echo '
'; // 类型筛选按钮组(对齐参考主题:全部 / 禁封申诉 / 用户举报) echo '
'; 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('%s ', $type === $t ? ' button-primary' : '', esc_url($url), esc_html($label)); } echo '
'; // 搜索(对齐参考主题:右侧搜索记录表单) echo '
'; echo ''; if ($type !== '') { echo ''; } echo ''; submit_button(__('提交', 'aksrt'), 'primary', '', false); echo '
'; // 状态 subsubsub(对齐参考主题:全部 | 待处理 | 已批准 | 已拒绝) echo ''; if ($s !== '') { echo '

"' . esc_html($s) . '" ' . esc_html__('的搜索结果', 'aksrt') . '

'; } NativeUi::card_close(); $process_id = isset($_GET['id']) ? (string) wp_unslash($_GET['id']) : ''; // 处理视图不设“查看全部”链接(参考主题无此元素):重置过滤由顶部 // 类型按钮组与状态 subsubsub 承担,工单行内直接展开处理表单。 NativeUi::table_card_open(); echo ''; echo ''; // 表头(对齐参考主题:状态/类型/提交用户/详情/提交时间/更新时间 + 排序箭头) $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 = ''; $orderby_html .= ''; $orderby_html .= ''; $orderby_html .= ''; } echo ''; } echo ''; if (!$tickets) { echo NativeUi::empty_row(6, __('暂无申请记录', 'aksrt')); } foreach ($tickets as $row) { $process_key = (string) $row['kind'] . '-' . (string) $row['id']; $expanded = $process_action === 'process' && $process_id === $process_key && $row['status'] === 'pending'; $this->render_ticket_row($row, $page_url, $expanded); if ($row['status'] !== 'pending') { continue; } // 展开与收起走 JS 切换(无整页刷新,即点即达):展开时隐藏列表行、 // 显示处理表单(信息只出现一次);取消时换回。无 JS 环境由立即处理 // 链接(action=process)整页兜底,直链进入时表单默认展开。 $hidden_attr = $expanded ? '' : ' style="display:none;"'; echo ''; } echo '
' . esc_html($thead['name']) . $orderby_html . '
'; $this->render_ticket_process_form($row, $action_url); echo '
'; NativeUi::table_wrap_close(); $this->ticket_pagenavi($total, self::TICKETS_PER_PAGE); NativeUi::card_close(); // 行内展开/收起的轻量交互(管理员环境必有 jQuery)。 echo ''; } /** 分页(对齐参考主题 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 '
'; echo '' . esc_html(sprintf(__('每页 %1$s 共%2$s', 'aksrt'), $per_page, $total_count)) . ''; echo ''; echo '«'; echo '‹'; echo '' . esc_html(sprintf(__('第 %1$s 页,共 ', 'aksrt'), $current_page)) . '' . esc_html((string) $total_pages) . '' . esc_html__(' 页', 'aksrt') . ''; echo '›'; echo '»'; echo '

'; } private function render_ticket_row(array $row, string $page_url, bool $hidden = false): void { $kind = $row['kind']; $status = $row['status']; $user_id = (int) $row['user_id']; $process_key = $kind . '-' . (string) $row['id']; $url = add_query_arg(['action' => 'process', 'id' => $process_key], $page_url); // 状态列(对齐参考主题:待处理→立即处理按钮 / 已批准 / 处理完成 / 已拒绝 / 正在处理) $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 = NativeUi::badge(__('正在处理', 'aksrt'), 'warning'); } elseif ($status === 'pending') { // JS 切换展开(无整页刷新);无 JS 时 href 整页兜底。 $status_html = '' . esc_html__('立即处理', 'aksrt') . ''; } elseif ($status === 'resolved') { $status_html = $kind === 'appeal' ? NativeUi::badge(__('已批准', 'aksrt'), 'success') : NativeUi::badge(__('处理完成', 'aksrt'), 'info'); } else { $status_html = NativeUi::badge(__('已拒绝', 'aksrt'), 'danger'); } // 类型列(对齐参考主题:不良举报红 / 禁封申诉绿) $type_html = $kind === 'report' ? NativeUi::badge(__('不良举报', 'aksrt'), 'danger', false) : NativeUi::badge(__('禁封申诉', 'aksrt'), 'success', false); // 提交用户列(显示名链接 + 管理|查看 row-actions) $user = $user_id > 0 ? get_userdata($user_id) : null; if ($user) { $user_html = '' . esc_html($user->display_name) . ''; $user_html .= '
'; $user_html .= '' . esc_html__('管理', 'aksrt') . ' | '; $home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id); $user_html .= '' . esc_html__('查看', 'aksrt') . ''; $user_html .= '
'; } 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 ''; echo '' . $status_html . ''; echo '' . $type_html . ''; echo '' . $user_html . ''; echo '
' . $desc_html . '
'; echo '' . esc_html((string) $row['time']) . ''; echo '' . esc_html((string) ($row['updated'] !== '' ? $row['updated'] : '—')) . ''; echo ''; } /** 举报行详情(对齐参考主题:举报用户/举报内容红字 + 原因) */ 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); if ($target_type === 'user') { $html = '
' . esc_html__('举报用户:', 'aksrt') . ($link !== '' ? $link : esc_html('#' . $target_id)) . '
'; } else { $html = '
' . esc_html__('举报内容:', 'aksrt') . ($link !== '' ? $link : esc_html($target_type . ' #' . $target_id)) . '
'; } $html .= '
' . esc_html(sprintf(__('举报原因:%s', 'aksrt'), (string) ($r['reason'] ?? ''))) . '
'; if (!empty($r['category'])) { $html .= '
' . esc_html(sprintf(__('分类:%s', 'aksrt'), (string) $r['category'])) . '
'; } return $html; } /** 申诉行详情(对齐参考主题:禁封状态/原因/起止时间,或已解封) */ private function ticket_appeal_desc_html(array $payload, int $user_id): string { $ban_html = $this->ticket_ban_info_html($user_id, 'appeal_row'); $html = $ban_html; if (!empty($payload['reason'])) { $html .= '
' . esc_html(sprintf(__('申诉说明:%s', 'aksrt'), (string) $payload['reason'])) . '
'; } return $html; } /** * 用户当前限制状态摘要(对齐参考主题禁封信息展示)。 * * @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); if ($ban) { $end = !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt'); $html = '
' . esc_html(sprintf(__('禁封状态:%s', 'aksrt'), __('封禁中(禁止登录)', 'aksrt'))) . '
'; $html .= '
' . esc_html(sprintf(__('禁封原因:%s', 'aksrt'), (string) ($ban['reason'] ?? ''))) . '
'; $html .= '
' . esc_html(sprintf(__('开始时间:%s', 'aksrt'), (string) ($ban['created_at'] ?? ''))) . '
'; $html .= '
' . esc_html(sprintf(__('结束时间:%s', 'aksrt'), $end)) . '
'; return $html; } if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_SUSPEND)) { return '
' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('停权中', 'aksrt'))) . '
'; } if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_MUTE)) { return '
' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '
'; } // 对齐参考主题:申诉列表行中用户已无禁封 → 显示已解封并自动结案 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 '
' . esc_html__('此用户已解封', 'aksrt') . '
'; } // 对齐参考主题:申诉处理表单中用户已解封的提示 if ($context === 'appeal_form') { return '
' . esc_html__('当前用户已解封', 'aksrt') . '
'; } return '
' . esc_html__('用户状态正常', 'aksrt') . '
'; } 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 '' . esc_html(get_the_title($post)) . ''; } } elseif ($target_type === 'comment') { $comment = get_comment($target_id); if ($comment instanceof \WP_Comment) { return '#' . $target_id . ''; } } 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 '' . esc_html($user->display_name) . ''; } } return ''; } /** 行内处理表单(对齐参考主题 form-table 结构) */ private function render_ticket_process_form(array $row, string $action_url): void { $kind = $row['kind']; echo '
'; // admin-post.php 依赖 $_REQUEST['action'] 触发 admin_post_{action} 钩子; // 缺失时 WordPress 不输出任何内容(白屏),这里按工单类型分发。 echo ''; echo ''; 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 ''; echo ''; if ($target_type === 'user') { echo ''; } echo ''; wp_nonce_field('aksrt_users_update_report_' . (int) $row['id']); } else { $uid = (int) $row['id']; $user = get_userdata($uid); echo ''; echo ''; echo ''; echo ''; echo ''; wp_nonce_field('aksrt_users_process_appeal_' . $uid); } echo ''; echo '
' . esc_html__('被举报用户', 'aksrt') . ''; echo $link !== '' ? $link : esc_html($target_type . ' #' . $target_id); echo ''; echo ''; echo '
' . esc_html__('举报信息', 'aksrt') . ''; echo esc_html(sprintf(__('举报原因:%s', 'aksrt'), (string) ($r['reason'] ?? ''))) . '
'; if (!empty($r['category'])) { echo esc_html(sprintf(__('分类:%s', 'aksrt'), (string) $r['category'])) . '
'; } echo esc_html(sprintf(__('提交时间:%s', 'aksrt'), (string) $row['time'])); echo '
' . esc_html__('被举报用户禁封信息', 'aksrt') . ''; 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 = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : get_author_posts_url($target_id); echo '
' . esc_html__('用户状态正常', 'aksrt') . '
'; echo '

' . esc_html__('请查阅相关资料以及举证,如举报事实清楚,可以', 'aksrt'); echo ' ' . esc_html__('点击此处', 'aksrt') . ' '; echo esc_html__('对用户做禁封处理,并将处理结果填写至下方', 'aksrt') . '

'; } echo '
' . esc_html__('处理说明', 'aksrt') . ''; echo ''; echo '

' . esc_html__('此处内容会反馈给举报人', 'aksrt') . '

'; echo '
' . esc_html__('申诉用户', 'aksrt') . ''; 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 '' . esc_html($user->display_name) . ''; } else { echo esc_html('#' . $uid); } echo ''; echo '
' . esc_html__('禁封信息', 'aksrt') . '' . $this->ticket_ban_info_html($uid, 'appeal_form') . '
' . esc_html__('申诉信息', 'aksrt') . ''; $payload = $row['payload']; echo esc_html(sprintf(__('申诉类型:%s', 'aksrt'), (string) ($payload['reason_key'] ?? '—'))) . '
'; echo esc_html(sprintf(__('申诉说明:%s', 'aksrt'), (string) ($payload['reason'] ?? ''))) . '
'; echo esc_html(sprintf(__('提交时间:%s', 'aksrt'), (string) ($payload['time'] ?? ''))); echo '
' . esc_html__('处理方式', 'aksrt') . ''; echo '

'; echo '

'; echo '
' . esc_html__('处理留言', 'aksrt') . ''; echo ''; echo '

' . esc_html__('如需给用户留言请填写此处,如果拒绝请填写拒绝原因', 'aksrt') . '

'; echo '

'; echo ' '; // 取消:收起表单、换回列表行(JS 切换;无 JS 时整页回列表兜底)。 echo '' . esc_html__('取消', 'aksrt') . ''; echo '

'; } }