/** * AKSRT Ticket Center — 用户中心「工单中心」面板。 * * 与「举报」链路同构(report.js): * - 列表 / 统计 / 详情全部走 REST,模板只输出容器(profile-tickets.php) * - 弹窗统一经 AksrtModalRuntime.open 直出 HTML:桌面 modal-mini(400px) * + 蓝色炫彩标题区,移动端由 mobileBottom 底部抽屉接管 * - 提交工单与回复前先过人机验证(ticket_create / ticket_reply,场景未启用自动放行) * - 举证图片走 /aksrt/v1/ticket/upload-attachment(复用评论图片上传的大小与类型校验) * * @package AKSRT */ (function () { 'use strict'; var API_ROOT = (function () { var d = window.aksrtData || {}; var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/aksrt/v1'; return String(raw).replace(/\/$/, ''); })(); var NONCE = (window.aksrtData && window.aksrtData.restNonce) || ''; var state = { page: 1, perPage: 10, total: 0, totalPages: 1, status: '', type: '', keywords: '', meta: null, attachmentMax: 5, }; function settings() { var d = window.aksrtData && window.aksrtData.ticket; return d && typeof d === 'object' ? d : {}; } function notyf(text, type, timeout, id) { if (typeof window.notyf === 'function') { return window.notyf(text, type, timeout, id); } if (typeof window.toast === 'function') { return window.toast(text, type); } } function escapeHtml(value) { return String(value === null || value === undefined ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function request(path, options) { var opts = options || {}; return fetch(API_ROOT + path, { method: opts.method || 'GET', headers: Object.assign({ 'X-WP-Nonce': NONCE }, opts.body ? { 'Content-Type': 'application/json' } : {}), body: opts.body ? JSON.stringify(opts.body) : undefined, credentials: 'same-origin', }).then(function (res) { return res.json(); }); } // 提交前的人机验证:场景未启用时无挂载点,run 直接 resolve {}; // 用户中途取消验证时 run resolve null,此时 reject {cancelled:true} 中止提交。 function runCaptcha(scene) { if (!window.AKSRTCaptcha || typeof window.AKSRTCaptcha.run !== 'function') { return Promise.resolve({}); } return window.AKSRTCaptcha.run(scene).then(function (payload) { return payload === null ? Promise.reject({ cancelled: true }) : (payload || {}); }); } function openModal(options) { if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') { notyf('工单组件加载失败,请刷新页面后重试', 'danger'); return null; } return window.AksrtModalRuntime.open(options); } function closeModal(reason) { if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === 'function') { window.AksrtModalRuntime.close(reason); } } function statusBadge(status, color, label) { return '' + escapeHtml(label || status) + ''; } function attachmentPreview(images) { if (!images || !images.length) return ''; var html = '
'; images.forEach(function (url) { html += '' + '' + ''; }); return html + '
'; } function quoteBlock(quote) { if (!quote) return ''; var text = quote.is_deleted ? '原回复已删除' : (quote.content || ''); return '
' + '' + escapeHtml(quote.author_name || '') + ' ' + escapeHtml(text) + '
'; } function modalShell(title, body, footer) { return '' + '
' + body + '
' + ''; } /* ------------------------------------------------------------------ * * 列表 / 统计 * ------------------------------------------------------------------ */ function loadMeta() { if (state.meta) return Promise.resolve(state.meta); return request('/tickets/meta').then(function (result) { state.meta = (result && result.data) || {}; if (state.meta.attachment) { state.attachmentMax = state.meta.attachment.max || 5; } return state.meta; }); } function loadList() { var panel = document.querySelector('[data-aksrt-ticket-center]'); if (!panel) return; var list = panel.querySelector('[data-aksrt-ticket-list]'); if (!list) return; var params = 'page=' + state.page + '&per_page=' + state.perPage; if (state.status) params += '&status=' + encodeURIComponent(state.status); if (state.type) params += '&ticket_type_id=' + encodeURIComponent(state.type); if (state.keywords) params += '&keywords=' + encodeURIComponent(state.keywords); request('/tickets?' + params).then(function (result) { var data = (result && result.data) || {}; state.total = data.total || 0; state.totalPages = data.total_pages || 1; renderList(Array.isArray(data.items) ? data.items : []); renderPagination(); }).catch(function () { notyf('工单列表加载失败', 'danger'); }); } function renderList(items) { var list = document.querySelector('[data-aksrt-ticket-list]'); if (!list) return; if (!items.length) { list.innerHTML = '
' + '' + '

还没有工单

' + '

遇到问题时提交工单,我们会尽快处理。

'; return; } var html = ''; items.forEach(function (ticket) { html += '
' + '
' + '#' + escapeHtml(ticket.ticket_num) + '' + statusBadge(ticket.status, ticket.status_color, ticket.status_label) + '
' + '

' + escapeHtml(ticket.title) + '

' + '
' + '' + escapeHtml(ticket.ticket_type_name) + '' + '' + (ticket.order_number ? '订单 ' + escapeHtml(ticket.order_number) + '' : '') + '
' + '' + '
'; }); list.innerHTML = html; } function renderPagination() { var nav = document.querySelector('[data-aksrt-ticket-pagination]'); if (!nav) return; if (state.totalPages <= 1) { nav.hidden = true; nav.innerHTML = ''; return; } nav.hidden = false; var html = ''; for (var i = 1; i <= state.totalPages; i++) { html += ''; } nav.innerHTML = html; } function loadStatistic() { request('/tickets/statistic').then(function (result) { var data = (result && result.data) || {}; Object.keys(data).forEach(function (key) { var node = document.querySelector('[data-aksrt-ticket-stat="' + key + '"] strong'); if (node) node.textContent = String(data[key]); }); }).catch(function () { /* 统计失败不阻塞列表 */ }); } /* ------------------------------------------------------------------ * * 详情弹窗 * ------------------------------------------------------------------ */ function openDetail(ticketId) { request('/tickets/' + ticketId).then(function (result) { var ticket = (result && result.data) || null; if (!ticket) { notyf('工单不存在', 'danger'); return; } var shell = openModal({ html: modalShell('#' + ticket.ticket_num, buildDetailHtml(ticket)), className: 'aksrt-ticket-detail-modal', kind: 'ticket-detail', mobileBottom: true, }); if (shell) bindDetail(shell, ticket); }).catch(function () { notyf('工单详情加载失败', 'danger'); }); } function buildDetailHtml(ticket) { var html = '
'; html += '
' + statusBadge(ticket.status, ticket.status_color, ticket.status_label) + '' + escapeHtml(ticket.ticket_type_name) + '' + '
'; html += '

' + escapeHtml(ticket.title) + '

'; if (ticket.order_number) { html += '

关联订单:' + escapeHtml(ticket.order_number) + '

'; } html += '
' + escapeHtml(ticket.content).replace(/\r\n|\r|\n/g, '
') + '
'; html += attachmentPreview(ticket.attachment); html += '
'; (ticket.replies || []).forEach(function (reply) { var who = reply.author_type === 'admin' ? '管理员' : '我'; html += '
' + '
' + escapeHtml(reply.author_name || who) + '' + '
' + quoteBlock(reply.quote) + '
' + escapeHtml(reply.content).replace(/\r\n|\r|\n/g, '
') + '
' + attachmentPreview(reply.attachment) + '' + '
'; }); html += '
'; if (ticket.is_closed) { html += '

工单已关闭,如需继续沟通请提交新工单。

'; return html + '
'; } html += '
'; html += ''; html += ''; html += '
'; if (settings().attachmentEnabled) { html += ''; } html += ''; html += ''; html += ''; html += '
'; return html + ''; } function bindDetail(shell, ticket) { var form = shell.querySelector('[data-aksrt-ticket-reply-form]'); var quoteField = shell.querySelector('[data-aksrt-ticket-quote-id]'); var fileInput = null; if (fileInput === null) { fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/gif,image/jpeg,image/jpg,image/png,image/webp'; fileInput.hidden = true; shell.appendChild(fileInput); } shell.addEventListener('click', function (event) { var quoteBtn = event.target.closest('[data-aksrt-ticket-quote]'); if (quoteBtn) { if (quoteField) quoteField.value = quoteBtn.getAttribute('data-aksrt-ticket-quote') || ''; var textarea = shell.querySelector('[data-aksrt-ticket-reply-form] textarea'); if (textarea) textarea.focus(); return; } if (event.target.closest('[data-aksrt-ticket-urge]')) { sendUrge(ticket); return; } if (event.target.closest('[data-aksrt-ticket-close]')) { sendClose(ticket); return; } if (event.target.closest('[data-aksrt-ticket-upload]')) { fileInput.click(); } }); fileInput.addEventListener('change', function () { var file = fileInput.files && fileInput.files[0]; if (!file) return; uploadAttachment(file, shell, function () { fileInput.value = ''; }); }); if (form) { form.addEventListener('submit', function (event) { event.preventDefault(); var textarea = form.querySelector('textarea'); var content = (textarea && textarea.value || '').trim(); if (!content) { notyf('请填写回复内容', 'danger'); return; } runCaptcha('ticket_reply').then(function () { sendReply(ticket, content, quoteField ? quoteField.value : '', collectAttachments(shell)); }).catch(function (error) { if (error && error.cancelled) return; notyf('人机验证未通过', 'danger'); }); }); } } function collectAttachments(shell) { var out = []; shell.querySelectorAll('[data-aksrt-ticket-image]').forEach(function (node) { var url = node.getAttribute('data-aksrt-ticket-image'); if (url && out.length < state.attachmentMax) out.push(url); }); return out; } function sendReply(ticket, content, quoteReplyId, attachment) { request('/tickets/' + ticket.id + '/reply', { method: 'POST', body: { content: content, quote_reply_id: quoteReplyId ? parseInt(quoteReplyId, 10) : 0, attachment: attachment, }, }).then(function (result) { if (result && result.success) { closeModal('submit'); notyf('回复已提交', 'success'); loadList(); loadStatistic(); } else { notyf((result && result.message) || '回复提交失败', 'danger'); } }).catch(function () { notyf('网络错误,请重试', 'danger'); }); } function sendUrge(ticket) { request('/tickets/' + ticket.id + '/urge', { method: 'PUT' }).then(function (result) { if (result && result.success) { notyf('已催单,请耐心等待', 'success'); } else { notyf((result && result.message) || '催单失败', 'danger'); } }).catch(function () { notyf('网络错误,请重试', 'danger'); }); } function sendClose(ticket) { if (!window.confirm('确认关闭该工单?关闭后无法继续回复。')) return; request('/tickets/' + ticket.id + '/close', { method: 'PUT' }).then(function (result) { if (result && result.success) { closeModal('submit'); notyf('工单已关闭', 'success'); loadList(); loadStatistic(); } else { notyf((result && result.message) || '关闭失败', 'danger'); } }).catch(function () { notyf('网络错误,请重试', 'danger'); }); } /* ------------------------------------------------------------------ * * 新建工单弹窗 * ------------------------------------------------------------------ */ function openCreate() { loadMeta().then(function (meta) { var shell = openModal({ html: modalShell('提交工单', buildCreateHtml(meta)), className: 'aksrt-ticket-create-modal', kind: 'ticket-create', mobileBottom: true, }); if (shell) bindCreate(shell); }); } function buildCreateHtml(meta) { var types = meta.types || []; var orders = meta.orders || []; var attachmentEnabled = meta.attachment && meta.attachment.enabled; var html = '
'; html += ''; html += ''; if (orders.length) { html += ''; } html += ''; if (attachmentEnabled) { html += '
图片(最多 ' + state.attachmentMax + ' 张)' + '
' + '
'; } html += ''; return html + '
'; } function bindCreate(shell) { var form = shell.querySelector('[data-aksrt-ticket-create-form]'); if (!form) return; var fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/gif,image/jpeg,image/jpg,image/png,image/webp'; fileInput.hidden = true; shell.appendChild(fileInput); shell.addEventListener('click', function (event) { if (event.target.closest('[data-aksrt-ticket-upload]')) fileInput.click(); }); fileInput.addEventListener('change', function () { var file = fileInput.files && fileInput.files[0]; if (!file) return; uploadAttachment(file, shell, function () { fileInput.value = ''; }); }); form.addEventListener('submit', function (event) { event.preventDefault(); var title = (form.querySelector('[name="title"]').value || '').trim(); var typeId = form.querySelector('[name="ticket_type_id"]').value; var content = (form.querySelector('[name="content"]').value || '').trim(); var orderField = form.querySelector('[name="order_number"]'); if (!title) { notyf('请填写工单标题', 'danger'); return; } if (!typeId) { notyf('请选择工单部门', 'danger'); return; } if (!content) { notyf('请填写问题描述', 'danger'); return; } runCaptcha('ticket_create').then(function () { request('/tickets', { method: 'POST', body: { title: title, ticket_type_id: parseInt(typeId, 10), content: content, order_number: orderField ? orderField.value : '', attachment: collectAttachments(shell), }, }).then(function (result) { if (result && result.success) { closeModal('submit'); notyf('工单已提交', 'success'); state.page = 1; loadList(); loadStatistic(); } else { notyf((result && result.message) || '工单提交失败', 'danger'); } }).catch(function () { notyf('网络错误,请重试', 'danger'); }); }).catch(function (error) { if (error && error.cancelled) return; notyf('人机验证未通过', 'danger'); }); }); } function uploadAttachment(file, shell, done) { if (collectAttachments(shell).length >= state.attachmentMax) { notyf('最多上传 ' + state.attachmentMax + ' 张图片', 'danger'); if (done) done(); return; } var body = new FormData(); body.append('image', file); notyf('正在上传图片…', 'load', 3000, 'aksrt-ticket-upload'); fetch(API_ROOT + '/ticket/upload-attachment', { method: 'POST', headers: { 'X-WP-Nonce': NONCE }, body: body, credentials: 'same-origin', }).then(function (res) { return res.json(); }) .then(function (result) { var url = result && (result.url || (result.data && result.data.url)); if (url) { appendImage(shell, url); } else { notyf((result && result.message) || '图片上传失败', 'danger'); } }) .catch(function () { notyf('图片上传失败', 'danger'); }) .then(function () { var toast = document.getElementById('aksrt-ticket-upload'); if (toast) toast.remove(); if (done) done(); }); } function appendImage(shell, url) { var holder = shell.querySelector('[data-aksrt-ticket-images]'); if (!holder) return; var item = document.createElement('div'); item.className = 'aksrt-ticket-image'; item.setAttribute('data-aksrt-ticket-image', url); item.innerHTML = '' + ''; item.querySelector('.aksrt-ticket-image__remove').addEventListener('click', function () { item.remove(); }); holder.appendChild(item); } /* ------------------------------------------------------------------ * * 面板初始化 * ------------------------------------------------------------------ */ function initPanel() { var panel = document.querySelector('[data-aksrt-ticket-center]'); if (!panel || panel.dataset.aksrtTicketReady === '1') return; panel.dataset.aksrtTicketReady = '1'; var configNode = panel.querySelector('[data-aksrt-ticket-config]'); if (configNode) { try { var config = JSON.parse(configNode.getAttribute('data-aksrt-ticket-config')); if (config && config.attachmentMax) state.attachmentMax = config.attachmentMax; } catch { /* invalid config attribute */ } } panel.addEventListener('click', function (event) { var view = event.target.closest('[data-aksrt-ticket-view]'); if (view) { openDetail(view.getAttribute('data-aksrt-ticket-view')); return; } var page = event.target.closest('[data-aksrt-ticket-page]'); if (page) { state.page = parseInt(page.getAttribute('data-aksrt-ticket-page'), 10) || 1; loadList(); return; } if (event.target.closest('[data-aksrt-ticket-create]')) { openCreate(); } }); var search = panel.querySelector('[data-aksrt-tickets-search]'); if (search) { search.addEventListener('submit', function (event) { event.preventDefault(); var input = search.querySelector('input'); state.keywords = (input && input.value || '').trim(); state.page = 1; loadList(); }); } panel.querySelectorAll('[data-aksrt-ticket-filter]').forEach(function (select) { select.addEventListener('change', function () { var key = select.getAttribute('data-aksrt-ticket-filter'); if (key === 'status') state.status = select.value; if (key === 'ticket_type_id') state.type = select.value; state.page = 1; loadList(); }); }); loadStatistic(); loadList(); } function boot() { initPanel(); // 面板是 AJAX 延迟加载的:user-center.js 每次切 Tab 都会触发 auto_fun。 if (window.jQuery) { window.jQuery(document).off('auto_fun.aksrt-ticket').on('auto_fun.aksrt-ticket', initPanel); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();