并非每次售后都真的退款——换货、补偿、仅作记录的情况都会把工单标成已完成, 此时退款金额要么是 0 要么没有实际意义,摆在前台只会让人误解钱已经退回去了。 两个前台入口都撤掉该字段: - 售后详细弹窗:明细行只留「申请时间」,处理记录时间线不受影响 - 订单详情弹窗的售后记录卡片:撤掉退款金额后 meta 的 dl 会空壳,连同 dl 一起移除;卡片仍保留状态徽章 + 申请时间 + 原因 + 「查看详细」 连带清理死代码:aftersaleAmountText / afterSaleAmount 两个格式化助手各自 只剩这一处调用,orders.css 里 5 条 .aksrt-order-detail__after-sale-meta 规则也随之无人引用。后台售后页与订单明细页的退款金额列不动——那边是运营 核对账目的口径,需要看到金额。 验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;抽取 buildDetailHtml 跑样例载荷,确认退款金额不再出现、申请时间 / 处理记录 / 再次申请售后按钮都还在。未做浏览器视觉验收。
701 lines
30 KiB
JavaScript
701 lines
30 KiB
JavaScript
/**
|
||
* AKSRT After-Sale — 前台售后弹窗(申请 / 超时效提示 / 售后详细)
|
||
*
|
||
* 与「禁封/举报」链路同构(report.js / user-ban.js):
|
||
* - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗)
|
||
* - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示)
|
||
* - [data-aksrt-after-sale-withdraw] → 撤销售后二次确认弹窗(仅待审核工单),确认后 DELETE 工单
|
||
* 两个入口的 data-* 属性携带订单与售后状态载荷(同 report.js 的 data-aksrt-report),
|
||
* 点击即同步弹窗,不做前置请求(避免首次点击无反馈)。
|
||
* - 提交走 REST /aksrt/v1/orders/{order_number}/after-sale
|
||
* - 撤销走 DELETE 同端点(仅待审核工单,后端置为已取消)
|
||
* - 提交与撤销前先过人机验证(after_sale_apply / after_sale_cancel,场景未启用自动放行)
|
||
* - 举证图片走 /orders/{order_number}/after-sale/upload-image(复用评论图片上传校验)
|
||
*
|
||
* 按钮形态由订单视图模型的 after_sale 状态决定(UserOrders::after_sale_state):
|
||
* 已申请 → 售后详细;未申请且在时效内 → 申请售后;超时效 → 申请售后(点击弹超时效提示)。
|
||
* 弹窗统一经 AksrtModalRuntime.open 直出 HTML:桌面为主题 modal-mini 几何
|
||
* (400px)+ 蓝色炫彩标题区(modal-colorful-header colorful-bg jb-blue),
|
||
* 移动端由 mobileBottom 底部抽屉接管。
|
||
*
|
||
* @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 MAX_IMAGES = 3;
|
||
|
||
var STATUS_LABELS = {
|
||
pending: '待审核',
|
||
processing: '处理中',
|
||
approved: '已通过',
|
||
rejected: '已拒绝',
|
||
cancelled: '已取消',
|
||
completed: '已完成',
|
||
};
|
||
|
||
function settings() {
|
||
var d = window.aksrtData && window.aksrtData.afterSale;
|
||
return d && typeof d === 'object' ? d : {};
|
||
}
|
||
|
||
function reasonOptions() {
|
||
var list = settings().reasons;
|
||
if (!Array.isArray(list)) return [];
|
||
return list
|
||
.map(function (item) {
|
||
return {
|
||
slug: String((item && (item.slug || item.value)) || ''),
|
||
name: String((item && (item.name || item.label || item.t)) || ''),
|
||
};
|
||
})
|
||
.filter(function (item) { return item.name !== ''; });
|
||
}
|
||
|
||
function materialOptions() {
|
||
var list = settings().materials;
|
||
if (!Array.isArray(list)) return [];
|
||
return list.map(function (item) { return String(item == null ? '' : item); })
|
||
.filter(function (text) { return text !== ''; });
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value == null ? '' : value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function colorfulHeader(title, icon) {
|
||
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
|
||
'<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 class="colorful-make"></div>' +
|
||
'<div class="text-center"><div class="em2x"><i class="fa fas ' + escapeHtml(icon) + '" aria-hidden="true"></i></div>' +
|
||
'<div class="mt10 em12 padding-w10">' + escapeHtml(title) + '</div></div></div>';
|
||
}
|
||
|
||
function orderAmountText(order) {
|
||
var raw = String(order && order.amount != null ? order.amount : '0');
|
||
if (order && order.is_points) {
|
||
return escapeHtml(String(order.currency_symbol || '积分')) + ' ' + (/^\d+$/.test(raw) ? raw : '0');
|
||
}
|
||
return '¥' + raw;
|
||
}
|
||
|
||
function orderSummaryHtml(order) {
|
||
if (!order) return '';
|
||
return '<div class="muted-box padding-10 mb20 em09 aksrt-after-sale-summary">' +
|
||
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">商品</span>' +
|
||
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.product_name || '订单') + '</span></div>' +
|
||
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">订单号</span>' +
|
||
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.order_number || '') + '</span></div>' +
|
||
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">实付</span>' +
|
||
'<span class="aksrt-after-sale-summary__value">' + orderAmountText(order) + '</span></div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function buildApplyHtml(order) {
|
||
var cfg = settings();
|
||
var reasons = reasonOptions();
|
||
var materials = materialOptions();
|
||
|
||
var html = '<div class="aksrt-after-sale-apply-modal">';
|
||
html += colorfulHeader('申请售后', 'fa-undo-alt');
|
||
html += '<form class="aksrt-after-sale-form" data-aksrt-after-sale-order="' + escapeHtml((order && order.order_number) || '') + '" novalidate>';
|
||
// 滚动容器与禁封申诉弹窗一致:no-scrollbar 完全隐藏滚动条(含细滚动条)+ 50vh 内层滚动。
|
||
html += '<div class="scroll-y no-scrollbar max-vh5">';
|
||
html += orderSummaryHtml(order);
|
||
|
||
if (reasons.length) {
|
||
html += '<div class="mb20">';
|
||
html += '<div class="em09 muted-2-color mb6">售后原因</div>';
|
||
html += '<div class="form-radio form-but-radio">';
|
||
reasons.forEach(function (reason) {
|
||
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-after-sale-reason="' + escapeHtml(reason.name) + '" data-reason-slug="' + escapeHtml(reason.slug) + '">' + escapeHtml(reason.name) + '</span>';
|
||
});
|
||
html += '</div></div>';
|
||
}
|
||
|
||
materials.forEach(function (label) {
|
||
html += '<div class="mb10"><div class="muted-2-color mb6">' + escapeHtml(label) + '</div>' +
|
||
'<input class="form-control" type="text" maxlength="120" data-aksrt-after-sale-material="' + escapeHtml(label) + '" placeholder="请输入' + escapeHtml(label) + '"></div>';
|
||
});
|
||
|
||
var placeholder = String(cfg.reasonPlaceholder || '必填,请您详细填写申请原因');
|
||
html += '<div class="mb10"><div class="muted-2-color mb6">申请说明</div>' +
|
||
'<textarea class="form-control" name="reason" rows="3" maxlength="2000" required placeholder="' + escapeHtml(placeholder) + '"></textarea></div>';
|
||
|
||
if (cfg.imgEnabled) {
|
||
html += '<div class="form-upload mb10">' +
|
||
'<div class="em09 muted-2-color mb6">上传图片</div>' +
|
||
'<div class="preview"><div class="add" data-aksrt-after-sale-add role="button" tabindex="0" aria-label="上传图片"></div></div>' +
|
||
'<input class="hide" type="file" accept="image/gif,image/jpeg,image/jpg,image/png,image/webp" data-aksrt-after-sale-file>' +
|
||
'<div class="muted-2-color em09 mt6">最多上传 ' + MAX_IMAGES + ' 张图片</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
// 申请售后说明(后台 after_sale_desc,服务端已 wp_kses_post)。
|
||
var desc = String(cfg.desc || '');
|
||
if (desc) {
|
||
html += '<div class="em09 padding-h10 muted-box mb10">' + desc + '</div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
html += '<div class="but-average modal-buts">' +
|
||
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
|
||
'<button type="button" class="but c-blue" data-aksrt-after-sale-submit><i class="fa fas fa-check" aria-hidden="true"></i>确认提交</button>' +
|
||
'</div>';
|
||
html += '</form></div>';
|
||
return html;
|
||
}
|
||
|
||
function detailRow(label, value) {
|
||
if (value === '' || value == null) return '';
|
||
return '<div class="aksrt-after-sale-detail__row"><span class="muted-2-color">' + escapeHtml(label) + '</span>' +
|
||
'<span class="aksrt-after-sale-detail__value">' + escapeHtml(value) + '</span></div>';
|
||
}
|
||
|
||
// 处理记录节点标题:用户侧提交/撤脱离不开工单状态,单独措辞;
|
||
// 后台提交直接用目标状态,与后台列表口径一致。
|
||
function handlingEntryTitle(entry) {
|
||
var status = String((entry && entry.status) || '');
|
||
if (String((entry && entry.source) || '') === 'user') {
|
||
return status === 'cancelled' ? '用户撤销售后' : '用户提交申请';
|
||
}
|
||
return STATUS_LABELS[status] || status;
|
||
}
|
||
|
||
// 处理记录时间线:左列绳结状态条(节点 + 状态 + 时间),右列处理备注。
|
||
// 数据源为 latest.handling_log(工单行内 JSON,按时间正序)。
|
||
function handlingLogHtml(log) {
|
||
var entries = Array.isArray(log) ? log : [];
|
||
if (!entries.length) {
|
||
return '<div class="aksrt-after-sale-log"><div class="aksrt-after-sale-log__empty">暂无处理记录</div></div>';
|
||
}
|
||
var html = '<div class="aksrt-after-sale-log"><div class="muted-2-color em09 mb6">处理记录</div>';
|
||
html += '<ol class="aksrt-after-sale-timeline">';
|
||
entries.forEach(function (entry) {
|
||
var status = String((entry && entry.status) || '');
|
||
var note = String((entry && entry.note) || '');
|
||
html += '<li class="aksrt-after-sale-timeline__item is-' + escapeHtml(status) + '">';
|
||
html += '<span class="aksrt-after-sale-timeline__knot"></span>';
|
||
html += '<div class="aksrt-after-sale-timeline__body">';
|
||
// [节点][处理状态][时间] 同一行,处理备注另起一行
|
||
html += '<div class="aksrt-after-sale-timeline__head">' +
|
||
'<span class="aksrt-after-sale-timeline__status">' + escapeHtml(handlingEntryTitle(entry)) + '</span>' +
|
||
'<time class="aksrt-after-sale-timeline__time">' + escapeHtml(String((entry && entry.created_at) || '')) + '</time>' +
|
||
'</div>';
|
||
if (note !== '') {
|
||
html += '<p class="aksrt-after-sale-timeline__note">' + escapeHtml(note).replace(/\r\n|\r|\n/g, '<br>') + '</p>';
|
||
}
|
||
html += '</div>';
|
||
html += '</li>';
|
||
});
|
||
html += '</ol></div>';
|
||
return html;
|
||
}
|
||
|
||
function buildDetailHtml(order) {
|
||
var afterSale = (order && order.after_sale) || {};
|
||
var latest = afterSale.latest || {};
|
||
var status = String(latest.status || '');
|
||
|
||
var html = '<div class="aksrt-after-sale-detail-modal">';
|
||
html += colorfulHeader('售后详细', 'fa-clipboard-list');
|
||
html += '<div class="scroll-y no-scrollbar max-vh5 aksrt-after-sale-detail__body">';
|
||
html += orderSummaryHtml(order);
|
||
|
||
html += '<div class="aksrt-after-sale-detail__statusRow">' +
|
||
'<span class="muted-2-color em09">售后状态</span>' +
|
||
'<span class="aksrt-after-sale-detail__statusCell">' +
|
||
'<span class="badg p2-10 aksrt-after-sale-status is-' + escapeHtml(status) + '">' + escapeHtml(STATUS_LABELS[status] || status) + '</span>' +
|
||
// 仅「待审核」可撤销(后端 cancel 仅放行 pending):入口随状态徽章渲染。
|
||
(status === 'pending'
|
||
? '<button type="button" class="aksrt-after-sale-withdraw" data-aksrt-after-sale-withdraw="' + escapeHtml((order && order.order_number) || '') + '">撤销售后<i class="fa fas fa-angle-right" aria-hidden="true"></i></button>'
|
||
: '') +
|
||
'</span></div>';
|
||
|
||
html += '<div class="aksrt-after-sale-detail__rows">';
|
||
html += detailRow('申请时间', String(latest.created_at || ''));
|
||
html += '</div>';
|
||
// 处理记录:每次后台提交一条,替代原先单行的「处理备注 / 完成时间」。
|
||
html += handlingLogHtml(latest.handling_log);
|
||
|
||
// 申请表单分行展示:预置原因 / 申请资料 / 申请说明各自独立成行,
|
||
// 与上方「申请时间」等明细行同格式;老数据无 apply_form 时兜底展示融合串。
|
||
var applyForm = latest.apply_form && typeof latest.apply_form === 'object' ? latest.apply_form : null;
|
||
if (applyForm) {
|
||
html += detailRow('原因', String(applyForm.reason_label || ''));
|
||
(Array.isArray(applyForm.materials) ? applyForm.materials : []).forEach(function (material) {
|
||
html += detailRow(String((material && material.label) || ''), String((material && material.value) || ''));
|
||
});
|
||
html += detailRow('申请说明', String(applyForm.description || ''));
|
||
} else if (latest.reason) {
|
||
html += '<div class="mb10"><div class="muted-2-color em09 mb6">申请原因</div>' +
|
||
'<div class="em09 aksrt-after-sale-reason-text">' + escapeHtml(latest.reason).replace(/\r\n|\r|\n/g, '<br>') + '</div></div>';
|
||
}
|
||
|
||
var images = Array.isArray(latest.images) ? latest.images : [];
|
||
if (images.length) {
|
||
html += '<div class="mb10"><div class="muted-2-color em09 mb6">举证图片</div><div class="aksrt-after-sale-images">';
|
||
images.forEach(function (url) {
|
||
html += '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer"><img src="' + escapeHtml(url) + '" alt=""></a>';
|
||
});
|
||
html += '</div></div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
|
||
if (afterSale.can_reapply) {
|
||
// 可再次申请:仅保留单个主按钮(与禁封申诉弹窗单按钮同式)。
|
||
html += '<div class="but-average modal-buts">' +
|
||
'<button type="button" class="but c-blue" data-aksrt-after-sale-reapply data-aksrt-after-sale-payload="' + escapeHtml(JSON.stringify(order)) + '"><i class="fa fas fa-undo-alt" aria-hidden="true"></i>再次申请售后</button>' +
|
||
'</div>';
|
||
} else {
|
||
// 不可再申请售后:不再展示阻断文案,仅保留关闭入口。
|
||
html += '<div class="but-average modal-buts"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
function buildExpiredHtml() {
|
||
var text = String(settings().expiredText || '抱歉,您的订单已超过售后申请时效');
|
||
var html = '<div class="aksrt-after-sale-expired-modal">';
|
||
html += colorfulHeader('售后提示', 'fa-clock');
|
||
// 超过时效提示(后台 after_sale_expired_text,服务端已 wp_kses_post,支持 HTML)。
|
||
html += '<div class="aksrt-after-sale-expired__body em09">' + text + '</div>';
|
||
html += '<div class="but-average modal-buts"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
// 撤销售后二次确认弹窗:待审核工单入口点入,确认后 DELETE 售后工单。
|
||
function buildWithdrawHtml(orderNumber) {
|
||
var html = '<div class="aksrt-after-sale-withdraw-modal">';
|
||
html += colorfulHeader('撤销售后', 'fa-undo-alt');
|
||
html += '<div class="aksrt-after-sale-withdraw__body em09">撤销后本次售后申请将关闭,订单恢复为可再次申请售后的状态。确定撤销吗?</div>';
|
||
html += '<div class="but-average modal-buts">' +
|
||
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
|
||
'<button type="button" class="but c-blue" data-aksrt-after-sale-withdraw-confirm="' + escapeHtml(orderNumber) + '">确认撤销</button>' +
|
||
'</div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
function openModal(options) {
|
||
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') {
|
||
notyf('售后组件加载失败,请刷新页面后重试', 'danger');
|
||
return null;
|
||
}
|
||
var shell = window.AksrtModalRuntime.open({
|
||
html: options.html,
|
||
className: options.className,
|
||
kind: options.kind,
|
||
mobileBottom: true,
|
||
});
|
||
bindShell(shell);
|
||
return shell;
|
||
}
|
||
|
||
function openApply(order) {
|
||
return openModal({
|
||
html: buildApplyHtml(order),
|
||
className: 'aksrt-after-sale-apply-modal',
|
||
kind: 'after-sale-apply',
|
||
});
|
||
}
|
||
|
||
function openDetail(order) {
|
||
return openModal({
|
||
html: buildDetailHtml(order),
|
||
className: 'aksrt-after-sale-detail-modal',
|
||
kind: 'after-sale-detail',
|
||
});
|
||
}
|
||
|
||
function openExpired() {
|
||
return openModal({
|
||
html: buildExpiredHtml(),
|
||
className: 'aksrt-after-sale-expired-modal',
|
||
kind: 'after-sale-expired',
|
||
});
|
||
}
|
||
|
||
function openWithdraw(orderNumber) {
|
||
if (!orderNumber) return;
|
||
return openModal({
|
||
html: buildWithdrawHtml(orderNumber),
|
||
className: 'aksrt-after-sale-withdraw-modal',
|
||
kind: 'after-sale-withdraw',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 解析按钮 data-* 上的售后载荷(与举报链路 data-aksrt-report 同模式):
|
||
* 入口数据随列表一起渲染,点击即可同步弹窗,不做前置请求。
|
||
*/
|
||
function parsePayload(raw) {
|
||
if (!raw) return null;
|
||
try {
|
||
var data = JSON.parse(raw);
|
||
return data && typeof data === 'object' ? data : null;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 申请售后入口:超时效 → 超时效提示;已申请 → 售后详细;否则 → 申请表单。
|
||
function openApplyFlow(payload) {
|
||
var afterSale = payload && payload.after_sale ? payload.after_sale : null;
|
||
if (!payload || !afterSale || !afterSale.enabled) {
|
||
notyf('售后申请暂不可用', 'danger');
|
||
return;
|
||
}
|
||
if (afterSale.expired) { openExpired(); return; }
|
||
if (afterSale.has_application) { openDetail(payload); return; }
|
||
openApply(payload);
|
||
}
|
||
|
||
function openDetailFlow(payload) {
|
||
var afterSale = payload && payload.after_sale ? payload.after_sale : null;
|
||
if (!payload || !afterSale || !afterSale.enabled || !afterSale.has_application) {
|
||
notyf('暂无售后记录', 'danger');
|
||
return;
|
||
}
|
||
openDetail(payload);
|
||
}
|
||
|
||
function collectImages(shell) {
|
||
return Array.prototype.map.call(
|
||
shell.querySelectorAll('.preview .preview-item img'),
|
||
function (img) { return img.getAttribute('src') || ''; }
|
||
).filter(function (url) { return url !== ''; });
|
||
}
|
||
|
||
function uploadImage(shell, file) {
|
||
var form = shell.querySelector('[data-aksrt-after-sale-order]');
|
||
var preview = shell.querySelector('.form-upload .preview');
|
||
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
|
||
if (!preview || !orderNumber) return Promise.resolve();
|
||
if (collectImages(shell).length >= MAX_IMAGES) {
|
||
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
|
||
return Promise.resolve();
|
||
}
|
||
|
||
var body = new FormData();
|
||
body.append('image', file);
|
||
|
||
return window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale/upload-image', {
|
||
method: 'POST',
|
||
headers: { 'X-WP-Nonce': NONCE },
|
||
body: body,
|
||
})
|
||
.then(function (res) { return res.json(); })
|
||
.then(function (result) {
|
||
var url = result && result.success && result.data && result.data.url;
|
||
if (!url) {
|
||
notyf((result && result.message) || '图片上传失败', 'danger');
|
||
return;
|
||
}
|
||
if (collectImages(shell).length >= MAX_IMAGES) {
|
||
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
|
||
return;
|
||
}
|
||
var item = document.createElement('div');
|
||
item.className = 'preview-item';
|
||
var img = document.createElement('img');
|
||
img.src = url;
|
||
img.alt = '';
|
||
var remove = document.createElement('button');
|
||
remove.type = 'button';
|
||
remove.className = 'remove';
|
||
remove.setAttribute('data-aksrt-after-sale-image-remove', '');
|
||
remove.setAttribute('aria-label', '删除图片');
|
||
remove.innerHTML = '×';
|
||
remove.addEventListener('click', function () { item.remove(); });
|
||
item.appendChild(img);
|
||
item.appendChild(remove);
|
||
preview.insertBefore(item, preview.querySelector('.add'));
|
||
})
|
||
.catch(function () {
|
||
notyf('图片上传失败,请重试', 'danger');
|
||
});
|
||
}
|
||
|
||
function selectedReasonLabel(shell) {
|
||
var active = shell.querySelector('[data-aksrt-after-sale-reason].active');
|
||
return active ? String(active.getAttribute('data-aksrt-after-sale-reason') || '') : '';
|
||
}
|
||
|
||
function collectMaterials(shell) {
|
||
var materials = [];
|
||
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-material]'), function (input) {
|
||
var value = String(input.value || '').trim();
|
||
if (value === '') return;
|
||
materials.push({
|
||
label: String(input.getAttribute('data-aksrt-after-sale-material') || ''),
|
||
value: value,
|
||
});
|
||
});
|
||
return materials;
|
||
}
|
||
|
||
function composeReason(shell) {
|
||
var parts = [];
|
||
var active = shell.querySelector('[data-aksrt-after-sale-reason].active');
|
||
if (active) {
|
||
var label = active.getAttribute('data-aksrt-after-sale-reason') || '';
|
||
if (label) parts.push(label);
|
||
}
|
||
var materials = [];
|
||
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-material]'), function (input) {
|
||
var value = String(input.value || '').trim();
|
||
if (value === '') return;
|
||
materials.push(String(input.getAttribute('data-aksrt-after-sale-material') || '') + ':' + value);
|
||
});
|
||
if (materials.length) parts.push(materials.join('\n'));
|
||
var reasonInput = shell.querySelector('[name="reason"]');
|
||
var reasonText = reasonInput ? String(reasonInput.value || '').trim() : '';
|
||
if (reasonText) parts.push(reasonText);
|
||
return parts.join('\n');
|
||
}
|
||
|
||
function clearToast(id) {
|
||
var toast = document.getElementById(id);
|
||
if (toast) toast.remove();
|
||
}
|
||
|
||
// 提交前的人机验证:场景未启用时无挂载点,run 直接 resolve {};
|
||
// 用户中途取消验证时 run resolve null,此时还原按钮并中止提交。
|
||
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 submitApply(shell) {
|
||
var form = shell.querySelector('[data-aksrt-after-sale-order]');
|
||
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
|
||
var reasonInput = shell.querySelector('[name="reason"]');
|
||
var reasonText = reasonInput ? String(reasonInput.value || '').trim() : '';
|
||
if (!reasonText) {
|
||
notyf('请填写申请说明', 'danger');
|
||
if (reasonInput) reasonInput.focus();
|
||
return;
|
||
}
|
||
// 预置原因被后台清空时不强制选择;有配置则必须选中一项(与举报弹窗一致)。
|
||
if (shell.querySelector('[data-aksrt-after-sale-reason]') && !shell.querySelector('[data-aksrt-after-sale-reason].active')) {
|
||
notyf('请选择售后原因', 'danger');
|
||
return;
|
||
}
|
||
|
||
var submitBtn = shell.querySelector('[data-aksrt-after-sale-submit]');
|
||
if (submitBtn) submitBtn.disabled = true;
|
||
notyf('正在提交售后申请…', 'load', 3000, 'aksrt-after-sale-submit');
|
||
|
||
var restoreButton = function () {
|
||
if (submitBtn) submitBtn.disabled = false;
|
||
};
|
||
var sendApply = function (captchaPayload) {
|
||
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-WP-Nonce': NONCE,
|
||
},
|
||
body: JSON.stringify(Object.assign({
|
||
reason: composeReason(shell),
|
||
reason_label: selectedReasonLabel(shell),
|
||
materials: collectMaterials(shell),
|
||
description: reasonText,
|
||
images: collectImages(shell),
|
||
}, captchaPayload)),
|
||
})
|
||
.then(function (res) { return res.json(); })
|
||
.then(function (result) {
|
||
if (result && result.success) {
|
||
window.AksrtModalRuntime.close('submit');
|
||
notyf('售后申请已提交', 'success');
|
||
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
|
||
window.AKSRTOrderCenter.refresh(orderNumber);
|
||
}
|
||
} else {
|
||
restoreButton();
|
||
notyf((result && result.message) || '售后申请提交失败', 'danger');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
restoreButton();
|
||
notyf('网络错误,请重试', 'danger');
|
||
})
|
||
.then(function () { clearToast('aksrt-after-sale-submit'); });
|
||
};
|
||
|
||
runCaptcha('after_sale_apply').then(sendApply).catch(function (error) {
|
||
restoreButton();
|
||
clearToast('aksrt-after-sale-submit');
|
||
if (error && error.cancelled) return;
|
||
notyf('人机验证失败,请重试', 'danger');
|
||
});
|
||
}
|
||
|
||
// 撤销售后:待审核工单置为已取消(DELETE 售后工单),成功后刷新订单列表与统计。
|
||
function submitWithdraw(shell, button) {
|
||
var orderNumber = button.getAttribute('data-aksrt-after-sale-withdraw-confirm') || '';
|
||
if (!orderNumber || button.disabled) return;
|
||
button.disabled = true;
|
||
notyf('正在撤销售后申请…', 'load', 3000, 'aksrt-after-sale-withdraw');
|
||
|
||
var restoreButton = function () {
|
||
button.disabled = false;
|
||
};
|
||
var sendWithdraw = function (captchaPayload) {
|
||
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-WP-Nonce': NONCE,
|
||
},
|
||
body: JSON.stringify(captchaPayload || {}),
|
||
})
|
||
.then(function (res) { return res.json(); })
|
||
.then(function (result) {
|
||
if (result && result.success) {
|
||
window.AksrtModalRuntime.close('withdraw');
|
||
notyf('售后申请已撤销', 'success');
|
||
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
|
||
window.AKSRTOrderCenter.refresh(orderNumber);
|
||
}
|
||
} else {
|
||
restoreButton();
|
||
notyf((result && result.message) || '撤销失败,请重试', 'danger');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
restoreButton();
|
||
notyf('网络错误,请重试', 'danger');
|
||
})
|
||
.then(function () { clearToast('aksrt-after-sale-withdraw'); });
|
||
};
|
||
|
||
runCaptcha('after_sale_cancel').then(sendWithdraw).catch(function (error) {
|
||
restoreButton();
|
||
clearToast('aksrt-after-sale-withdraw');
|
||
if (error && error.cancelled) return;
|
||
notyf('人机验证失败,请重试', 'danger');
|
||
});
|
||
}
|
||
|
||
function bindShell(shell) {
|
||
if (!shell) return;
|
||
|
||
shell.addEventListener('click', function (event) {
|
||
// 原因徽章:单选 active(与举报弹窗同语义,重复点击已选徽章保持选中)。
|
||
var chip = event.target.closest('[data-aksrt-after-sale-reason]');
|
||
if (chip) {
|
||
event.preventDefault();
|
||
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-reason].active'), function (el) {
|
||
el.classList.remove('active');
|
||
});
|
||
chip.classList.add('active');
|
||
return;
|
||
}
|
||
|
||
var addBtn = event.target.closest('[data-aksrt-after-sale-add]');
|
||
if (addBtn) {
|
||
event.preventDefault();
|
||
var fileInput = shell.querySelector('[data-aksrt-after-sale-file]');
|
||
if (fileInput) fileInput.click();
|
||
return;
|
||
}
|
||
|
||
var removeBtn = event.target.closest('[data-aksrt-after-sale-image-remove]');
|
||
if (removeBtn) {
|
||
event.preventDefault();
|
||
var item = removeBtn.closest('.preview-item');
|
||
if (item) item.remove();
|
||
return;
|
||
}
|
||
|
||
var submitBtn = event.target.closest('[data-aksrt-after-sale-submit]');
|
||
if (submitBtn) {
|
||
event.preventDefault();
|
||
submitApply(shell);
|
||
return;
|
||
}
|
||
|
||
// 再次申请售后:载荷已随按钮渲染。can_reapply 必然伴随 has_application=true,
|
||
// 直接走 openApplyFlow 会被 has_application 分流回「售后详细」打转;
|
||
// 清除标记落回申请表单分支(expired 超时效拦截仍然生效)。
|
||
var reapplyBtn = event.target.closest('[data-aksrt-after-sale-reapply]');
|
||
if (reapplyBtn) {
|
||
event.preventDefault();
|
||
var payload = parsePayload(reapplyBtn.getAttribute('data-aksrt-after-sale-payload'));
|
||
if (payload && payload.after_sale) payload.after_sale.has_application = false;
|
||
window.AksrtModalRuntime.close('replace');
|
||
openApplyFlow(payload);
|
||
return;
|
||
}
|
||
|
||
// 撤销售后:入口在售后详细弹窗状态行(仅待审核),点击先弹二次确认。
|
||
var withdrawBtn = event.target.closest('[data-aksrt-after-sale-withdraw]');
|
||
if (withdrawBtn) {
|
||
event.preventDefault();
|
||
openWithdraw(withdrawBtn.getAttribute('data-aksrt-after-sale-withdraw') || '');
|
||
return;
|
||
}
|
||
|
||
var withdrawConfirm = event.target.closest('[data-aksrt-after-sale-withdraw-confirm]');
|
||
if (withdrawConfirm) {
|
||
event.preventDefault();
|
||
submitWithdraw(shell, withdrawConfirm);
|
||
}
|
||
});
|
||
|
||
shell.addEventListener('change', function (event) {
|
||
var fileInput = event.target.closest('[data-aksrt-after-sale-file]');
|
||
if (!fileInput || !fileInput.files || !fileInput.files.length) return;
|
||
Array.prototype.forEach.call(fileInput.files, function (file) {
|
||
uploadImage(shell, file);
|
||
});
|
||
fileInput.value = '';
|
||
});
|
||
}
|
||
|
||
function initAfterSaleButtons() {
|
||
if (document.documentElement.dataset.aksrtAfterSaleInitialized === '1') return;
|
||
document.documentElement.dataset.aksrtAfterSaleInitialized = '1';
|
||
document.addEventListener('click', function (event) {
|
||
var apply = event.target.closest('[data-aksrt-after-sale-apply]');
|
||
if (apply) {
|
||
event.preventDefault();
|
||
openApplyFlow(parsePayload(apply.getAttribute('data-aksrt-after-sale-apply')));
|
||
return;
|
||
}
|
||
var detail = event.target.closest('[data-aksrt-after-sale-detail]');
|
||
if (detail) {
|
||
event.preventDefault();
|
||
openDetailFlow(parsePayload(detail.getAttribute('data-aksrt-after-sale-detail')));
|
||
}
|
||
});
|
||
}
|
||
|
||
jQuery(document).off('auto_fun.aksrt-after-sale').on('auto_fun.aksrt-after-sale', initAfterSaleButtons);
|
||
// 委托监听不依赖 DOM 扫描,脚本加载时立即挂上;auto_fun 钩子保留作幂等重扫。
|
||
initAfterSaleButtons();
|
||
})();
|