- 后台「支付&付费 → 售后策略」:售后功能开关、申请时效、超时效提示(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 项契约测试通过
682 lines
34 KiB
JavaScript
682 lines
34 KiB
JavaScript
/**
|
|
* Knowledge-payment order center interactions.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
(function () {
|
|
"use strict";
|
|
|
|
var instances = new WeakMap();
|
|
var liveStates = [];
|
|
|
|
function request(path, body, method, operationKey) {
|
|
var options = { method: method || "POST" };
|
|
if (body !== undefined) options.body = JSON.stringify(body || {});
|
|
if (operationKey) options.headers = { "Idempotency-Key": operationKey };
|
|
return window.AksrtRequest.request(path, options);
|
|
}
|
|
|
|
function orderPath(orderNumber, suffix) {
|
|
return "/orders/" + encodeURIComponent(orderNumber || "") + (suffix || "");
|
|
}
|
|
|
|
function errorMessage(error, fallback) {
|
|
if (error && error.message) return String(error.message);
|
|
return fallback || "请求失败";
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value == null ? "" : value)
|
|
.replace(/&/g, "&").replace(/</g, "<")
|
|
.replace(/>/g, ">").replace(/\"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function formatMoney(order) {
|
|
var raw = String(order && order.amount != null ? order.amount : "0");
|
|
if (order && order.is_points) {
|
|
return escapeHtml(order.currency_symbol || "积分") + " " + (/^\d+$/.test(raw) ? raw : "0");
|
|
}
|
|
var match = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/.exec(raw);
|
|
var amount = match ? match[1] + "." + (match[2] || "").padEnd(2, "0") : "0.00";
|
|
return "¥" + amount;
|
|
}
|
|
|
|
function orderTag(orderNumber) {
|
|
return "order:" + String(orderNumber || "").toLowerCase();
|
|
}
|
|
|
|
function invalidateOrder(orderNumber, order) {
|
|
var tags = order && Array.isArray(order.invalidation_tags) ? order.invalidation_tags : [
|
|
"orders",
|
|
orderTag(orderNumber),
|
|
];
|
|
window.AksrtModalRuntime.invalidate(tags);
|
|
}
|
|
|
|
function activeDetail(state, number) {
|
|
return state.detailOrderNumber === number
|
|
&& !!state.detailSession
|
|
&& !state.detailSession.closed
|
|
&& state.detailSession.options
|
|
&& state.detailSession.options.kind === "orders"
|
|
&& window.AksrtModalRuntime.getActive() === state.detailSession.shell;
|
|
}
|
|
|
|
function detailSkeleton() {
|
|
return '<div class="aksrt-order-detail__skeleton" aria-live="polite" aria-busy="true">' +
|
|
'<span class="aksrt-modal-runtime__skeleton-line is-wide"></span>' +
|
|
'<span class="aksrt-modal-runtime__skeleton-line"></span>' +
|
|
'<span class="aksrt-modal-runtime__skeleton-block"></span>' +
|
|
'</div>';
|
|
}
|
|
|
|
function statusMeta(status) {
|
|
var map = {
|
|
pending: { icon: "fa-clock", desc: "订单已生成,请在有效期内完成支付" },
|
|
paid: { icon: "fa-check-circle", desc: "支付已完成,相关权益已发放至账户" },
|
|
refunded: { icon: "fa-undo-alt", desc: "退款已完成,款项将原路退回" },
|
|
closed: { icon: "fa-times-circle", desc: "订单已关闭,如有需要可重新下单" },
|
|
};
|
|
return map[status] || map.closed;
|
|
}
|
|
|
|
function amountCaption(status) {
|
|
if (status === "pending") return "应付款";
|
|
if (status === "closed") return "订单金额";
|
|
return "实付款";
|
|
}
|
|
|
|
function paymentMethodText(order) {
|
|
var method = order && order.payment_method ? order.payment_method : {};
|
|
// 卡密核销不是真实收银渠道,直接回退的原始 key 对用户没有意义。
|
|
if (method.value === "card_pass") return "卡密核销";
|
|
return String(method.label || "");
|
|
}
|
|
|
|
function detailRow(label, value, copyable) {
|
|
var text = value == null ? "" : String(value);
|
|
var cell = '<span class="aksrt-order-detail__value-text' + (copyable ? " is-mono" : "") + '">' +
|
|
escapeHtml(text || "—") + "</span>";
|
|
if (copyable && text !== "") {
|
|
cell += '<button class="aksrt-order-detail__copy" type="button" data-aksrt-order-copy="' +
|
|
escapeHtml(text) + '" aria-label="复制' + escapeHtml(label) + '" title="复制' + escapeHtml(label) + '">' +
|
|
'<i class="fa fas fa-copy" aria-hidden="true"></i></button>';
|
|
}
|
|
return "<div><dt>" + escapeHtml(label) + "</dt><dd>" + cell + "</dd></div>";
|
|
}
|
|
|
|
function gatewayResultText(value) {
|
|
if (!value) return "";
|
|
var result = value;
|
|
if (typeof value === "string") {
|
|
try { result = JSON.parse(value); } catch (_) { return String(value); }
|
|
}
|
|
if (!result || typeof result !== "object") return String(result);
|
|
var labels = { succeeded: "退款成功", failed: "渠道明确拒绝", pending: "渠道处理中", unknown: "结果待确认" };
|
|
var parts = [];
|
|
if (result.status) parts.push(labels[String(result.status)] || String(result.status));
|
|
if (result.message) parts.push(String(result.message));
|
|
if (result.refund_id) parts.push("退款单号 " + String(result.refund_id));
|
|
return parts.join(" · ") || "渠道暂未提供可读结果";
|
|
}
|
|
|
|
function afterSaleAmount(value, order) {
|
|
var amount = String(value == null ? "" : value);
|
|
return order && order.is_points ? amount + " " + String(order.currency_symbol || "积分") : "¥" + amount;
|
|
}
|
|
|
|
function renderDetail(state, order, session) {
|
|
var allowedActions = order.allowed_actions || {};
|
|
var reconciliation = order.reconciliation || {};
|
|
var paymentMethod = order.payment_method || {};
|
|
var image = order.image || "";
|
|
var title = order.product_name || "订单";
|
|
var status = order.status_group || "closed";
|
|
var meta = statusMeta(status);
|
|
var timeLeft = Number(order.time_left || 0);
|
|
var html = "";
|
|
|
|
// 状态横幅:整块配色只由 --hero-accent 驱动,换状态只改一处变量。
|
|
html += '<section class="aksrt-order-detail__hero is-' + escapeHtml(status) + '">';
|
|
html += '<span class="aksrt-order-detail__hero-icon" aria-hidden="true"><i class="fa fas ' + meta.icon + '"></i></span>';
|
|
html += '<div class="aksrt-order-detail__hero-copy">';
|
|
html += '<strong class="aksrt-order-detail__status">' + escapeHtml(order.status_label || "") + "</strong>";
|
|
html += '<p class="aksrt-order-detail__desc">' + escapeHtml(meta.desc) + "</p>";
|
|
if (status === "pending" && timeLeft > 0) {
|
|
html += '<span class="aksrt-order-detail__countdown" data-aksrt-order-countdown="' + timeLeft + '">剩余支付时间 ' + timeLeft + " 秒</span>";
|
|
}
|
|
html += "</div>";
|
|
html += '<div class="aksrt-order-detail__hero-amount"><strong>' + formatMoney(order) + "</strong>";
|
|
html += "<span>" + escapeHtml(amountCaption(status)) + "</span></div></section>";
|
|
|
|
html += '<section class="aksrt-order-detail__group">';
|
|
html += '<h4 class="aksrt-order-detail__group-title">商品信息</h4>';
|
|
html += '<div class="aksrt-order-detail__product">';
|
|
html += image
|
|
? '<img src="' + escapeHtml(image) + '" class="aksrt-order-detail__thumb" alt="">'
|
|
: '<span class="aksrt-order-detail__thumb aksrt-order-detail__thumb--empty" aria-hidden="true"><i class="fa fas fa-book"></i></span>';
|
|
html += '<div class="aksrt-order-detail__product-copy">';
|
|
html += '<strong class="aksrt-order-detail__product-title">' + escapeHtml(title) + "</strong>";
|
|
html += '<span class="aksrt-order-detail__product-meta">' + escapeHtml(order.product_type_label || "") + "</span>";
|
|
html += "</div></div></section>";
|
|
|
|
html += '<section class="aksrt-order-detail__group">';
|
|
html += '<h4 class="aksrt-order-detail__group-title">交易信息</h4>';
|
|
html += '<dl class="aksrt-order-detail__list">';
|
|
[
|
|
["订单号", order.order_number],
|
|
["支付方式", paymentMethodText(order)],
|
|
["创建时间", order.created_at],
|
|
["支付时间", order.paid_at],
|
|
].forEach(function (row) {
|
|
html += detailRow(row[0], row[1], row[0] === "订单号");
|
|
});
|
|
html += "</dl></section>";
|
|
|
|
var afterSales = Array.isArray(order.after_sales) ? order.after_sales : [];
|
|
if (afterSales.length) {
|
|
html += '<section class="aksrt-order-detail__group"><h4 class="aksrt-order-detail__group-title">售后记录</h4><ul class="aksrt-order-detail__after-sales">';
|
|
afterSales.forEach(function (item) {
|
|
var labels = { pending: "待审核", processing: "处理中", approved: "已通过", rejected: "已拒绝", cancelled: "已取消", completed: "已完成" };
|
|
html += "<li><strong>" + escapeHtml(labels[item.status] || item.status || "") + "</strong><span>" + escapeHtml(item.reason || "") + "</span><small>退款金额:" + escapeHtml(afterSaleAmount(item.refund_amount, order)) + "</small>";
|
|
if (item.admin_note) html += "<small>处理备注:" + escapeHtml(item.admin_note) + "</small>";
|
|
if (item.gateway_result) html += "<small>渠道结果:" + escapeHtml(gatewayResultText(item.gateway_result)) + "</small>";
|
|
if (item.status === "pending") html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-cancel="' + escapeHtml(order.order_number) + '">取消申请</button>';
|
|
html += "</li>";
|
|
});
|
|
html += "</ul></section>";
|
|
}
|
|
|
|
if (reconciliation.state === "refresh_available") {
|
|
html += '<div class="aksrt-order-detail__refresh-state" role="status">';
|
|
html += '<i class="fa fas fa-info-circle" aria-hidden="true"></i>';
|
|
html += "<p>支付状态可能仍在同步,请刷新后查看。</p>";
|
|
html += '<button class="btn-outline" type="button" data-aksrt-order-refresh="' + escapeHtml(order.order_number) + '">刷新支付状态</button>';
|
|
html += "</div>";
|
|
}
|
|
|
|
html += '<footer class="aksrt-modal__footer aksrt-order-detail__actions">';
|
|
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>';
|
|
// 售后申请为独立弹窗,不再占用订单详情(order-after-sale.js)。
|
|
html += afterSaleButtons(order.order_number, order.after_sale || {});
|
|
html += "</footer>";
|
|
|
|
session.content.innerHTML = html;
|
|
}
|
|
|
|
function detailOptions(state, orderNumber, refresh) {
|
|
var actionKey = refresh ? "refresh:" + orderNumber : "";
|
|
if (actionKey && !state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey();
|
|
return {
|
|
url: orderPath(orderNumber, refresh ? "/refresh" : ""),
|
|
method: refresh ? "POST" : "GET",
|
|
headers: actionKey ? { "Idempotency-Key": state.operationKeys[actionKey] } : {},
|
|
kind: "orders",
|
|
title: "订单详情",
|
|
className: "aksrt-order-detail-modal",
|
|
responseType: "json",
|
|
cache: !refresh,
|
|
cacheKey: "orders:detail:" + orderNumber,
|
|
cacheTags: ["orders", orderTag(orderNumber)],
|
|
skeleton: detailSkeleton(),
|
|
onState: function (_, session) {
|
|
if (orderNumber === state.detailOrderNumber) state.detailSession = session;
|
|
},
|
|
renderData: function (data, session) {
|
|
if (orderNumber !== state.detailOrderNumber || !instances.has(state.root)) return false;
|
|
state.detailSession = session;
|
|
if (!data || typeof data !== "object") {
|
|
session.content.innerHTML = '<p class="c-red">订单不存在</p>';
|
|
return;
|
|
}
|
|
renderDetail(state, data, session);
|
|
},
|
|
onData: function (data) {
|
|
if (refresh) {
|
|
delete state.operationKeys[actionKey];
|
|
invalidateOrder(orderNumber, data);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function openDetail(state, orderNumber) {
|
|
if (!orderNumber) return;
|
|
state.detailSession = null;
|
|
state.detailReplacement = false;
|
|
state.detailReplacementSession = null;
|
|
state.detailOrderNumber = orderNumber;
|
|
window.AksrtModalRuntime.openRemote(detailOptions(state, orderNumber, false));
|
|
}
|
|
|
|
function refreshDetail(state, orderNumber) {
|
|
if (!orderNumber || !activeDetail(state, orderNumber)) return;
|
|
state.detailReplacement = true;
|
|
state.detailReplacementSession = state.detailSession;
|
|
var refreshPromise = window.AksrtModalRuntime.openRemote(detailOptions(state, orderNumber, true));
|
|
refreshPromise.then(function () {
|
|
state.detailReplacement = false;
|
|
state.detailReplacementSession = null;
|
|
}, function () {
|
|
state.detailReplacement = false;
|
|
state.detailReplacementSession = null;
|
|
});
|
|
}
|
|
|
|
function currentViewPath(state) {
|
|
var query = new URLSearchParams();
|
|
if (state.query.status && state.query.status !== "all") query.set("order_status", state.query.status);
|
|
if (state.query.type) query.set("order_type", state.query.type);
|
|
if (state.query.orderNumber) query.set("order_number", state.query.orderNumber);
|
|
if (Number(state.query.page) > 1) query.set("order_page", String(state.query.page));
|
|
return "/orders/view" + (query.toString() ? "?" + query.toString() : "");
|
|
}
|
|
|
|
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>';
|
|
// 售后入口:申请售后 / 售后详细走 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;
|
|
}
|
|
|
|
function cardHtml(order) {
|
|
var image = order.image
|
|
? '<img src="' + escapeHtml(order.image) + '" class="aksrt-orders-page__card-img" alt="">'
|
|
: '<div class="aksrt-orders-page__card-art" aria-hidden="true"><i class="fa fas fa-book"></i></div>';
|
|
var countdown = Number(order.time_left || 0) > 0
|
|
? '<span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="' + Number(order.time_left) + '"></span>'
|
|
: "";
|
|
return '<article class="aksrt-orders-page__card" data-aksrt-order-card="' + escapeHtml(order.order_number) + '" data-order-status="' + escapeHtml(order.status_group || "") + '">' +
|
|
'<div class="aksrt-orders-page__card-rail"><span>' + escapeHtml(order.product_type_label || "") + '</span><time>' + escapeHtml(order.created_at || "") + '</time><span class="aksrt-orders-page__card-status is-' + escapeHtml(order.status_group || "") + '">' + escapeHtml(order.status_label || "") + "</span></div>" +
|
|
'<div class="aksrt-orders-page__card-main">' + image + '<div class="aksrt-orders-page__card-copy"><h3>' + escapeHtml(order.product_name || "订单") + "</h3><p>订单号 " + escapeHtml(order.order_number) + "</p></div><div class=\"aksrt-orders-page__card-value\"><strong>" + formatMoney(order) + "</strong><span>实付</span></div></div>" +
|
|
'<footer class="aksrt-orders-page__card-actions">' + countdown + actionHtml(order) + "</footer></article>";
|
|
}
|
|
|
|
function renderStats(state, stats) {
|
|
var root = state.root.querySelector("[data-aksrt-order-stats]");
|
|
if (!root) return;
|
|
root.innerHTML = '<div class="aksrt-orders-page__stat"><span>全部</span><strong>' + Number(stats.total || 0) + "</strong></div>" +
|
|
'<div class="aksrt-orders-page__stat"><span>待支付</span><strong class="is-pending">' + Number(stats.pending || 0) + "</strong></div>" +
|
|
'<div class="aksrt-orders-page__stat"><span>已支付</span><strong class="is-paid">' + Number(stats.paid || 0) + "</strong></div>" +
|
|
'<div class="aksrt-orders-page__stat"><span>已退款</span><strong>' + Number(stats.refunded || 0) + "</strong></div>" +
|
|
'<div class="aksrt-orders-page__stat"><span>已关闭</span><strong>' + Number(stats.closed || 0) + "</strong></div>" +
|
|
'<div class="aksrt-orders-page__stat"><span>售后待处理</span><strong>' + Number(stats.after_sale_pending || 0) + "</strong></div>";
|
|
var pendingCount = state.root.querySelector('[data-aksrt-order-filter="status"][data-aksrt-order-filter-value="pending"] .aksrt-orders-page__filter-count');
|
|
if (pendingCount) {
|
|
pendingCount.textContent = String(Number(stats.pending || 0));
|
|
pendingCount.hidden = Number(stats.pending || 0) < 1;
|
|
}
|
|
}
|
|
|
|
function renderPagination(state, view) {
|
|
var existing = state.root.querySelector(".aksrt-orders-page__pagination");
|
|
if (existing) existing.remove();
|
|
if (Number(view.total_pages || 1) <= 1) return;
|
|
var root = document.createElement("nav");
|
|
root.className = "aksrt-orders-page__pagination";
|
|
root.setAttribute("aria-label", "订单分页");
|
|
for (var i = 1; i <= Number(view.total_pages); i++) {
|
|
var button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "aksrt-orders-page__page-chip" + (i === Number(view.page) ? " is-active" : "");
|
|
button.setAttribute("data-aksrt-order-page", String(i));
|
|
if (i === Number(view.page)) button.setAttribute("aria-current", "page");
|
|
button.textContent = String(i);
|
|
root.appendChild(button);
|
|
}
|
|
var list = state.root.querySelector("[data-aksrt-order-list]");
|
|
if (list) list.after(root);
|
|
}
|
|
|
|
function countdownScopes(state) {
|
|
// 详情弹窗挂在 body 上,不在 state.root 内,需要单独纳入倒计时扫描。
|
|
var scopes = [state.root];
|
|
if (state.detailSession && state.detailSession.content) scopes.push(state.detailSession.content);
|
|
return scopes;
|
|
}
|
|
|
|
function updateCountdowns(state) {
|
|
countdownScopes(state).forEach(function (scope) {
|
|
if (!scope || !scope.querySelectorAll) return;
|
|
scope.querySelectorAll("[data-aksrt-order-countdown]").forEach(function (node) {
|
|
var value = Math.max(0, Number(node.getAttribute("data-aksrt-order-countdown") || 0) - 1);
|
|
node.setAttribute("data-aksrt-order-countdown", String(value));
|
|
node.textContent = value ? "剩余支付时间 " + value + " 秒" : "订单即将关闭";
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderView(state, view) {
|
|
var viewQuery = view.query || {};
|
|
state.query.status = String(viewQuery.order_status || "all");
|
|
state.query.type = String(viewQuery.order_type || "");
|
|
state.query.orderNumber = String(viewQuery.order_number || "");
|
|
state.query.page = Math.max(1, Number(view.page || viewQuery.order_page || 1));
|
|
renderStats(state, view.stats || {});
|
|
var list = state.root.querySelector("[data-aksrt-order-list]");
|
|
if (!list) return;
|
|
var items = Array.isArray(view.items) ? view.items : [];
|
|
if (!items.length) {
|
|
list.innerHTML = '<i class="fa fas fa-file-alt" aria-hidden="true"></i><h3>没有匹配的订单</h3><p>换个条件试试,或清除当前筛选。</p>';
|
|
list.classList.remove("aksrt-orders-page__list");
|
|
list.classList.add("aksrt-orders-page__empty");
|
|
} else {
|
|
list.classList.remove("aksrt-orders-page__empty");
|
|
list.classList.add("aksrt-orders-page__list");
|
|
list.innerHTML = items.map(cardHtml).join("");
|
|
}
|
|
renderPagination(state, view);
|
|
syncControls(state);
|
|
updateCountdowns(state);
|
|
}
|
|
|
|
function syncControls(state) {
|
|
state.root.querySelectorAll("[data-aksrt-order-filter]").forEach(function (button) {
|
|
var field = button.getAttribute("data-aksrt-order-filter");
|
|
var expected = field === "status" ? state.query.status : state.query.type;
|
|
var active = button.getAttribute("data-aksrt-order-filter-value") === expected;
|
|
button.classList.toggle("is-active", active);
|
|
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
});
|
|
var input = state.root.querySelector("#aksrt-order-number");
|
|
if (input && input.value !== state.query.orderNumber) input.value = state.query.orderNumber;
|
|
}
|
|
|
|
function setViewLoading(state, loading) {
|
|
state.root.classList.toggle("is-loading", loading);
|
|
state.root.setAttribute("aria-busy", loading ? "true" : "false");
|
|
}
|
|
|
|
function loadView(state, patch) {
|
|
Object.keys(patch || {}).forEach(function (key) { state.query[key] = patch[key]; });
|
|
var requestId = ++state.viewRequest;
|
|
setViewLoading(state, true);
|
|
return request(currentViewPath(state), undefined, "GET").then(function (result) {
|
|
if (requestId !== state.viewRequest) return null;
|
|
if (!result || !result.data) throw new Error("刷新失败");
|
|
renderView(state, result.data);
|
|
return result.data;
|
|
}).catch(function (error) {
|
|
if (requestId === state.viewRequest) notyf(errorMessage(error, "订单加载失败,请稍后重试"), "danger");
|
|
return null;
|
|
}).finally(function () {
|
|
if (requestId === state.viewRequest) setViewLoading(state, false);
|
|
});
|
|
}
|
|
|
|
function refreshView(state) {
|
|
return loadView(state);
|
|
}
|
|
|
|
function startPayment(state, button) {
|
|
var number = button.getAttribute("data-aksrt-order-pay") || "";
|
|
var method = button.getAttribute("data-aksrt-order-method") || "";
|
|
if (!number || button.disabled) return;
|
|
var actionKey = "pay:" + number;
|
|
if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey();
|
|
button.disabled = true;
|
|
request(orderPath(number, "/pay"), method ? { payment_method: method } : {}, "POST", state.operationKeys[actionKey]).then(function (result) {
|
|
delete state.operationKeys[actionKey];
|
|
var data = result.data || {};
|
|
invalidateOrder(number, data.order);
|
|
if (data.redirect && data.payment_url) {
|
|
window.location.assign(data.payment_url);
|
|
return;
|
|
}
|
|
notyf(data.message || "支付成功", "success");
|
|
refreshView(state);
|
|
if (activeDetail(state, number)) openDetail(state, number);
|
|
}).catch(function (error) {
|
|
button.disabled = false;
|
|
notyf(errorMessage(error, "网络错误,请稍后重试"), "danger");
|
|
});
|
|
}
|
|
|
|
function closeOrder(state, button) {
|
|
var number = button.getAttribute("data-aksrt-order-cancel") || "";
|
|
if (!number || button.disabled) return;
|
|
if (button.getAttribute("data-aksrt-confirm") !== "true") {
|
|
button.setAttribute("data-aksrt-confirm", "true");
|
|
button.textContent = "确认关闭";
|
|
return;
|
|
}
|
|
button.disabled = true;
|
|
var actionKey = "cancel:" + number;
|
|
if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey();
|
|
request(orderPath(number, "/cancel"), {}, "POST", state.operationKeys[actionKey]).then(function (result) {
|
|
delete state.operationKeys[actionKey];
|
|
var data = result.data || {};
|
|
invalidateOrder(number, data.order);
|
|
notyf("订单已关闭", "success");
|
|
refreshView(state);
|
|
if (activeDetail(state, number)) openDetail(state, number);
|
|
}).catch(function (error) {
|
|
button.disabled = false;
|
|
button.removeAttribute("data-aksrt-confirm");
|
|
button.textContent = "关闭订单";
|
|
notyf(errorMessage(error, "网络错误,请稍后重试"), "danger");
|
|
});
|
|
}
|
|
|
|
function copyOrderValue(button) {
|
|
var value = button.getAttribute("data-aksrt-order-copy") || "";
|
|
if (!value) return;
|
|
var icon = button.querySelector("i");
|
|
var settle = function (ok) {
|
|
notyf(ok ? "订单号已复制" : "复制失败,请手动选择复制", ok ? "success" : "danger");
|
|
if (!ok) return;
|
|
button.classList.add("is-copied");
|
|
if (icon) icon.className = "fa fas fa-check";
|
|
window.setTimeout(function () {
|
|
button.classList.remove("is-copied");
|
|
if (icon) icon.className = "fa fas fa-copy";
|
|
}, 1600);
|
|
};
|
|
var fallback = function () {
|
|
try {
|
|
var field = document.createElement("textarea");
|
|
field.value = value;
|
|
field.setAttribute("readonly", "readonly");
|
|
field.style.position = "fixed";
|
|
field.style.top = "-1000px";
|
|
field.style.opacity = "0";
|
|
document.body.appendChild(field);
|
|
field.select();
|
|
document.execCommand("copy");
|
|
document.body.removeChild(field);
|
|
settle(true);
|
|
} catch (error) {
|
|
settle(false);
|
|
}
|
|
};
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(value).then(function () { settle(true); }, fallback);
|
|
return;
|
|
}
|
|
fallback();
|
|
}
|
|
|
|
function closest(target, selector) {
|
|
return target && target.closest ? target.closest(selector) : null;
|
|
}
|
|
|
|
function initRoot(root) {
|
|
if (instances.has(root)) return;
|
|
var state = {
|
|
root: root,
|
|
detailOrderNumber: "",
|
|
detailSession: null,
|
|
detailReplacement: false,
|
|
detailReplacementSession: null,
|
|
countdownTimer: null,
|
|
operationKeys: {},
|
|
query: { status: "all", type: "", orderNumber: "", page: 1 },
|
|
viewRequest: 0,
|
|
};
|
|
|
|
state.onRootClick = function (event) {
|
|
var filterToggle = closest(event.target, "[data-aksrt-orders-filter-toggle]");
|
|
if (filterToggle && root.contains(filterToggle)) {
|
|
var panel = root.querySelector("[data-aksrt-orders-filter-panel]");
|
|
var expanded = filterToggle.getAttribute("aria-expanded") === "true";
|
|
filterToggle.setAttribute("aria-expanded", String(!expanded));
|
|
if (panel) panel.hidden = expanded;
|
|
return;
|
|
}
|
|
var filter = closest(event.target, "[data-aksrt-order-filter]");
|
|
if (filter && root.contains(filter)) {
|
|
event.preventDefault();
|
|
var field = filter.getAttribute("data-aksrt-order-filter");
|
|
var patch = { page: 1 };
|
|
patch[field] = filter.getAttribute("data-aksrt-order-filter-value") || "";
|
|
loadView(state, patch);
|
|
return;
|
|
}
|
|
var page = closest(event.target, "[data-aksrt-order-page]");
|
|
if (page && root.contains(page)) {
|
|
event.preventDefault();
|
|
loadView(state, { page: Math.max(1, Number(page.getAttribute("data-aksrt-order-page") || 1)) });
|
|
return;
|
|
}
|
|
var clear = closest(event.target, "[data-aksrt-orders-clear]");
|
|
if (clear && root.contains(clear)) {
|
|
event.preventDefault();
|
|
loadView(state, { status: "all", type: "", orderNumber: "", page: 1 });
|
|
return;
|
|
}
|
|
var detail = closest(event.target, "[data-aksrt-order-detail]");
|
|
if (detail && root.contains(detail)) { openDetail(state, detail.getAttribute("data-aksrt-order-detail")); return; }
|
|
var pay = closest(event.target, "[data-aksrt-order-pay]");
|
|
if (pay && root.contains(pay)) { event.preventDefault(); startPayment(state, pay); return; }
|
|
var cancel = closest(event.target, "[data-aksrt-order-cancel]");
|
|
if (cancel && root.contains(cancel)) { event.preventDefault(); closeOrder(state, cancel); }
|
|
};
|
|
state.onRootSubmit = function (event) {
|
|
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.onModalClose = function (event) {
|
|
var detail = event && event.detail ? event.detail : {};
|
|
if (!state.detailSession || detail.session !== state.detailSession) return;
|
|
if (detail.reason === "replace" && state.detailReplacement && detail.session === state.detailReplacementSession) return;
|
|
state.detailSession = null;
|
|
state.detailOrderNumber = "";
|
|
state.detailReplacement = false;
|
|
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;
|
|
}
|
|
}
|
|
if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return;
|
|
var copy = closest(event.target, "[data-aksrt-order-copy]");
|
|
if (copy) {
|
|
event.preventDefault();
|
|
copyOrderValue(copy);
|
|
return;
|
|
}
|
|
var refresh = closest(event.target, "[data-aksrt-order-refresh]");
|
|
if (refresh) {
|
|
event.preventDefault();
|
|
refreshDetail(state, refresh.getAttribute("data-aksrt-order-refresh") || "");
|
|
return;
|
|
}
|
|
var pay = closest(event.target, "[data-aksrt-order-pay]");
|
|
if (pay && !root.contains(pay)) {
|
|
event.preventDefault();
|
|
startPayment(state, pay);
|
|
return;
|
|
}
|
|
var cancel = closest(event.target, "[data-aksrt-order-cancel]");
|
|
if (cancel && !root.contains(cancel)) {
|
|
event.preventDefault();
|
|
closeOrder(state, cancel);
|
|
}
|
|
};
|
|
|
|
root.addEventListener("click", state.onRootClick);
|
|
root.addEventListener("submit", state.onRootSubmit);
|
|
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) {
|
|
if (!root) return [];
|
|
var roots = [];
|
|
if (root.matches && root.matches("[data-aksrt-order-center]")) roots.push(root);
|
|
if (root.querySelectorAll) root.querySelectorAll("[data-aksrt-order-center]").forEach(function (item) { roots.push(item); });
|
|
return roots;
|
|
}
|
|
|
|
function init(root) {
|
|
rootsIn(root || document).forEach(initRoot);
|
|
}
|
|
|
|
function destroy(root) {
|
|
rootsIn(root).forEach(function (item) {
|
|
var state = instances.get(item);
|
|
if (!state) return;
|
|
if (activeDetail(state, state.detailOrderNumber)) window.AksrtModalRuntime.close("replace");
|
|
item.removeEventListener("click", state.onRootClick);
|
|
item.removeEventListener("submit", state.onRootSubmit);
|
|
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; });
|
|
});
|
|
}
|
|
|
|
// 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); });
|
|
}());
|