diff --git a/assets/css/admin/tickets.css b/assets/css/admin/tickets.css index 74baed94..81bca69d 100644 --- a/assets/css/admin/tickets.css +++ b/assets/css/admin/tickets.css @@ -22,7 +22,7 @@ .aksrt-ticket-user img { flex: none; border-radius: 50%; } .aksrt-ticket-user span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -/* 工作区与创建页共用:页头、提交反馈与表单控件。 */ +/* 工作区与代客提交弹窗共用:页头、提交反馈与表单控件。 */ .aksrt-tickets-admin__detail-header .aksrt-native-header__title { overflow-wrap: anywhere; } .aksrt-tickets-admin__detail-header .aksrt-native-header__desc { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .aksrt-tickets-admin [hidden] { display: none !important; } @@ -34,8 +34,21 @@ .aksrt-ticket-feedback p { margin: 0; } .aksrt-ticket-feedback.is-error { color: #a61b36; background: #fdf0f2; border-color: #f6d3da; } .aksrt-ticket-feedback .button { flex: none; } -.aksrt-tickets-admin__create { max-width: 880px; } -.aksrt-tickets-admin__create .aksrt-native-form-foot .aksrt-ticket-hint { margin-left: auto; } + +/* 代客提交:原生 top layer 弹窗,头尾固定,仅正文滚动。 */ +.aksrt-ticket-dialog { box-sizing: border-box; width: min(720px, calc(100vw - 48px)); max-width: none; max-height: calc(100dvh - 48px); margin: auto; padding: 0; color: var(--an-text); background: var(--an-card); border: 0; border-radius: var(--an-radius); box-shadow: 0 12px 32px rgba(15, 23, 42, .12), 0 32px 80px rgba(15, 23, 42, .16); overflow: hidden; font: inherit; } +.aksrt-ticket-dialog:not([open]) { display: none; } +.aksrt-ticket-dialog::backdrop { background: rgba(15, 23, 42, .45); } +.aksrt-ticket-dialog__frame { display: flex; flex-direction: column; max-height: calc(100dvh - 48px); margin: 0; } +.aksrt-ticket-dialog__head { display: flex; flex: none; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 18px 24px; border-bottom: 1px solid var(--an-border-soft); } +.aksrt-ticket-dialog__head h2 { margin: 0; font-size: 18px; font-weight: 600; line-height: 1.5; } +.aksrt-ticket-dialog__head h2:focus { outline: 0; } +.aksrt-ticket-dialog__head p { margin: 4px 0 0; color: var(--an-muted); font-size: 12px; } +.aksrt-ticket-dialog__close { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 32px; height: 32px; margin: -2px -6px 0 0; padding: 0; color: var(--an-muted); background: transparent; border: 0; border-radius: 8px; cursor: pointer; } +.aksrt-ticket-dialog__close:hover { color: var(--an-text); background: var(--an-bg-soft); } +.aksrt-ticket-dialog__body { min-height: 0; padding: 20px 24px; overflow-y: auto; overscroll-behavior: contain; } +.aksrt-ticket-dialog__foot { display: flex; flex: none; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 10px; padding: 14px 24px; border-top: 1px solid var(--an-border-soft); } +.aksrt-ticket-dialog__foot > .aksrt-ticket-hint { margin-right: auto; } /* 处理工作区:会话与回复在左,处理信息与内部备注在右。 */ .aksrt-ticket-detail { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; } @@ -142,7 +155,12 @@ .aksrt-ticket-detail__avatar { width: 28px; height: 28px; } .aksrt-ticket-composer__bar select { flex-basis: 100%; max-width: 100%; } .aksrt-ticket-feedback { align-items: flex-start; flex-direction: column; } - .aksrt-tickets-admin__create .aksrt-native-form-foot .aksrt-ticket-hint { flex-basis: 100%; margin-left: 0; } + .aksrt-ticket-dialog { width: calc(100vw - 16px); max-height: calc(100dvh - 16px); } + .aksrt-ticket-dialog__frame { max-height: calc(100dvh - 16px); } + .aksrt-ticket-dialog__head, + .aksrt-ticket-dialog__body, + .aksrt-ticket-dialog__foot { padding: 12px 14px; } + .aksrt-ticket-dialog__foot > .aksrt-ticket-hint { flex-basis: 100%; } } @media (prefers-reduced-motion: reduce) { .aksrt-tickets-admin *, .aksrt-tickets-admin *::before { scroll-behavior: auto !important; transition: none !important; animation: none !important; } diff --git a/assets/js/admin-tickets.js b/assets/js/admin-tickets.js index 485219c1..4ecf270f 100644 --- a/assets/js/admin-tickets.js +++ b/assets/js/admin-tickets.js @@ -1,4 +1,4 @@ -/** 工单后台:列表 / 处理工作区 / 代客提交均为页内视图;写操作只使用工单 REST 接口,成功后整页跳转刷新。 */ +/** 工单后台:列表与处理工作区为页内视图,代客提交为列表页上的原生 dialog;写操作只使用工单 REST 接口,成功后整页跳转刷新。 */ (function () { 'use strict'; @@ -7,6 +7,8 @@ if (!root || !config) return; var strings = config.strings; + var dialog = document.getElementById('aksrt-ticket-create-dialog'); + var opener = null; var feedbackBox = root.querySelector('[data-aksrt-ticket-feedback]'); var detail = root.querySelector('[data-aksrt-ticket-detail]'); var ticketId = detail ? parseInt(detail.dataset.aksrtTicketDetail, 10) : 0; @@ -133,7 +135,6 @@ /** 成功后跳回工作区并带结果码;未提交的其它表单草稿暂存到 sessionStorage,加载后恢复。 */ function go(id, kind, remaining) { var url = new URL(window.location.href); - url.searchParams.delete('view'); url.searchParams.set('id', String(id)); url.searchParams.set('aksrt_ticket_msg', kind); url.hash = ''; @@ -176,12 +177,50 @@ } } + /** 关闭代客提交弹窗:保存中不可关;有未保存输入时先确认。不响应遮罩点击,避免误关丢失输入。 */ + function closeDialog() { + if (saving || !dialog.open) return; + if (!stale && Object.keys(drafts()).length && !window.confirm(strings.discard)) return; + dialog.close(); + } + + if (dialog) { + dialog.addEventListener('cancel', function (event) { + event.preventDefault(); + closeDialog(); + }); + dialog.addEventListener('close', function () { + var form = dialog.querySelector('[data-aksrt-ticket-form]'); + if (form && !stale) { + form.reset(); + baselines.set(form, values(form)); + form.querySelectorAll('[name]').forEach(validateText); + form.querySelectorAll('[data-aksrt-ticket-editor]').forEach(counter); + feedback('', false, false); + } + if (opener && opener.isConnected) opener.focus(); + }); + } + root.addEventListener('click', function (event) { if (event.target.closest('[data-aksrt-ticket-reload]')) { leaving = true; window.location.reload(); return; } + if (dialog && event.target.closest('[data-aksrt-dialog-close]')) { + closeDialog(); + return; + } + var open = dialog && event.target.closest('[data-aksrt-dialog-open="' + dialog.id + '"]'); + if (open) { + if (saving) return; + opener = open; + // 结果未确认时保留「重新加载」提示,重新打开也不能再提交。 + if (!stale) feedback('', false, false); + dialog.showModal(); + return; + } if (saving || stale) return; var assign = event.target.closest('[data-aksrt-ticket-assign]'); if (assign) { diff --git a/src/Admin/Native/UsersMenusTicketsCenter.php b/src/Admin/Native/UsersMenusTicketsCenter.php index 26b3dbe9..d7a73fa1 100644 --- a/src/Admin/Native/UsersMenusTicketsCenter.php +++ b/src/Admin/Native/UsersMenusTicketsCenter.php @@ -1,6 +1,6 @@ __('连接中断,提交结果尚未确认。请先重新加载核对,避免重复提交。', 'aksrt'), 'required' => __('请填写内容,不能只包含空格。', 'aksrt'), 'tooLong' => __('插入后将超过字数限制,请先精简回复内容。', 'aksrt'), + 'discard' => __('有尚未保存的内容。关闭后将丢弃,是否继续?', 'aksrt'), ], ]); } @@ -55,12 +56,9 @@ trait UsersMenusTicketsCenter { $filters = $this->ticket_filters($_GET); $paged = max(1, absint($_GET['paged'] ?? 1)); $id = absint($_GET['id'] ?? 0); - $view = isset($_GET['view']) && is_scalar($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : ''; NativeUi::page_open('aksrt-tickets-admin'); if ($id > 0) { $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); } diff --git a/src/Admin/Native/UsersMenusTicketsCreate.php b/src/Admin/Native/UsersMenusTicketsCreate.php index b4aa88c9..57f496f6 100644 --- a/src/Admin/Native/UsersMenusTicketsCreate.php +++ b/src/Admin/Native/UsersMenusTicketsCreate.php @@ -1,5 +1,5 @@ ,创建成功后跳转到新工单的工作区。@package AKSRT */ namespace AKSRT\Admin\Native; @@ -10,40 +10,42 @@ 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']] - ); + private const TICKET_CREATE_DIALOG = 'aksrt-ticket-create-dialog'; + + private function render_ticket_create_dialog(): void { $types = TicketTypeService::all(); - NativeUi::card_open('', '', false, 'aksrt-tickets-admin__create'); + 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_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ($types === []) { echo NativeUi::empty_state(__('尚未配置工单部门', 'aksrt'), __('请先添加部门,再为用户创建工单。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '

' . esc_html__('前往工单配置', 'aksrt') . '

'; - NativeUi::card_close(); - return; + } else { + echo '
'; + echo ''; + echo ''; + echo ''; + if (TicketSettings::order_link_enabled()) { + echo ''; + } + echo '
'; } - 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 '
'; - echo ''; - echo ''; - echo ''; - if (TicketSettings::order_link_enabled()) { - echo ''; + echo ''; + if ($types !== []) { + echo ''; } - echo '
'; - echo '
'; - echo '' . esc_html__('取消', 'aksrt') . ''; - echo '' . esc_html__('新工单状态为「已回复」', 'aksrt') . '
'; - NativeUi::card_close(); + echo '' . ($types === [] ? '
' : '') . '
'; } } diff --git a/src/Admin/Native/UsersMenusTicketsList.php b/src/Admin/Native/UsersMenusTicketsList.php index 9c827045..309ac4a6 100644 --- a/src/Admin/Native/UsersMenusTicketsList.php +++ b/src/Admin/Native/UsersMenusTicketsList.php @@ -12,7 +12,7 @@ defined('ABSPATH') || exit; trait UsersMenusTicketsList { private function render_ticket_list_page(array $filters, int $paged): void { - // 先查询并夹取页码,页头「代客提交」链接才能带上实际页码。 + // 先查询并夹取页码,创建成功后跳转的工作区链接才能带上实际页码。 [$result, $paged] = $this->ticket_list_result($filters, $paged); NativeUi::header( __('工单管理', 'aksrt'), @@ -21,7 +21,7 @@ trait UsersMenusTicketsList { [ [ 'label' => __('代客提交工单', 'aksrt'), - 'url' => $this->ticket_page_url(['view' => 'new'] + $this->ticket_context($filters, $paged)), + 'dialog' => self::TICKET_CREATE_DIALOG, 'primary' => true, 'icon' => 'dashicons-plus-alt2', ], @@ -33,6 +33,7 @@ trait UsersMenusTicketsList { ] ); $this->render_ticket_list($filters, $paged, $result); + $this->render_ticket_create_dialog(); } /** 越界页码夹到末页,避免处理最后一条工单后停在空页。返回 [查询结果, 实际页码]。 */ diff --git a/tests/admin/ticket-native-render-harness.php b/tests/admin/ticket-native-render-harness.php index 1503def2..25af1702 100644 --- a/tests/admin/ticket-native-render-harness.php +++ b/tests/admin/ticket-native-render-harness.php @@ -135,16 +135,15 @@ namespace { $closed = render_page($page, ['id' => 99]); $missing = render_page($page, ['id' => 404]); $detail_list_queries = count($repo::$queries) - $list_queries; - $create = render_page($page, ['view' => 'new', 'status' => 'pending', 'paged' => 2]); $bogus_notice = render_page($page, ['id' => 21, 'aksrt_ticket_msg' => '