Files
wordpress-theme-aksrt/assets/js/ticket-center.js
T
kate522andClaude Fable 5 3bd15feccb feat(ticket): redesign user-center ticket center UI/UX
- Status stat cards double as quick filters; debounced search with stale-response guard
- Card list with status stripe, windowed pagination, skeleton/empty/error states
- Detail dialog as chat timeline with in-place refresh, quote preview, Ctrl+Enter send
- Create form with department chips, inline validation, multi-image upload, success notice
- Split shared helpers into ticket-center-core.js and enqueue it before the panel script
- Drop dead data-aksrt-ticket-config and legacy colorful modal header

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-26 23:06:34 +08:00

742 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AKSRT Ticket Center — 用户中心「工单中心」面板。
*
* - 列表 / 统计 / 详情全部走 REST,模板只输出容器与首屏骨架(profile-tickets.php)
* - 统计卡兼作状态快捷筛选,与状态下拉双向同步;搜索输入防抖,旧请求结果按序号丢弃
* - 弹窗经 AksrtModalRuntime.open 打开(运行时自带标题栏 / 焦点陷阱 / 移动端底部抽屉),
* 先出运行时骨架,数据到达后填充,避免点击后无反馈
* - 详情为对话时间线:问题描述作为首条消息,用户靠右、客服靠左;回复后原地刷新而不是关窗
* - 提交工单与回复前先过人机验证(ticket_create / ticket_reply,场景未启用自动放行)
* - 图片走 /aksrt/v1/ticket/upload-attachment,上传中显示占位格,未传完禁止提交
*
* @package AKSRT
*/
(function () {
'use strict';
var core = window.AksrtTicketCore;
if (!core) return;
var settings = core.settings;
var attachmentEnabled = core.attachmentEnabled;
var attachmentMax = core.attachmentMax;
var escapeHtml = core.escapeHtml;
var notyf = core.notyf;
var request = core.request;
var expectSuccess = core.expectSuccess;
var runCaptcha = core.runCaptcha;
var failMessage = core.failMessage;
var timeTag = core.timeTag;
var icon = core.icon;
var statusBadge = core.statusBadge;
var emptyHtml = core.emptyHtml;
var setBusy = core.setBusy;
var markInvalid = core.markInvalid;
var openModal = core.openModal;
var alive = core.alive;
var setModalContent = core.setModalContent;
var modalError = core.modalError;
var collectAttachments = core.collectAttachments;
var isUploading = core.isUploading;
var bindShell = core.bindShell;
var uploaderHtml = core.uploaderHtml;
var SEARCH_DEBOUNCE = 400;
var CLOSE_CONFIRM_MS = 4000;
var state = {
page: 1,
perPage: 10,
totalPages: 1,
status: '',
type: '',
keywords: '',
meta: null,
listSeq: 0,
};
/* ------------------------------------------------------------------ *
* 列表 / 统计 / 筛选
* ------------------------------------------------------------------ */
function panel() {
return document.querySelector('[data-aksrt-ticket-center]');
}
function listNode() {
var root = panel();
return root ? root.querySelector('[data-aksrt-ticket-list]') : null;
}
function hasFilters() {
return !!(state.status || state.type || state.keywords);
}
function skeletonHtml() {
var card = '<div class="aksrt-tickets-page__card is-placeholder">'
+ '<div class="placeholder s1"></div><div class="placeholder k1"></div>'
+ '<div class="placeholder k3" style="width: 42%;"></div></div>';
return '<div class="post_ajax_loader is-loading" aria-hidden="true">' + card + card + card + '</div>';
}
function loadList() {
var list = listNode();
if (!list) return;
var seq = ++state.listSeq;
// 已有卡片时只做淡化,避免整块替换造成跳动;空态 / 错误态则换回骨架。
if (!list.querySelector('[data-aksrt-ticket-id]')) list.innerHTML = skeletonHtml();
list.classList.add('is-busy');
list.setAttribute('aria-busy', 'true');
var params = ['page=' + state.page, 'per_page=' + state.perPage];
if (state.status) params.push('status=' + encodeURIComponent(state.status));
if (state.type) params.push('ticket_type_id=' + encodeURIComponent(state.type));
if (state.keywords) params.push('keywords=' + encodeURIComponent(state.keywords));
request('/tickets?' + params.join('&')).then(expectSuccess).then(function (result) {
if (seq !== state.listSeq) return;
var data = result.data || {};
state.totalPages = Math.max(1, parseInt(data.total_pages, 10) || 1);
if (state.page > state.totalPages) {
state.page = state.totalPages;
loadList();
return;
}
renderList(Array.isArray(data.items) ? data.items : []);
renderPagination();
}).catch(function (error) {
if (seq !== state.listSeq) return;
list.innerHTML = emptyHtml('fa-exclamation-circle', '工单列表加载失败', failMessage(error, '网络异常,请稍后重试。'),
'<button type="button" class="btn-outline" data-aksrt-ticket-retry>重新加载</button>');
renderPagination(true);
}).then(function () {
if (seq !== state.listSeq) return;
list.classList.remove('is-busy');
list.setAttribute('aria-busy', 'false');
});
}
function renderList(items) {
var list = listNode();
if (!list) return;
if (!items.length) {
list.innerHTML = hasFilters()
? emptyHtml('fa-search', '没有符合条件的工单', '换个关键词,或清除筛选后再试。',
'<button type="button" class="btn-outline" data-aksrt-ticket-reset>清除筛选</button>')
: emptyHtml('fa-headset', '还没有工单', '遇到问题时提交工单,我们会尽快处理。',
'<button type="button" class="btn-primary" data-aksrt-ticket-create>' + icon('fa-plus') + '提交第一个工单</button>');
return;
}
list.innerHTML = items.map(cardHtml).join('');
}
function cardHtml(ticket) {
var id = escapeHtml(ticket.id);
var meta = [];
if (ticket.ticket_type_name) meta.push('<span>' + icon('fa-layer-group') + escapeHtml(ticket.ticket_type_name) + '</span>');
if (ticket.order_number) meta.push('<span>' + icon('fa-receipt') + '订单 ' + escapeHtml(ticket.order_number) + '</span>');
meta.push('<span>' + icon('fa-clock') + '更新于 ' + timeTag(ticket.updated_at || ticket.created_at) + '</span>');
// 标题按钮的 ::after 铺满整卡,整张卡可点且只有一个可聚焦目标。
return '<article class="aksrt-tickets-page__card' + (ticket.is_closed ? ' is-closed' : '') + '"'
+ ' data-aksrt-ticket-id="' + id + '" style="--aksrt-ticket-color:' + escapeHtml(ticket.status_color || '#6b7280') + '">'
+ '<div class="aksrt-tickets-page__card-head">' + statusBadge(ticket)
+ '<span class="aksrt-tickets-page__card-num">#' + escapeHtml(ticket.ticket_num) + '</span></div>'
+ '<h3 class="aksrt-tickets-page__card-title"><button type="button" data-aksrt-ticket-view="' + id + '">'
+ escapeHtml(ticket.title) + '</button></h3>'
+ '<div class="aksrt-tickets-page__card-meta">' + meta.join('') + '</div>'
+ '<span class="aksrt-tickets-page__card-arrow" aria-hidden="true">' + icon('fa-chevron-right') + '</span>'
+ '</article>';
}
/** 1 … 4 5 6 … 20 窗口分页。 */
function pageList(current, total) {
var out = [1];
var from = Math.max(2, current - 1);
var to = Math.min(total - 1, current + 1);
if (from > 2) out.push('gap');
for (var i = from; i <= to; i++) out.push(i);
if (to < total - 1) out.push('gap');
if (total > 1) out.push(total);
return out;
}
function stepButton(page, label, iconName, disabled) {
return '<button type="button" class="aksrt-tickets-page__page-chip is-step" data-aksrt-ticket-page="' + page + '"'
+ ' aria-label="' + label + '"' + (disabled ? ' disabled' : '') + '>' + icon(iconName) + '</button>';
}
function renderPagination(hide) {
var root = panel();
var nav = root && root.querySelector('[data-aksrt-ticket-pagination]');
if (!nav) return;
if (hide || state.totalPages <= 1) {
nav.hidden = true;
nav.innerHTML = '';
return;
}
var html = stepButton(state.page - 1, '上一页', 'fa-chevron-left', state.page <= 1);
pageList(state.page, state.totalPages).forEach(function (page) {
if (page === 'gap') {
html += '<span class="aksrt-tickets-page__page-gap" aria-hidden="true">…</span>';
return;
}
var active = page === state.page;
html += '<button type="button" class="aksrt-tickets-page__page-chip' + (active ? ' is-active' : '') + '"'
+ ' data-aksrt-ticket-page="' + page + '"' + (active ? ' aria-current="page"' : '') + '>' + page + '</button>';
});
html += stepButton(state.page + 1, '下一页', 'fa-chevron-right', state.page >= state.totalPages);
nav.innerHTML = html;
nav.hidden = false;
}
function loadStatistic() {
request('/tickets/statistic').then(expectSuccess).then(function (result) {
var root = panel();
var data = result.data || {};
if (!root) return;
var total = 0;
Object.keys(data).forEach(function (key) {
var count = parseInt(data[key], 10) || 0;
total += count;
var node = root.querySelector('[data-aksrt-ticket-stat="' + key + '"] strong');
if (node) node.textContent = String(count);
});
var all = root.querySelector('[data-aksrt-ticket-stat=""] strong');
if (all) all.textContent = String(total);
}).catch(function () { /* 统计失败不阻塞列表 */ });
}
function syncFilters() {
var root = panel();
if (!root) return;
root.querySelectorAll('[data-aksrt-ticket-stat]').forEach(function (button) {
var active = button.getAttribute('data-aksrt-ticket-stat') === state.status;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
var select = root.querySelector('[data-aksrt-ticket-filter="status"]');
if (select) select.value = state.status;
var reset = root.querySelector('[data-aksrt-tickets-search] [data-aksrt-ticket-reset]');
if (reset) reset.hidden = !hasFilters();
}
function applyFilters(patch) {
Object.assign(state, patch, { page: 1 });
syncFilters();
loadList();
}
function resetFilters() {
var root = panel();
if (root) {
var input = root.querySelector('[data-aksrt-tickets-search] input');
if (input) input.value = '';
var type = root.querySelector('[data-aksrt-ticket-filter="ticket_type_id"]');
if (type) type.value = '';
}
applyFilters({ status: '', type: '', keywords: '' });
}
function refreshPanel() {
loadList();
loadStatistic();
}
function scrollPanelIntoView() {
var root = panel();
if (root && root.getBoundingClientRect().top < 0) root.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/* ------------------------------------------------------------------ *
* 详情弹窗(对话时间线 + 回复框)
* ------------------------------------------------------------------ */
function openDetail(ticketId) {
var shell = openModal({ title: '工单详情', className: 'aksrt-ticket-modal aksrt-ticket-detail-modal', kind: 'ticket-detail' });
if (shell) fetchDetail(shell, ticketId, { initial: true });
}
function fetchDetail(shell, ticketId, opts) {
return request('/tickets/' + ticketId).then(expectSuccess).then(function (result) {
if (!alive(shell)) return;
if (!result.data) return modalError(shell, '工单不存在');
renderDetail(shell, result.data, opts || {});
}).catch(function (error) {
if (alive(shell)) modalError(shell, failMessage(error, '工单详情加载失败,请重试'));
});
}
function renderDetail(shell, ticket, opts) {
var ctx = shell.__aksrtTicket || (shell.__aksrtTicket = bindShell(shell, onDetailClick));
ctx.ticket = ticket;
ctx.quoteId = 0;
var title = shell.querySelector('.aksrt-modal-runtime__title');
if (title) title.textContent = '工单 #' + ticket.ticket_num;
setModalContent(shell, buildDetailHtml(ticket));
// 首次打开且沟通较多、或刚发完回复时,滚到最新一条。
var replies = ticket.replies || [];
if (opts.scrollEnd || (opts.initial && replies.length > 3)) {
var last = shell.querySelector('[data-aksrt-ticket-thread] > li:last-child');
if (last) last.scrollIntoView({ block: 'end' });
}
var textarea = shell.querySelector('[data-aksrt-ticket-reply-form] textarea');
if (opts.scrollEnd && textarea) textarea.focus();
if (textarea) {
textarea.addEventListener('keydown', function (event) {
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
submitReply(shell, ctx);
}
});
}
var form = shell.querySelector('[data-aksrt-ticket-reply-form]');
if (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
submitReply(shell, ctx);
});
}
}
function fact(label, value) {
return '<div><dt>' + label + '</dt><dd>' + value + '</dd></div>';
}
function buildDetailHtml(ticket) {
var facts = fact('提交时间', timeTag(ticket.created_at))
+ (ticket.ticket_type_name ? fact('工单部门', escapeHtml(ticket.ticket_type_name)) : '')
+ (ticket.order_number ? fact('关联订单', escapeHtml(ticket.order_number)) : '')
+ (ticket.admin_name ? fact('处理客服', escapeHtml(ticket.admin_name)) : '');
var html = '<div class="aksrt-ticket-detail" data-aksrt-ticket-detail-id="' + escapeHtml(ticket.id) + '">'
+ '<section class="aksrt-ticket-detail__summary">'
+ '<div class="aksrt-ticket-detail__head">' + statusBadge(ticket) + '</div>'
+ '<h4 class="aksrt-ticket-detail__title">' + escapeHtml(ticket.title) + '</h4>'
+ '<dl class="aksrt-ticket-detail__facts">' + facts + '</dl></section>';
html += '<ol class="aksrt-ticket-thread" data-aksrt-ticket-thread>';
html += messageHtml({
id: 0,
author_type: 'user',
author_name: ticket.user_name,
content: ticket.content,
attachment: ticket.attachment,
created_at: ticket.created_at,
isOrigin: true,
}, false);
(ticket.replies || []).forEach(function (reply) {
html += messageHtml(reply, !ticket.is_closed);
});
html += '</ol>';
html += ticket.is_closed
? '<p class="aksrt-ticket-detail__closed">' + icon('fa-lock') + '工单已关闭,如需继续沟通请提交新工单。</p>'
: composerHtml(ticket);
return html + '</div>';
}
function attachmentPreview(images) {
if (!images || !images.length) return '';
return '<div class="aksrt-ticket-attachments">' + images.map(function (url) {
return '<a class="aksrt-ticket-attachment" href="' + escapeHtml(url) + '" target="_blank" rel="noopener">'
+ '<img src="' + escapeHtml(url) + '" alt="附件图片" loading="lazy"></a>';
}).join('') + '</div>';
}
function quoteBlock(quote) {
if (!quote) return '';
var text = quote.is_deleted ? '原回复已删除' : (quote.content || '');
return '<blockquote class="aksrt-ticket-quote' + (quote.is_deleted ? ' is-deleted' : '') + '">'
+ (quote.author_name ? '<strong>' + escapeHtml(quote.author_name) + '</strong>' : '')
+ '<span>' + escapeHtml(text) + '</span></blockquote>';
}
function excerpt(text) {
var flat = String(text || '').replace(/\s+/g, ' ').trim();
return flat.length > 60 ? flat.slice(0, 60) + '…' : flat;
}
function messageHtml(msg, canQuote) {
var admin = msg.author_type === 'admin';
var name = msg.author_name || (admin ? '客服' : '我');
var avatar = admin ? icon('fa-headset') : escapeHtml(Array.from(name)[0] || '我');
var tags = (admin ? '<span class="aksrt-ticket-msg__tag">客服</span>' : '')
+ (msg.isOrigin ? '<span class="aksrt-ticket-msg__tag">问题描述</span>' : '');
return '<li class="aksrt-ticket-msg is-' + (admin ? 'admin' : 'user') + (msg.isOrigin ? ' is-origin' : '') + '">'
+ '<span class="aksrt-ticket-msg__avatar" aria-hidden="true">' + avatar + '</span>'
+ '<div class="aksrt-ticket-msg__main">'
+ '<div class="aksrt-ticket-msg__head"><strong>' + escapeHtml(name) + '</strong>' + tags + timeTag(msg.created_at) + '</div>'
+ '<div class="aksrt-ticket-msg__bubble">' + quoteBlock(msg.quote)
+ '<div class="aksrt-ticket-msg__body">' + escapeHtml(msg.content) + '</div>'
+ attachmentPreview(msg.attachment) + '</div>'
+ (canQuote && msg.id
? '<button type="button" class="aksrt-ticket-msg__quote" data-aksrt-ticket-quote="' + escapeHtml(msg.id) + '"'
+ ' data-name="' + escapeHtml(name) + '" data-excerpt="' + escapeHtml(excerpt(msg.content)) + '">'
+ icon('fa-reply') + '引用</button>'
: '')
+ '</div></li>';
}
function composerHtml(ticket) {
var fieldId = 'aksrt-ticket-reply-' + escapeHtml(ticket.id);
return '<form class="aksrt-ticket-composer" data-aksrt-ticket-reply-form novalidate>'
+ '<div class="aksrt-ticket-composer__quote" data-aksrt-ticket-quote-preview hidden></div>'
+ '<div class="aksrt-ticket-field">'
+ '<label class="screen-reader-text" for="' + fieldId + '">回复内容</label>'
+ '<textarea id="' + fieldId + '" name="reply" rows="3" maxlength="3000"'
+ ' placeholder="补充说明或回复客服,Ctrl + Enter 发送"></textarea></div>'
+ uploaderHtml()
+ '<div class="aksrt-ticket-composer__bar">'
+ '<div class="aksrt-ticket-composer__tools">'
+ '<button type="button" class="aksrt-ticket-tool" data-aksrt-ticket-urge title="提醒客服尽快处理">' + icon('fa-bell') + '<span>催单</span></button>'
+ '<button type="button" class="aksrt-ticket-tool is-danger" data-aksrt-ticket-close>' + icon('fa-check-circle') + '<span>关闭工单</span></button>'
+ '</div>'
+ '<span class="aksrt-ticket-counter" data-aksrt-ticket-counter="reply">0 / 3000</span>'
+ '<button type="submit" class="btn-primary" data-aksrt-ticket-submit>' + icon('fa-paper-plane') + '发送</button>'
+ '</div></form>';
}
function setQuote(shell, ctx, button) {
var preview = shell.querySelector('[data-aksrt-ticket-quote-preview]');
if (!preview) return;
ctx.quoteId = button ? parseInt(button.getAttribute('data-aksrt-ticket-quote'), 10) || 0 : 0;
if (!ctx.quoteId) {
preview.hidden = true;
preview.innerHTML = '';
return;
}
preview.innerHTML = icon('fa-reply')
+ '<span>回复 <strong>' + escapeHtml(button.getAttribute('data-name')) + '</strong>:'
+ escapeHtml(button.getAttribute('data-excerpt')) + '</span>'
+ '<button type="button" data-aksrt-ticket-quote-cancel aria-label="取消引用">' + icon('fa-times') + '</button>';
preview.hidden = false;
var textarea = shell.querySelector('[data-aksrt-ticket-reply-form] textarea');
if (textarea) textarea.focus();
}
function onDetailClick(event, ctx) {
var shell = event.currentTarget;
var quote = event.target.closest('[data-aksrt-ticket-quote]');
if (quote) return setQuote(shell, ctx, quote);
if (event.target.closest('[data-aksrt-ticket-quote-cancel]')) return setQuote(shell, ctx, null);
var urge = event.target.closest('[data-aksrt-ticket-urge]');
if (urge) return handleUrge(urge, ctx);
var close = event.target.closest('[data-aksrt-ticket-close]');
if (close) handleClose(close, shell, ctx);
}
function submitReply(shell, ctx) {
var form = shell.querySelector('[data-aksrt-ticket-reply-form]');
var button = form && form.querySelector('[data-aksrt-ticket-submit]');
if (!form || !button || button.disabled) return;
var textarea = form.querySelector('textarea');
var content = (textarea.value || '').trim();
if (!content) return markInvalid(textarea, '请填写回复内容');
if (isUploading(form)) return notyf('图片仍在上传,请稍候', 'danger');
setBusy(button, true, '发送中…');
runCaptcha('ticket_reply').then(function () {
return sendReply(ctx.ticket, content, ctx.quoteId, collectAttachments(form));
}).then(function () {
notyf('回复已发送', 'success');
refreshPanel();
return fetchDetail(shell, ctx.ticket.id, { scrollEnd: true });
}).catch(function (error) {
if (error && error.cancelled) return;
notyf(failMessage(error, '回复提交失败'), 'danger');
}).then(function () { setBusy(button, false); });
}
function sendReply(ticket, content, quoteReplyId, attachment) {
return request('/tickets/' + ticket.id + '/reply', {
method: 'POST',
body: { content: content, quote_reply_id: quoteReplyId || 0, attachment: attachment },
}).then(expectSuccess);
}
function handleUrge(button, ctx) {
if (button.disabled) return;
setBusy(button, true);
sendUrge(ctx.ticket).then(function () {
notyf('已提醒客服,请耐心等待', 'success');
button.classList.remove('is-busy');
button.innerHTML = icon('fa-check') + '<span>已催单</span>';
delete button.dataset.label;
}).catch(function (error) {
setBusy(button, false);
notyf(failMessage(error, '催单失败'), 'danger');
});
}
function sendUrge(ticket) {
return request('/tickets/' + ticket.id + '/urge', { method: 'PUT' }).then(expectSuccess);
}
// 两步确认:首次点击变为「再次点击确认」,超时自动复原,替代阻塞式 window.confirm。
function handleClose(button, shell, ctx) {
if (button.disabled) return;
var label = button.querySelector('span');
if (!button.classList.contains('is-confirming')) {
button.classList.add('is-confirming');
if (label) label.textContent = '再次点击确认关闭';
ctx.closeTimer = window.setTimeout(function () {
button.classList.remove('is-confirming');
if (label) label.textContent = '关闭工单';
}, CLOSE_CONFIRM_MS);
return;
}
window.clearTimeout(ctx.closeTimer);
button.classList.remove('is-confirming');
setBusy(button, true, '关闭中…');
sendClose(ctx.ticket).then(function () {
notyf('工单已关闭', 'success');
refreshPanel();
return fetchDetail(shell, ctx.ticket.id);
}).catch(function (error) {
setBusy(button, false);
notyf(failMessage(error, '关闭失败'), 'danger');
});
}
function sendClose(ticket) {
return request('/tickets/' + ticket.id + '/close', { method: 'PUT' }).then(expectSuccess);
}
/* ------------------------------------------------------------------ *
* 新建工单弹窗
* ------------------------------------------------------------------ */
function loadMeta() {
if (state.meta) return Promise.resolve(state.meta);
return request('/tickets/meta').then(expectSuccess).then(function (result) {
state.meta = result.data || {};
return state.meta;
});
}
function openCreate() {
var shell = openModal({ title: '提交工单', className: 'aksrt-ticket-modal aksrt-ticket-create-modal', kind: 'ticket-create' });
if (!shell) return;
loadMeta().then(function (meta) {
if (!alive(shell)) return;
if (!(meta.types || []).length) {
return modalError(shell, '暂无可提交的工单部门,请稍后再试。');
}
setModalContent(shell, buildCreateHtml(meta));
bindCreate(shell);
var title = shell.querySelector('[name="title"]');
if (title) title.focus();
}).catch(function (error) {
if (alive(shell)) modalError(shell, failMessage(error, '表单加载失败,请重试'));
});
}
function field(label, control, extra) {
return '<div class="aksrt-ticket-field">' + label + control + (extra || '') + '</div>';
}
function buildCreateHtml(meta) {
var types = meta.types || [];
var orders = meta.orders || [];
var html = '<form class="aksrt-ticket-form" data-aksrt-ticket-create-form novalidate>';
html += field(
'<label for="aksrt-ticket-f-title">工单标题<em aria-hidden="true">*</em></label>',
'<input id="aksrt-ticket-f-title" type="text" name="title" maxlength="200" autocomplete="off" placeholder="一句话概括问题,如:支付成功但会员未开通">',
'<span class="aksrt-ticket-counter" data-aksrt-ticket-counter="title">0 / 200</span>'
);
html += '<fieldset class="aksrt-ticket-field aksrt-ticket-types" data-aksrt-ticket-types>'
+ '<legend>工单部门<em aria-hidden="true">*</em></legend><div class="aksrt-ticket-types__list">'
+ types.map(function (type) {
return '<label class="aksrt-ticket-type"><input type="radio" name="ticket_type_id" value="' + escapeHtml(type.id) + '">'
+ '<span>' + escapeHtml(type.name) + '</span></label>';
}).join('')
+ '</div></fieldset>';
if (orders.length) {
html += field(
'<label for="aksrt-ticket-f-order">关联订单<span class="aksrt-ticket-field__optional">可选</span></label>',
'<select id="aksrt-ticket-f-order" name="order_number"><option value="">不关联订单</option>'
+ orders.map(function (order) {
return '<option value="' + escapeHtml(order.order_number) + '">'
+ escapeHtml(order.order_number + (order.product_name ? ' · ' + order.product_name : '')) + '</option>';
}).join('') + '</select>'
);
}
html += field(
'<label for="aksrt-ticket-f-content">问题描述<em aria-hidden="true">*</em></label>',
'<textarea id="aksrt-ticket-f-content" name="content" rows="5" maxlength="5000" placeholder="请描述问题现象、发生时间、操作步骤等,信息越完整处理越快"></textarea>',
'<span class="aksrt-ticket-counter" data-aksrt-ticket-counter="content">0 / 5000</span>'
);
if (attachmentEnabled()) {
html += '<div class="aksrt-ticket-field"><span class="aksrt-ticket-field__label">图片附件<span class="aksrt-ticket-field__optional">可选</span></span>'
+ uploaderHtml()
+ '<p class="aksrt-ticket-field__hint">支持 JPG / PNG / GIF / WebP,最多 ' + attachmentMax() + ' 张</p></div>';
}
html += '<div class="aksrt-ticket-form__actions">'
+ '<button type="button" class="btn-outline" data-aksrt-modal-close>取消</button>'
+ '<button type="submit" class="btn-primary" data-aksrt-ticket-submit>提交工单</button></div>';
return html + '</form>';
}
function bindCreate(shell) {
var form = shell.querySelector('[data-aksrt-ticket-create-form]');
if (!form) return;
bindShell(shell, function (event) {
var view = event.target.closest('[data-aksrt-ticket-view]');
if (view) openDetail(view.getAttribute('data-aksrt-ticket-view'));
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitCreate(shell, form);
});
}
function submitCreate(shell, form) {
var button = form.querySelector('[data-aksrt-ticket-submit]');
if (!button || button.disabled) return;
var titleField = form.querySelector('[name="title"]');
var contentField = form.querySelector('[name="content"]');
var typeField = form.querySelector('[name="ticket_type_id"]:checked');
var orderField = form.querySelector('[name="order_number"]');
var title = (titleField.value || '').trim();
var content = (contentField.value || '').trim();
if (!title) return markInvalid(titleField, '请填写工单标题');
if (!typeField) return markInvalid(form.querySelector('[data-aksrt-ticket-types]'), '请选择工单部门');
if (!content) return markInvalid(contentField, '请填写问题描述');
if (isUploading(form)) return notyf('图片仍在上传,请稍候', 'danger');
setBusy(button, true, '提交中…');
runCaptcha('ticket_create').then(function () {
return request('/tickets', {
method: 'POST',
body: {
title: title,
ticket_type_id: parseInt(typeField.value, 10),
content: content,
order_number: orderField ? orderField.value : '',
attachment: collectAttachments(form),
},
}).then(expectSuccess);
}).then(function (result) {
state.page = 1;
refreshPanel();
if (alive(shell)) renderCreated(shell, result.data && result.data.id);
}).catch(function (error) {
setBusy(button, false);
if (error && error.cancelled) return;
notyf(failMessage(error, '工单提交失败'), 'danger');
});
}
// 成功页:承接后台「提交后显示平台公告」(notice_text 已经 wp_kses_post 过滤),并给出下一步。
function renderCreated(shell, id) {
var notice = (state.meta && state.meta.notice) || settings().notice || {};
var html = '<div class="aksrt-ticket-done" role="status">'
+ '<span class="aksrt-ticket-done__icon">' + icon('fa-check') + '</span>'
+ '<h4>工单已提交</h4>'
+ '<p>我们会尽快处理,处理进度可随时在工单中心查看。</p>'
+ (notice.open && notice.text ? '<div class="aksrt-ticket-done__notice">' + notice.text + '</div>' : '')
+ '<div class="aksrt-ticket-form__actions">'
+ '<button type="button" class="btn-outline" data-aksrt-modal-close>完成</button>'
+ (id ? '<button type="button" class="btn-primary" data-aksrt-ticket-view="' + escapeHtml(id) + '">查看工单</button>' : '')
+ '</div></div>';
setModalContent(shell, html);
if (!id) return;
var view = shell.querySelector('[data-aksrt-ticket-view]');
if (view) view.focus();
}
/* ------------------------------------------------------------------ *
* 面板初始化
* ------------------------------------------------------------------ */
function initPanel() {
var root = panel();
if (!root || root.dataset.aksrtTicketReady === '1') return;
root.dataset.aksrtTicketReady = '1';
root.addEventListener('click', function (event) {
var target = event.target;
var view = target.closest('[data-aksrt-ticket-view]');
if (view) return openDetail(view.getAttribute('data-aksrt-ticket-view'));
var stat = target.closest('[data-aksrt-ticket-stat]');
if (stat) {
var key = stat.getAttribute('data-aksrt-ticket-stat') || '';
return applyFilters({ status: key === state.status ? '' : key });
}
var page = target.closest('[data-aksrt-ticket-page]');
if (page) {
var next = parseInt(page.getAttribute('data-aksrt-ticket-page'), 10) || 1;
if (page.disabled || next < 1 || next > state.totalPages || next === state.page) return;
state.page = next;
loadList();
return scrollPanelIntoView();
}
if (target.closest('[data-aksrt-ticket-reset]')) return resetFilters();
if (target.closest('[data-aksrt-ticket-retry]')) return refreshPanel();
if (target.closest('[data-aksrt-ticket-create]')) openCreate();
});
var search = root.querySelector('[data-aksrt-tickets-search]');
if (search) {
var input = search.querySelector('input');
var timer = 0;
var commit = function () {
window.clearTimeout(timer);
var keywords = (input.value || '').trim();
if (keywords !== state.keywords) applyFilters({ keywords: keywords });
};
search.addEventListener('submit', function (event) {
event.preventDefault();
commit();
});
input.addEventListener('input', function (event) {
if (event.isComposing) return;
window.clearTimeout(timer);
timer = window.setTimeout(commit, SEARCH_DEBOUNCE);
});
input.addEventListener('compositionend', function () {
window.clearTimeout(timer);
timer = window.setTimeout(commit, SEARCH_DEBOUNCE);
});
}
root.querySelectorAll('[data-aksrt-ticket-filter]').forEach(function (select) {
select.addEventListener('change', function () {
var key = select.getAttribute('data-aksrt-ticket-filter');
applyFilters(key === 'status' ? { status: select.value } : { type: select.value });
});
});
refreshPanel();
}
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();
}
})();