ticket_page_url($context), false, 'dashicons-arrow-left-alt'); $ticket = TicketRepository::get_instance()->find($id); echo '

'; if ($ticket === null) { echo esc_html__('工单详情', 'aksrt') . '

' . $back . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo NativeUi::empty_state(__('工单不存在', 'aksrt'), __('工单可能已被删除,或链接有误。', 'aksrt'), 'dashicons-search'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return; } echo '' . esc_html((string) $ticket['title']) . ''; echo '

' . esc_html((string) $ticket['ticket_num']) . ' ' . $this->ticket_status_badge($ticket) . $this->ticket_urge_badge($ticket) . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
' . $back . '

'; // 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 = '
'; $html .= $this->ticket_conversation($ticket, TicketRepository::get_instance()->replies($id)); $html .= !empty($ticket['is_closed']) ? '

' . esc_html__('工单已关闭,不能继续回复或变更处理信息;仍可添加内部备注。', 'aksrt') . '

' : $this->ticket_reply_form($id); $html .= '
'; } /** 原始问题与回复同属一条时间线:用户在左、客服在右、系统事件居中。 */ private function ticket_conversation(array $ticket, array $replies): string { $html = '
'; $html .= '

' . esc_html__('沟通记录', 'aksrt') . ' ' . esc_html(sprintf(__('%d 条记录', 'aksrt'), count($replies))) . '

'; if ($replies !== []) { $html .= ''; } $html .= '
    '; $html .= $this->ticket_message('origin', (int) $ticket['user_id'], (string) $ticket['user_name'], !empty($ticket['post_admin_id']) ? __('代客提交', 'aksrt') : __('提交了工单', 'aksrt'), (string) $ticket['post_time'], (string) $ticket['content'], (array) $ticket['attachment']); if ($replies === []) { $html .= '
  1. ' . esc_html__('暂无回复,请核对问题后联系用户。', 'aksrt') . '
  2. '; } foreach ($replies as $reply) { if ((string) $reply['author_type'] === 'system') { $html .= '
  3. ' . esc_html((string) $reply['content']) . '
  4. '; continue; } $admin = (string) $reply['author_type'] === 'admin'; $quote_html = ''; if (is_array($reply['quote'] ?? null)) { $quote = $reply['quote']; $quote_html = '
    ' . esc_html(!empty($quote['is_deleted']) ? __('原回复已删除', 'aksrt') : (string) $quote['content']) . '
    '; } $html .= $this->ticket_message($admin ? 'admin' : 'user', (int) $reply['author_id'], (string) $reply['author_name'], $admin ? __('客服', 'aksrt') : __('用户', 'aksrt'), (string) $reply['created_at'], (string) $reply['content'], (array) $reply['attachment'], $quote_html); } return $html . '
