Files
wordpress-theme-aksrt/assets/js/order-center.js
T
kate522 ad40d9a7ca fix(orders): wire detail progress timeline and two-step cancel
Order progress was stuck on "订单创建" because timeline only read sparse
timestamp columns and status transitions didn't always stamp them.
Drive timeline from current status + related records, backfill lifecycle
timestamps on update_status, and replace window.confirm cancel with a
关闭订单 → 确认取消 button step in the detail modal.
2026-07-27 15:15:35 +08:00

1152 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AKSRT Order Center — v2.0
*
* 前台订单中心交互:
* - 支付 / 取消 / 售后:操作成功后局部刷新订单卡片,不再整页 reload
* - 倒计时
* - Toast 通知替代 window.alert
*
* @package AKSRT
*/
(function () {
"use strict";
var page = document.querySelector("[data-aksrt-order-center]");
if (!page || !window.aksrtData) {
return;
}
/* ── Toast helper ── */
function showToast(text, duration) {
duration = duration || 2500;
var container = document.getElementById("toast-container");
if (!container) {
container = document.createElement("div");
container.id = "toast-container";
container.style.cssText = "position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column-reverse;gap:10px;pointer-events:none;";
document.body.appendChild(container);
}
var toast = document.createElement("div");
toast.style.cssText = "pointer-events:auto;display:flex;align-items:center;gap:8px;padding:10px 20px;background:rgba(0,0,0,0.78);backdrop-filter:blur(12px);color:#fff;font-family:var(--font-ui);font-size:14px;font-weight:500;border-radius:100px;box-shadow:0 4px 20px rgba(0,0,0,0.2);animation:aksrtToastIn 0.35s cubic-bezier(0.22,1,0.36,1) forwards;white-space:nowrap;";
toast.textContent = text;
container.appendChild(toast);
setTimeout(function () {
toast.style.animation = "aksrtToastOut 0.4s ease forwards";
setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 400);
}, duration);
}
// Inject toast keyframes if not present
if (!document.getElementById("aksrt-toast-keyframes")) {
var style = document.createElement("style");
style.id = "aksrt-toast-keyframes";
style.textContent = "@keyframes aksrtToastIn{0%{opacity:0;transform:translateY(16px) scale(0.92)}100%{opacity:1;transform:translateY(0) scale(1)}}@keyframes aksrtToastOut{0%{opacity:1;transform:translateY(0) scale(1)}100%{opacity:0;transform:translateY(8px) scale(0.95)}}";
document.head.appendChild(style);
}
/* ── REST helper ── */
function restRoot() {
return ((aksrtData.siteUrl || "").replace(/\/$/, "")) + "/wp-json/aksrt/v1";
}
function request(path, body, method) {
var opts = {
method: method || "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"X-WP-Nonce": (aksrtData && aksrtData.restNonce) || ""
}
};
if (body !== undefined) {
opts.body = JSON.stringify(body || {});
}
return fetch(restRoot() + path, opts).then(function (response) {
return response.text().then(function (text) {
var data = {};
try {
data = text ? JSON.parse(text) : {};
} catch (e) {
data = {
success: false,
message: response.status === 404
? "支付接口未找到(404),请刷新页面或联系管理员。"
: ("请求失败(HTTP " + response.status + ")")
};
}
if (typeof data.success === "undefined" && response.ok === false) {
data.success = false;
data.message = data.message || data.code || ("HTTP " + response.status);
}
return data;
});
});
}
function orderPath(orderNumber, suffix) {
var base = "/shop/frontend/orders/" + encodeURIComponent(orderNumber || "");
return suffix ? base + suffix : base;
}
function cartUrl() {
return aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart) || "/";
}
/* ── Order review modal (zibll 1:1) ── */
var STAR_SVG =
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>';
var SCORE_LABELS = ["", "非常差", "较差", "一般", "较好", "非常好"];
function formatRemain(sec) {
sec = Math.max(0, sec | 0);
var d = Math.floor(sec / 86400);
var h = Math.floor((sec % 86400) / 3600);
var m = Math.floor((sec % 3600) / 60);
var s = sec % 60;
function z(n) { return n < 10 ? "0" + n : String(n); }
if (d > 0) return d + "天" + z(h) + ":" + z(m) + ":" + z(s);
return z(h) + ":" + z(m) + ":" + z(s);
}
function bindShopScore(root) {
if (!root) return;
root.querySelectorAll("[data-aksrt-score-box], .shop-score-box").forEach(function (box) {
if (box.dataset.scoreBound === "1") return;
box.dataset.scoreBound = "1";
var iconBox = box.querySelector(".score-icon");
var textBox = box.querySelector(".score-text");
var input = box.querySelector(".score-input");
if (!iconBox) return;
var value = parseInt(box.getAttribute("score-value") || (input && input.value) || "0", 10) || 0;
var html = "";
for (var i = 1; i <= 5; i++) {
html += '<i class="icon-star' + (i <= value ? " active" : "") + '" data-score="' + i + '">' + STAR_SVG + "</i>";
}
iconBox.innerHTML = html;
function paint(score, hover) {
iconBox.querySelectorAll(".icon-star").forEach(function (star) {
var s = parseInt(star.getAttribute("data-score"), 10) || 0;
star.classList.toggle("active", !hover && s <= score);
star.classList.toggle("hover", !!hover && s <= score);
});
if (textBox) textBox.textContent = SCORE_LABELS[score] || "";
}
paint(value, false);
iconBox.addEventListener("mouseover", function (e) {
var star = e.target.closest(".icon-star");
if (!star || !iconBox.contains(star)) return;
paint(parseInt(star.getAttribute("data-score"), 10) || 0, true);
});
iconBox.addEventListener("mouseleave", function () {
var cur = parseInt(box.getAttribute("score-value") || "0", 10) || 0;
paint(cur, false);
});
iconBox.addEventListener("click", function (e) {
var star = e.target.closest(".icon-star");
if (!star || !iconBox.contains(star)) return;
var score = parseInt(star.getAttribute("data-score"), 10) || 0;
box.setAttribute("score-value", String(score));
if (input) input.value = String(score);
paint(score, false);
});
});
}
function bindReviewImages(form) {
var preview = form.querySelector("[data-aksrt-review-preview]");
var fileInput = form.querySelector("[data-aksrt-review-file]");
var addBtn = form.querySelector("[data-aksrt-review-add]");
if (!preview || !fileInput) return;
var max = parseInt(preview.getAttribute("data-max") || "6", 10) || 6;
var files = [];
function render() {
preview.querySelectorAll(".preview-item").forEach(function (n) { n.remove(); });
files.forEach(function (item, idx) {
var node = document.createElement("div");
node.className = "preview-item";
node.innerHTML = '<img alt=""><button type="button" class="remove" aria-label="删除">&times;</button>';
node.querySelector("img").src = item.url;
node.querySelector(".remove").addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
if (item.url && item.url.indexOf("blob:") === 0) {
try { URL.revokeObjectURL(item.url); } catch (err) {}
}
files.splice(idx, 1);
render();
});
preview.insertBefore(node, addBtn);
});
if (addBtn) addBtn.style.display = files.length >= max ? "none" : "";
form._aksrtReviewFiles = files.slice();
}
function onPick() {
if (files.length >= max) return;
fileInput.click();
}
if (addBtn) {
addBtn.addEventListener("click", onPick);
addBtn.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onPick();
}
});
}
fileInput.addEventListener("change", function () {
var list = Array.prototype.slice.call(fileInput.files || []);
list.forEach(function (file) {
if (files.length >= max) return;
if (!file.type || file.type.indexOf("image/") !== 0) return;
files.push({ file: file, url: URL.createObjectURL(file) });
});
fileInput.value = "";
render();
});
form._aksrtReviewFiles = [];
render();
}
function bindReviewCountdown(root) {
var el = root.querySelector("[data-aksrt-review-countdown]");
if (!el) return;
var remain = parseInt(el.getAttribute("data-aksrt-review-countdown") || "0", 10) || 0;
if (remain <= 0) return;
el.textContent = formatRemain(remain);
var timer = window.setInterval(function () {
remain -= 1;
if (remain <= 0) {
window.clearInterval(timer);
el.textContent = "交易已关闭";
return;
}
el.textContent = formatRemain(remain);
}, 1000);
root._aksrtReviewTimer = timer;
}
function fileToDataUrl(file) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function () { resolve(String(reader.result || "")); };
reader.onerror = function () { reject(new Error("read failed")); };
reader.readAsDataURL(file);
});
}
function openOrderReviewModal(orderNumber, triggerBtn) {
if (!window.AksrtRemoteModal || typeof window.AksrtRemoteModal.openRemote !== "function") {
showToast("评价组件未加载");
return;
}
if (triggerBtn) triggerBtn.disabled = true;
var url = restRoot() + orderPath(orderNumber, "/review-modal");
window.AksrtRemoteModal.openRemote({
url: url,
className: "modal-mini full-sm",
height: 380,
mobileBottom: true
}).then(function (shell) {
if (triggerBtn) triggerBtn.disabled = false;
if (!shell) return;
var form = shell.querySelector("[data-aksrt-order-review-form]");
if (!form) return;
bindShopScore(shell);
bindReviewImages(form);
bindReviewCountdown(shell);
form.addEventListener("submit", function (e) {
e.preventDefault();
var errBox = form.querySelector("[data-aksrt-review-error]");
function showErr(msg) {
if (errBox) {
errBox.hidden = false;
errBox.textContent = msg;
} else {
showToast(msg);
}
}
if (errBox) {
errBox.hidden = true;
errBox.textContent = "";
}
var productId = parseInt(form.getAttribute("data-product-id") || "0", 10) || 0;
var needShipping = form.getAttribute("data-need-shipping") === "1";
var scoreProduct = parseInt((form.querySelector('input[name="score[product]"]') || {}).value || "0", 10) || 0;
var scoreService = parseInt((form.querySelector('input[name="score[service]"]') || {}).value || "0", 10) || 0;
var scoreShipping = parseInt((form.querySelector('input[name="score[shipping]"]') || {}).value || "0", 10) || 0;
if (scoreProduct < 1 || scoreService < 1) {
showErr("请对商品进行评分");
return;
}
if (needShipping && scoreShipping < 1) {
showErr("请对物流服务进行评分");
return;
}
var comment = (form.querySelector('textarea[name="comment"]') || {}).value || "";
var submitBtn = form.querySelector("[data-aksrt-review-submit]");
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = "提交中…";
}
var pending = form._aksrtReviewFiles || [];
Promise.all(pending.map(function (item) {
return item.file ? fileToDataUrl(item.file) : Promise.resolve(item.url || "");
})).then(function (images) {
images = images.filter(Boolean);
return request(orderPath(orderNumber, "/review"), {
product_id: productId,
content: comment,
rating_product: scoreProduct,
rating_service: scoreService,
rating_shipping: needShipping ? scoreShipping : scoreService,
images: images
});
}).then(function (result) {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fa fa-check" aria-hidden="true"></i> 确认提交';
}
if (!result || !result.success) {
showErr((result && result.message) || "评价失败");
return;
}
showToast((result.message) || "评价成功");
if (window.AksrtRemoteModal) window.AksrtRemoteModal.close();
var card = findOrderCard(orderNumber);
updateCardStatus(card, "completed", "已完成");
if (card) {
var btn = card.querySelector("[data-aksrt-order-review]");
if (btn) btn.remove();
}
}).catch(function () {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fa fa-check" aria-hidden="true"></i> 确认提交';
}
showErr("网络错误");
});
});
}).catch(function (err) {
if (triggerBtn) triggerBtn.disabled = false;
showToast((err && err.message) || "无法打开评价");
});
}
/* ── 扫码支付弹层(微信 Native code_url 等)── */
function showPaymentQr(content, orderNumber) {
var existing = document.getElementById("aksrt-pay-qr-overlay");
if (existing) existing.remove();
var overlay = document.createElement("div");
overlay.id = "aksrt-pay-qr-overlay";
overlay.style.cssText = "position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);";
var qrImg = "";
if (window.AksrtShare && typeof window.AksrtShare.buildQrDataUrl === "function") {
var dataUrl = window.AksrtShare.buildQrDataUrl(content, 220);
if (dataUrl) {
qrImg = '<img src="' + dataUrl + '" alt="支付二维码" width="220" height="220" style="display:block;margin:0 auto;">';
}
}
if (!qrImg) {
qrImg = '<p style="word-break:break-all;font-size:12px;color:#666;">' + String(content).replace(/[<>&]/g, "") + "</p>";
}
var card = document.createElement("div");
card.style.cssText = "background:#fff;border-radius:12px;padding:24px;max-width:300px;text-align:center;box-shadow:0 10px 40px rgba(0,0,0,0.2);";
card.innerHTML =
'<h3 style="margin:0 0 12px;font-size:16px;">请扫码完成支付</h3>' +
qrImg +
'<p style="margin:12px 0 0;font-size:13px;color:#888;">支付完成后本页将自动更新</p>' +
'<button type="button" id="aksrt-pay-qr-close" style="margin-top:14px;padding:8px 20px;border:none;border-radius:8px;background:var(--color-primary,#f04494);color:#fff;cursor:pointer;">完成 / 关闭</button>';
overlay.appendChild(card);
document.body.appendChild(overlay);
function close() { overlay.remove(); }
overlay.addEventListener("click", function (e) { if (e.target === overlay) close(); });
card.querySelector("#aksrt-pay-qr-close").addEventListener("click", function () {
close();
// 关闭后查询订单是否已到账
if (orderNumber) {
request(orderPath(orderNumber), {}, "GET").then(function () {
window.location.reload();
}).catch(function () {});
}
});
}
/* ── 第三方支付返回提示(?payment=success|pending|cancelled)── */
(function paymentReturnNotice() {
var params = new URLSearchParams(window.location.search);
var pay = params.get("payment");
if (!pay) return;
var map = { success: "支付成功!", pending: "支付处理中,到账后将自动更新。", cancelled: "支付已取消。", fail: "支付失败,请重试。" };
showToast(map[pay] || "支付状态:" + pay, 3500);
})();
/* ── Local card refresh helpers ── */
function findOrderCard(orderNumber) {
return page.querySelector('[data-aksrt-order-card="' + orderNumber + '"]');
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function formatMoney(order) {
var amount = Number(order && order.amount != null ? order.amount : 0);
if (order && order.is_points) {
var symbol = order.currency_symbol || "积分";
return escapeHtml(symbol) + " " + Math.round(amount);
}
return "¥" + amount.toFixed(2);
}
/** Plain money for table cells (formatMoney already escapes). */
function formatMoneyText(order) {
var amount = Number(order && order.amount != null ? order.amount : 0);
if (order && order.is_points) {
var symbol = order.currency_symbol || "积分";
return String(symbol) + " " + Math.round(amount);
}
return "¥" + amount.toFixed(2);
}
function updateCardStatus(card, newStatus, statusLabel) {
if (!card) return;
if (newStatus) {
card.setAttribute("data-order-status", newStatus);
}
var badge = card.querySelector(".aksrt-orders-page__card-status, .aksrt-orders-page__item-badge");
if (badge) {
badge.textContent = statusLabel || newStatus;
}
// Hide action buttons that are no longer relevant
var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel], [data-aksrt-order-after-sale], [data-aksrt-order-receipt], [data-aksrt-order-rebuy]");
for (var i = 0; i < actions.length; i++) {
actions[i].style.display = "none";
}
// Hide countdown if present
var countdown = card.querySelector("[data-aksrt-order-countdown]");
if (countdown) {
countdown.style.display = "none";
}
}
/* ── Order detail modal ── */
var detailModal = page.querySelector("[data-aksrt-order-detail-modal]");
var detailBody = page.querySelector("[data-aksrt-order-detail-body]");
var detailFooter = page.querySelector("[data-aksrt-order-detail-footer]");
var detailOrderNumber = "";
function closeOrderDetailModal() {
if (detailModal) detailModal.hidden = true;
detailOrderNumber = "";
document.documentElement.classList.remove("aksrt-modal-open");
}
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && detailModal && !detailModal.hidden) {
closeOrderDetailModal();
}
});
function openOrderDetailModal(orderNumber) {
if (!detailModal || !detailBody || !detailFooter) {
showToast("订单详情组件未加载");
return;
}
detailOrderNumber = orderNumber || "";
detailBody.innerHTML = '<p class="muted-2-color">加载中…</p>';
detailFooter.innerHTML = "";
detailModal.hidden = false;
document.documentElement.classList.add("aksrt-modal-open");
request(orderPath(orderNumber), undefined, "GET").then(function (result) {
var order = null;
if (result && result.data && result.data.order_number) {
order = result.data;
} else if (result && result.data && result.data.data && result.data.data.order_number) {
order = result.data.data;
} else if (result && result.order_number) {
order = result;
}
if (result && result.success === false) {
detailBody.innerHTML = '<p class="c-red">' + escapeHtml((result && result.message) || "加载失败") + "</p>";
return;
}
if (!order || !order.order_number) {
detailBody.innerHTML = '<p class="c-red">订单不存在</p>';
return;
}
renderOrderDetail(order);
}).catch(function () {
detailBody.innerHTML = '<p class="c-red">网络错误,请稍后重试</p>';
});
}
function renderOrderDetail(order) {
if (!detailBody || !detailFooter) return;
var actions = (order && order.actions) || {};
var payment = order.payment_method || {};
var paymentLabel = payment.label || payment.value || "—";
var qty = order.quantity || 1;
if ((!qty || qty < 1) && order.items && order.items.length) {
qty = 0;
order.items.forEach(function (item) {
qty += Math.max(1, parseInt(item.quantity || item.qty || 1, 10) || 1);
});
}
if (!qty || qty < 1) qty = 1;
var status = order.status || "";
var statusLabel = order.status_label || status || "—";
var statusClass = "is-" + String(status).replace(/[^a-z0-9_-]/gi, "-");
var title = order.product_name || (order.product && order.product.title) || "订单";
var image = order.image || (order.product && order.product.image) || "";
if (!image && order.items && order.items.length && order.items[0].image) {
image = order.items[0].image;
}
var orderNo = order.order_number || "—";
// Meta fields only — amount lives in hero; type is already on the list card.
var rows = [
{ key: "qty", label: "数量", value: "×" + String(qty) },
{ key: "no", label: "订单编号", value: orderNo, copyable: true },
{ key: "created", label: "创建时间", value: order.created_at || "—" },
{ key: "paid", label: "支付时间", value: order.paid_at || "—" },
{ key: "pay", label: "支付方式", value: paymentLabel }
];
if (Number(order.shipping_fee || 0) > 0) {
rows.push({ key: "ship", label: "运费", value: "¥" + Number(order.shipping_fee || 0).toFixed(2) });
}
if (Number(order.discount || 0) > 0) {
rows.push({ key: "disc", label: "优惠", value: "-¥" + Number(order.discount || 0).toFixed(2), accent: "discount" });
}
if (order.customer_note) {
rows.push({ key: "note", label: "备注", value: order.customer_note });
}
var html = "";
html += '<div class="aksrt-order-detail__hero ' + escapeHtml(statusClass) + '">';
html += ' <div class="aksrt-order-detail__hero-top">';
html += ' <span class="aksrt-order-detail__status-pill ' + escapeHtml(statusClass) + '">' + escapeHtml(statusLabel) + "</span>";
if (order.product_type_label) {
html += ' <span class="aksrt-order-detail__type-chip">' + escapeHtml(order.product_type_label) + "</span>";
}
html += " </div>";
html += ' <div class="aksrt-order-detail__hero-amount-wrap">';
html += ' <span class="aksrt-order-detail__hero-label">应付金额</span>';
html += ' <strong class="aksrt-order-detail__hero-amount">' + escapeHtml(formatMoneyText(order)) + "</strong>";
html += " </div>";
html += "</div>";
html += '<div class="aksrt-order-detail__product">';
if (image) {
html += '<img class="aksrt-order-detail__thumb" src="' + escapeHtml(image) + '" alt="" loading="lazy">';
} else {
html += '<div class="aksrt-order-detail__thumb aksrt-order-detail__thumb--empty" aria-hidden="true"><i class="fas fa-receipt"></i></div>';
}
html += ' <div class="aksrt-order-detail__product-info">';
html += ' <div class="aksrt-order-detail__product-title">' + escapeHtml(title) + "</div>";
html += ' <div class="aksrt-order-detail__product-meta">';
html += ' <span>数量 ' + escapeHtml(String(qty)) + "</span>";
if (paymentLabel && paymentLabel !== "—") {
html += ' <span class="aksrt-order-detail__dot" aria-hidden="true"></span>';
html += ' <span>' + escapeHtml(paymentLabel) + "</span>";
}
html += " </div>";
html += " </div>";
html += "</div>";
html += '<section class="aksrt-order-detail__section">';
html += ' <h4 class="aksrt-order-detail__section-title">订单信息</h4>';
html += ' <dl class="aksrt-order-detail__list">';
rows.forEach(function (row) {
html += '<div class="aksrt-order-detail__row' + (row.accent ? " is-" + row.accent : "") + '">';
html += '<dt>' + escapeHtml(row.label) + '</dt>';
html += '<dd>';
html += '<span class="aksrt-order-detail__value">' + escapeHtml(row.value) + "</span>";
if (row.copyable && orderNo && orderNo !== "—") {
html += '<button type="button" class="aksrt-order-detail__copy" data-aksrt-copy-order="' + escapeHtml(orderNo) + '" title="复制订单号" aria-label="复制订单号">';
html += '<i class="far fa-copy" aria-hidden="true"></i>';
html += "</button>";
}
html += "</dd>";
html += "</div>";
});
html += "</dl></section>";
if (order.items && order.items.length > 1) {
html += '<section class="aksrt-order-detail__section">';
html += ' <h4 class="aksrt-order-detail__section-title">商品明细</h4>';
html += ' <ul class="aksrt-order-detail__items">';
order.items.forEach(function (item) {
var itemTitle = item.product_title || item.title || order.product_name || "商品";
var itemQty = item.quantity || item.qty || 1;
var price = item.pay_price != null ? item.pay_price : (item.unit_price || 0);
var itemImg = item.image || "";
html += '<li class="aksrt-order-detail__item">';
if (itemImg) {
html += '<img class="aksrt-order-detail__item-img" src="' + escapeHtml(itemImg) + '" alt="" loading="lazy">';
}
html += ' <span class="aksrt-order-detail__item-title">' + escapeHtml(itemTitle) + "</span>";
html += ' <em class="aksrt-order-detail__item-qty">×' + escapeHtml(String(itemQty)) + '</em>';
if (order.is_points) {
html += '<strong class="aksrt-order-detail__item-price">' + escapeHtml(order.currency_symbol || "积分") + " " + Math.round(Number(price || 0)) + "</strong>";
} else {
html += '<strong class="aksrt-order-detail__item-price">¥' + Number(price || 0).toFixed(2) + "</strong>";
}
html += "</li>";
});
html += "</ul></section>";
}
if (order.timeline && order.timeline.length) {
// Newest first for a natural "latest activity on top" feel.
var timeline = order.timeline.slice().reverse();
html += '<section class="aksrt-order-detail__section">';
html += ' <h4 class="aksrt-order-detail__section-title">订单进度</h4>';
html += ' <ol class="aksrt-order-detail__timeline">';
timeline.forEach(function (tl, idx) {
html += '<li class="aksrt-order-detail__timeline-item' + (idx === 0 ? " is-latest" : "") + '">';
html += '<span class="aksrt-order-detail__timeline-dot"></span>';
html += '<div class="aksrt-order-detail__timeline-body">';
html += '<span class="aksrt-order-detail__timeline-label">' + escapeHtml(tl.label || "") + "</span>";
html += '<time class="aksrt-order-detail__timeline-time">' + escapeHtml(tl.time || "") + "</time>";
html += "</div></li>";
});
html += "</ol></section>";
}
detailBody.innerHTML = html;
var footer = "";
// Status-aware footer. Top-right ✕ is the only generic dismiss control.
if (actions.can_cancel) {
// Two-step cancel inside the modal — no window.confirm.
footer += '<button type="button" class="btn-ghost" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '" data-aksrt-cancel-step="1">关闭订单</button>';
}
if (actions.can_receipt) {
footer += '<button type="button" class="btn-primary" data-aksrt-order-receipt="' + escapeHtml(order.order_number) + '">确认收货</button>';
}
if (actions.can_review) {
footer += '<button type="button" class="btn-primary c-green" data-aksrt-order-review="' + escapeHtml(order.order_number) + '">评价</button>';
}
if (actions.can_pay) {
footer += '<button type="button" class="btn-primary" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((payment && payment.value) || "") + '">立即支付</button>';
}
if (actions.can_after_sale) {
footer += '<button type="button" class="btn-outline" data-aksrt-order-after-sale="' + escapeHtml(order.order_number) + '">申请售后</button>';
}
if (actions.can_rebuy) {
footer += '<button type="button" class="btn-outline" data-aksrt-order-rebuy="' + escapeHtml(order.order_number) + '">再次购买</button>';
}
if (actions.can_view_product && order.permalink) {
footer += '<a class="btn-ghost" href="' + escapeHtml(order.permalink) + '">查看商品</a>';
}
if (footer === "") {
footer = '<button type="button" class="btn-outline" data-aksrt-order-detail-close>关闭</button>';
}
detailFooter.innerHTML = footer;
}
function copyOrderNumber(code, trigger) {
if (!code) return;
function done() {
showToast("已复制订单号");
if (trigger) {
trigger.classList.add("is-copied");
window.setTimeout(function () { trigger.classList.remove("is-copied"); }, 1200);
}
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).then(done).catch(function () {
showToast(code);
});
return;
}
try {
var ta = document.createElement("textarea");
ta.value = code;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
done();
} catch (e) {
showToast(code);
}
}
/* ── After-Sale Modal ── */
var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]");
var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]");
var afterSaleOrderInput = afterSaleForm ? afterSaleForm.querySelector('[name="order_number"]') : null;
function openAfterSaleModal(orderNumber) {
if (!afterSaleModal || !afterSaleForm) {
showToast("售后表单加载失败,请刷新页面后重试。");
return;
}
afterSaleForm.reset();
if (afterSaleOrderInput) afterSaleOrderInput.value = orderNumber;
afterSaleModal.hidden = false;
}
function closeAfterSaleModal() {
if (afterSaleModal) afterSaleModal.hidden = true;
}
if (afterSaleForm) {
afterSaleForm.addEventListener("submit", function (event) {
event.preventDefault();
var formData = new FormData(afterSaleForm);
var orderNumber = formData.get("order_number");
var reason = (formData.get("reason") || "").trim();
if (!reason) {
showToast("请填写售后原因。");
return;
}
var submitBtn = afterSaleForm.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = "提交中...";
}
request(orderPath(orderNumber, "/after-sale"), {
type: formData.get("type") || "refund",
reason: reason,
note: (formData.get("note") || "").trim()
}).then(function (result) {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = "提交售后";
}
if (!result.success) {
showToast(result.message || "提交售后失败。");
return;
}
closeAfterSaleModal();
showToast("售后申请已提交。");
// Local refresh: update card status
var card = findOrderCard(orderNumber);
updateCardStatus(card, "refunded", "已退款");
}).catch(function () {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = "提交售后";
}
showToast("网络错误,请稍后重试。");
});
});
}
/* ── Countdown timer ── */
function updateCountdowns() {
var nodes = page.querySelectorAll("[data-aksrt-order-countdown]");
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var value = Number(node.getAttribute("data-aksrt-order-countdown") || 0);
if (!value) continue;
value -= 1;
node.setAttribute("data-aksrt-order-countdown", String(value));
node.textContent = value > 0 ? "剩余支付时间 " + value + " 秒" : "待支付已超时,请重新发起。";
}
}
setInterval(updateCountdowns, 1000);
/* ── Event delegation ── */
page.addEventListener("click", function (event) {
// Order detail open
var detailBtn = event.target.closest("[data-aksrt-order-detail]");
if (detailBtn && !event.target.closest("[data-aksrt-order-detail-modal]")) {
var detailOrder = detailBtn.getAttribute("data-aksrt-order-detail");
if (detailOrder) openOrderDetailModal(detailOrder);
return;
}
// Copy order number inside detail modal
var copyBtn = event.target.closest("[data-aksrt-copy-order]");
if (copyBtn) {
event.preventDefault();
copyOrderNumber(copyBtn.getAttribute("data-aksrt-copy-order") || "", copyBtn);
return;
}
// Order detail close
if (event.target.closest("[data-aksrt-order-detail-close]")) {
closeOrderDetailModal();
return;
}
// Timeline toggle
var timelineToggle = event.target.closest("[data-aksrt-toggle-timeline]");
if (timelineToggle) {
var timeline = timelineToggle.closest("[data-aksrt-timeline]");
var body = timeline ? timeline.querySelector(".aksrt-orders-page__timeline-body") : null;
if (body) body.hidden = !body.hidden;
return;
}
// Confirm receipt
var receiptBtn = event.target.closest("[data-aksrt-order-receipt]");
if (receiptBtn) {
var receiptOrder = receiptBtn.getAttribute("data-aksrt-order-receipt");
if (!receiptOrder) return;
if (!window.confirm("确认已收到货物?")) return;
receiptBtn.disabled = true;
request(orderPath(receiptOrder, "/confirm-receipt"), {})
.then(function (result) {
receiptBtn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "确认收货失败");
return;
}
showToast("已确认收货");
updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成");
if (detailOrderNumber === receiptOrder) {
closeOrderDetailModal();
}
})
.catch(function () {
receiptBtn.disabled = false;
showToast("网络错误");
});
return;
}
// Rebuy
var rebuyBtn = event.target.closest("[data-aksrt-order-rebuy]");
if (rebuyBtn) {
var rebuyOrder = rebuyBtn.getAttribute("data-aksrt-order-rebuy");
if (!rebuyOrder) return;
rebuyBtn.disabled = true;
request(orderPath(rebuyOrder, "/rebuy"), {})
.then(function (result) {
rebuyBtn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "再次购买失败");
return;
}
showToast("已加入购物车");
window.location.href = cartUrl();
})
.catch(function () {
rebuyBtn.disabled = false;
showToast("网络错误");
});
return;
}
// Review modal (zibll order_comment_modal 1:1 UX)
var reviewBtn = event.target.closest("[data-aksrt-order-review]");
if (reviewBtn) {
var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review");
if (!reviewOrder) return;
closeOrderDetailModal();
openOrderReviewModal(reviewOrder, reviewBtn);
return;
}
// Pay
var payButton = event.target.closest("[data-aksrt-order-pay]");
if (payButton) {
var payOrderNumber = payButton.getAttribute("data-aksrt-order-pay");
// 支付方式取订单创建时选定的方式(按钮 data 属性可覆盖);
// 留空则由服务端使用订单存储的 payment_method。
var payMethod = payButton.getAttribute("data-aksrt-order-method") || "";
var payLabel = payButton.textContent || "去支付";
payButton.disabled = true;
payButton.textContent = "处理中...";
request(orderPath(payOrderNumber, "/pay"), payMethod ? { payment_method: payMethod } : {})
.then(function (result) {
if (!result.success) {
payButton.disabled = false;
payButton.textContent = payLabel;
showToast(result.message || "支付失败。");
return;
}
var data = result.data || {};
// 跳转/扫码型网关:到第三方支付页或展示二维码,订单仍为待支付,
// 由回调确认到账。
if (data.redirect) {
if (data.payment_url && /^https?:\/\//i.test(data.payment_url)) {
window.location.href = data.payment_url;
return;
}
if (data.qr_content || data.payment_url) {
showPaymentQr(data.qr_content || data.payment_url, payOrderNumber);
payButton.disabled = false;
payButton.textContent = "已下单,待支付";
return;
}
}
// 同步型网关(站内余额):已到账。
showToast("支付成功!");
var card = findOrderCard(payOrderNumber);
updateCardStatus(card, "paid", "已支付");
if (detailOrderNumber === payOrderNumber) {
closeOrderDetailModal();
}
})
.catch(function () {
payButton.disabled = false;
payButton.textContent = payLabel;
showToast("网络错误,请稍后重试。");
});
return;
}
// Cancel — two-step in detail modal (关闭订单 → 确认取消), list card same.
var cancelButton = event.target.closest("[data-aksrt-order-cancel]");
if (cancelButton) {
var cancelOrderNumber = cancelButton.getAttribute("data-aksrt-order-cancel");
if (!cancelOrderNumber) return;
// First click: transform into "确认取消" (no browser confirm dialog).
var step = cancelButton.getAttribute("data-aksrt-cancel-step") || "1";
if (step !== "2") {
cancelButton.setAttribute("data-aksrt-cancel-step", "2");
cancelButton.classList.remove("btn-ghost");
cancelButton.classList.add("btn-primary", "is-cancel-confirm");
cancelButton.textContent = "确认取消";
// Auto-reset if user doesn't confirm within a few seconds.
if (cancelButton._aksrtCancelReset) {
window.clearTimeout(cancelButton._aksrtCancelReset);
}
cancelButton._aksrtCancelReset = window.setTimeout(function () {
if (!cancelButton.isConnected) return;
if (cancelButton.getAttribute("data-aksrt-cancel-step") !== "2") return;
cancelButton.setAttribute("data-aksrt-cancel-step", "1");
cancelButton.classList.add("btn-ghost");
cancelButton.classList.remove("btn-primary", "is-cancel-confirm");
// Detail modal uses "关闭订单"; list cards use "取消".
var inDetail = !!(detailModal && detailModal.contains(cancelButton));
cancelButton.textContent = inDetail ? "关闭订单" : "取消";
}, 5000);
return;
}
if (cancelButton._aksrtCancelReset) {
window.clearTimeout(cancelButton._aksrtCancelReset);
cancelButton._aksrtCancelReset = null;
}
cancelButton.disabled = true;
cancelButton.textContent = "取消中...";
request(orderPath(cancelOrderNumber, "/cancel"))
.then(function (result) {
if (!result.success) {
cancelButton.disabled = false;
cancelButton.setAttribute("data-aksrt-cancel-step", "1");
cancelButton.classList.add("btn-ghost");
cancelButton.classList.remove("btn-primary", "is-cancel-confirm");
var inDetailFail = !!(detailModal && detailModal.contains(cancelButton));
cancelButton.textContent = inDetailFail ? "关闭订单" : "取消";
showToast(result.message || "取消失败。");
return;
}
showToast("订单已取消。");
var card = findOrderCard(cancelOrderNumber);
updateCardStatus(card, "cancelled", "已取消");
if (detailOrderNumber === cancelOrderNumber) {
// Refresh modal body with cancelled timeline, then close shortly.
if (result.data && result.data.order) {
renderOrderDetail(result.data.order);
}
window.setTimeout(function () {
closeOrderDetailModal();
}, 600);
}
})
.catch(function () {
cancelButton.disabled = false;
cancelButton.setAttribute("data-aksrt-cancel-step", "1");
cancelButton.classList.add("btn-ghost");
cancelButton.classList.remove("btn-primary", "is-cancel-confirm");
var inDetailErr = !!(detailModal && detailModal.contains(cancelButton));
cancelButton.textContent = inDetailErr ? "关闭订单" : "取消";
showToast("网络错误,请稍后重试。");
});
return;
}
// After-Sale
var afterSaleButton = event.target.closest("[data-aksrt-order-after-sale]");
if (afterSaleButton) {
var afterSaleOrderNumber = afterSaleButton.getAttribute("data-aksrt-order-after-sale");
closeOrderDetailModal();
openAfterSaleModal(afterSaleOrderNumber);
return;
}
// Return shipping
var returnShipBtn = event.target.closest("[data-aksrt-return-ship]");
if (returnShipBtn) {
openReturnShipModal(returnShipBtn.getAttribute("data-aksrt-return-ship"));
return;
}
// Cancel after-sale ticket
var asCancelBtn = event.target.closest("[data-aksrt-as-cancel]");
if (asCancelBtn) {
var tid = asCancelBtn.getAttribute("data-aksrt-as-cancel");
if (!tid || !window.confirm("确认取消该售后申请?")) return;
asCancelBtn.disabled = true;
request("/shop/frontend/after-sale/" + tid + "/cancel", { reason: "" })
.then(function (result) {
asCancelBtn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "取消失败");
return;
}
showToast(result.message || "已取消售后");
window.location.reload();
})
.catch(function () {
asCancelBtn.disabled = false;
showToast("网络错误");
});
return;
}
// Close after-sale modal
if (event.target.closest("[data-aksrt-aftersale-modal-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-aftersale-modal]"))) {
closeAfterSaleModal();
return;
}
// Close return-ship modal
if (event.target.closest("[data-aksrt-return-ship-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-return-ship-modal]"))) {
closeReturnShipModal();
}
});
/* ── Return shipping modal ── */
var returnShipModal = page.querySelector("[data-aksrt-return-ship-modal]");
var returnShipForm = page.querySelector("[data-aksrt-return-ship-form]");
function openReturnShipModal(ticketId) {
if (!returnShipModal || !returnShipForm) return;
returnShipForm.reset();
var idInput = returnShipForm.querySelector('[name="ticket_id"]');
if (idInput) idInput.value = ticketId || "";
returnShipModal.hidden = false;
}
function closeReturnShipModal() {
if (returnShipModal) returnShipModal.hidden = true;
}
if (returnShipForm) {
returnShipForm.addEventListener("submit", function (event) {
event.preventDefault();
var fd = new FormData(returnShipForm);
var tid = fd.get("ticket_id");
var carrier = (fd.get("carrier") || "").trim();
var tracking = (fd.get("tracking_no") || "").trim();
if (!carrier || !tracking) {
showToast("请填写快递公司与运单号");
return;
}
var btn = returnShipForm.querySelector('button[type="submit"]');
if (btn) { btn.disabled = true; }
request("/shop/frontend/after-sale/" + tid + "/return-shipping", {
carrier: carrier,
tracking_no: tracking
}).then(function (result) {
if (btn) btn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "提交失败");
return;
}
closeReturnShipModal();
showToast(result.message || "退货物流已提交");
window.location.reload();
}).catch(function () {
if (btn) btn.disabled = false;
showToast("网络错误");
});
});
}
/* ── Coupon wallet ── */
var couponRoot = page.querySelector("[data-aksrt-coupon-wallet-list]");
if (couponRoot) {
request("/shop/frontend/coupons").then(function (result) {
var items = (result && result.data && result.data.items) || (result && result.items) || [];
// normalize success envelope
if (result && result.data && result.data.data && result.data.data.items) {
items = result.data.data.items;
}
if (result && result.success && result.data && result.data.items) {
items = result.data.items;
}
// handle this success() wrapper: {success,data:{items}}
if (Array.isArray(result && result.data)) items = result.data;
if (!items.length && result && result.data && Array.isArray(result.data.items)) {
items = result.data.items;
}
// fallback parse
try {
if (!items.length && result && result.data && result.data.data) {
items = result.data.data.items || [];
}
} catch (e) {}
if (!items || !items.length) {
couponRoot.innerHTML = '<p class="muted-2-color">暂无可用优惠券</p>';
return;
}
var html = '<div class="aksrt-coupon-grid">';
items.forEach(function (c) {
var val = c.type === "percent" ? (c.value + "%") : ("¥" + Number(c.value || 0).toFixed(2));
html += '<div class="aksrt-coupon-card">';
html += '<div class="aksrt-coupon-card__value">' + val + '</div>';
html += '<div class="aksrt-coupon-card__code">' + (c.code || "") + '</div>';
if (c.min_amount > 0) {
html += '<div class="aksrt-coupon-card__meta">满 ¥' + Number(c.min_amount).toFixed(2) + ' 可用</div>';
}
if (c.expires_at) {
html += '<div class="aksrt-coupon-card__meta">有效期至 ' + c.expires_at + '</div>';
}
html += '<button type="button" class="btn-outline btn-sm" data-aksrt-copy-coupon="' + (c.code || "") + '">复制券码</button>';
html += '</div>';
});
html += '</div>';
couponRoot.innerHTML = html;
}).catch(function () {
couponRoot.innerHTML = '<p class="muted-2-color">加载失败</p>';
});
page.addEventListener("click", function (e) {
var b = e.target.closest("[data-aksrt-copy-coupon]");
if (!b) return;
var code = b.getAttribute("data-aksrt-copy-coupon") || "";
if (!code) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).then(function () { showToast("已复制 " + code); });
} else {
showToast(code);
}
});
}
})();