The old 售后 flow was an order-refund appendage: tickets were hard-bound to an order number, users could only file them from 「我的订单」against a paid order, and the admin action bottomed out at 「标记订单已退款」. It could not carry account questions or feature requests that have nothing to do with an order. Replace it with a general ticket system modelled on 魔方财务云's idcsmart_ticket: title + department + description are required, linking an order is optional (user's own paid orders only). Add departments, custom statuses, preset replies and internal admin notes as four configuration surfaces. Conversation history moves off the ticket row's JSON log into dedicated ticket_reply rows, which unlocks quoted replies. Front end gets a 工单中心 panel under 我的服务; back end moves from 支付中心 → 售后服务 to users.php → 工单管理. Per CLAUDE.md's rewrite-over-compatibility rule: drop the old table, build new ones, delete the old code. No dual paths, no data migration. The upstream plugin's upstream/downstream delivery and internal-ticket conversion are deliberately left out — both are multi-provider platform concerns with no meaning on a single WordPress site. Data: order_aftersale becomes six aksrt_ticket* tables (ticket, ticket_reply, ticket_status, ticket_type, ticket_prereply, ticket_note). SCHEMA_VERSION 2.6.0 -> 3.0.0. Status and department seeds are idempotent; the default status 「待接单」is flagged as non-editable. Domain: new src/Ticket/ (13 files, all under 460 lines). Attachments reuse UserController::rest_upload_comment_image with a ticket-specific permission callback, mirroring the existing /report/upload-image route rather than growing a second upload path. Notifications go through AKSRT\Messages\Message::send() and deep-link to the right surface; email/SMS stay out of scope since the theme's mail and SMS stacks are separate implementations. Refund gate opens up: marking an order refunded no longer requires an in-flight ticket, only that the order is paid. Also generalise a hardcoded special case in tools/audit-admin-contracts.php — schema domain inference now reads the parent directory instead of matching "PostList" by name, so new leaf schema files need no per-name exception. Verification: php tools/audit-admin-contracts.php exit 0, errors=0 node --test (widget-system + admin) 664 pass / 0 fail php -l over all new/changed files clean node --check over all JS clean npm run build + build:public clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
639 lines
24 KiB
JavaScript
639 lines
24 KiB
JavaScript
/**
|
||
* 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, '"')
|
||
.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 '<span class="aksrt-ticket-badge" style="--aksrt-ticket-color:' + escapeHtml(color || '#6b7280') + '">'
|
||
+ escapeHtml(label || status) + '</span>';
|
||
}
|
||
|
||
function attachmentPreview(images) {
|
||
if (!images || !images.length) return '';
|
||
var html = '<div class="aksrt-ticket-attachments">';
|
||
images.forEach(function (url) {
|
||
html += '<a class="aksrt-ticket-attachment" href="' + escapeHtml(url) + '" target="_blank" rel="noopener">'
|
||
+ '<img src="' + escapeHtml(url) + '" alt="">'
|
||
+ '</a>';
|
||
});
|
||
return html + '</div>';
|
||
}
|
||
|
||
function quoteBlock(quote) {
|
||
if (!quote) return '';
|
||
var text = quote.is_deleted ? '原回复已删除' : (quote.content || '');
|
||
return '<blockquote class="aksrt-ticket-quote">'
|
||
+ '<strong>' + escapeHtml(quote.author_name || '') + '</strong> '
|
||
+ escapeHtml(text) + '</blockquote>';
|
||
}
|
||
|
||
function modalShell(title, body, footer) {
|
||
return '<div class="aksrt-modal-colorful-header modal-colorful-header colorful-bg jb-blue">'
|
||
+ '<h3>' + escapeHtml(title) + '</h3>'
|
||
+ '<button type="button" class="close" data-aksrt-modal-close aria-label="关闭">'
|
||
+ '<svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>'
|
||
+ '</div>'
|
||
+ '<div class="aksrt-modal-runtime__body">' + body + '</div>'
|
||
+ '<div class="but-average modal-buts">' + (footer || '') + '</div>';
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* 列表 / 统计
|
||
* ------------------------------------------------------------------ */
|
||
|
||
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 = '<div class="aksrt-tickets-page__empty">'
|
||
+ '<i class="fa fas fa-headset" aria-hidden="true"></i>'
|
||
+ '<h3>还没有工单</h3>'
|
||
+ '<p>遇到问题时提交工单,我们会尽快处理。</p></div>';
|
||
return;
|
||
}
|
||
var html = '';
|
||
items.forEach(function (ticket) {
|
||
html += '<article class="aksrt-tickets-page__card is-' + escapeHtml(ticket.status) + '" data-aksrt-ticket-id="' + escapeHtml(ticket.id) + '">'
|
||
+ '<div class="aksrt-tickets-page__card-head">'
|
||
+ '<span class="aksrt-tickets-page__card-num">#' + escapeHtml(ticket.ticket_num) + '</span>'
|
||
+ statusBadge(ticket.status, ticket.status_color, ticket.status_label)
|
||
+ '</div>'
|
||
+ '<h3 class="aksrt-tickets-page__card-title">' + escapeHtml(ticket.title) + '</h3>'
|
||
+ '<div class="aksrt-tickets-page__card-meta">'
|
||
+ '<span>' + escapeHtml(ticket.ticket_type_name) + '</span>'
|
||
+ '<time>' + escapeHtml(ticket.created_at) + '</time>'
|
||
+ (ticket.order_number ? '<span>订单 ' + escapeHtml(ticket.order_number) + '</span>' : '')
|
||
+ '</div>'
|
||
+ '<footer class="aksrt-tickets-page__card-actions">'
|
||
+ '<button class="btn-outline" type="button" data-aksrt-ticket-view="' + escapeHtml(ticket.id) + '">查看详情</button>'
|
||
+ '</footer>'
|
||
+ '</article>';
|
||
});
|
||
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 += '<button class="aksrt-tickets-page__page-chip' + (i === state.page ? ' is-active' : '') + '" type="button"'
|
||
+ ' data-aksrt-ticket-page="' + i + '"' + (i === state.page ? ' aria-current="page"' : '') + '>' + i + '</button>';
|
||
}
|
||
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 = '<div class="aksrt-ticket-detail" data-aksrt-ticket-detail-id="' + escapeHtml(ticket.id) + '">';
|
||
html += '<div class="aksrt-ticket-detail__head">'
|
||
+ statusBadge(ticket.status, ticket.status_color, ticket.status_label)
|
||
+ '<span class="aksrt-ticket-detail__type">' + escapeHtml(ticket.ticket_type_name) + '</span>'
|
||
+ '</div>';
|
||
html += '<h4 class="aksrt-ticket-detail__title">' + escapeHtml(ticket.title) + '</h4>';
|
||
if (ticket.order_number) {
|
||
html += '<p class="aksrt-ticket-detail__order">关联订单:' + escapeHtml(ticket.order_number) + '</p>';
|
||
}
|
||
html += '<div class="aksrt-ticket-detail__content">' + escapeHtml(ticket.content).replace(/\r\n|\r|\n/g, '<br>') + '</div>';
|
||
html += attachmentPreview(ticket.attachment);
|
||
|
||
html += '<div class="aksrt-ticket-detail__replies">';
|
||
(ticket.replies || []).forEach(function (reply) {
|
||
var who = reply.author_type === 'admin' ? '管理员' : '我';
|
||
html += '<div class="aksrt-ticket-detail__reply is-' + escapeHtml(reply.author_type) + '">'
|
||
+ '<div class="aksrt-ticket-detail__reply-head"><strong>' + escapeHtml(reply.author_name || who) + '</strong>'
|
||
+ '<time>' + escapeHtml(reply.created_at) + '</time></div>'
|
||
+ quoteBlock(reply.quote)
|
||
+ '<div class="aksrt-ticket-detail__reply-body">' + escapeHtml(reply.content).replace(/\r\n|\r|\n/g, '<br>') + '</div>'
|
||
+ attachmentPreview(reply.attachment)
|
||
+ '<button class="aksrt-ticket-detail__quote-btn" type="button" data-aksrt-ticket-quote="' + escapeHtml(reply.id) + '">引用</button>'
|
||
+ '</div>';
|
||
});
|
||
html += '</div>';
|
||
|
||
if (ticket.is_closed) {
|
||
html += '<p class="aksrt-ticket-detail__closed">工单已关闭,如需继续沟通请提交新工单。</p>';
|
||
return html + '</div>';
|
||
}
|
||
|
||
html += '<form class="aksrt-ticket-reply-form" data-aksrt-ticket-reply-form novalidate>';
|
||
html += '<input type="hidden" name="quote_reply_id" value="" data-aksrt-ticket-quote-id>';
|
||
html += '<textarea name="content" rows="3" maxlength="3000" placeholder="填写回复内容"></textarea>';
|
||
html += '<div class="aksrt-ticket-reply-form__actions">';
|
||
if (settings().attachmentEnabled) {
|
||
html += '<button type="button" class="btn-ghost" data-aksrt-ticket-upload>上传图片</button>';
|
||
}
|
||
html += '<button type="submit" class="btn-primary">提交回复</button>';
|
||
html += '<button type="button" class="btn-outline" data-aksrt-ticket-urge>催单</button>';
|
||
html += '<button type="button" class="btn-ghost" data-aksrt-ticket-close>关闭工单</button>';
|
||
html += '</div></form>';
|
||
return html + '</div>';
|
||
}
|
||
|
||
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 = '<form class="aksrt-ticket-create-form" data-aksrt-ticket-create-form novalidate>';
|
||
html += '<label class="aksrt-ticket-field"><span>工单标题</span>'
|
||
+ '<input type="text" name="title" maxlength="200" placeholder="简要描述问题"></label>';
|
||
|
||
html += '<label class="aksrt-ticket-field"><span>工单部门</span><select name="ticket_type_id">';
|
||
html += '<option value="">请选择部门</option>';
|
||
types.forEach(function (type) {
|
||
html += '<option value="' + escapeHtml(type.id) + '">' + escapeHtml(type.name) + '</option>';
|
||
});
|
||
html += '</select></label>';
|
||
|
||
if (orders.length) {
|
||
html += '<label class="aksrt-ticket-field"><span>关联订单(可选)</span><select name="order_number">';
|
||
html += '<option value="">不关联订单</option>';
|
||
orders.forEach(function (order) {
|
||
html += '<option value="' + escapeHtml(order.order_number) + '">'
|
||
+ escapeHtml(order.order_number + (order.product_name ? ' ' + order.product_name : '')) + '</option>';
|
||
});
|
||
html += '</select></label>';
|
||
}
|
||
|
||
html += '<label class="aksrt-ticket-field"><span>问题描述</span>'
|
||
+ '<textarea name="content" rows="5" maxlength="5000" placeholder="请详细描述遇到的问题"></textarea></label>';
|
||
|
||
if (attachmentEnabled) {
|
||
html += '<div class="aksrt-ticket-field"><span>图片(最多 ' + state.attachmentMax + ' 张)</span>'
|
||
+ '<div class="aksrt-ticket-images" data-aksrt-ticket-images></div>'
|
||
+ '<button type="button" class="btn-ghost" data-aksrt-ticket-upload>上传图片</button></div>';
|
||
}
|
||
|
||
html += '<button type="submit" class="btn-primary">提交工单</button>';
|
||
return html + '</form>';
|
||
}
|
||
|
||
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 = '<img src="' + escapeHtml(url) + '" alt="">'
|
||
+ '<button type="button" class="aksrt-ticket-image__remove" aria-label="移除图片">×</button>';
|
||
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();
|
||
}
|
||
})();
|