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:
1 parent
b447c41f13
commit
1a9a2762f3
19 files changed
+1835
-86
No files matched your search
+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