/** 工单后台:按需加载弹窗,写操作只使用工单 REST 接口。 */ (function () { 'use strict'; var root = document.querySelector('.aksrt-tickets-admin'); var config = window.AKSRTAdminTickets; var createDialog = document.getElementById('aksrt-ticket-create-dialog'); var detailDialog = document.getElementById('aksrt-ticket-detail-dialog'); if (!root || !config || !createDialog || !detailDialog || typeof detailDialog.showModal !== 'function') return; var strings = config.strings; var panel = detailDialog.querySelector('[data-aksrt-ticket-panel]'); var list = root.querySelector('[data-aksrt-ticket-list]'); var detailTitle = document.getElementById('aksrt-ticket-detail-title'); var initialTitle = detailTitle.textContent; var baselines = new WeakMap(); var active = null; var opener = null; var ticketId = 0; var saving = false; var loading = false; var stale = false; var retryPage = false; var savedMessage = ''; var savedDrafts = {}; var disabledControls = []; var readController = null; var generation = 0; var pointerOutside = false; var feedbackTimer = 0; function values(form) { var result = {}; form.querySelectorAll('input[name], select[name], textarea[name]').forEach(function (field) { result[field.name] = field.value; }); return result; } function drafts(except) { var result = {}; if (!active) return result; active.querySelectorAll('[data-aksrt-ticket-form]').forEach(function (form) { if (form === except) return; var base = baselines.get(form) || {}; var changed = {}; var current = values(form); Object.keys(current).forEach(function (key) { if (current[key] !== base[key]) changed[key] = current[key]; }); if (Object.keys(changed).length) result[form.dataset.aksrtTicketForm] = changed; }); return result; } function dirty() { return Object.keys(drafts()).length > 0 || Object.keys(savedDrafts).length > 0; } function counter(field) { var output = root.querySelector('[data-aksrt-ticket-counter="' + field.id + '"]'); if (!output) return; output.textContent = field.value.length + ' / ' + field.maxLength; output.classList.toggle('is-over', field.value.length >= field.maxLength); } function validateText(field) { if (field.required && (field.tagName === 'TEXTAREA' || field.type === 'text')) { field.setCustomValidity(field.value.trim() ? '' : strings.required); } } function syncStatus(form) { var select = form.querySelector('[data-aksrt-ticket-status]'); if (!select) return; var option = select.options[select.selectedIndex]; var closed = !!option && option.dataset.closed === '1'; var warning = form.querySelector('[data-aksrt-ticket-close-warning]'); var confirm = form.querySelector('[data-aksrt-ticket-close-confirm]'); warning.hidden = !closed; confirm.required = closed; if (!closed) confirm.checked = false; } function setupForms(container, restore) { container.querySelectorAll('[data-aksrt-ticket-form]').forEach(function (form) { baselines.set(form, values(form)); var changes = (restore || {})[form.dataset.aksrtTicketForm] || {}; form.querySelectorAll('[name]').forEach(function (field) { if (Object.prototype.hasOwnProperty.call(changes, field.name)) field.value = changes[field.name]; validateText(field); }); syncStatus(form); }); container.querySelectorAll('[data-aksrt-ticket-editor]').forEach(counter); } /** 成功与可忽略的错误提示自动消失;加载/保存中与需重试的提示保留到状态变化。 */ function feedback(dialog, message, error, retry) { var box = dialog.querySelector('[data-aksrt-ticket-feedback]'); clearTimeout(feedbackTimer); box.hidden = !message; box.classList.toggle('is-error', !!error); box.querySelector('p').textContent = message || ''; box.querySelector('[data-aksrt-ticket-retry]').hidden = !retry; if (!message || retry || message === strings.loading || message === strings.saving) return; feedbackTimer = setTimeout(function () { box.hidden = true; }, error ? 6000 : 3000); } function busy(value) { saving = value; if (!active) return; active.setAttribute('aria-busy', String(value)); if (value) { disabledControls = []; active.querySelectorAll('input, select, textarea, button').forEach(function (control) { disabledControls.push([control, control.disabled]); control.disabled = true; }); } else { disabledControls.forEach(function (entry) { entry[0].disabled = entry[1]; }); disabledControls = []; } } function lockStaleForms() { if (!stale || !active) return; active.querySelectorAll('form input, form select, form textarea, form button[type="submit"]').forEach(function (control) { control.disabled = true; }); active.querySelectorAll('[data-aksrt-ticket-assign]').forEach(function (button) { button.disabled = true; }); } /** 会话已在底部或无需滚动时隐藏「查看最新记录」,避免点击无反应。 */ function syncLatest() { var conversation = panel.querySelector('.aksrt-ticket-conversation__scroll'); var button = panel.querySelector('[data-aksrt-ticket-latest]'); if (!conversation || !button) return; button.hidden = conversation.scrollHeight - conversation.clientHeight - conversation.scrollTop <= 8; } /** 会话内滚到底部;手动触发时同时让弹窗正文露出会话底部。 */ function showLatest(manual) { var conversation = panel.querySelector('.aksrt-ticket-conversation__scroll'); if (!conversation) return; conversation.scrollTop = conversation.scrollHeight; if (manual) { var gap = conversation.getBoundingClientRect().bottom - panel.getBoundingClientRect().bottom; if (gap > 0) panel.scrollTop += gap; conversation.focus({ preventScroll: true }); } syncLatest(); } function updateUrl(id, page) { var url = new URL(window.location.href); if (id) url.searchParams.set('id', String(id)); else url.searchParams.delete('id'); if (page) { if (page > 1) url.searchParams.set('paged', String(page)); else url.searchParams.delete('paged'); } url.hash = ''; window.history.replaceState(window.history.state, '', url.toString()); } async function request(url, options) { var response = await fetch(url, Object.assign({ credentials: 'same-origin' }, options)); var result = await response.json(); if (!response.ok || !result || result.success !== true) { var error = new Error(result && (result.message || (result.data && result.data.message)) || strings.saveFailed); error.confirmed = !!result && (result.success === false || typeof result.code === 'string'); error.status = response.status; throw error; } return result.data; } function readUrl(id, refresh) { var url = new URL(config.ajaxUrl, window.location.href); var current = new URL(window.location.href); ['status', 'ticket_type_id', 's', 'paged'].forEach(function (key) { if (current.searchParams.has(key)) url.searchParams.set(key, current.searchParams.get(key)); }); url.searchParams.set('action', 'aksrt_ticket_view'); url.searchParams.set('nonce', config.readNonce); url.searchParams.set('id', String(id)); if (refresh) url.searchParams.set('refresh_list', '1'); return url.toString(); } async function loadDetail(id, refresh, message, restore) { if (readController) readController.abort(); var current = ++generation; var controller = new AbortController(); readController = controller; var timer = setTimeout(function () { controller.abort(); }, 30000); loading = true; panel.setAttribute('aria-busy', 'true'); feedback(detailDialog, strings.loading, false, false); try { var data = await request(readUrl(id, refresh), { signal: controller.signal, cache: 'no-store' }); if (current !== generation || active !== detailDialog) return; panel.innerHTML = data.panel; detailTitle.textContent = data.title; if (typeof data.list === 'string') list.innerHTML = data.list; updateUrl(id, data.page); setupForms(panel, restore); showLatest(false); var notes = panel.querySelector('.aksrt-ticket-detail__notes'); if (notes) notes.scrollTop = notes.scrollHeight; stale = false; savedDrafts = {}; savedMessage = ''; feedback(detailDialog, message, false, false); if (message) detailTitle.focus(); } catch (error) { if (current !== generation || active !== detailDialog) return; stale = true; retryPage = [401, 403, 404].indexOf(error.status) !== -1; if (error.status === 404) updateUrl(0); savedDrafts = restore || {}; savedMessage = message || ''; feedback(detailDialog, message ? strings.refreshFailed : (error.confirmed ? error.message : strings.loadFailed), true, true); } finally { clearTimeout(timer); if (current === generation) { loading = false; panel.removeAttribute('aria-busy'); readController = null; lockStaleForms(); } } } function show(dialog, source) { active = dialog; opener = source || null; pointerOutside = false; stale = false; retryPage = false; feedback(dialog, '', false, false); dialog.showModal(); document.documentElement.classList.add('aksrt-ticket-modal-open'); } function openDetail(id, source, refresh, message) { ticketId = id; detailTitle.textContent = initialTitle; show(detailDialog, source); updateUrl(id); return loadDetail(id, !!refresh, message || '', {}); } function close() { if (!active || saving) return; if (dirty() && !window.confirm(strings.discard)) return; active.close(); } root.addEventListener('click', function (event) { var create = event.target.closest('[data-aksrt-dialog-open="aksrt-ticket-create-dialog"]'); var view = event.target.closest('[data-aksrt-ticket-view]'); if (create || view) { event.preventDefault(); if (active) return; if (create) show(createDialog, create); else openDetail(parseInt(view.dataset.aksrtTicketView, 10), view); return; } if (!active || saving) return; var retry = event.target.closest('[data-aksrt-ticket-retry]'); if (retry) { if (retryPage) { window.location.reload(); return; } if (!loading) loadDetail(ticketId, true, savedMessage, Object.assign({}, savedDrafts, drafts())); return; } if (stale || loading) return; var assign = event.target.closest('[data-aksrt-ticket-assign]'); if (assign) { var statusForm = assign.closest('section').querySelector('form'); statusForm.querySelector('[name="admin_id"]').value = String(config.currentUser); var statusSelect = statusForm.querySelector('[name="status"]'); if (statusSelect.value === 'pending') statusSelect.value = 'handling'; syncStatus(statusForm); statusForm.querySelector('[type="submit"]').focus(); } if (event.target.closest('[data-aksrt-ticket-latest]')) showLatest(true); }); root.addEventListener('input', function (event) { validateText(event.target); if (event.target.matches('[data-aksrt-ticket-editor]')) counter(event.target); }); root.addEventListener('change', function (event) { var select = event.target; if (select.matches('[data-aksrt-ticket-status]')) { select.form.querySelector('[data-aksrt-ticket-close-confirm]').checked = false; syncStatus(select.form); } if (!select.matches('[data-aksrt-ticket-prereply]') || !select.value) return; var field = document.getElementById(select.dataset.aksrtTicketPrereply); if (!field) return; var start = field.selectionStart || 0; var end = field.selectionEnd || 0; var before = field.value.slice(0, start); var insert = (before && !/\n$/.test(before) ? '\n' : '') + select.value; var next = before + insert + field.value.slice(end); if (next.length > field.maxLength) { feedback(active, strings.tooLong, true, false); } else { field.value = next; field.focus(); field.setSelectionRange(start + insert.length, start + insert.length); validateText(field); counter(field); } select.value = ''; }); root.addEventListener('submit', async function (event) { var form = event.target.closest('[data-aksrt-ticket-form]'); if (!form) return; event.preventDefault(); if (saving || loading || stale || !active) return; form.querySelectorAll('input, textarea').forEach(validateText); if (!form.reportValidity()) return; var kind = form.dataset.aksrtTicketForm; var body = JSON.stringify(values(form)); var remaining = drafts(form); var controller = new AbortController(); var timer = setTimeout(function () { controller.abort(); }, 30000); busy(true); feedback(active, strings.saving, false, false); try { var data = await request(form.action, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.restNonce }, body: body, signal: controller.signal, }); clearTimeout(timer); baselines.set(form, values(form)); if (kind === 'create') { var source = opener; busy(false); await new Promise(function (resolve) { createDialog.addEventListener('close', resolve, { once: true }); createDialog.close(); }); await openDetail(data.id, source, true, strings.created); } else { if (kind !== 'status') form.reset(); baselines.set(form, values(form)); await loadDetail(ticketId, true, strings[kind], remaining); } } catch (error) { var reload = !error.confirmed || [401, 403, 404].indexOf(error.status) !== -1; stale = reload || error.status === 409; retryPage = reload || (kind === 'create' && stale); if (error.status === 404) updateUrl(0); feedback(active, error.confirmed ? error.message : strings.uncertain, true, stale); } finally { clearTimeout(timer); busy(false); lockStaleForms(); } }); [createDialog, detailDialog].forEach(function (dialog) { function outside(event) { var rect = dialog.getBoundingClientRect(); return event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom; } dialog.addEventListener('pointerdown', function (event) { pointerOutside = event.target === dialog && outside(event); }); dialog.addEventListener('pointercancel', function () { pointerOutside = false; }); dialog.addEventListener('click', function (event) { if (event.target.closest('[data-aksrt-dialog-close]') || (pointerOutside && event.target === dialog && outside(event))) { event.preventDefault(); close(); } pointerOutside = false; }); dialog.addEventListener('cancel', function (event) { event.preventDefault(); close(); }); dialog.addEventListener('close', function () { var previous = opener; var previousId = ticketId; if (readController) readController.abort(); generation += 1; readController = null; loading = false; stale = false; savedDrafts = {}; savedMessage = ''; if (dialog === detailDialog) { panel.replaceChildren(); updateUrl(0); } else { var form = dialog.querySelector('form'); if (form) form.reset(); setupForms(dialog); } active = null; opener = null; ticketId = 0; document.documentElement.classList.remove('aksrt-ticket-modal-open'); if (!previous || !previous.isConnected) previous = root.querySelector('[data-aksrt-ticket-view="' + previousId + '"]') || root.querySelector('[data-aksrt-dialog-open]'); if (previous) previous.focus(); }); }); // scroll 不冒泡,用捕获监听会话容器。 panel.addEventListener('scroll', syncLatest, true); // 把正文可视高度写给 CSS,用于限制右侧栏高度。 if (typeof ResizeObserver === 'function') { new ResizeObserver(function () { panel.style.setProperty('--aksrt-ticket-panel-height', panel.clientHeight + 'px'); }).observe(panel); } window.addEventListener('beforeunload', function (event) { if (!saving && !dirty()) return; event.preventDefault(); event.returnValue = ''; }); setupForms(createDialog); var initialId = parseInt(new URL(window.location.href).searchParams.get('id'), 10); if (initialId > 0) openDetail(initialId, root.querySelector('[data-aksrt-ticket-view="' + initialId + '"]')); })();