From a1e35a44995b14868b082a3b65323a480d9d5507 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Jul 2026 12:39:21 +0800 Subject: [PATCH] feat(orders): order detail modal, list actions, use configured prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- assets/css/routes/orders.css | 153 ++++++++++++ assets/js/order-center.js | 225 +++++++++++++++++- .../user-center-renderers/profile-orders.php | 75 +++--- src/AKSRT/Payment/OrderRepositoryCore.php | 44 +++- src/AKSRT/Payment/UserOrders.php | 41 +++- 5 files changed, 492 insertions(+), 46 deletions(-) diff --git a/assets/css/routes/orders.css b/assets/css/routes/orders.css index a0b1010f..97fe8533 100644 --- a/assets/css/routes/orders.css +++ b/assets/css/routes/orders.css @@ -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); diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 1d5ca379..24cb862b 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -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, "'"); + } + + 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 = '

加载中…

'; + 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 = '

' + escapeHtml((result && result.message) || "加载失败") + "

"; + return; + } + if (!order || !order.order_number) { + detailBody.innerHTML = '

订单不存在

'; + return; + } + renderOrderDetail(order); + }).catch(function () { + detailBody.innerHTML = '

网络错误,请稍后重试

'; + }); + } + + 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 = '
'; + rows.forEach(function (row) { + html += '
'; + html += '
' + escapeHtml(row[0]) + '
'; + // Money strings already composed; still escape all cells for safety. + html += '
' + escapeHtml(row[1]) + '
'; + html += '
'; + }); + html += "
"; + + if (order.items && order.items.length) { + html += '

商品明细

"; + } + + if (order.timeline && order.timeline.length) { + html += '

订单进度

    '; + order.timeline.forEach(function (tl) { + html += '
  1. " + escapeHtml(tl.label || "") + "
  2. "; + }); + html += "
"; + } + + detailBody.innerHTML = html; + + var footer = ""; + // Status-aware footer actions. + if (actions.can_cancel) { + footer += ''; + } + if (actions.can_pay) { + footer += ''; + } + if (actions.can_receipt) { + footer += ''; + } + if (actions.can_review) { + footer += ''; + } + if (actions.can_after_sale) { + footer += ''; + } + if (actions.can_rebuy) { + footer += ''; + } + if (actions.can_view_product && order.permalink) { + footer += '查看商品'; + } + if (order.secondary_url && order.secondary_label) { + footer += '' + escapeHtml(order.secondary_label) + ""; + } + footer += ''; + 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; } diff --git a/inc/helpers/user-center-renderers/profile-orders.php b/inc/helpers/user-center-renderers/profile-orders.php index 847eefe0..246ff971 100644 --- a/inc/helpers/user-center-renderers/profile-orders.php +++ b/inc/helpers/user-center-renderers/profile-orders.php @@ -209,9 +209,11 @@ $pending_count = (int) ($stats['pending'] ?? 0); ? esc_html($symbol) . ' ' . esc_html((string) (int) round((float) ($order['amount'] ?? 0))) : '¥' . esc_html(number_format((float) ($order['amount'] ?? 0), 2)); ?> -
+
- + + + @@ -290,34 +292,6 @@ $pending_count = (int) ($stats['pending'] ?? 0);
- - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + +
@@ -358,6 +358,21 @@ $pending_count = (int) ($stats['pending'] ?? 0); + + +