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:
1 parent
5e1e2e2634
commit
a1e35a4499
5 files changed
+492
-46
No files matched your search
@@ -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
@@ -396,8 +396,39 @@
|
||||
return page.querySelector('[data-aksrt-order-card="' + orderNumber + '"]');
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? "" : value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user