feat(admin): streamline ticket management with dialogs
Move on-behalf ticket creation into a header dialog and load ticket handling on demand. Reuse the admin REST write path, preserve drafts and filters, and cover dialog and server boundaries with regression tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b45fd161f0
commit
264015b7e9
16 files changed
+2178
-1048
No files matched your search
@@ -1,264 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* 工单管理后台页 — 行内展开的详情面板。
|
||||
*
|
||||
* 双栏:左侧为原始问题 + 沟通记录 + 回复框;右侧为工单信息、处理(状态 / 部门 / 跟进人)
|
||||
* 与内部备注。已关闭工单只读:不再输出回复框与处理表单。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
/** 工单处理弹窗:会话、回复、处理信息与内部备注。@package AKSRT */
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
use AKSRT\Ticket\TicketNoteService;
|
||||
use AKSRT\Ticket\TicketPrereplyService;
|
||||
use AKSRT\Ticket\TicketRepository;
|
||||
use AKSRT\Ticket\TicketSettings;
|
||||
use AKSRT\Ticket\TicketStatusService;
|
||||
use AKSRT\Ticket\TicketTypeService;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusTicketsDetail {
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $ticket
|
||||
*/
|
||||
private function render_ticket_detail_panel(array $ticket): string {
|
||||
$id = (int) $ticket['id'];
|
||||
$repo = \AKSRT\Ticket\TicketRepository::get_instance();
|
||||
$replies = $repo->replies($id);
|
||||
$notes = \AKSRT\Ticket\TicketNoteService::for_ticket($id);
|
||||
$replies = TicketRepository::get_instance()->replies($id);
|
||||
$notes = TicketNoteService::for_ticket($id);
|
||||
$closed = !empty($ticket['is_closed']);
|
||||
|
||||
$html = '<div class="aksrt-ticket-detail" data-aksrt-ticket-detail="' . esc_attr((string) $id) . '">';
|
||||
|
||||
// ---- 左栏:对话 ----
|
||||
$html .= '<div class="aksrt-ticket-detail__main">';
|
||||
$html = '<div class="aksrt-ticket-detail" data-aksrt-ticket-detail="' . $id . '"><div class="aksrt-ticket-detail__main">';
|
||||
$html .= '<section class="aksrt-ticket-conversation" aria-label="' . esc_attr__('问题与沟通记录', 'aksrt') . '">';
|
||||
$html .= '<div class="aksrt-ticket-section-head"><h3>' . esc_html__('沟通记录', 'aksrt') . ' <span class="aksrt-ticket-hint">' . esc_html(sprintf(__('%d 条记录', 'aksrt'), count($replies))) . '</span></h3>';
|
||||
if ($replies !== []) {
|
||||
$html .= '<button type="button" class="button-link" data-aksrt-ticket-latest>' . esc_html__('查看最新记录', 'aksrt') . '</button>';
|
||||
}
|
||||
$html .= '</div><div class="aksrt-ticket-conversation__scroll" tabindex="0" role="region" aria-label="' . esc_attr__('可滚动的沟通记录', 'aksrt') . '">';
|
||||
$html .= '<article class="aksrt-ticket-detail__origin">';
|
||||
$html .= $this->ticket_message_head(
|
||||
(int) $ticket['user_id'],
|
||||
(string) $ticket['user_name'],
|
||||
__('提交了工单', 'aksrt'),
|
||||
(string) $ticket['post_time']
|
||||
);
|
||||
$html .= $this->ticket_message_head((int) $ticket['user_id'], (string) $ticket['user_name'], !empty($ticket['post_admin_id']) ? __('代客提交', 'aksrt') : __('提交了工单', 'aksrt'), (string) $ticket['post_time']);
|
||||
$html .= '<div class="aksrt-ticket-detail__body">' . wpautop(esc_html((string) $ticket['content'])) . '</div>';
|
||||
$html .= $this->ticket_attachments_html((array) $ticket['attachment']);
|
||||
$html .= '</article>';
|
||||
|
||||
$html .= '<h4 class="aksrt-ticket-detail__heading">' . esc_html(sprintf(
|
||||
/* translators: %d reply count */
|
||||
__('沟通记录(%d)', 'aksrt'),
|
||||
count($replies)
|
||||
)) . '</h4>';
|
||||
$html .= $this->ticket_attachments_html((array) $ticket['attachment']) . '</article>';
|
||||
if ($replies === []) {
|
||||
$html .= '<p class="aksrt-ticket-detail__empty">' . esc_html__('暂无沟通记录', 'aksrt') . '</p>';
|
||||
$html .= '<p class="aksrt-ticket-detail__empty">' . esc_html__('暂无回复,请核对问题后联系用户。', 'aksrt') . '</p>';
|
||||
} else {
|
||||
$html .= '<ol class="aksrt-ticket-detail__replies">';
|
||||
foreach ($replies as $reply) {
|
||||
if ((string) $reply['author_type'] === 'system') {
|
||||
// 系统记录(如催单):居中提示行,无头像 / 引用 / 附件。
|
||||
$html .= '<li class="aksrt-ticket-detail__system"><span>' . esc_html((string) $reply['content'])
|
||||
. '</span><time>' . esc_html((string) $reply['created_at']) . '</time></li>';
|
||||
$html .= '<li class="aksrt-ticket-detail__system"><span>' . esc_html((string) $reply['content']) . '</span><time>' . esc_html((string) $reply['created_at']) . '</time></li>';
|
||||
continue;
|
||||
}
|
||||
$is_admin = (string) $reply['author_type'] === 'admin';
|
||||
$html .= '<li class="aksrt-ticket-detail__reply is-' . ($is_admin ? 'admin' : 'user') . '">';
|
||||
$html .= $this->ticket_message_head(
|
||||
(int) $reply['author_id'],
|
||||
(string) $reply['author_name'],
|
||||
$is_admin ? __('客服', 'aksrt') : __('用户', 'aksrt'),
|
||||
(string) $reply['created_at']
|
||||
);
|
||||
$admin = (string) $reply['author_type'] === 'admin';
|
||||
$html .= '<li class="aksrt-ticket-detail__reply is-' . ($admin ? 'admin' : 'user') . '">';
|
||||
$html .= $this->ticket_message_head((int) $reply['author_id'], (string) $reply['author_name'], $admin ? __('客服', 'aksrt') : __('用户', 'aksrt'), (string) $reply['created_at']);
|
||||
if (is_array($reply['quote'] ?? null)) {
|
||||
$quote = $reply['quote'];
|
||||
$html .= '<blockquote class="aksrt-ticket-detail__quote">'
|
||||
. esc_html(!empty($quote['is_deleted']) ? __('原回复已删除', 'aksrt') : (string) $quote['content'])
|
||||
. '</blockquote>';
|
||||
$html .= '<blockquote class="aksrt-ticket-detail__quote">' . esc_html(!empty($quote['is_deleted']) ? __('原回复已删除', 'aksrt') : (string) $quote['content']) . '</blockquote>';
|
||||
}
|
||||
$html .= '<div class="aksrt-ticket-detail__body">' . wpautop(esc_html((string) $reply['content'])) . '</div>';
|
||||
$html .= $this->ticket_attachments_html((array) $reply['attachment']);
|
||||
$html .= '</li>';
|
||||
$html .= $this->ticket_attachments_html((array) $reply['attachment']) . '</li>';
|
||||
}
|
||||
$html .= '</ol>';
|
||||
}
|
||||
|
||||
if ($closed) {
|
||||
$html .= '<p class="aksrt-ticket-detail__closed"><span class="dashicons dashicons-lock" aria-hidden="true"></span>'
|
||||
. esc_html__('工单已关闭,无法继续回复。', 'aksrt') . '</p>';
|
||||
} else {
|
||||
$html .= $this->ticket_reply_form($id);
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
// ---- 右栏:信息 / 处理 / 备注 ----
|
||||
$html .= '<aside class="aksrt-ticket-detail__side">';
|
||||
$html .= '</div></section>';
|
||||
$html .= $closed
|
||||
? '<p class="aksrt-ticket-detail__closed"><span class="dashicons dashicons-lock" aria-hidden="true"></span>' . esc_html__('工单已关闭,不能继续回复或变更处理信息;仍可添加内部备注。', 'aksrt') . '</p>'
|
||||
: $this->ticket_reply_form($id);
|
||||
$html .= '</div><aside class="aksrt-ticket-detail__side" aria-label="' . esc_attr__('工单信息与处理', 'aksrt') . '">';
|
||||
$html .= $this->ticket_info_card($ticket);
|
||||
if (!$closed) {
|
||||
$html .= $this->ticket_status_form($ticket);
|
||||
}
|
||||
|
||||
$html .= '<section class="aksrt-ticket-detail__card">';
|
||||
$html .= '<h4 class="aksrt-ticket-detail__heading">' . esc_html__('内部备注', 'aksrt')
|
||||
. ' <span class="aksrt-native-muted">' . esc_html__('用户不可见', 'aksrt') . '</span></h4>';
|
||||
if ($notes === []) {
|
||||
$html .= '<p class="aksrt-ticket-detail__empty">' . esc_html__('暂无备注', 'aksrt') . '</p>';
|
||||
} else {
|
||||
$html .= '<section class="aksrt-ticket-detail__card aksrt-ticket-detail__private"><h3 class="aksrt-ticket-detail__heading">' . esc_html__('内部备注', 'aksrt') . '</h3><p class="aksrt-ticket-hint">' . esc_html__('仅管理员可见,不会发送给用户。', 'aksrt') . '</p>';
|
||||
if ($notes !== []) {
|
||||
$html .= '<ul class="aksrt-ticket-detail__notes">';
|
||||
foreach ($notes as $note) {
|
||||
$html .= '<li><div class="aksrt-ticket-detail__note-head"><strong>' . esc_html((string) $note['admin_name']) . '</strong>'
|
||||
. '<time>' . esc_html((string) $note['created_at']) . '</time></div>'
|
||||
. '<div>' . nl2br(esc_html((string) $note['content'])) . '</div></li>';
|
||||
$html .= '<li><div class="aksrt-ticket-detail__note-head"><strong>' . esc_html((string) $note['admin_name']) . '</strong><time>' . esc_html((string) $note['created_at']) . '</time></div><div>' . nl2br(esc_html((string) $note['content'])) . '</div></li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
}
|
||||
$html .= $this->ticket_form_open('note', $id, 'aksrt-ticket-detail__note-form');
|
||||
$html .= wp_nonce_field('aksrt_ticket_note_' . $id, '_wpnonce', true, false);
|
||||
$html .= '<label class="screen-reader-text" for="aksrt-ticket-note-' . $id . '">' . esc_html__('备注内容', 'aksrt') . '</label>';
|
||||
$html .= '<textarea id="aksrt-ticket-note-' . $id . '" name="content" rows="2" required maxlength="' . (int) \AKSRT\Ticket\TicketSettings::reply_max() . '" placeholder="' . esc_attr__('记录处理进展、排查结论…', 'aksrt') . '"></textarea>';
|
||||
$html .= '<button type="submit" class="button">' . esc_html__('添加备注', 'aksrt') . '</button>';
|
||||
$html .= '</form>';
|
||||
$html .= '</section>';
|
||||
|
||||
$html .= '</aside>';
|
||||
$html .= '</div>';
|
||||
$html .= $this->ticket_editor('aksrt-ticket-note-' . $id, __('备注内容', 'aksrt'), __('记录处理进展、排查结论…', 'aksrt'), 3);
|
||||
$html .= '<button type="submit" class="button">' . esc_html__('添加内部备注', 'aksrt') . '</button></form></section></aside></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function ticket_reply_form(int $id): string {
|
||||
$field = 'aksrt-ticket-reply-' . $id;
|
||||
$html = $this->ticket_form_open('reply', $id, 'aksrt-ticket-detail__reply-form');
|
||||
$html .= wp_nonce_field('aksrt_ticket_reply_' . $id, '_wpnonce', true, false);
|
||||
$html .= '<label class="aksrt-ticket-detail__heading" for="' . esc_attr($field) . '">' . esc_html__('回复用户', 'aksrt') . '</label>';
|
||||
$html .= '<textarea id="' . esc_attr($field) . '" name="content" rows="5" required maxlength="' . (int) \AKSRT\Ticket\TicketSettings::reply_max() . '"'
|
||||
. ' data-aksrt-ticket-reply-input placeholder="' . esc_attr__('填写回复内容,提交后用户将收到通知', 'aksrt') . '"></textarea>';
|
||||
$html .= $this->ticket_editor($field, __('回复用户', 'aksrt'), __('填写回复内容,发送后用户将收到通知', 'aksrt'), 5);
|
||||
$html .= '<div class="aksrt-ticket-detail__reply-foot">';
|
||||
|
||||
$prereplies = \AKSRT\Ticket\TicketPrereplyService::all();
|
||||
$prereplies = TicketPrereplyService::all();
|
||||
if ($prereplies !== []) {
|
||||
$html .= '<label class="screen-reader-text" for="' . esc_attr($field) . '-pre">' . esc_html__('插入预设回复', 'aksrt') . '</label>';
|
||||
$html .= '<select id="' . esc_attr($field) . '-pre" data-aksrt-ticket-prereply="' . esc_attr($field) . '">';
|
||||
$html .= '<option value="">' . esc_html__('插入预设回复…', 'aksrt') . '</option>';
|
||||
$html .= '<label class="screen-reader-text" for="' . $field . '-pre">' . esc_html__('插入预设回复', 'aksrt') . '</label><select id="' . $field . '-pre" data-aksrt-ticket-prereply="' . $field . '"><option value="">' . esc_html__('插入预设回复…', 'aksrt') . '</option>';
|
||||
foreach ($prereplies as $row) {
|
||||
$content = (string) $row['content'];
|
||||
$html .= sprintf(
|
||||
'<option value="%s">%s</option>',
|
||||
esc_attr($content),
|
||||
esc_html(mb_strimwidth($content, 0, 40, '…'))
|
||||
);
|
||||
$html .= '<option value="' . esc_attr((string) $row['content']) . '">' . esc_html(mb_strimwidth((string) $row['content'], 0, 40, '…')) . '</option>';
|
||||
}
|
||||
$html .= '</select>';
|
||||
}
|
||||
$html .= '<span class="aksrt-ticket-detail__counter" data-aksrt-ticket-counter="' . esc_attr($field) . '">0 / ' . (int) \AKSRT\Ticket\TicketSettings::reply_max() . '</span>';
|
||||
$html .= '<button type="submit" class="button button-primary">' . esc_html__('提交回复', 'aksrt') . '</button>';
|
||||
$html .= '</div></form>';
|
||||
return $html;
|
||||
return $html . '<button type="submit" class="button button-primary">' . esc_html__('发送回复', 'aksrt') . '</button></div></form>';
|
||||
}
|
||||
|
||||
private function ticket_editor(string $id, string $label, string $placeholder, int $rows): string {
|
||||
return '<label class="aksrt-ticket-detail__heading" for="' . esc_attr($id) . '">' . esc_html($label) . '</label>'
|
||||
. '<textarea id="' . esc_attr($id) . '" name="content" rows="' . $rows . '" maxlength="' . TicketSettings::reply_max() . '" required data-aksrt-ticket-editor aria-describedby="' . esc_attr($id) . '-count" placeholder="' . esc_attr($placeholder) . '"></textarea>'
|
||||
. '<span id="' . esc_attr($id) . '-count" class="aksrt-ticket-counter" data-aksrt-ticket-counter="' . esc_attr($id) . '">0 / ' . TicketSettings::reply_max() . '</span>';
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $ticket */
|
||||
private function ticket_info_card(array $ticket): string {
|
||||
$rows = [
|
||||
__('工单号', 'aksrt') => '<code>' . esc_html((string) $ticket['ticket_num']) . '</code>',
|
||||
__('状态', 'aksrt') => '<span class="aksrt-ticket-badge" style="--aksrt-ticket-color:' . esc_attr((string) $ticket['status_color']) . '">'
|
||||
. esc_html((string) $ticket['status_label']) . '</span>',
|
||||
__('工单号', 'aksrt') => esc_html((string) $ticket['ticket_num']),
|
||||
__('状态', 'aksrt') => $this->ticket_status_badge($ticket),
|
||||
__('用户', 'aksrt') => $this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name']),
|
||||
__('部门', 'aksrt') => esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—'),
|
||||
__('跟进人', 'aksrt') => (int) $ticket['admin_id'] > 0
|
||||
? esc_html((string) $ticket['admin_name'])
|
||||
: '<span class="aksrt-native-muted">' . esc_html__('未指派', 'aksrt') . '</span>',
|
||||
__('跟进人', 'aksrt') => esc_html((int) $ticket['admin_id'] > 0 ? (string) $ticket['admin_name'] : __('未指派', 'aksrt')),
|
||||
];
|
||||
if ((string) $ticket['order_number'] !== '') {
|
||||
$rows[__('关联订单', 'aksrt')] = '<code>' . esc_html((string) $ticket['order_number']) . '</code>';
|
||||
$rows[__('关联订单', 'aksrt')] = esc_html((string) $ticket['order_number']);
|
||||
}
|
||||
$rows[__('提交时间', 'aksrt')] = esc_html((string) $ticket['post_time']);
|
||||
$rows[__('最后回复', 'aksrt')] = esc_html((string) $ticket['last_reply_time'] !== '' ? (string) $ticket['last_reply_time'] : '—');
|
||||
if ((int) $ticket['urge_count'] > 0) {
|
||||
$rows[__('催单', 'aksrt')] = '<span class="aksrt-ticket-urge">' . esc_html(sprintf(
|
||||
/* translators: %d urge count */
|
||||
__('%d 次', 'aksrt'),
|
||||
(int) $ticket['urge_count']
|
||||
)) . '</span> <span class="aksrt-native-muted">' . esc_html(sprintf(
|
||||
/* translators: %s last urge time */
|
||||
__('最近 %s', 'aksrt'),
|
||||
(string) $ticket['last_urge_time']
|
||||
)) . '</span>';
|
||||
$rows[__('催单', 'aksrt')] = esc_html(sprintf(__('%1$d 次,最近 %2$s', 'aksrt'), (int) $ticket['urge_count'], (string) $ticket['last_urge_time']));
|
||||
}
|
||||
|
||||
$html = '<section class="aksrt-ticket-detail__card"><dl class="aksrt-ticket-detail__info">';
|
||||
$html = '<section class="aksrt-ticket-detail__card"><h3 class="aksrt-ticket-detail__heading">' . esc_html__('工单信息', 'aksrt') . '</h3><dl class="aksrt-ticket-detail__info">';
|
||||
foreach ($rows as $label => $value) {
|
||||
$html .= '<dt>' . esc_html($label) . '</dt><dd>' . $value . '</dd>';
|
||||
}
|
||||
return $html . '</dl></section>';
|
||||
}
|
||||
|
||||
/** 处理表单:状态仅列出 can_transition 允许的目标。 */
|
||||
private function ticket_status_form(array $ticket): string {
|
||||
$id = (int) $ticket['id'];
|
||||
$from = (string) $ticket['status'];
|
||||
|
||||
$html = '<section class="aksrt-ticket-detail__card">';
|
||||
$html .= '<h4 class="aksrt-ticket-detail__heading">' . esc_html__('处理', 'aksrt') . '</h4>';
|
||||
$html = '<section class="aksrt-ticket-detail__card"><div class="aksrt-ticket-section-head"><h3>' . esc_html__('处理工单', 'aksrt') . '</h3><button type="button" class="button-link" data-aksrt-ticket-assign>' . esc_html__('由我跟进', 'aksrt') . '</button></div>';
|
||||
$html .= $this->ticket_form_open('status', $id, 'aksrt-ticket-detail__status-form');
|
||||
$html .= wp_nonce_field('aksrt_ticket_status_' . $id, '_wpnonce', true, false);
|
||||
|
||||
$html .= '<label><span>' . esc_html__('状态', 'aksrt') . '</span><select name="status">';
|
||||
foreach (\AKSRT\Ticket\TicketStatusService::all() as $row) {
|
||||
$html .= '<label><span>' . esc_html__('状态', 'aksrt') . '</span><select name="status" data-aksrt-ticket-status>';
|
||||
foreach (TicketStatusService::map() as $row) {
|
||||
$key = (string) $row['status_key'];
|
||||
if (!\AKSRT\Ticket\TicketStatusService::can_transition($from, $key)) {
|
||||
continue;
|
||||
if (TicketStatusService::can_transition($from, $key)) {
|
||||
$html .= '<option value="' . esc_attr($key) . '" data-closed="' . (empty($row['is_closed']) ? '0' : '1') . '"' . selected($from, $key, false) . '>' . esc_html((string) $row['name']) . '</option>';
|
||||
}
|
||||
$html .= sprintf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($from, $key, false),
|
||||
esc_html((string) $row['name'])
|
||||
);
|
||||
}
|
||||
$html .= '</select></label>';
|
||||
|
||||
$html .= '<label><span>' . esc_html__('部门', 'aksrt') . '</span><select name="ticket_type_id">';
|
||||
foreach (\AKSRT\Ticket\TicketTypeService::all() as $row) {
|
||||
$html .= sprintf(
|
||||
'<option value="%d"%s>%s</option>',
|
||||
(int) $row['id'],
|
||||
selected((int) $ticket['ticket_type_id'], (int) $row['id'], false),
|
||||
esc_html((string) $row['name'])
|
||||
);
|
||||
$html .= '</select></label><label><span>' . esc_html__('部门', 'aksrt') . '</span><select name="ticket_type_id" required>';
|
||||
foreach (TicketTypeService::all() as $row) {
|
||||
$html .= '<option value="' . (int) $row['id'] . '"' . selected((int) $ticket['ticket_type_id'], (int) $row['id'], false) . '>' . esc_html((string) $row['name']) . '</option>';
|
||||
}
|
||||
$html .= '</select></label>';
|
||||
|
||||
$html .= '<label><span>' . esc_html__('跟进人', 'aksrt') . '</span><select name="admin_id">';
|
||||
$html .= '<option value="0">' . esc_html__('未指派', 'aksrt') . '</option>';
|
||||
$html .= '</select></label><label><span>' . esc_html__('跟进人', 'aksrt') . '</span><select name="admin_id"><option value="0">' . esc_html__('未指派', 'aksrt') . '</option>';
|
||||
$current = (int) $ticket['admin_id'];
|
||||
$admins = $this->ticket_admin_options();
|
||||
if ($current > 0 && !isset($admins[$current])) {
|
||||
$admins[$current] = (string) $ticket['admin_name'];
|
||||
}
|
||||
foreach ($admins as $admin_id => $name) {
|
||||
$html .= sprintf(
|
||||
'<option value="%d"%s>%s</option>',
|
||||
(int) $admin_id,
|
||||
selected($current, (int) $admin_id, false),
|
||||
esc_html($admin_id === get_current_user_id() ? sprintf(
|
||||
/* translators: %s current admin name */
|
||||
__('%s(我)', 'aksrt'),
|
||||
$name
|
||||
) : $name)
|
||||
);
|
||||
$html .= '<option value="' . $admin_id . '"' . selected($current, $admin_id, false) . '>' . esc_html($admin_id === get_current_user_id() ? sprintf(__('%s(我)', 'aksrt'), $name) : $name) . '</option>';
|
||||
}
|
||||
$html .= '</select></label>';
|
||||
|
||||
$html .= '<button type="submit" class="button">' . esc_html__('保存', 'aksrt') . '</button>';
|
||||
$html .= '</form></section>';
|
||||
return $html;
|
||||
$html .= '</select></label><div class="aksrt-ticket-close-warning" data-aksrt-ticket-close-warning hidden><label><input type="checkbox" data-aksrt-ticket-close-confirm> <span>' . esc_html__('确认关闭工单:关闭后无法再次回复,尚未发送的回复不会保存。', 'aksrt') . '</span></label></div>';
|
||||
return $html . '<button type="submit" class="button">' . esc_html__('保存处理信息', 'aksrt') . '</button></form></section>';
|
||||
}
|
||||
|
||||
/** @return array<int,string> 可指派的管理员 user_id => 显示名(单请求内缓存)。 */
|
||||
private function ticket_admin_options(): array {
|
||||
static $options = null;
|
||||
if ($options === null) {
|
||||
$options = [];
|
||||
foreach (get_users([
|
||||
'capability' => self::CAPABILITY,
|
||||
'fields' => ['ID', 'display_name'],
|
||||
'orderby' => 'display_name',
|
||||
'number' => 200,
|
||||
]) as $user) {
|
||||
foreach (get_users(['capability' => self::CAPABILITY, 'fields' => ['ID', 'display_name'], 'orderby' => 'display_name', 'number' => 200]) as $user) {
|
||||
$options[(int) $user->ID] = (string) $user->display_name;
|
||||
}
|
||||
$me = wp_get_current_user();
|
||||
$options[(int) $me->ID] = (string) $me->display_name;
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
@@ -266,21 +163,17 @@ trait UsersMenusTicketsDetail {
|
||||
private function ticket_message_head(int $user_id, string $name, string $role, string $time): string {
|
||||
$avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 64) : '';
|
||||
return '<header class="aksrt-ticket-detail__msg-head">'
|
||||
. ($avatar !== '' ? '<img src="' . esc_url($avatar) . '" alt="" width="32" height="32" />' : '')
|
||||
. '<strong>' . esc_html($name !== '' ? $name : '#' . $user_id) . '</strong>'
|
||||
. '<span class="aksrt-native-muted">' . esc_html($role) . '</span>'
|
||||
. '<time>' . esc_html($time) . '</time></header>';
|
||||
. ($avatar !== '' ? '<img src="' . esc_url($avatar) . '" alt="" width="32" height="32">' : '')
|
||||
. '<strong>' . esc_html($name !== '' ? $name : '#' . $user_id) . '</strong><span class="aksrt-ticket-hint">' . esc_html($role) . '</span><time>' . esc_html($time) . '</time></header>';
|
||||
}
|
||||
|
||||
/** @param list<string> $urls 图片附件(TicketAttachment 已清洗) */
|
||||
private function ticket_attachments_html(array $urls): string {
|
||||
if ($urls === []) {
|
||||
return '';
|
||||
}
|
||||
$html = '<ul class="aksrt-ticket-detail__attachments">';
|
||||
foreach ($urls as $url) {
|
||||
$html .= '<li><a href="' . esc_url((string) $url) . '" target="_blank" rel="noopener noreferrer">'
|
||||
. '<img src="' . esc_url((string) $url) . '" alt="' . esc_attr__('附件', 'aksrt') . '" /></a></li>';
|
||||
foreach ($urls as $index => $url) {
|
||||
$html .= '<li><a href="' . esc_url((string) $url) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url((string) $url) . '" alt="' . esc_attr(sprintf(__('查看附件 %d(新窗口)', 'aksrt'), $index + 1)) . '"></a></li>';
|
||||
}
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user