feat(orders): order detail modal, list actions, use configured prefix

Wire admin payment_order_prefix into order number generation (with 32-char
cap for gateways), make 查看 open a full order-detail modal with
status-aware footer actions, keep type badge top-left only, and right-align
unpaid list actions with countdown on the left.
This commit is contained in:
kate522 committed 2026-07-27 12:39:21 +08:00
1 parent 5e1e2e2634
commit a1e35a4499
5 files changed
+492 -46

No files matched your search

+153
View File
@@ -521,6 +521,159 @@
font-variant-numeric: tabular-nums;
}
/* Order detail modal content */
.aksrt-order-detail-dialog {
width: min(560px, 100%);
}
.aksrt-order-detail__list {
margin: 0;
display: grid;
gap: 10px;
}
.aksrt-order-detail__row {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 10px 14px;
align-items: start;
font-size: 13.5px;
}
.aksrt-order-detail__row dt {
margin: 0;
color: var(--color-ink-3, #8b8f97);
font-weight: 500;
}
.aksrt-order-detail__row dd {
margin: 0;
color: var(--color-ink, #1f2329);
font-weight: 600;
word-break: break-all;
font-variant-numeric: tabular-nums;
}
.aksrt-order-detail__items,
.aksrt-order-detail__timeline {
margin-top: 16px;
padding-top: 14px;
border-top: 1px dashed color-mix(in srgb, var(--color-line, #e5e7eb) 85%, transparent);
}
.aksrt-order-detail__items h4,
.aksrt-order-detail__timeline h4 {
margin: 0 0 10px;
font-size: 13.5px;
font-weight: 700;
color: var(--color-ink, #1f2329);
}
.aksrt-order-detail__items ul,
.aksrt-order-detail__timeline ol {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 8px;
}
.aksrt-order-detail__items li {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 10px;
align-items: center;
font-size: 13px;
color: var(--color-ink-2, #4b5563);
}
.aksrt-order-detail__items li em {
font-style: normal;
color: var(--color-ink-3, #8b8f97);
}
.aksrt-order-detail__items li strong {
font-weight: 700;
color: var(--color-ink, #1f2329);
font-variant-numeric: tabular-nums;
}
.aksrt-order-detail__timeline li {
display: grid;
grid-template-columns: 140px minmax(0, 1fr);
gap: 10px;
font-size: 12.5px;
color: var(--color-ink-2, #4b5563);
}
.aksrt-order-detail__timeline time {
color: var(--color-ink-3, #8b8f97);
font-variant-numeric: tabular-nums;
}
.aksrt-modal__footer .btn-primary,
.aksrt-modal__footer .btn-outline,
.aksrt-modal__footer .btn-ghost,
.aksrt-modal__footer a.btn-ghost,
.aksrt-modal__footer a.btn-outline {
min-height: 34px;
padding: 0 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
line-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
cursor: pointer;
}
.aksrt-modal__footer .btn-primary {
border: 1px solid transparent;
background: var(--color-primary, #3b82f6);
color: var(--color-on-primary, #fff);
}
.aksrt-modal__footer .btn-primary.c-green {
background: var(--color-success, #16a34a);
}
.aksrt-modal__footer .btn-outline {
border: 1px solid var(--color-line, #e5e7eb);
background: var(--surface-1, #fff);
color: var(--color-ink-2, #4b5563);
}
.aksrt-modal__footer .btn-ghost {
border: 1px solid transparent;
background: transparent;
color: var(--color-ink-3, #8b8f97);
}
@media (max-width: 767px) {
.aksrt-order-detail__row {
grid-template-columns: 1fr;
gap: 2px;
}
.aksrt-order-detail__timeline li {
grid-template-columns: 1fr;
gap: 2px;
}
.aksrt-modal__footer {
flex-wrap: wrap;
}
.aksrt-modal__footer .btn-primary,
.aksrt-modal__footer .btn-outline,
.aksrt-modal__footer .btn-ghost,
.aksrt-modal__footer a {
flex: 1 1 auto;
}
}
/* legacy item classes (if any remain) */
.aksrt-orders-page__item {
background: var(--surface-1);
+221 -4
View File
@@ -396,8 +396,39 @@
return page.querySelector('[data-aksrt-order-card="' + orderNumber + '"]');
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&")
.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;
@@ -414,6 +445,162 @@
}
}
/* ── 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 rows = [
["订单状态", order.status_label || order.status || "—"],
["订单名称", order.product_name || (order.product && order.product.title) || "—"],
["订单类型", order.product_type_label || order.product_type || "—"],
["订单数量", String(qty)],
["应付金额", formatMoneyText(order)],
["订单编号", order.order_number || "—"],
["创建时间", order.created_at || "—"],
["支付时间", order.paid_at || "—"],
["支付方式", paymentLabel]
];
if (Number(order.shipping_fee || 0) > 0) {
rows.push(["运费", "¥" + Number(order.shipping_fee || 0).toFixed(2)]);
}
if (Number(order.discount || 0) > 0) {
rows.push(["优惠", "¥" + Number(order.discount || 0).toFixed(2)]);
}
if (order.customer_note) {
rows.push(["备注", order.customer_note]);
}
var html = '<dl class="aksrt-order-detail__list">';
rows.forEach(function (row) {
html += '<div class="aksrt-order-detail__row">';
html += '<dt>' + escapeHtml(row[0]) + '</dt>';
// Money strings already composed; still escape all cells for safety.
html += '<dd>' + escapeHtml(row[1]) + '</dd>';
html += '</div>';
});
html += "</dl>";
if (order.items && order.items.length) {
html += '<div class="aksrt-order-detail__items"><h4>商品明细</h4><ul>';
order.items.forEach(function (item) {
var title = 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);
html += '<li><span>' + escapeHtml(title) + '</span>';
html += '<em>×' + escapeHtml(String(itemQty)) + '</em>';
if (order.is_points) {
html += '<strong>' + escapeHtml(order.currency_symbol || "积分") + " " + Math.round(Number(price || 0)) + "</strong>";
} else {
html += '<strong>¥' + Number(price || 0).toFixed(2) + "</strong>";
}
html += "</li>";
});
html += "</ul></div>";
}
if (order.timeline && order.timeline.length) {
html += '<div class="aksrt-order-detail__timeline"><h4>订单进度</h4><ol>';
order.timeline.forEach(function (tl) {
html += '<li><time>' + escapeHtml(tl.time || "") + "</time><span>" + escapeHtml(tl.label || "") + "</span></li>";
});
html += "</ol></div>";
}
detailBody.innerHTML = html;
var footer = "";
// Status-aware footer actions.
if (actions.can_cancel) {
footer += '<button type="button" class="btn-ghost" data-aksrt-order-cancel="' + 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_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_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 (order.secondary_url && order.secondary_label) {
footer += '<a class="btn-ghost" href="' + escapeHtml(order.secondary_url) + '">' + escapeHtml(order.secondary_label) + "</a>";
}
footer += '<button type="button" class="btn-outline" data-aksrt-order-detail-close>关闭</button>';
detailFooter.innerHTML = footer;
}
/* ── After-Sale Modal ── */
var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]");
var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]");
@@ -496,6 +683,20 @@
/* ── 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;
}
// 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) {
@@ -521,6 +722,9 @@
}
showToast("已确认收货");
updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成");
if (detailOrderNumber === receiptOrder) {
closeOrderDetailModal();
}
})
.catch(function () {
receiptBtn.disabled = false;
@@ -557,6 +761,7 @@
if (reviewBtn) {
var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review");
if (!reviewOrder) return;
closeOrderDetailModal();
openOrderReviewModal(reviewOrder, reviewBtn);
return;
}
@@ -568,13 +773,14 @@
// 支付方式取订单创建时选定的方式(按钮 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 = "立即支付";
payButton.textContent = payLabel;
showToast(result.message || "支付失败。");
return;
}
@@ -600,10 +806,13 @@
showToast("支付成功!");
var card = findOrderCard(payOrderNumber);
updateCardStatus(card, "paid", "已支付");
if (detailOrderNumber === payOrderNumber) {
closeOrderDetailModal();
}
})
.catch(function () {
payButton.disabled = false;
payButton.textContent = "立即支付";
payButton.textContent = payLabel;
showToast("网络错误,请稍后重试。");
});
return;
@@ -613,23 +822,30 @@
var cancelButton = event.target.closest("[data-aksrt-order-cancel]");
if (cancelButton) {
var cancelOrderNumber = cancelButton.getAttribute("data-aksrt-order-cancel");
if (!window.confirm("确认关闭该订单?关闭后不可再支付。")) {
return;
}
var cancelLabel = cancelButton.textContent || "取消";
cancelButton.disabled = true;
cancelButton.textContent = "取消中...";
request(orderPath(cancelOrderNumber, "/cancel"))
.then(function (result) {
if (!result.success) {
cancelButton.disabled = false;
cancelButton.textContent = "取消订单";
cancelButton.textContent = cancelLabel;
showToast(result.message || "取消失败。");
return;
}
showToast("订单已取消。");
var card = findOrderCard(cancelOrderNumber);
updateCardStatus(card, "cancelled", "已取消");
if (detailOrderNumber === cancelOrderNumber) {
closeOrderDetailModal();
}
})
.catch(function () {
cancelButton.disabled = false;
cancelButton.textContent = "取消订单";
cancelButton.textContent = cancelLabel;
showToast("网络错误,请稍后重试。");
});
return;
@@ -639,6 +855,7 @@
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;
}