feat(payment): 新增「售后策略」配置与前台售后弹窗链路
- 后台「支付&付费 → 售后策略」:售后功能开关、申请时效、超时效提示(HTML)、 预置原因、申请售后说明、所需材料、申请说明占位文案、可上传图片开关; PHP schema 与 SPA schema 1:1 对齐,group 字段支持拖拽排序 - PaymentRuntime 新增售后设置解析(缺键回退 schema 默认、HTML 走 kses、 中文原因稳定 slug)与 after_sale_expired 时效判定(支付时间起算) - 售后工单支持举证图片(order_aftersale.images 列,最多 3 张, 复用评论图片上传校验)与上传路由;订单视图模型输出 after_sale 按钮状态 - 前台申请 / 超时效提示 / 售后详细三个独立弹窗(modal-mini + 蓝色炫彩头), 订单详情弹窗不再内嵌售后表单并清理相关死代码 - 新增 after-sale-policy 契约测试与运行时 harness,527 项契约测试通过
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
/**
|
||||
* AKSRT After-Sale — 前台售后弹窗(申请 / 超时效提示 / 售后详细)
|
||||
*
|
||||
* 与「禁封/举报」链路同构(report.js / user-ban.js):
|
||||
* - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗)
|
||||
* - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示)
|
||||
* - 提交走 REST /aksrt/v1/orders/{order_number}/after-sale
|
||||
* - 举证图片走 /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 REAPPLY_BLOCKED_TEXT = '该无法再次申请售后,该订单已售后,暂不支持再次申请售后。';
|
||||
|
||||
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 aftersaleAmountText(value, order) {
|
||||
var amount = String(value == null ? '' : value);
|
||||
return order && order.is_points ? amount + ' ' + String((order && order.currency_symbol) || '积分') : '¥' + amount;
|
||||
}
|
||||
|
||||
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>';
|
||||
html += '<div class="scroll-y mini-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 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 mini-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="badg p2-10 aksrt-after-sale-status is-' + escapeHtml(status) + '">' + escapeHtml(STATUS_LABELS[status] || status) + '</span>' +
|
||||
'</div>';
|
||||
|
||||
html += '<div class="aksrt-after-sale-detail__rows">';
|
||||
html += detailRow('申请时间', String(latest.created_at || ''));
|
||||
html += detailRow('退款金额', aftersaleAmountText(latest.refund_amount, order));
|
||||
html += detailRow('处理备注', String(latest.admin_note || ''));
|
||||
html += detailRow('完成时间', String(latest.completed_at || ''));
|
||||
html += '</div>';
|
||||
|
||||
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" data-aksrt-modal-close>关闭</button>' +
|
||||
'<button type="button" class="but c-blue" data-aksrt-after-sale-reapply="' + escapeHtml((order && order.order_number) || '') + '"><i class="fa fas fa-undo-alt" aria-hidden="true"></i>再次申请售后</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
// 不可再申请售后(工单未结束 / 已完结 / 超过时效):仅提示 + 我知道了。
|
||||
html += '<div class="aksrt-after-sale-detail__blocked">' +
|
||||
'<div class="c-yellow box-body">' + escapeHtml(REAPPLY_BLOCKED_TEXT) + '</div>' +
|
||||
'<div class="but-average modal-buts mt10"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>' +
|
||||
'</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;
|
||||
}
|
||||
|
||||
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 loadOrder(number) {
|
||||
if (!window.AksrtRequest || typeof window.AksrtRequest.request !== 'function') {
|
||||
return Promise.reject(new Error('请求组件加载失败,请刷新页面后重试'));
|
||||
}
|
||||
return window.AksrtRequest.request('/orders/' + encodeURIComponent(number), { method: 'GET' })
|
||||
.then(function (result) { return (result && result.data) || null; });
|
||||
}
|
||||
|
||||
function openApplyFlow(number) {
|
||||
if (!number) return;
|
||||
loadOrder(number).then(function (order) {
|
||||
var afterSale = order && order.after_sale ? order.after_sale : null;
|
||||
if (!order || !afterSale || !afterSale.enabled) {
|
||||
notyf('售后申请暂不可用', 'danger');
|
||||
return;
|
||||
}
|
||||
if (afterSale.expired) { openExpired(); return; }
|
||||
if (afterSale.has_application) { openDetail(order); return; }
|
||||
openApply(order);
|
||||
}).catch(function (error) {
|
||||
notyf((error && error.message) || '订单加载失败,请刷新后重试', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function openDetailFlow(number) {
|
||||
if (!number) return;
|
||||
loadOrder(number).then(function (order) {
|
||||
var afterSale = order && order.after_sale ? order.after_sale : null;
|
||||
if (!order || !afterSale || !afterSale.enabled || !afterSale.has_application) {
|
||||
notyf('暂无售后记录', 'danger');
|
||||
return;
|
||||
}
|
||||
openDetail(order);
|
||||
}).catch(function (error) {
|
||||
notyf((error && error.message) || '订单加载失败,请刷新后重试', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
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 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 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');
|
||||
|
||||
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': NONCE,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reason: composeReason(shell),
|
||||
images: collectImages(shell),
|
||||
}),
|
||||
})
|
||||
.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 {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
notyf((result && result.message) || '售后申请提交失败', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
notyf('网络错误,请重试', 'danger');
|
||||
})
|
||||
.then(function () {
|
||||
var toast = document.getElementById('aksrt-after-sale-submit');
|
||||
if (toast) toast.remove();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 再次申请售后:重新拉取订单状态,时效/工单口径以最新为准。
|
||||
var reapplyBtn = event.target.closest('[data-aksrt-after-sale-reapply]');
|
||||
if (reapplyBtn) {
|
||||
event.preventDefault();
|
||||
var number = reapplyBtn.getAttribute('data-aksrt-after-sale-reapply') || '';
|
||||
window.AksrtModalRuntime.close('replace');
|
||||
openApplyFlow(number);
|
||||
}
|
||||
});
|
||||
|
||||
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(apply.getAttribute('data-aksrt-after-sale-apply') || '');
|
||||
return;
|
||||
}
|
||||
var detail = event.target.closest('[data-aksrt-after-sale-detail]');
|
||||
if (detail) {
|
||||
event.preventDefault();
|
||||
openDetailFlow(detail.getAttribute('data-aksrt-after-sale-detail') || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(document).off('auto_fun.aksrt-after-sale').on('auto_fun.aksrt-after-sale', initAfterSaleButtons);
|
||||
})();
|
||||
+29
-35
@@ -7,6 +7,7 @@
|
||||
"use strict";
|
||||
|
||||
var instances = new WeakMap();
|
||||
var liveStates = [];
|
||||
|
||||
function request(path, body, method, operationKey) {
|
||||
var options = { method: method || "POST" };
|
||||
@@ -199,7 +200,8 @@
|
||||
html += '<button class="btn-outline" type="button" data-aksrt-modal-close>关闭</button>';
|
||||
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
|
||||
if (allowedActions.can_after_sale) html += '<form class="aksrt-order-detail__after-sale-form" data-aksrt-after-sale-form="' + escapeHtml(order.order_number) + '"><label>申请售后原因<textarea name="reason" required maxlength="2000" rows="3"></textarea></label><button class="btn-ghost" type="submit">提交售后申请</button></form>';
|
||||
// 售后申请为独立弹窗,不再占用订单详情(order-after-sale.js)。
|
||||
html += afterSaleButtons(order.order_number, order.after_sale || {});
|
||||
html += "</footer>";
|
||||
|
||||
session.content.innerHTML = html;
|
||||
@@ -275,10 +277,24 @@
|
||||
|
||||
function actionHtml(order) {
|
||||
var allowedActions = order.allowed_actions || {};
|
||||
var afterSale = order.after_sale || {};
|
||||
var html = '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">订单详情</button>';
|
||||
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
|
||||
if (allowedActions.can_after_sale) html += '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">申请售后</button>';
|
||||
// 售后入口:申请售后 / 售后详细走 order-after-sale.js 的独立弹窗。
|
||||
if (afterSale.enabled) html += afterSaleButtons(order.order_number, afterSale);
|
||||
return html;
|
||||
}
|
||||
|
||||
// 售后按钮(列表卡片与订单详情弹窗共用):已申请 → 售后详细,否则 → 申请售后。
|
||||
function afterSaleButtons(orderNumber, afterSale) {
|
||||
var html = "";
|
||||
if (!afterSale || !afterSale.enabled) return html;
|
||||
if (afterSale.has_application) {
|
||||
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-detail="' + escapeHtml(orderNumber) + '">售后详细</button>';
|
||||
return html;
|
||||
}
|
||||
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-apply="' + escapeHtml(orderNumber) + '">申请售后</button>';
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -555,42 +571,12 @@
|
||||
if (cancel && root.contains(cancel)) { event.preventDefault(); closeOrder(state, cancel); }
|
||||
};
|
||||
state.onRootSubmit = function (event) {
|
||||
var afterSaleForm = closest(event.target, "[data-aksrt-after-sale-form]");
|
||||
if (afterSaleForm) {
|
||||
event.preventDefault();
|
||||
var number = afterSaleForm.getAttribute("data-aksrt-after-sale-form") || "";
|
||||
var reason = afterSaleForm.querySelector("[name=reason]");
|
||||
if (!number || !reason || !String(reason.value || "").trim()) return;
|
||||
request(orderPath(number, "/after-sale"), { reason: String(reason.value).trim() }, "POST", window.AksrtRequest.createIdempotencyKey()).then(function () {
|
||||
notyf("售后申请已提交", "success");
|
||||
invalidateOrder(number);
|
||||
openDetail(state, number);
|
||||
refreshView(state);
|
||||
}).catch(function (error) { notyf(errorMessage(error, "售后申请提交失败"), "danger"); });
|
||||
return;
|
||||
}
|
||||
var form = closest(event.target, "[data-aksrt-orders-search]");
|
||||
if (!form || !root.contains(form)) return;
|
||||
event.preventDefault();
|
||||
var input = form.querySelector("#aksrt-order-number");
|
||||
loadView(state, { orderNumber: input ? String(input.value || "").trim() : "", page: 1 });
|
||||
};
|
||||
state.onDocumentSubmit = function (event) {
|
||||
var afterSaleForm = closest(event.target, "[data-aksrt-after-sale-form]");
|
||||
if (!afterSaleForm || root.contains(afterSaleForm) || event.__aksrtAfterSaleHandled) return;
|
||||
var number = afterSaleForm.getAttribute("data-aksrt-after-sale-form") || "";
|
||||
if (!activeDetail(state, number)) return;
|
||||
event.__aksrtAfterSaleHandled = true;
|
||||
event.preventDefault();
|
||||
var reason = afterSaleForm.querySelector("[name=reason]");
|
||||
if (!number || !reason || !String(reason.value || "").trim()) return;
|
||||
request(orderPath(number, "/after-sale"), { reason: String(reason.value).trim() }, "POST", window.AksrtRequest.createIdempotencyKey()).then(function () {
|
||||
notyf("售后申请已提交", "success");
|
||||
invalidateOrder(number);
|
||||
openDetail(state, number);
|
||||
refreshView(state);
|
||||
}).catch(function (error) { notyf(errorMessage(error, "售后申请提交失败"), "danger"); });
|
||||
};
|
||||
state.onModalClose = function (event) {
|
||||
var detail = event && event.detail ? event.detail : {};
|
||||
if (!state.detailSession || detail.session !== state.detailSession) return;
|
||||
@@ -648,11 +634,11 @@
|
||||
|
||||
root.addEventListener("click", state.onRootClick);
|
||||
root.addEventListener("submit", state.onRootSubmit);
|
||||
document.addEventListener("submit", state.onDocumentSubmit);
|
||||
document.addEventListener("aksrt:modal:close", state.onModalClose);
|
||||
document.addEventListener("click", state.onDocumentClick);
|
||||
state.countdownTimer = window.setInterval(function () { updateCountdowns(state); }, 1000);
|
||||
instances.set(root, state);
|
||||
liveStates.push(state);
|
||||
}
|
||||
|
||||
function rootsIn(root) {
|
||||
@@ -674,14 +660,22 @@
|
||||
if (activeDetail(state, state.detailOrderNumber)) window.AksrtModalRuntime.close("replace");
|
||||
item.removeEventListener("click", state.onRootClick);
|
||||
item.removeEventListener("submit", state.onRootSubmit);
|
||||
document.removeEventListener("submit", state.onDocumentSubmit);
|
||||
document.removeEventListener("aksrt:modal:close", state.onModalClose);
|
||||
document.removeEventListener("click", state.onDocumentClick);
|
||||
window.clearInterval(state.countdownTimer);
|
||||
instances.delete(item);
|
||||
liveStates = liveStates.filter(function (entry) { return entry !== state; });
|
||||
});
|
||||
}
|
||||
|
||||
window.AKSRTOrderCenter = { init: init, destroy: destroy };
|
||||
// refresh 供售后弹窗(order-after-sale.js)提交/取消后刷新列表与统计。
|
||||
function refresh(orderNumber) {
|
||||
invalidateOrder(orderNumber);
|
||||
liveStates.forEach(function (state) {
|
||||
refreshView(state);
|
||||
});
|
||||
}
|
||||
|
||||
window.AKSRTOrderCenter = { init: init, destroy: destroy, refresh: refresh };
|
||||
jQuery(document).off("auto_fun.aksrt-order-center").on("auto_fun.aksrt-order-center", function () { init(document); });
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user