Files
wordpress-theme-aksrt/src/Admin/Native/UsersMenusTicketsList.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

179 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/** 工单筛选与列表;标题与操作直接链接到处理工作区(?id=)。@package Jio */
namespace Jio\Admin\Native;
use Jio\Ticket\TicketRepository;
use Jio\Ticket\TicketStatusService;
use Jio\Ticket\TicketTypeService;
defined('ABSPATH') || exit;
trait UsersMenusTicketsList {
private function render_ticket_list_page(array $filters, int $paged): void {
// 先查询并夹取页码,创建成功后跳转的工作区链接才能带上实际页码。
[$result, $paged] = $this->ticket_list_result($filters, $paged);
NativeUi::header(
__('工单管理', 'jio'),
__('查看用户问题、跟进沟通并记录处理结果。', 'jio'),
'dashicons-tickets-alt',
[
[
'label' => __('代客提交工单', 'jio'),
'dialog' => self::TICKET_CREATE_DIALOG,
'primary' => true,
'icon' => 'dashicons-plus-alt2',
],
[
'label' => __('工单配置', 'jio'),
'url' => admin_url('admin.php?page=jio-settings#/users/ticket'),
'icon' => 'dashicons-admin-generic',
],
]
);
$this->render_ticket_list($filters, $paged, $result);
$this->render_ticket_create_dialog();
}
/** 越界页码夹到末页,避免处理最后一条工单后停在空页。返回 [查询结果, 实际页码]。 */
private function ticket_list_result(array $filters, int $paged): array {
$repo = TicketRepository::get_instance();
$args = [
'status' => $filters['status'] === '' ? [] : [$filters['status']],
'ticket_type_id' => $filters['ticket_type_id'],
'keywords' => $filters['s'],
'limit' => self::TICKET_PER_PAGE,
'offset' => ($paged - 1) * self::TICKET_PER_PAGE,
];
$result = $repo->paginate($args, true);
$last_page = max(1, (int) ceil($result['total'] / self::TICKET_PER_PAGE));
if ($paged > $last_page) {
$paged = $last_page;
$args['offset'] = ($paged - 1) * self::TICKET_PER_PAGE;
$result = $repo->paginate($args, true);
}
return [$result, $paged];
}
private function render_ticket_list(array $filters, int $paged, array $result): void {
NativeUi::card_open('', '', true, 'jio-tickets-admin__list-card');
$this->render_ticket_toolbar($filters);
echo '<div class="jio-native-table-wrap"><table class="widefat striped jio-tickets-admin__table">';
echo '<caption class="screen-reader-text">' . esc_html__('工单处理列表', 'jio') . '</caption><thead><tr>';
foreach ([
'title' => __('工单 / 问题', 'jio'),
'status' => __('状态', 'jio'),
'type' => __('部门', 'jio'),
'user' => __('用户', 'jio'),
'admin' => __('跟进人', 'jio'),
'time' => __('最后活动', 'jio'),
'actions' => __('操作', 'jio'),
] as $key => $label) {
echo '<th scope="col" class="column-' . esc_attr($key) . '">' . esc_html($label) . '</th>';
}
echo '</tr></thead><tbody>';
if ($result['items'] === []) {
$filtered = $filters['status'] !== '' || $filters['ticket_type_id'] > 0 || $filters['s'] !== '';
echo NativeUi::empty_row(7, __('暂无工单', 'jio'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('请调整筛选条件,或使用标题右侧的「代客提交工单」。', 'jio')
: __('暂无未关闭工单。可切换状态查看历史记录,或代用户创建工单。', 'jio'));
}
$context = $this->ticket_context($filters, $paged);
foreach ($result['items'] as $ticket) {
$this->render_ticket_center_row($ticket, $context);
}
echo '</tbody></table>';
NativeUi::table_wrap_close();
AdminListPagination::render(self::TICKET_PAGE_SLUG, $result['total'], $paged, self::TICKET_PER_PAGE, $filters, 'users.php');
NativeUi::card_close();
}
private function render_ticket_toolbar(array $filters): void {
$statuses = array_values(TicketStatusService::map());
$counts = TicketRepository::get_instance()->statistic([
'status' => array_column($statuses, 'status_key'),
'ticket_type_id' => $filters['ticket_type_id'],
'keywords' => $filters['s'],
], true);
$open_total = 0;
$views = [];
foreach ($statuses as $row) {
$key = (string) $row['status_key'];
$count = (int) ($counts[$key] ?? 0);
if (empty($row['is_closed'])) {
$open_total += $count;
}
$views[] = [$key, (string) $row['name'], $count, (string) $row['color']];
}
array_unshift($views, ['', __('未关闭', 'jio'), $open_total, '']);
echo '<div class="jio-tickets-admin__toolbar"><h2 class="jio-native-card__title">' . esc_html__('工单列表', 'jio') . '</h2>';
echo '<nav aria-label="' . esc_attr__('按工单状态筛选', 'jio') . '"><ul class="subsubsub">';
foreach ($views as [$key, $label, $count, $color]) {
echo '<li><a href="' . esc_url($this->ticket_page_url(array_merge($filters, ['status' => $key]))) . '"' . ($filters['status'] === $key ? ' class="current" aria-current="page"' : '') . '>';
if ($color !== '') {
echo '<span class="jio-ticket-dot" aria-hidden="true" style="--jio-ticket-color:' . esc_attr($color) . '"></span>';
}
echo esc_html($label) . ' <span class="count">' . esc_html(number_format_i18n($count)) . '</span></a></li>';
}
echo '</ul></nav>';
$this->render_ticket_filter_form($filters);
echo '</div>';
}
private function render_ticket_filter_form(array $filters): void {
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="jio-native-filters">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::TICKET_PAGE_SLUG) . '"><input type="hidden" name="status" value="' . esc_attr($filters['status']) . '">';
echo '<label class="screen-reader-text" for="jio-ticket-filter-type">' . esc_html__('按部门筛选', 'jio') . '</label><select name="ticket_type_id" id="jio-ticket-filter-type"><option value="">' . esc_html__('全部部门', 'jio') . '</option>';
foreach (TicketTypeService::all() as $row) {
echo '<option value="' . (int) $row['id'] . '"' . selected($filters['ticket_type_id'], (int) $row['id'], false) . '>' . esc_html((string) $row['name']) . '</option>';
}
echo '</select><label class="screen-reader-text" for="jio-ticket-filter-s">' . esc_html__('搜索工单', 'jio') . '</label>';
echo '<input type="search" id="jio-ticket-filter-s" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('工单号 / 标题', 'jio') . '">';
echo '<button type="submit" class="button">' . esc_html__('筛选', 'jio') . '</button>';
if ($filters['ticket_type_id'] > 0 || $filters['s'] !== '') {
echo '<a class="button-link" href="' . esc_url($this->ticket_page_url(['status' => $filters['status']])) . '">' . esc_html__('清除筛选', 'jio') . '</a>';
}
echo '</form>';
}
private function render_ticket_center_row(array $ticket, array $context): void {
$id = (int) $ticket['id'];
$url = esc_url($this->ticket_page_url(['id' => $id] + $context));
$last = (string) $ticket['last_reply_time'] !== '' ? (string) $ticket['last_reply_time'] : (string) $ticket['post_time'];
$awaiting = in_array((string) $ticket['status'], [TicketStatusService::KEY_PENDING, TicketStatusService::KEY_USER_REPLIED], true);
echo '<tr class="jio-tickets-admin__row' . ($awaiting ? ' is-awaiting' : '') . '">';
echo '<td class="column-title"><a class="row-title" href="' . $url . '">' . esc_html((string) $ticket['title']) . '</a>' . $this->ticket_urge_badge($ticket); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="jio-tickets-admin__sub">' . esc_html((string) $ticket['ticket_num']) . '</div>';
if ((string) $ticket['order_number'] !== '') {
echo '<div class="jio-tickets-admin__sub">' . esc_html__('订单:', 'jio') . esc_html((string) $ticket['order_number']) . '</div>';
}
echo '</td><td class="column-status">' . $this->ticket_status_badge($ticket) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<td class="column-type">' . esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—') . '</td>';
echo '<td class="column-user">' . $this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name']) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<td class="column-admin">' . ((int) $ticket['admin_id'] > 0 ? esc_html((string) $ticket['admin_name']) : '<span class="jio-ticket-hint">' . esc_html__('未指派', 'jio') . '</span>') . '</td>';
echo '<td class="column-time"><time>' . esc_html($last) . '</time><div class="jio-tickets-admin__sub">' . esc_html(sprintf(__('提交于 %s', 'jio'), (string) $ticket['post_time'])) . '</div></td>';
echo '<td class="column-actions"><a class="button' . ($awaiting ? ' button-primary' : '') . '" href="' . $url . '">' . esc_html(!empty($ticket['is_closed']) ? __('查看记录', 'jio') : __('处理工单', 'jio')) . '</a></td></tr>';
}
private function ticket_status_badge(array $ticket): string {
return '<span class="jio-ticket-badge" style="--jio-ticket-color:' . esc_attr((string) $ticket['status_color']) . '">' . esc_html((string) $ticket['status_label']) . '</span>';
}
private function ticket_urge_badge(array $ticket): string {
$count = (int) ($ticket['urge_count'] ?? 0);
if ($count <= 0 || !empty($ticket['is_closed'])) {
return '';
}
return ' <span class="jio-ticket-urge" title="' . esc_attr(sprintf(__('最近催单:%s', 'jio'), (string) $ticket['last_urge_time'])) . '">' . esc_html(sprintf(__('催单 ×%d', 'jio'), $count)) . '</span>';
}
private function ticket_user_chip(int $user_id, string $name): string {
$avatar = function_exists('jio_get_avatar_url') ? jio_get_avatar_url($user_id, 48) : '';
$label = $name !== '' ? $name : '#' . $user_id;
return '<a class="jio-ticket-user" href="' . esc_url(get_edit_user_link($user_id)) . '" target="_blank" rel="noopener noreferrer" aria-label="' . esc_attr(sprintf(__('%s 的用户资料(新窗口)', 'jio'), $label)) . '">'
. ($avatar !== '' ? '<img src="' . esc_url($avatar) . '" alt="" width="24" height="24">' : '')
. '<span>' . esc_html($label) . '</span></a>';
}
}