折叠,失败回跳时自动展开。 */
private function render_ticket_create_form(): void {
$types = \AKSRT\Ticket\TicketTypeService::all();
$failed = isset($_GET['aksrt_ticket_msg']) && sanitize_key((string) wp_unslash($_GET['aksrt_ticket_msg'])) === 'create_failed';
echo '';
echo '' . esc_html__('代客提交工单', 'aksrt') . '
';
if ($types === []) {
printf(
'%s %s
',
esc_html__('尚未配置工单部门,无法创建工单。', 'aksrt'),
esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')),
esc_html__('前往配置', 'aksrt')
);
return;
}
echo $this->ticket_form_open('create', 0, 'aksrt-tickets-admin__create-form'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_nonce_field('aksrt_ticket_create_0');
echo '
';
printf(
'',
esc_html__('用户 ID', 'aksrt'),
esc_attr__('工单归属用户', 'aksrt')
);
echo '';
printf(
'',
esc_html__('标题', 'aksrt'),
\AKSRT\Ticket\TicketSettings::title_max()
);
if (\AKSRT\Ticket\TicketSettings::order_link_enabled()) {
printf(
'',
esc_html__('关联订单', 'aksrt'),
esc_attr__('可选', 'aksrt')
);
}
printf(
'',
esc_html__('问题描述', 'aksrt'),
\AKSRT\Ticket\TicketSettings::content_max()
);
echo '
';
printf(
'',
esc_html__('创建后状态为「已回复」并由你跟进,用户会收到通知。', 'aksrt'),
esc_html__('创建工单', 'aksrt')
);
echo '';
}
private function render_ticket_list_tab(): void {
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$type_id = isset($_GET['ticket_type_id']) ? absint($_GET['ticket_type_id']) : 0;
$s = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$paged = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
$open_id = isset($_GET['id']) ? absint($_GET['id']) : 0;
$filters = array_filter([
'status' => $status,
'ticket_type_id' => $type_id,
's' => $s,
], static fn($value): bool => $value !== '' && $value !== 0);
$view_url = $this->ticket_page_url(array_merge($filters, $paged > 1 ? ['paged' => $paged] : []));
$this->render_ticket_status_views($status);
$this->render_ticket_filter_form($status, $type_id, $s);
$repo = \AKSRT\Ticket\TicketRepository::get_instance();
$result = $repo->paginate([
'status' => $status === '' ? [] : [$status],
'ticket_type_id' => $type_id,
'keywords' => $s,
'limit' => self::TICKET_PER_PAGE,
'offset' => ($paged - 1) * self::TICKET_PER_PAGE,
], true);
$items = $result['items'];
$total = $result['total'];
echo '';
echo '
';
echo '';
echo '| ' . esc_html__('工单', 'aksrt') . ' | ';
echo '' . esc_html__('状态', 'aksrt') . ' | ';
echo '' . esc_html__('部门', 'aksrt') . ' | ';
echo '' . esc_html__('用户', 'aksrt') . ' | ';
echo '' . esc_html__('跟进人', 'aksrt') . ' | ';
echo '' . esc_html__('最后活动', 'aksrt') . ' | ';
echo '
';
if ($items === []) {
$hint = $filters === []
? __('暂无进行中的工单,已关闭工单可在「已关闭」视图查看。', 'aksrt')
: __('当前筛选条件下没有工单。', 'aksrt');
printf(
'%s%s |
',
esc_html__('暂无工单', 'aksrt'),
esc_html($hint)
);
}
foreach ($items as $ticket) {
$this->render_ticket_center_row($ticket, $view_url, $open_id === (int) $ticket['id']);
}
echo '
';
\AKSRT\Admin\Native\AdminListPagination::render(
self::TICKET_PAGE_SLUG,
$total,
$paged,
self::TICKET_PER_PAGE,
$filters,
'users.php'
);
}
/** 状态视图:未关闭(默认)+ 各进行中状态 + 完结态,均带计数。 */
private function render_ticket_status_views(string $current): void {
$repo = \AKSRT\Ticket\TicketRepository::get_instance();
// 默认口径(不带状态)已排除已关闭;完结态单独计数。
$counts = $repo->statistic([], true);
$open_total = 0;
$views = [];
foreach (\AKSRT\Ticket\TicketStatusService::all() as $row) {
$key = (string) $row['status_key'];
$count = !empty($row['is_closed']) ? $repo->count_by_status($key) : (int) ($counts[$key] ?? 0);
if (empty($row['is_closed'])) {
$open_total += $count;
}
$views[] = [$key, (string) $row['name'], $count, (string) $row['color']];
}
array_unshift($views, ['', __('未关闭', 'aksrt'), $open_total, '']);
echo '';
$last = count($views) - 1;
foreach ($views as $i => [$key, $label, $count, $color]) {
printf(
'- %s%s (%d)%s
',
esc_url($this->ticket_page_url($key === '' ? [] : ['status' => $key])),
$current === $key ? ' class="current" aria-current="page"' : '',
$color !== '' ? '' : '',
esc_html($label),
$count,
$i < $last ? ' |' : ''
);
}
echo '
';
}
/** 部门 + 关键词筛选(状态由视图链接决定,以隐藏域保留)。 */
private function render_ticket_filter_form(string $status, int $type_id, string $s): void {
echo '';
}
/** @param array $ticket */
private function render_ticket_center_row(array $ticket, string $view_url, bool $is_open): void {
$id = (int) $ticket['id'];
$toggle_url = $is_open
? remove_query_arg('id', $view_url)
: add_query_arg('id', $id, $view_url) . '#aksrt-ticket-' . $id;
$last = (string) $ticket['last_reply_time'] !== '' ? (string) $ticket['last_reply_time'] : (string) $ticket['post_time'];
$awaiting = in_array((string) $ticket['status'], [
\AKSRT\Ticket\TicketStatusService::KEY_PENDING,
\AKSRT\Ticket\TicketStatusService::KEY_USER_REPLIED,
], true);
printf(
''
. '| %5$s%15$s'
. ' %6$s%7$s
| '
. '%9$s | '
. '%10$s | '
. '%11$s | '
. '%12$s | '
. '%13$s %14$s |
',
$id,
($is_open ? ' is-open' : '') . ($awaiting ? ' is-awaiting' : ''),
esc_url($toggle_url),
$is_open ? 'true' : 'false',
esc_html((string) $ticket['title']),
esc_html((string) $ticket['ticket_num']),
(string) $ticket['order_number'] !== ''
? ' · ' . esc_html__('订单', 'aksrt') . ' ' . esc_html((string) $ticket['order_number'])
: '',
esc_attr((string) $ticket['status_color']),
esc_html((string) $ticket['status_label']),
esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—'),
$this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name']), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
(int) $ticket['admin_id'] > 0
? esc_html((string) $ticket['admin_name'])
: '' . esc_html__('未指派', 'aksrt') . '',
esc_html($last),
esc_html(sprintf(
/* translators: %s submit time */
__('提交于 %s', 'aksrt'),
(string) $ticket['post_time']
)),
$this->ticket_urge_badge($ticket) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
if ($is_open) {
echo '| ' . $this->render_ticket_detail_panel($ticket) . ' |
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
/** 催单徽标:次数 + 悬停显示最近催单时间;已完结或未催过不输出。 */
private function ticket_urge_badge(array $ticket): string {
$count = (int) ($ticket['urge_count'] ?? 0);
if ($count <= 0 || !empty($ticket['is_closed'])) {
return '';
}
return ' ' . esc_html(sprintf(
/* translators: %d urge count */
__('催单 ×%d', 'aksrt'),
$count
)) . '';
}
/** 头像 + 名称(链接到用户编辑页)。后台不走前台懒加载,直接输出 src。 */
private function ticket_user_chip(int $user_id, string $name): string {
$avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 48) : '';
$label = $name !== '' ? $name : '#' . $user_id;
return ''
. ($avatar !== '' ? '
' : '')
. '' . esc_html($label) . '';
}
}