'; } private function ticket_message(string $variant, int $user_id, string $name, string $role, string $time, string $content, array $attachments, string $quote_html = ''): string { $avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 64) : ''; return '
  • ' . ($avatar !== '' ? '' : '') . '
    ' . esc_html($name !== '' ? $name : '#' . $user_id) . '' . '' . esc_html($role) . '
    ' . $quote_html . '
    ' . wpautop(esc_html($content)) . '
    ' . $this->ticket_attachments_html($attachments) . '
  • '; } private function ticket_reply_form(int $id): string { $field = 'aksrt-ticket-reply-' . $id; $tools = ''; $prereplies = TicketPrereplyService::all(); if ($prereplies !== []) { $tools = ''; } return $this->ticket_composer('reply', $id, $field, __('回复用户', 'aksrt'), __('发送后用户将收到通知', 'aksrt'), __('填写回复内容…', 'aksrt'), 4, $tools, __('发送回复', 'aksrt'), true); } /** 输入框与工具栏合为一个整体:预设回复、字数与提交按钮同排。 */ private function ticket_composer(string $action, int $id, string $field, string $label, string $hint, string $placeholder, int $rows, string $tools, string $submit, bool $primary): string { $max = TicketSettings::reply_max(); return $this->ticket_form_open($action, $id, 'aksrt-ticket-composer aksrt-ticket-composer--' . $action) . '
    ' . ($hint !== '' ? '' . esc_html($hint) . '' : '') . '
    ' . '
    ' . '
    ' . $tools . '0 / ' . $max . '' . '
    '; } private function ticket_info_card(array $ticket): string { $id = (int) $ticket['id']; $editable = empty($ticket['is_closed']); $controls = $editable ? $this->ticket_info_controls($ticket) : []; $rows = [ [__('工单号', 'aksrt'), esc_html((string) $ticket['ticket_num'])], [__('状态', 'aksrt'), $controls['status'] ?? $this->ticket_status_badge($ticket), 'status'], [__('用户', 'aksrt'), $this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name'])], [__('部门', 'aksrt'), $controls['ticket_type_id'] ?? esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—'), 'ticket_type_id'], [__('跟进人', 'aksrt'), $controls['admin_id'] ?? esc_html((int) $ticket['admin_id'] > 0 ? (string) $ticket['admin_name'] : __('未指派', 'aksrt')), 'admin_id'], ]; if ((string) $ticket['order_number'] !== '') { $rows[] = [__('关联订单', 'aksrt'), esc_html((string) $ticket['order_number'])]; } $rows[] = [__('提交时间', 'aksrt'), esc_html((string) $ticket['post_time'])]; if ((int) $ticket['urge_count'] > 0) { $rows[] = [__('催单', 'aksrt'), '' . esc_html(sprintf(__('%1$d 次,最近 %2$s', 'aksrt'), (int) $ticket['urge_count'], (string) $ticket['last_urge_time'])) . '']; } $html = '

    ' . esc_html__('工单信息', 'aksrt') . '

    '; if ($editable) { $html .= ''; } $html .= '
    '; if ($editable) { $html .= $this->ticket_form_open('status', $id, 'aksrt-ticket-detail__status-form'); } $html .= '
    '; foreach ($rows as $row) { [$label, $value] = $row; $label_html = esc_html($label); if ($editable && isset($row[2])) { $label_html = ''; } $html .= '
    ' . $label_html . '
    ' . $value . '
    '; } $html .= '
    '; if ($editable) { $html .= ''; $html .= ''; } return $html . '
    '; } /** 工单信息中的可编辑值;不再重复渲染只读值或独立处理表单。 */ private function ticket_info_controls(array $ticket): array { $id = (int) $ticket['id']; $from = (string) $ticket['status']; $controls = ['status' => ''; $controls['ticket_type_id'] = ''; $controls['admin_id'] = ''; return $controls; } 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) { $options[(int) $user->ID] = (string) $user->display_name; } $me = wp_get_current_user(); $options[(int) $me->ID] = (string) $me->display_name; } return $options; } /** 默认只展示最新 3 条备注,更早的收进原生
    ,避免历史备注撑满侧栏。 */ private function ticket_notes_card(int $id): string { $notes = TicketNoteService::for_ticket($id); $visible = 3; $html = '

    ' . esc_html__('内部备注', 'aksrt'); if ($notes !== []) { $html .= ' ' . count($notes) . ''; } $html .= '

    ' . esc_html__('仅管理员可见,不会发送给用户。', 'aksrt') . '

    '; if ($notes !== []) { $older = array_slice($notes, 0, -$visible); $html .= '
    '; if ($older !== []) { $html .= '
    ' . esc_html(sprintf(__('更早的 %d 条备注', 'aksrt'), count($older))) . '' . $this->ticket_notes_list($older) . '
    '; } $html .= $this->ticket_notes_list(array_slice($notes, -$visible)) . '
    '; } return $html . $this->ticket_composer('note', $id, 'aksrt-ticket-note-' . $id, __('添加备注', 'aksrt'), '', __('记录处理进展、排查结论…', 'aksrt'), 3, '', __('添加内部备注', 'aksrt'), false) . '
    '; } private function ticket_notes_list(array $notes): string { $html = '
      '; foreach ($notes as $note) { $html .= '
    1. ' . esc_html((string) $note['admin_name']) . '
      ' . nl2br(esc_html((string) $note['content'])) . '
    2. '; } return $html . '
    '; } private function ticket_attachments_html(array $urls): string { if ($urls === []) { return ''; } $html = ''; } }