diff --git a/assets/css/admin/tickets.css b/assets/css/admin/tickets.css index 81bca69d..7158baad 100644 --- a/assets/css/admin/tickets.css +++ b/assets/css/admin/tickets.css @@ -1,4 +1,4 @@ -/* 工单运营页:列表、处理工作区(?id=)与代客提交(?view=new)均为页内视图。复用 NativeUi 令牌。 */ +/* 工单运营页:列表与处理工作区(?id=)为页内视图,代客提交与内部备注为原生 dialog。复用 NativeUi 令牌。 */ .aksrt-tickets-admin__toolbar { display: flex; flex-direction: column; gap: 14px; padding: 20px; } .aksrt-tickets-admin__toolbar .subsubsub { margin: 0; padding: 0; box-shadow: none; } .aksrt-tickets-admin__table { min-width: 1000px; table-layout: fixed; } @@ -57,22 +57,23 @@ /* 侧栏在页面滚动时吸顶(让出 WP 管理工具栏),超出视口高度时栏内滚动。 */ .aksrt-ticket-detail__side { position: sticky; top: 48px; max-height: calc(100vh - 64px); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; } .aksrt-ticket-conversation, -.aksrt-ticket-composer--reply, .aksrt-ticket-detail__card { background: var(--an-card); border: 1px solid var(--an-border); border-radius: var(--an-radius); } +/* 会话面板(IM 式):头部、时间线、回复框同属一张卡片;卡片高度跟随视口,开屏即可看到回复框与发送按钮,只有时间线滚动。 */ +.aksrt-ticket-conversation { display: flex; flex-direction: column; height: calc(100vh - 200px); min-height: 460px; overflow: hidden; } .aksrt-ticket-detail__card { padding: 16px; } .aksrt-ticket-section-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; } .aksrt-ticket-section-head h3 { display: inline-flex; align-items: center; gap: 6px; margin: 0; color: var(--an-text); font-size: 14px; font-weight: 600; } .aksrt-ticket-section-head .button-link { font-size: 12px; text-decoration: none; } -.aksrt-ticket-conversation > .aksrt-ticket-section-head { margin: 0; padding: 12px 16px; border-bottom: 1px solid var(--an-border-soft); } +.aksrt-ticket-conversation > .aksrt-ticket-section-head { flex: none; margin: 0; padding: 12px 16px; border-bottom: 1px solid var(--an-border-soft); } /* 会话时间线:用户在左、客服在右、系统事件居中。 */ -.aksrt-ticket-conversation__scroll { max-height: 60vh; padding: 20px 16px; overflow-y: auto; overscroll-behavior: contain; } +.aksrt-ticket-conversation__scroll { flex: 1 1 auto; min-height: 0; padding: 20px 16px; background: var(--an-bg-soft); overflow-y: auto; overscroll-behavior: contain; } .aksrt-ticket-detail__thread { display: flex; flex-direction: column; gap: 18px; margin: 0; padding: 0; list-style: none; } .aksrt-ticket-detail__message { display: flex; align-items: flex-start; gap: 10px; max-width: 86%; margin: 0; } .aksrt-ticket-detail__message.is-admin { flex-direction: row-reverse; align-self: flex-end; } .aksrt-ticket-detail__message.is-origin { max-width: none; } .aksrt-ticket-detail__avatar { flex: none; width: 32px; height: 32px; background: var(--an-border); border-radius: 50%; object-fit: cover; } -.aksrt-ticket-detail__bubble { min-width: 0; padding: 10px 14px 12px; background: var(--an-bg-soft); border: 1px solid var(--an-border-soft); border-radius: 4px 12px 12px 12px; } +.aksrt-ticket-detail__bubble { min-width: 0; padding: 10px 14px 12px; background: var(--an-card); border: 1px solid var(--an-border); border-radius: 4px 12px 12px 12px; } .aksrt-ticket-detail__message.is-admin .aksrt-ticket-detail__bubble { background: var(--an-primary-soft); border-color: color-mix(in srgb, var(--an-primary) 20%, transparent); border-radius: 12px 4px 12px 12px; } .aksrt-ticket-detail__message.is-origin .aksrt-ticket-detail__bubble { flex: 1; background: var(--an-card); border-color: var(--an-border); border-left: 3px solid var(--an-info); border-radius: 4px 12px 12px 12px; } .aksrt-ticket-detail__meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-bottom: 6px; font-size: 12px; } @@ -94,11 +95,12 @@ .aksrt-ticket-detail__attachments a:hover { transform: translateY(-1px); } .aksrt-ticket-detail__attachments img { display: block; width: 72px; height: 72px; object-fit: cover; border: 1px solid var(--an-border); border-radius: 8px; } -/* 输入器:文本框与工具栏合成一个可聚焦整体。 */ -.aksrt-ticket-composer { margin: 0; } -.aksrt-ticket-composer--reply { padding: 14px 16px 16px; } -.aksrt-ticket-composer__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-bottom: 8px; } -.aksrt-ticket-composer__head label { color: var(--an-text); font-size: 14px; font-weight: 600; } +/* 回复框:固定在会话卡片底部,文本框与工具栏合成一个可聚焦整体。 */ +.aksrt-ticket-composer { flex: none; margin: 0; padding: 12px 16px 14px; background: var(--an-card); border-top: 1px solid var(--an-border-soft); } +.aksrt-ticket-composer textarea { max-height: 30vh; } +.aksrt-ticket-composer__bar .button .dashicons { margin: 0 4px 0 -2px; font-size: 16px; width: 16px; height: 16px; vertical-align: -3px; } +.aksrt-ticket-composer__shortcut { margin-left: auto; } +.aksrt-ticket-composer__shortcut + .aksrt-ticket-counter { margin-left: 0; } .aksrt-ticket-composer__box { background: var(--an-card); border: 1px solid var(--an-border); border-radius: var(--an-radius-sm); transition: border-color .15s, box-shadow .15s; } .aksrt-ticket-composer__box:focus-within { border-color: var(--an-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--an-primary) 18%, transparent); } .aksrt-native .aksrt-ticket-composer__box textarea { display: block; padding: 10px 12px; background: transparent; border: 0; border-radius: var(--an-radius-sm) var(--an-radius-sm) 0 0; box-shadow: none; } @@ -109,7 +111,7 @@ .aksrt-ticket-counter { margin-left: auto; color: var(--an-muted); font-size: 12px; font-variant-numeric: tabular-nums; } .aksrt-ticket-counter.is-over { color: var(--an-danger); } .aksrt-native-field .aksrt-ticket-counter { align-self: flex-end; } -.aksrt-ticket-detail__closed { display: flex; align-items: center; gap: 8px; margin: 0; padding: 14px 16px; color: var(--an-text-2); background: var(--an-card); border: 1px dashed var(--an-border); border-radius: var(--an-radius); font-size: 13px; } +.aksrt-ticket-detail__closed { display: flex; flex: none; align-items: center; gap: 8px; margin: 0; padding: 14px 16px; color: var(--an-text-2); background: var(--an-card); border-top: 1px solid var(--an-border-soft); font-size: 13px; } .aksrt-ticket-detail__closed .dashicons { flex: none; color: var(--an-muted); } /* 工单信息:标签列固定宽度,可编辑项直接内联。 */ @@ -139,18 +141,22 @@ .aksrt-ticket-detail__notes li { margin: 0; padding: 8px 10px; background: var(--an-card); border: 1px solid #f3e2b6; border-radius: var(--an-radius-sm); font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; } .aksrt-ticket-detail__note-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px; margin-bottom: 4px; font-size: 12px; } .aksrt-ticket-detail__note-head time { color: var(--an-muted); font-variant-numeric: tabular-nums; } -.aksrt-ticket-detail__private .aksrt-ticket-composer__box { border-color: #f3e2b6; } -/* 备注输入平时收成一行;聚焦或已有内容时展开为多行并显示提交栏。卡片标题已说明用途,标签仅供读屏。 */ -.aksrt-ticket-composer--note .aksrt-ticket-composer__head { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } -.aksrt-ticket-composer--note:not(:focus-within):has(textarea:placeholder-shown) textarea { height: 42px; min-height: 0; resize: none; overflow: hidden; } -.aksrt-ticket-composer--note:not(:focus-within):has(textarea:placeholder-shown) .aksrt-ticket-composer__bar { display: none; } +.aksrt-ticket-detail__private .aksrt-ticket-section-head .button .dashicons { margin: 0 2px 0 -4px; font-size: 14px; width: 14px; height: 14px; vertical-align: -2px; } +.aksrt-ticket-detail__notes-empty { margin: 0; color: var(--an-muted); font-size: 12px; } + +/* 内部备注弹窗:窄版,计数器右对齐在输入框下方。 */ +.aksrt-ticket-dialog--note { width: min(560px, calc(100vw - 48px)); } +.aksrt-ticket-dialog__counter { display: block; margin-top: 6px; text-align: right; } @media (max-width: 960px) { .aksrt-ticket-detail { grid-template-columns: minmax(0, 1fr); } .aksrt-ticket-detail__side { position: static; max-height: none; overflow: visible; } } @media (max-width: 600px) { - .aksrt-ticket-conversation__scroll { max-height: 50vh; padding: 14px 12px; } + .aksrt-ticket-conversation { height: calc(100vh - 160px); min-height: 400px; } + .aksrt-ticket-conversation__scroll { padding: 14px 12px; } + .aksrt-ticket-composer { padding: 10px 12px 12px; } + .aksrt-ticket-composer__shortcut { display: none; } .aksrt-ticket-detail__message { max-width: 100%; } .aksrt-ticket-detail__avatar { width: 28px; height: 28px; } .aksrt-ticket-composer__bar select { flex-basis: 100%; max-width: 100%; } diff --git a/assets/js/admin-tickets.js b/assets/js/admin-tickets.js index 4ecf270f..c93de967 100644 --- a/assets/js/admin-tickets.js +++ b/assets/js/admin-tickets.js @@ -1,4 +1,4 @@ -/** 工单后台:列表与处理工作区为页内视图,代客提交为列表页上的原生 dialog;写操作只使用工单 REST 接口,成功后整页跳转刷新。 */ +/** 工单后台:列表与处理工作区为页内视图;代客提交与内部备注为原生 dialog。写操作只使用工单 REST 接口,成功后整页跳转刷新。 */ (function () { 'use strict'; @@ -7,9 +7,13 @@ if (!root || !config) return; var strings = config.strings; - var dialog = document.getElementById('aksrt-ticket-create-dialog'); + var dialogs = Array.prototype.slice.call(root.querySelectorAll('dialog.aksrt-ticket-dialog')); var opener = null; - var feedbackBox = root.querySelector('[data-aksrt-ticket-feedback]'); + // 页面级提示(工作区顶部);弹窗内的表单使用弹窗自己的提示框。 + var pageFeedback = Array.prototype.slice.call(root.querySelectorAll('[data-aksrt-ticket-feedback]')).filter(function (box) { + return !box.closest('dialog'); + })[0] || null; + var feedbackBox = pageFeedback; var detail = root.querySelector('[data-aksrt-ticket-detail]'); var ticketId = detail ? parseInt(detail.dataset.aksrtTicketDetail, 10) : 0; var draftKey = 'aksrt-ticket-drafts-' + ticketId; @@ -177,17 +181,32 @@ } } - /** 关闭代客提交弹窗:保存中不可关;有未保存输入时先确认。不响应遮罩点击,避免误关丢失输入。 */ - function closeDialog() { + /** 表单所在弹窗的提示框优先,否则用页面级提示框。 */ + function feedbackFor(node) { + var host = node && node.closest('dialog'); + return (host && host.querySelector('[data-aksrt-ticket-feedback]')) || pageFeedback; + } + + /** 只比较弹窗内表单的改动,弹窗外的回复草稿不影响关闭确认。 */ + function dialogDirty(dialog) { + var form = dialog.querySelector('[data-aksrt-ticket-form]'); + if (!form) return false; + var base = baselines.get(form) || {}; + var current = values(form); + return Object.keys(current).some(function (key) { return current[key] !== base[key]; }); + } + + /** 关闭弹窗:保存中不可关;有未保存输入时先确认。不响应遮罩点击,避免误关丢失输入。 */ + function closeDialog(dialog) { if (saving || !dialog.open) return; - if (!stale && Object.keys(drafts()).length && !window.confirm(strings.discard)) return; + if (!stale && dialogDirty(dialog) && !window.confirm(strings.discard)) return; dialog.close(); } - if (dialog) { + dialogs.forEach(function (dialog) { dialog.addEventListener('cancel', function (event) { event.preventDefault(); - closeDialog(); + closeDialog(dialog); }); dialog.addEventListener('close', function () { var form = dialog.querySelector('[data-aksrt-ticket-form]'); @@ -196,11 +215,14 @@ baselines.set(form, values(form)); form.querySelectorAll('[name]').forEach(validateText); form.querySelectorAll('[data-aksrt-ticket-editor]').forEach(counter); + feedbackBox = feedbackFor(dialog); feedback('', false, false); } + feedbackBox = pageFeedback; if (opener && opener.isConnected) opener.focus(); + opener = null; }); - } + }); root.addEventListener('click', function (event) { if (event.target.closest('[data-aksrt-ticket-reload]')) { @@ -208,17 +230,22 @@ window.location.reload(); return; } - if (dialog && event.target.closest('[data-aksrt-dialog-close]')) { - closeDialog(); + var closer = event.target.closest('[data-aksrt-dialog-close]'); + if (closer && closer.closest('dialog')) { + closeDialog(closer.closest('dialog')); return; } - var open = dialog && event.target.closest('[data-aksrt-dialog-open="' + dialog.id + '"]'); - if (open) { + var open = event.target.closest('[data-aksrt-dialog-open]'); + var target = open && document.getElementById(open.dataset.aksrtDialogOpen); + if (target && dialogs.indexOf(target) !== -1) { if (saving) return; opener = open; + feedbackBox = feedbackFor(target); // 结果未确认时保留「重新加载」提示,重新打开也不能再提交。 if (!stale) feedback('', false, false); - dialog.showModal(); + target.showModal(); + var focus = target.querySelector('[autofocus]') || target.querySelector('h2'); + if (focus) focus.focus(); return; } if (saving || stale) return; @@ -241,6 +268,18 @@ if (event.target.matches('[data-aksrt-ticket-editor]')) counter(event.target); }); + // 回复框 Ctrl/⌘ + Enter 直接发送;输入法组字中不触发。 + root.addEventListener('keydown', function (event) { + if (event.key !== 'Enter' || !(event.ctrlKey || event.metaKey) || event.isComposing) return; + if (!event.target.matches('[data-aksrt-ticket-send-shortcut]')) return; + var form = event.target.form; + if (!form) return; + event.preventDefault(); + // 走 submit 事件,保证与点击「发送回复」经过同一套校验与防重。 + if (typeof form.requestSubmit === 'function') form.requestSubmit(); + else form.querySelector('[type="submit"]').click(); + }); + root.addEventListener('change', function (event) { var select = event.target; if (select.matches('[data-aksrt-ticket-status]')) { @@ -256,6 +295,7 @@ var insert = (before && !/\n$/.test(before) ? '\n' : '') + select.value; var next = before + insert + field.value.slice(end); if (next.length > field.maxLength) { + feedbackBox = feedbackFor(field); feedback(strings.tooLong, true, false); } else { field.value = next; @@ -279,6 +319,7 @@ var remaining = drafts(form); var controller = new AbortController(); var timer = setTimeout(function () { controller.abort(); }, 30000); + feedbackBox = feedbackFor(form); busy(form, true); feedback(strings.saving, false, false); try { diff --git a/src/Admin/Native/UsersMenusTicketsCenter.php b/src/Admin/Native/UsersMenusTicketsCenter.php index d7a73fa1..710ea0ae 100644 --- a/src/Admin/Native/UsersMenusTicketsCenter.php +++ b/src/Admin/Native/UsersMenusTicketsCenter.php @@ -103,6 +103,13 @@ trait UsersMenusTicketsCenter { . ''; } + /** 代客提交与内部备注弹窗共用的头部:标题、说明与关闭按钮。 */ + private function ticket_dialog_head(string $key, string $title, string $description): string { + return '

' . esc_html($title) . '

' + . '

' . esc_html($description) . '

' + . '
'; + } + /** 所有表单只提交到现有 REST 写路由,不保留 admin_post 分支。 */ private function ticket_form_open(string $action, int $id, string $class = ''): string { $path = $action === 'create' ? '' : '/' . $id . '/' . ($action === 'note' ? 'notes' : $action); diff --git a/src/Admin/Native/UsersMenusTicketsCreate.php b/src/Admin/Native/UsersMenusTicketsCreate.php index 57f496f6..bd1b4edd 100644 --- a/src/Admin/Native/UsersMenusTicketsCreate.php +++ b/src/Admin/Native/UsersMenusTicketsCreate.php @@ -16,9 +16,7 @@ trait UsersMenusTicketsCreate { $types = TicketTypeService::all(); echo ''; echo $types === [] ? '
' : $this->ticket_form_open('create', 0, 'aksrt-ticket-dialog__frame'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo '

' . esc_html__('代客提交工单', 'aksrt') . '

'; - echo '

' . esc_html__('为指定用户创建工单,创建后由你跟进,用户会收到通知。', 'aksrt') . '

'; - echo '
'; + echo $this->ticket_dialog_head('create', __('代客提交工单', 'aksrt'), __('为指定用户创建工单,创建后由你跟进,用户会收到通知。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
' . $this->ticket_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ($types === []) { echo NativeUi::empty_state(__('尚未配置工单部门', 'aksrt'), __('请先添加部门,再为用户创建工单。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped diff --git a/src/Admin/Native/UsersMenusTicketsDetail.php b/src/Admin/Native/UsersMenusTicketsDetail.php index 193abce2..1e3211a4 100644 --- a/src/Admin/Native/UsersMenusTicketsDetail.php +++ b/src/Admin/Native/UsersMenusTicketsDetail.php @@ -1,5 +1,5 @@ ticket_page_url($context), false, 'dashicons-arrow-left-alt'); @@ -30,21 +32,26 @@ trait UsersMenusTicketsDetail { $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 + echo $this->ticket_note_dialog((int) $ticket['id']); // 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']) + $footer = !empty($ticket['is_closed']) ? '

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

' : $this->ticket_reply_form($id); + $html = '
'; + $html .= $this->ticket_conversation($ticket, TicketRepository::get_instance()->replies($id), $footer); $html .= '
'; } - /** 原始问题与回复同属一条时间线:用户在左、客服在右、系统事件居中。 */ - private function ticket_conversation(array $ticket, array $replies): string { + /** + * 会话卡片:头部、可滚动时间线与底部固定的回复框同属一个面板(IM 式布局), + * 卡片高度跟随视口,开屏即可看到回复框和发送按钮。 + * 原始问题与回复同属一条时间线:用户在左、客服在右、系统事件居中。 + */ + private function ticket_conversation(array $ticket, array $replies, string $footer): string { $html = '
'; $html .= '

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

'; if ($replies !== []) { @@ -68,7 +75,7 @@ trait UsersMenusTicketsDetail { } $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 . '
'; + return $html . '
' . $footer . ''; } private function ticket_message(string $variant, int $user_id, string $name, string $role, string $time, string $content, array $attachments, string $quote_html = ''): string { @@ -92,19 +99,15 @@ trait UsersMenusTicketsDetail { } $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) . '' : '') . '
' - . '
' + // 输入框与工具栏合为一个整体:预设回复、字数与发送按钮同排;Ctrl/⌘ + Enter 发送。 + return $this->ticket_form_open('reply', $id, 'aksrt-ticket-composer') + . '' + . '
' . '
' . $tools + . '' . esc_html__('Ctrl + Enter 发送', 'aksrt') . '' . '0 / ' . $max . '' - . '
'; + . '
'; } private function ticket_info_card(array $ticket): string { @@ -193,7 +196,10 @@ trait UsersMenusTicketsDetail { return $options; } - /** 默认只展示最新 3 条备注,更早的收进原生
,避免历史备注撑满侧栏。 */ + /** + * 备注卡片只展示记录与「添加备注」入口,输入在弹窗内完成,侧栏高度不随输入变化。 + * 默认只展示最新 3 条备注,更早的收进原生
,避免历史备注撑满侧栏。 + */ private function ticket_notes_card(int $id): string { $notes = TicketNoteService::for_ticket($id); $visible = 3; @@ -201,16 +207,32 @@ trait UsersMenusTicketsDetail { 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)) . '
'; + $html .= '
'; + $html .= '

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

'; + if ($notes === []) { + return $html . '

' . esc_html__('暂无备注', 'aksrt') . '

'; } - return $html . $this->ticket_composer('note', $id, 'aksrt-ticket-note-' . $id, __('添加备注', 'aksrt'), '', __('记录处理进展、排查结论…', 'aksrt'), 3, '', __('添加内部备注', 'aksrt'), false) . ''; + $older = array_slice($notes, 0, -$visible); + $html .= '
'; + if ($older !== []) { + $html .= '
' . esc_html(sprintf(__('更早的 %d 条备注', 'aksrt'), count($older))) . '' . $this->ticket_notes_list($older) . '
'; + } + return $html . $this->ticket_notes_list(array_slice($notes, -$visible)) . '
'; + } + + private function ticket_note_dialog(int $id): string { + $field = 'aksrt-ticket-note-' . $id; + $max = TicketSettings::reply_max(); + return '' + . $this->ticket_form_open('note', $id, 'aksrt-ticket-dialog__frame') + . $this->ticket_dialog_head('note', __('添加内部备注', 'aksrt'), __('仅管理员可见,不会发送给用户。', 'aksrt')) + . '
' . $this->ticket_feedback() + . '' + . '' + . '0 / ' . $max . '
' + . '
' + . '
'; } private function ticket_notes_list(array $notes): string { diff --git a/tests/admin/ticket-native-render.test.cjs b/tests/admin/ticket-native-render.test.cjs index ed8c1d00..9d06c5d8 100644 --- a/tests/admin/ticket-native-render.test.cjs +++ b/tests/admin/ticket-native-render.test.cjs @@ -44,7 +44,18 @@ test('workspace loads by ID independent of list filters and keeps the list conte assert.match(closed, /aksrt-ticket-detail__closed/); assert.match(closed, /data-aksrt-ticket-form="note"/); assert.doesNotMatch(closed, /data-aksrt-ticket-form="(?:reply|status)"/); - for (const html of [open, closed]) assert.doesNotMatch(html, //)?.[0] || ''; + assert.match(conversation, /aksrt-ticket-conversation__scroll[^]*data-aksrt-ticket-form="reply"/); + assert.match(conversation, /data-aksrt-ticket-send-shortcut/); + assert.match(conversation, /