The old 售后 flow was an order-refund appendage: tickets were hard-bound to an order number, users could only file them from 「我的订单」against a paid order, and the admin action bottomed out at 「标记订单已退款」. It could not carry account questions or feature requests that have nothing to do with an order. Replace it with a general ticket system modelled on 魔方财务云's idcsmart_ticket: title + department + description are required, linking an order is optional (user's own paid orders only). Add departments, custom statuses, preset replies and internal admin notes as four configuration surfaces. Conversation history moves off the ticket row's JSON log into dedicated ticket_reply rows, which unlocks quoted replies. Front end gets a 工单中心 panel under 我的服务; back end moves from 支付中心 → 售后服务 to users.php → 工单管理. Per CLAUDE.md's rewrite-over-compatibility rule: drop the old table, build new ones, delete the old code. No dual paths, no data migration. The upstream plugin's upstream/downstream delivery and internal-ticket conversion are deliberately left out — both are multi-provider platform concerns with no meaning on a single WordPress site. Data: order_aftersale becomes six aksrt_ticket* tables (ticket, ticket_reply, ticket_status, ticket_type, ticket_prereply, ticket_note). SCHEMA_VERSION 2.6.0 -> 3.0.0. Status and department seeds are idempotent; the default status 「待接单」is flagged as non-editable. Domain: new src/Ticket/ (13 files, all under 460 lines). Attachments reuse UserController::rest_upload_comment_image with a ticket-specific permission callback, mirroring the existing /report/upload-image route rather than growing a second upload path. Notifications go through AKSRT\Messages\Message::send() and deep-link to the right surface; email/SMS stay out of scope since the theme's mail and SMS stacks are separate implementations. Refund gate opens up: marking an order refunded no longer requires an in-flight ticket, only that the order is paid. Also generalise a hardcoded special case in tools/audit-admin-contracts.php — schema domain inference now reads the parent directory instead of matching "PostList" by name, so new leaf schema files need no per-name exception. Verification: php tools/audit-admin-contracts.php exit 0, errors=0 node --test (widget-system + admin) 664 pass / 0 fail php -l over all new/changed files clean node --check over all JS clean npm run build + build:public clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
622 lines
31 KiB
JavaScript
622 lines
31 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 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 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 = "";
|
|
// 炫彩标题区(modal-mini + jb-blue),运行时不再渲染默认标题栏。
|
|
html += colorfulHeader("订单详情", "fa-receipt");
|
|
|
|
// 状态横幅:整块配色只由 --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>";
|
|
|
|
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>";
|
|
}
|
|
|
|
// 底部按钮:but-average modal-buts;「关闭」与「我知道了」
|
|
// 同款(but c-blue),平铺观感由 orders.css 的 footer 按钮规则统一呈现。
|
|
html += '<footer class="aksrt-order-detail__actions but-average modal-buts">';
|
|
html += '<button type="button" class="but c-blue" data-aksrt-modal-close>关闭</button>';
|
|
if (allowedActions.can_cancel) html += '<button type="button" class="but" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
|
if (allowedActions.can_pay) html += '<button type="button" class="but c-blue" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
|
|
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",
|
|
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 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>';
|
|
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.closed || 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) {
|
|
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 供弹窗提交/取消后刷新列表与统计。
|
|
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); });
|
|
}());
|