/**
* 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 + '
'
+ '' + (footer || '') + '
';
}
/* ------------------------------------------------------------------ *
* 列表 / 统计
* ------------------------------------------------------------------ */
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 += '';
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 = '';
}
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();
}
})();