feat(after-sale): 支持撤销售后工单并接入人机验证

This commit is contained in:
2026-10-07 09:20:59 +08:00
parent 918ca371be
commit 8023e2e472
14 changed files with 427 additions and 82 deletions
+151 -34
View File
@@ -4,9 +4,12 @@
* 与「禁封/举报」链路同构(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):
@@ -178,8 +181,13 @@
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>' +
'</div>';
// 仅「待审核」可撤销(后端 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 || ''));
@@ -238,6 +246,19 @@
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');
@@ -277,6 +298,15 @@
});
}
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 同模式):
* 入口数据随列表一起渲染,点击即可同步弹窗,不做前置请求。
@@ -407,6 +437,22 @@
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') : '';
@@ -427,41 +473,97 @@
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),
reason_label: selectedReasonLabel(shell),
materials: collectMaterials(shell),
description: reasonText,
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);
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');
}
} else {
if (submitBtn) submitBtn.disabled = false;
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 || {}),
})
.catch(function () {
if (submitBtn) submitBtn.disabled = false;
notyf('网络错误,请重试', 'danger');
})
.then(function () {
var toast = document.getElementById('aksrt-after-sale-submit');
if (toast) toast.remove();
});
.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) {
@@ -509,6 +611,21 @@
var payload = parsePayload(reapplyBtn.getAttribute('data-aksrt-after-sale-payload'));
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);
}
});
+1 -20
View File
@@ -237,7 +237,6 @@
}));
html += '<div class="aksrt-order-detail__after-sale-foot">';
html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-detail="' + recordPayload + '">查看详细</button>';
if (asStatus === "pending") html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-cancel="' + escapeHtml(order.order_number) + '">取消申请</button>';
html += "</div>";
html += "</li>";
});
@@ -662,25 +661,7 @@
state.detailReplacementSession = null;
};
state.onDocumentClick = function (event) {
var afterSaleCancel = closest(event.target, "[data-aksrt-after-sale-cancel]");
if (afterSaleCancel && !event.__aksrtAfterSaleCancelHandled) {
var afterSaleNumber = afterSaleCancel.getAttribute("data-aksrt-after-sale-cancel") || "";
if (activeDetail(state, afterSaleNumber)) {
event.__aksrtAfterSaleCancelHandled = true;
event.preventDefault();
afterSaleCancel.disabled = true;
request(orderPath(afterSaleNumber, "/after-sale"), undefined, "DELETE", window.AksrtRequest.createIdempotencyKey()).then(function () {
notyf("售后申请已取消", "success");
invalidateOrder(afterSaleNumber);
openDetail(state, afterSaleNumber);
refreshView(state);
}).catch(function (error) {
afterSaleCancel.disabled = false;
notyf(errorMessage(error, "取消申请失败"), "danger");
});
return;
}
}
// 取消/撤销售后已移入售后详细弹窗(order-after-sale.js 的「撤销售后」入口)。
if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return;
var copy = closest(event.target, "[data-aksrt-order-copy]");
if (copy) {