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:
kate522 committed 2026-10-06 22:21:47 +08:00
1 parent b447c41f13
commit 1a9a2762f3
19 files changed
+1835 -86

No files matched your search

+29 -35
View File
@@ -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); });
}());