feat(admin): replace ticket dialogs with in-page views
Ticket handling on users.php?page=aksrt_tickets no longer uses dialogs. The list, the workspace (?id=N) and the assisted create form (?view=new) are server-rendered page views that carry the list filters and page for going back. Writes still go through the ticket REST routes; on success the page reloads with a native notice, and unsent drafts in other forms survive the reload via sessionStorage. Uncertain or conflicting writes lock every form and only offer a reload. Remove dead code: UsersMenusTicketsDialogs, the aksrt_ticket_view AJAX read and its nonce, dialog CSS/JS (backdrop, scroll lock, focus return, read races) and the dialog runtime test. Fix the create link carrying an unclamped page number on out-of-range list pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48e54dc115
commit
d86a85e6a6
13 files changed
+720
-1264
No files matched your search
@@ -42,7 +42,6 @@ class UsersMenus {
|
||||
add_action('admin_post_aksrt_users_update_report', [$this, 'handle_update_report']);
|
||||
add_action('admin_post_aksrt_users_process_appeal', [$this, 'handle_process_appeal']);
|
||||
add_action('admin_post_aksrt_users_mutate_vip', [$this, 'handle_mutate_vip']);
|
||||
add_action('wp_ajax_aksrt_ticket_view', [$this, 'ajax_ticket_view']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_ticket_assets']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_message_assets']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_invite_assets']);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* 工单管理:原生列表与按需加载的处理弹窗。
|
||||
* 写操作统一使用 TicketRestAdmin;此处只负责后台页面与只读片段。
|
||||
* 工单管理:列表、处理工作区(?id=)与代客提交(?view=new)三个页内视图。
|
||||
* 写操作统一使用 TicketRestAdmin;此处只负责服务端渲染。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -17,7 +17,7 @@ trait UsersMenusTicketsCenter {
|
||||
|
||||
use UsersMenusTicketsList;
|
||||
use UsersMenusTicketsDetail;
|
||||
use UsersMenusTicketsDialogs;
|
||||
use UsersMenusTicketsCreate;
|
||||
|
||||
private const TICKET_PAGE_SLUG = 'aksrt_tickets';
|
||||
private const TICKET_PER_PAGE = 20;
|
||||
@@ -38,82 +38,33 @@ trait UsersMenusTicketsCenter {
|
||||
wp_enqueue_style('aksrt-admin-tickets', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
|
||||
wp_enqueue_script('aksrt-admin-tickets', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
|
||||
wp_localize_script('aksrt-admin-tickets', 'AKSRTAdminTickets', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'readNonce' => wp_create_nonce('aksrt_ticket_view'),
|
||||
'restNonce' => wp_create_nonce('wp_rest'),
|
||||
'currentUser' => get_current_user_id(),
|
||||
'strings' => [
|
||||
'loading' => __('正在加载工单…', 'aksrt'),
|
||||
'saving' => __('正在保存…', 'aksrt'),
|
||||
'loadFailed' => __('加载失败,请重试;若登录已过期,请重新登录。', 'aksrt'),
|
||||
'saveFailed' => __('保存失败,请检查填写内容后重试。', 'aksrt'),
|
||||
'uncertain' => __('连接中断,提交结果尚未确认。请先重新加载工单核对,避免重复提交。', 'aksrt'),
|
||||
'refreshFailed' => __('操作已保存,但页面刷新失败。请重新加载查看最新记录,不要重复提交。', 'aksrt'),
|
||||
'discard' => __('有尚未保存的内容。关闭后将丢弃,是否继续?', 'aksrt'),
|
||||
'uncertain' => __('连接中断,提交结果尚未确认。请先重新加载核对,避免重复提交。', 'aksrt'),
|
||||
'required' => __('请填写内容,不能只包含空格。', 'aksrt'),
|
||||
'tooLong' => __('插入后将超过字数限制,请先精简回复内容。', 'aksrt'),
|
||||
'created' => __('工单已创建,用户将收到通知。', 'aksrt'),
|
||||
'reply' => __('回复已发送,用户将收到通知。', 'aksrt'),
|
||||
'status' => __('工单信息已保存。', 'aksrt'),
|
||||
'note' => __('内部备注已添加,仅管理员可见。', 'aksrt'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function render_tickets_page(): void {
|
||||
$this->ensure_cap();
|
||||
NativeUi::page_open('aksrt-tickets-admin');
|
||||
NativeUi::header(
|
||||
__('工单管理', 'aksrt'),
|
||||
__('查看用户问题、跟进沟通并记录处理结果。', 'aksrt'),
|
||||
'dashicons-tickets-alt',
|
||||
[
|
||||
[
|
||||
'label' => __('代客提交工单', 'aksrt'),
|
||||
'dialog' => 'aksrt-ticket-create-dialog',
|
||||
'primary' => true,
|
||||
'icon' => 'dashicons-plus-alt2',
|
||||
],
|
||||
[
|
||||
'label' => __('工单配置', 'aksrt'),
|
||||
'url' => admin_url('admin.php?page=aksrt-settings#/users/ticket'),
|
||||
'icon' => 'dashicons-admin-generic',
|
||||
],
|
||||
]
|
||||
);
|
||||
echo '<div data-aksrt-ticket-list>';
|
||||
$this->render_ticket_list($this->ticket_filters($_GET), max(1, absint($_GET['paged'] ?? 1)));
|
||||
echo '</div>';
|
||||
$this->render_ticket_dialogs();
|
||||
echo '<noscript><p class="aksrt-native-callout">' . esc_html__('请启用 JavaScript 后创建工单或打开工单处理弹窗。', 'aksrt') . '</p></noscript>';
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
/** 只读 AJAX:详情按 ID 查询,不受当前列表筛选和分页影响。 */
|
||||
public function ajax_ticket_view(): void {
|
||||
if (!current_user_can(self::CAPABILITY) || !check_ajax_referer('aksrt_ticket_view', 'nonce', false)) {
|
||||
wp_send_json_error(['message' => __('权限不足或登录已过期,请刷新页面后重试。', 'aksrt')], 403);
|
||||
}
|
||||
$filters = $this->ticket_filters($_GET);
|
||||
$paged = max(1, absint($_GET['paged'] ?? 1));
|
||||
$id = absint($_GET['id'] ?? 0);
|
||||
$refresh_list = !empty($_GET['refresh_list']);
|
||||
if ($id <= 0 && !$refresh_list) {
|
||||
wp_send_json_error(['message' => __('请选择工单。', 'aksrt')], 400);
|
||||
}
|
||||
$data = [];
|
||||
$view = isset($_GET['view']) && is_scalar($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : '';
|
||||
NativeUi::page_open('aksrt-tickets-admin');
|
||||
if ($id > 0) {
|
||||
$ticket = TicketRepository::get_instance()->find($id);
|
||||
if ($ticket === null) {
|
||||
wp_send_json_error(['message' => __('工单不存在或已被删除。', 'aksrt')], 404);
|
||||
}
|
||||
$data['panel'] = $this->render_ticket_detail_panel($ticket);
|
||||
$data['title'] = (string) $ticket['title'];
|
||||
$this->render_ticket_detail_page($id, $this->ticket_context($filters, $paged));
|
||||
} elseif ($view === 'new') {
|
||||
$this->render_ticket_create_page($this->ticket_context($filters, $paged));
|
||||
} else {
|
||||
$this->render_ticket_list_page($filters, $paged);
|
||||
}
|
||||
if ($refresh_list) {
|
||||
ob_start();
|
||||
$data['page'] = $this->render_ticket_list($this->ticket_filters($_GET), max(1, absint($_GET['paged'] ?? 1)));
|
||||
$data['list'] = (string) ob_get_clean();
|
||||
}
|
||||
wp_send_json_success($data);
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
private function ticket_filters(array $request): array {
|
||||
@@ -125,10 +76,35 @@ trait UsersMenusTicketsCenter {
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表上下文(非空筛选 + 页码),随详情/创建链接传递,返回列表时原样恢复。 */
|
||||
private function ticket_context(array $filters, int $paged): array {
|
||||
return array_filter($filters + ['paged' => $paged > 1 ? $paged : 0], static fn($value): bool => $value !== '' && $value !== 0);
|
||||
}
|
||||
|
||||
private function ticket_page_url(array $extra = []): string {
|
||||
return add_query_arg(array_merge(['page' => self::TICKET_PAGE_SLUG], $extra), admin_url('users.php'));
|
||||
}
|
||||
|
||||
/** 写操作成功后脚本跳转回工作区并带上结果码,这里转成原生通知。 */
|
||||
private function render_ticket_notice(): void {
|
||||
$code = isset($_GET['aksrt_ticket_msg']) && is_scalar($_GET['aksrt_ticket_msg']) ? sanitize_key((string) wp_unslash($_GET['aksrt_ticket_msg'])) : '';
|
||||
$map = [
|
||||
'created' => __('工单已创建,用户将收到通知。', 'aksrt'),
|
||||
'reply' => __('回复已发送,用户将收到通知。', 'aksrt'),
|
||||
'status' => __('工单信息已保存。', 'aksrt'),
|
||||
'note' => __('内部备注已添加,仅管理员可见。', 'aksrt'),
|
||||
];
|
||||
if (isset($map[$code])) {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交中与失败提示;失败且结果不确定时显示「重新加载」。 */
|
||||
private function ticket_feedback(): string {
|
||||
return '<div class="aksrt-ticket-feedback" hidden data-aksrt-ticket-feedback><p role="status" aria-live="polite" aria-atomic="true"></p>'
|
||||
. '<button type="button" class="button" data-aksrt-ticket-reload hidden>' . esc_html__('重新加载', 'aksrt') . '</button></div>';
|
||||
}
|
||||
|
||||
/** 所有表单只提交到现有 REST 写路由,不保留 admin_post 分支。 */
|
||||
private function ticket_form_open(string $action, int $id, string $class = ''): string {
|
||||
$path = $action === 'create' ? '' : '/' . $id . '/' . ($action === 'note' ? 'notes' : $action);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/** 代客提交工单(?view=new):独立页内表单,创建成功后跳转到新工单的工作区。@package AKSRT */
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
use AKSRT\Ticket\TicketSettings;
|
||||
use AKSRT\Ticket\TicketTypeService;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusTicketsCreate {
|
||||
|
||||
private function render_ticket_create_page(array $context): void {
|
||||
$back = $this->ticket_page_url($context);
|
||||
NativeUi::header(
|
||||
__('代客提交工单', 'aksrt'),
|
||||
__('为指定用户创建工单,创建后由你跟进,用户会收到通知。', 'aksrt'),
|
||||
'dashicons-plus-alt2',
|
||||
[['label' => __('返回工单列表', 'aksrt'), 'url' => $back, 'icon' => 'dashicons-arrow-left-alt']]
|
||||
);
|
||||
$types = TicketTypeService::all();
|
||||
NativeUi::card_open('', '', false, 'aksrt-tickets-admin__create');
|
||||
if ($types === []) {
|
||||
echo NativeUi::empty_state(__('尚未配置工单部门', 'aksrt'), __('请先添加部门,再为用户创建工单。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '<p><a class="button" href="' . esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')) . '">' . esc_html__('前往工单配置', 'aksrt') . '</a></p>';
|
||||
NativeUi::card_close();
|
||||
return;
|
||||
}
|
||||
echo $this->ticket_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $this->ticket_form_open('create', 0, 'aksrt-tickets-admin__create-form'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('用户 ID', 'aksrt') . '</span><input type="number" name="user_id" min="1" step="1" required placeholder="' . esc_attr__('填写工单归属用户的 ID', 'aksrt') . '"></label>';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('工单部门', 'aksrt') . '</span><select name="ticket_type_id" required><option value="">' . esc_html__('请选择部门', 'aksrt') . '</option>';
|
||||
foreach ($types as $type) {
|
||||
echo '<option value="' . (int) $type['id'] . '">' . esc_html((string) $type['name']) . '</option>';
|
||||
}
|
||||
echo '</select></label>';
|
||||
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('工单标题', 'aksrt') . '</span><input type="text" name="title" maxlength="' . TicketSettings::title_max() . '" required placeholder="' . esc_attr__('简要说明用户遇到的问题', 'aksrt') . '"></label>';
|
||||
if (TicketSettings::order_link_enabled()) {
|
||||
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('关联订单(可选)', 'aksrt') . '</span><input type="text" name="order_number" placeholder="' . esc_attr__('填写订单号', 'aksrt') . '"></label>';
|
||||
}
|
||||
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('问题描述', 'aksrt') . '</span><textarea id="aksrt-ticket-create-content" name="content" rows="8" maxlength="' . TicketSettings::content_max() . '" required data-aksrt-ticket-editor aria-describedby="aksrt-ticket-create-count" placeholder="' . esc_attr__('描述问题、发生时间和已尝试的处理方式', 'aksrt') . '"></textarea>';
|
||||
echo '<span id="aksrt-ticket-create-count" class="aksrt-ticket-counter" data-aksrt-ticket-counter="aksrt-ticket-create-content">0 / ' . TicketSettings::content_max() . '</span></label></div>';
|
||||
echo '<div class="aksrt-native-form-foot"><button type="submit" class="button button-primary">' . esc_html__('创建工单并通知用户', 'aksrt') . '</button>';
|
||||
echo '<a class="button" href="' . esc_url($back) . '">' . esc_html__('取消', 'aksrt') . '</a>';
|
||||
echo '<span class="aksrt-ticket-hint">' . esc_html__('新工单状态为「已回复」', 'aksrt') . '</span></div></form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
/** 工单处理弹窗:左侧会话与回复输入,右侧处理信息与内部备注。@package AKSRT */
|
||||
/** 工单处理工作区(?id=):左侧会话与回复输入,右侧处理信息与内部备注。@package AKSRT */
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
@@ -14,6 +14,24 @@ defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusTicketsDetail {
|
||||
|
||||
/** 详情按 ID 查询,不受列表筛选影响;$context 只用于「返回工单列表」。 */
|
||||
private function render_ticket_detail_page(int $id, array $context): void {
|
||||
$back = NativeUi::button_link(__('返回工单列表', 'aksrt'), $this->ticket_page_url($context), false, 'dashicons-arrow-left-alt');
|
||||
$ticket = TicketRepository::get_instance()->find($id);
|
||||
echo '<div class="aksrt-native-header aksrt-tickets-admin__detail-header"><div class="aksrt-native-header__main"><h1 class="aksrt-native-header__title">';
|
||||
if ($ticket === null) {
|
||||
echo esc_html__('工单详情', 'aksrt') . '</h1></div><div class="aksrt-native-header__actions">' . $back . '</div></div><hr class="wp-header-end">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo NativeUi::empty_state(__('工单不存在', 'aksrt'), __('工单可能已被删除,或链接有误。', 'aksrt'), 'dashicons-search'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
return;
|
||||
}
|
||||
echo '<span class="aksrt-native-header__icon dashicons dashicons-tickets-alt" aria-hidden="true"></span>' . esc_html((string) $ticket['title']) . '</h1>';
|
||||
echo '<p class="aksrt-native-header__desc"><span>' . esc_html((string) $ticket['ticket_num']) . '</span> ' . $this->ticket_status_badge($ticket) . $this->ticket_urge_badge($ticket) . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</div><div class="aksrt-native-header__actions">' . $back . '</div></div><hr class="wp-header-end">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
$this->render_ticket_notice();
|
||||
echo $this->ticket_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $this->render_ticket_detail_panel($ticket); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
private function render_ticket_detail_panel(array $ticket): string {
|
||||
$id = (int) $ticket['id'];
|
||||
$html = '<div class="aksrt-ticket-detail" data-aksrt-ticket-detail="' . $id . '"><div class="aksrt-ticket-detail__main">';
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
/** 工单创建与处理弹窗外壳。@package AKSRT */
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
use AKSRT\Ticket\TicketSettings;
|
||||
use AKSRT\Ticket\TicketTypeService;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusTicketsDialogs {
|
||||
|
||||
private function render_ticket_dialogs(): void {
|
||||
echo '<dialog id="aksrt-ticket-create-dialog" class="aksrt-ticket-dialog aksrt-ticket-dialog--create" aria-labelledby="aksrt-ticket-create-title" aria-describedby="aksrt-ticket-create-description">';
|
||||
$types = TicketTypeService::all();
|
||||
echo $types === [] ? '<div class="aksrt-ticket-dialog__frame">' : $this->ticket_form_open('create', 0, 'aksrt-ticket-dialog__frame'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $this->ticket_dialog_header('create', __('代客提交工单', 'aksrt'), __('为指定用户创建工单,创建后由你跟进,用户会收到通知。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $this->ticket_dialog_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '<div class="aksrt-ticket-dialog__body">';
|
||||
if ($types === []) {
|
||||
echo NativeUi::empty_state(__('尚未配置工单部门', 'aksrt'), __('请先添加部门,再为用户创建工单。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '<a class="button" href="' . esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')) . '">' . esc_html__('前往工单配置', 'aksrt') . '</a>';
|
||||
} else {
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('用户 ID', 'aksrt') . '</span><input type="number" name="user_id" min="1" step="1" required placeholder="' . esc_attr__('填写工单归属用户的 ID', 'aksrt') . '"></label>';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('工单部门', 'aksrt') . '</span><select name="ticket_type_id" required><option value="">' . esc_html__('请选择部门', 'aksrt') . '</option>';
|
||||
foreach ($types as $type) {
|
||||
echo '<option value="' . (int) $type['id'] . '">' . esc_html((string) $type['name']) . '</option>';
|
||||
}
|
||||
echo '</select></label>';
|
||||
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('工单标题', 'aksrt') . '</span><input type="text" name="title" maxlength="' . TicketSettings::title_max() . '" required placeholder="' . esc_attr__('简要说明用户遇到的问题', 'aksrt') . '"></label>';
|
||||
if (TicketSettings::order_link_enabled()) {
|
||||
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('关联订单(可选)', 'aksrt') . '</span><input type="text" name="order_number" placeholder="' . esc_attr__('填写订单号', 'aksrt') . '"></label>';
|
||||
}
|
||||
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('问题描述', 'aksrt') . '</span><textarea id="aksrt-ticket-create-content" name="content" rows="7" maxlength="' . TicketSettings::content_max() . '" required data-aksrt-ticket-editor aria-describedby="aksrt-ticket-create-count" placeholder="' . esc_attr__('描述问题、发生时间和已尝试的处理方式', 'aksrt') . '"></textarea>';
|
||||
echo '<span id="aksrt-ticket-create-count" class="aksrt-ticket-counter" data-aksrt-ticket-counter="aksrt-ticket-create-content">0 / ' . TicketSettings::content_max() . '</span></label></div>';
|
||||
}
|
||||
echo '</div><footer class="aksrt-ticket-dialog__foot"><span class="aksrt-ticket-hint">' . esc_html__('新工单状态为「已回复」', 'aksrt') . '</span><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>';
|
||||
if ($types !== []) {
|
||||
echo '<button type="submit" class="button button-primary">' . esc_html__('创建工单并通知用户', 'aksrt') . '</button>';
|
||||
}
|
||||
echo '</footer>' . ($types === [] ? '</div>' : '</form>') . '</dialog>';
|
||||
|
||||
echo '<dialog id="aksrt-ticket-detail-dialog" class="aksrt-ticket-dialog aksrt-ticket-dialog--detail" aria-labelledby="aksrt-ticket-detail-title"><div class="aksrt-ticket-dialog__frame">';
|
||||
echo $this->ticket_dialog_header('detail', __('工单详情', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $this->ticket_dialog_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '<div class="aksrt-ticket-dialog__body" data-aksrt-ticket-panel></div></div></dialog>';
|
||||
}
|
||||
|
||||
private function ticket_dialog_header(string $key, string $title, string $description = ''): string {
|
||||
return '<header class="aksrt-ticket-dialog__head"><div><h2 id="aksrt-ticket-' . esc_attr($key) . '-title" tabindex="-1" autofocus>' . esc_html($title) . '</h2>'
|
||||
. ($description !== '' ? '<p id="aksrt-ticket-' . esc_attr($key) . '-description">' . esc_html($description) . '</p>' : '') . '</div>'
|
||||
. '<button type="button" class="aksrt-ticket-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
|
||||
}
|
||||
|
||||
private function ticket_dialog_feedback(): string {
|
||||
return '<div class="aksrt-ticket-feedback" hidden data-aksrt-ticket-feedback><p role="status" aria-live="polite" aria-atomic="true"></p>'
|
||||
. '<button type="button" class="button" data-aksrt-ticket-retry hidden>' . esc_html__('重新加载', 'aksrt') . '</button></div>';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
/** 工单筛选与列表;详情统一在弹窗内按需加载。@package AKSRT */
|
||||
/** 工单筛选与列表;标题与操作直接链接到处理工作区(?id=)。@package AKSRT */
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
@@ -11,8 +11,32 @@ defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusTicketsList {
|
||||
|
||||
/** 返回实际页码,避免处理最后一条工单后停在空的末页。 */
|
||||
private function render_ticket_list(array $filters, int $paged): int {
|
||||
private function render_ticket_list_page(array $filters, int $paged): void {
|
||||
// 先查询并夹取页码,页头「代客提交」链接才能带上实际页码。
|
||||
[$result, $paged] = $this->ticket_list_result($filters, $paged);
|
||||
NativeUi::header(
|
||||
__('工单管理', 'aksrt'),
|
||||
__('查看用户问题、跟进沟通并记录处理结果。', 'aksrt'),
|
||||
'dashicons-tickets-alt',
|
||||
[
|
||||
[
|
||||
'label' => __('代客提交工单', 'aksrt'),
|
||||
'url' => $this->ticket_page_url(['view' => 'new'] + $this->ticket_context($filters, $paged)),
|
||||
'primary' => true,
|
||||
'icon' => 'dashicons-plus-alt2',
|
||||
],
|
||||
[
|
||||
'label' => __('工单配置', 'aksrt'),
|
||||
'url' => admin_url('admin.php?page=aksrt-settings#/users/ticket'),
|
||||
'icon' => 'dashicons-admin-generic',
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->render_ticket_list($filters, $paged, $result);
|
||||
}
|
||||
|
||||
/** 越界页码夹到末页,避免处理最后一条工单后停在空页。返回 [查询结果, 实际页码]。 */
|
||||
private function ticket_list_result(array $filters, int $paged): array {
|
||||
$repo = TicketRepository::get_instance();
|
||||
$args = [
|
||||
'status' => $filters['status'] === '' ? [] : [$filters['status']],
|
||||
@@ -28,6 +52,10 @@ trait UsersMenusTicketsList {
|
||||
$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, 'aksrt-tickets-admin__list-card');
|
||||
$this->render_ticket_toolbar($filters);
|
||||
echo '<div class="aksrt-native-table-wrap"><table class="widefat striped aksrt-tickets-admin__table">';
|
||||
@@ -50,14 +78,14 @@ trait UsersMenusTicketsList {
|
||||
? __('请调整筛选条件,或使用标题右侧的「代客提交工单」。', 'aksrt')
|
||||
: __('暂无未关闭工单。可切换状态查看历史记录,或代用户创建工单。', 'aksrt'));
|
||||
}
|
||||
$context = $this->ticket_context($filters, $paged);
|
||||
foreach ($result['items'] as $ticket) {
|
||||
$this->render_ticket_center_row($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();
|
||||
return $paged;
|
||||
}
|
||||
|
||||
private function render_ticket_toolbar(array $filters): void {
|
||||
@@ -108,13 +136,13 @@ trait UsersMenusTicketsList {
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
private function render_ticket_center_row(array $ticket): void {
|
||||
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);
|
||||
$hook = ' data-aksrt-ticket-view="' . $id . '" aria-haspopup="dialog" aria-controls="aksrt-ticket-detail-dialog"';
|
||||
echo '<tr class="aksrt-tickets-admin__row' . ($awaiting ? ' is-awaiting' : '') . '">';
|
||||
echo '<td class="column-title"><button type="button" class="button-link row-title"' . $hook . '>' . esc_html((string) $ticket['title']) . '</button>' . $this->ticket_urge_badge($ticket); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
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="aksrt-tickets-admin__sub">' . esc_html((string) $ticket['ticket_num']) . '</div>';
|
||||
if ((string) $ticket['order_number'] !== '') {
|
||||
echo '<div class="aksrt-tickets-admin__sub">' . esc_html__('订单:', 'aksrt') . esc_html((string) $ticket['order_number']) . '</div>';
|
||||
@@ -124,7 +152,7 @@ trait UsersMenusTicketsList {
|
||||
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="aksrt-ticket-hint">' . esc_html__('未指派', 'aksrt') . '</span>') . '</td>';
|
||||
echo '<td class="column-time"><time>' . esc_html($last) . '</time><div class="aksrt-tickets-admin__sub">' . esc_html(sprintf(__('提交于 %s', 'aksrt'), (string) $ticket['post_time'])) . '</div></td>';
|
||||
echo '<td class="column-actions"><button type="button" class="button' . ($awaiting ? ' button-primary' : '') . '"' . $hook . '>' . esc_html(!empty($ticket['is_closed']) ? __('查看记录', 'aksrt') : __('处理工单', 'aksrt')) . '</button></td></tr>';
|
||||
echo '<td class="column-actions"><a class="button' . ($awaiting ? ' button-primary' : '') . '" href="' . $url . '">' . esc_html(!empty($ticket['is_closed']) ? __('查看记录', 'aksrt') : __('处理工单', 'aksrt')) . '</a></td></tr>';
|
||||
}
|
||||
|
||||
private function ticket_status_badge(array $ticket): string {
|
||||
|
||||
Reference in new issue
Block a user