Merge pull request 'feat(orders): order detail modal, list actions, use configured prefix' (#309) from worktree-order-center-list into main
Reviewed-on: CYL/wordpress-theme-aksrt#309
This commit is contained in:
commit
1648df8457
5 files changed
+492
-46
No files matched your search
@@ -521,6 +521,159 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
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) */
|
/* legacy item classes (if any remain) */
|
||||||
.aksrt-orders-page__item {
|
.aksrt-orders-page__item {
|
||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
|
|||||||
+221
-4
@@ -396,8 +396,39 @@
|
|||||||
return page.querySelector('[data-aksrt-order-card="' + orderNumber + '"]');
|
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) {
|
function updateCardStatus(card, newStatus, statusLabel) {
|
||||||
if (!card) return;
|
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");
|
var badge = card.querySelector(".aksrt-orders-page__card-status, .aksrt-orders-page__item-badge");
|
||||||
if (badge) {
|
if (badge) {
|
||||||
badge.textContent = statusLabel || newStatus;
|
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 ── */
|
/* ── After-Sale Modal ── */
|
||||||
var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]");
|
var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]");
|
||||||
var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]");
|
var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]");
|
||||||
@@ -496,6 +683,20 @@
|
|||||||
|
|
||||||
/* ── Event delegation ── */
|
/* ── Event delegation ── */
|
||||||
page.addEventListener("click", function (event) {
|
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
|
// Timeline toggle
|
||||||
var timelineToggle = event.target.closest("[data-aksrt-toggle-timeline]");
|
var timelineToggle = event.target.closest("[data-aksrt-toggle-timeline]");
|
||||||
if (timelineToggle) {
|
if (timelineToggle) {
|
||||||
@@ -521,6 +722,9 @@
|
|||||||
}
|
}
|
||||||
showToast("已确认收货");
|
showToast("已确认收货");
|
||||||
updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成");
|
updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成");
|
||||||
|
if (detailOrderNumber === receiptOrder) {
|
||||||
|
closeOrderDetailModal();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
receiptBtn.disabled = false;
|
receiptBtn.disabled = false;
|
||||||
@@ -557,6 +761,7 @@
|
|||||||
if (reviewBtn) {
|
if (reviewBtn) {
|
||||||
var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review");
|
var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review");
|
||||||
if (!reviewOrder) return;
|
if (!reviewOrder) return;
|
||||||
|
closeOrderDetailModal();
|
||||||
openOrderReviewModal(reviewOrder, reviewBtn);
|
openOrderReviewModal(reviewOrder, reviewBtn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -568,13 +773,14 @@
|
|||||||
// 支付方式取订单创建时选定的方式(按钮 data 属性可覆盖);
|
// 支付方式取订单创建时选定的方式(按钮 data 属性可覆盖);
|
||||||
// 留空则由服务端使用订单存储的 payment_method。
|
// 留空则由服务端使用订单存储的 payment_method。
|
||||||
var payMethod = payButton.getAttribute("data-aksrt-order-method") || "";
|
var payMethod = payButton.getAttribute("data-aksrt-order-method") || "";
|
||||||
|
var payLabel = payButton.textContent || "去支付";
|
||||||
payButton.disabled = true;
|
payButton.disabled = true;
|
||||||
payButton.textContent = "处理中...";
|
payButton.textContent = "处理中...";
|
||||||
request(orderPath(payOrderNumber, "/pay"), payMethod ? { payment_method: payMethod } : {})
|
request(orderPath(payOrderNumber, "/pay"), payMethod ? { payment_method: payMethod } : {})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
payButton.disabled = false;
|
payButton.disabled = false;
|
||||||
payButton.textContent = "立即支付";
|
payButton.textContent = payLabel;
|
||||||
showToast(result.message || "支付失败。");
|
showToast(result.message || "支付失败。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -600,10 +806,13 @@
|
|||||||
showToast("支付成功!");
|
showToast("支付成功!");
|
||||||
var card = findOrderCard(payOrderNumber);
|
var card = findOrderCard(payOrderNumber);
|
||||||
updateCardStatus(card, "paid", "已支付");
|
updateCardStatus(card, "paid", "已支付");
|
||||||
|
if (detailOrderNumber === payOrderNumber) {
|
||||||
|
closeOrderDetailModal();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
payButton.disabled = false;
|
payButton.disabled = false;
|
||||||
payButton.textContent = "立即支付";
|
payButton.textContent = payLabel;
|
||||||
showToast("网络错误,请稍后重试。");
|
showToast("网络错误,请稍后重试。");
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -613,23 +822,30 @@
|
|||||||
var cancelButton = event.target.closest("[data-aksrt-order-cancel]");
|
var cancelButton = event.target.closest("[data-aksrt-order-cancel]");
|
||||||
if (cancelButton) {
|
if (cancelButton) {
|
||||||
var cancelOrderNumber = cancelButton.getAttribute("data-aksrt-order-cancel");
|
var cancelOrderNumber = cancelButton.getAttribute("data-aksrt-order-cancel");
|
||||||
|
if (!window.confirm("确认关闭该订单?关闭后不可再支付。")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cancelLabel = cancelButton.textContent || "取消";
|
||||||
cancelButton.disabled = true;
|
cancelButton.disabled = true;
|
||||||
cancelButton.textContent = "取消中...";
|
cancelButton.textContent = "取消中...";
|
||||||
request(orderPath(cancelOrderNumber, "/cancel"))
|
request(orderPath(cancelOrderNumber, "/cancel"))
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
cancelButton.disabled = false;
|
cancelButton.disabled = false;
|
||||||
cancelButton.textContent = "取消订单";
|
cancelButton.textContent = cancelLabel;
|
||||||
showToast(result.message || "取消失败。");
|
showToast(result.message || "取消失败。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showToast("订单已取消。");
|
showToast("订单已取消。");
|
||||||
var card = findOrderCard(cancelOrderNumber);
|
var card = findOrderCard(cancelOrderNumber);
|
||||||
updateCardStatus(card, "cancelled", "已取消");
|
updateCardStatus(card, "cancelled", "已取消");
|
||||||
|
if (detailOrderNumber === cancelOrderNumber) {
|
||||||
|
closeOrderDetailModal();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
cancelButton.disabled = false;
|
cancelButton.disabled = false;
|
||||||
cancelButton.textContent = "取消订单";
|
cancelButton.textContent = cancelLabel;
|
||||||
showToast("网络错误,请稍后重试。");
|
showToast("网络错误,请稍后重试。");
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -639,6 +855,7 @@
|
|||||||
var afterSaleButton = event.target.closest("[data-aksrt-order-after-sale]");
|
var afterSaleButton = event.target.closest("[data-aksrt-order-after-sale]");
|
||||||
if (afterSaleButton) {
|
if (afterSaleButton) {
|
||||||
var afterSaleOrderNumber = afterSaleButton.getAttribute("data-aksrt-order-after-sale");
|
var afterSaleOrderNumber = afterSaleButton.getAttribute("data-aksrt-order-after-sale");
|
||||||
|
closeOrderDetailModal();
|
||||||
openAfterSaleModal(afterSaleOrderNumber);
|
openAfterSaleModal(afterSaleOrderNumber);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,9 +209,11 @@ $pending_count = (int) ($stats['pending'] ?? 0);
|
|||||||
? esc_html($symbol) . ' ' . esc_html((string) (int) round((float) ($order['amount'] ?? 0)))
|
? esc_html($symbol) . ' ' . esc_html((string) (int) round((float) ($order['amount'] ?? 0)))
|
||||||
: '¥' . esc_html(number_format((float) ($order['amount'] ?? 0), 2));
|
: '¥' . esc_html(number_format((float) ($order['amount'] ?? 0), 2));
|
||||||
?>
|
?>
|
||||||
<article class="aksrt-orders-page__card" data-aksrt-order-card="<?php echo esc_attr($on); ?>" data-product-type="<?php echo esc_attr($ptype); ?>">
|
<article class="aksrt-orders-page__card" data-aksrt-order-card="<?php echo esc_attr($on); ?>" data-product-type="<?php echo esc_attr($ptype); ?>" data-order-status="<?php echo esc_attr($os); ?>">
|
||||||
<div class="aksrt-orders-page__card-head">
|
<div class="aksrt-orders-page__card-head">
|
||||||
<span class="aksrt-orders-page__card-type"><?php echo esc_html((string) ($order['product_type_label'] ?? '')); ?></span>
|
<?php if ((string) ($order['product_type_label'] ?? '') !== '') : ?>
|
||||||
|
<span class="aksrt-orders-page__card-type"><?php echo esc_html((string) $order['product_type_label']); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
<span class="aksrt-orders-page__card-number"><?php echo esc_html($on); ?></span>
|
<span class="aksrt-orders-page__card-number"><?php echo esc_html($on); ?></span>
|
||||||
<time class="aksrt-orders-page__card-time"><?php echo esc_html((string) ($order['created_at'] ?? '')); ?></time>
|
<time class="aksrt-orders-page__card-time"><?php echo esc_html((string) ($order['created_at'] ?? '')); ?></time>
|
||||||
<span class="aksrt-orders-page__card-status"><?php echo esc_html((string) ($order['status_label'] ?? '')); ?></span>
|
<span class="aksrt-orders-page__card-status"><?php echo esc_html((string) ($order['status_label'] ?? '')); ?></span>
|
||||||
@@ -290,34 +292,6 @@ $pending_count = (int) ($stats['pending'] ?? 0);
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="aksrt-orders-page__card-actions">
|
<div class="aksrt-orders-page__card-actions">
|
||||||
<?php if (!empty($actions['can_pay'])) : ?>
|
|
||||||
<button type="button" class="btn-primary btn-sm" data-aksrt-order-pay="<?php echo esc_attr($on); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('去支付', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_receipt'])) : ?>
|
|
||||||
<button type="button" class="btn-primary btn-sm" data-aksrt-order-receipt="<?php echo esc_attr($on); ?>"><?php esc_html_e('确认收货', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_review']) || (class_exists('\AKSRT\Payment\Order') && $os === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW)) : ?>
|
|
||||||
<button type="button" class="btn-primary btn-sm c-green" data-aksrt-order-review="<?php echo esc_attr($on); ?>"><?php esc_html_e('评价', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_cancel'])) : ?>
|
|
||||||
<button type="button" class="btn-ghost btn-sm" data-aksrt-order-cancel="<?php echo esc_attr($on); ?>"><?php esc_html_e('取消', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_rebuy'])) : ?>
|
|
||||||
<button type="button" class="btn-outline btn-sm" data-aksrt-order-rebuy="<?php echo esc_attr($on); ?>"><?php esc_html_e('再次购买', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_after_sale'])) : ?>
|
|
||||||
<button type="button" class="btn-outline btn-sm" data-aksrt-order-after-sale="<?php echo esc_attr($on); ?>"><?php esc_html_e('售后', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_view']) && !empty($order['permalink'])) : ?>
|
|
||||||
<a class="btn-outline btn-sm" href="<?php echo esc_url((string) $order['permalink']); ?>"><?php
|
|
||||||
echo (class_exists('\AKSRT\Core\DomainModel') && $ptype === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE)
|
|
||||||
? esc_html__('去查看', 'aksrt')
|
|
||||||
: esc_html__('查看', 'aksrt');
|
|
||||||
?></a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($order['secondary_url']) && !empty($order['secondary_label'])) : ?>
|
|
||||||
<a class="btn-ghost btn-sm" href="<?php echo esc_url((string) $order['secondary_url']); ?>"><?php echo esc_html((string) $order['secondary_label']); ?></a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($order['time_left'])) : ?>
|
<?php if (!empty($order['time_left'])) : ?>
|
||||||
<span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="<?php echo esc_attr((string) $order['time_left']); ?>">
|
<span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="<?php echo esc_attr((string) $order['time_left']); ?>">
|
||||||
<?php
|
<?php
|
||||||
@@ -329,6 +303,32 @@ $pending_count = (int) ($stats['pending'] ?? 0);
|
|||||||
?>
|
?>
|
||||||
</span>
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<button type="button" class="btn-outline btn-sm" data-aksrt-order-detail="<?php echo esc_attr($on); ?>"><?php esc_html_e('查看', 'aksrt'); ?></button>
|
||||||
|
<?php if (!empty($actions['can_cancel'])) : ?>
|
||||||
|
<button type="button" class="btn-ghost btn-sm" data-aksrt-order-cancel="<?php echo esc_attr($on); ?>"><?php esc_html_e('取消', 'aksrt'); ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_pay'])) : ?>
|
||||||
|
<button type="button" class="btn-primary btn-sm" data-aksrt-order-pay="<?php echo esc_attr($on); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('去支付', 'aksrt'); ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_receipt'])) : ?>
|
||||||
|
<button type="button" class="btn-primary btn-sm" data-aksrt-order-receipt="<?php echo esc_attr($on); ?>"><?php esc_html_e('确认收货', 'aksrt'); ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_review']) || (class_exists('\AKSRT\Payment\Order') && $os === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW)) : ?>
|
||||||
|
<button type="button" class="btn-primary btn-sm c-green" data-aksrt-order-review="<?php echo esc_attr($on); ?>"><?php esc_html_e('评价', 'aksrt'); ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_rebuy'])) : ?>
|
||||||
|
<button type="button" class="btn-outline btn-sm" data-aksrt-order-rebuy="<?php echo esc_attr($on); ?>"><?php esc_html_e('再次购买', 'aksrt'); ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_after_sale'])) : ?>
|
||||||
|
<button type="button" class="btn-outline btn-sm" data-aksrt-order-after-sale="<?php echo esc_attr($on); ?>"><?php esc_html_e('售后', 'aksrt'); ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_view_product']) && !empty($order['permalink'])) : ?>
|
||||||
|
<a class="btn-ghost btn-sm" href="<?php echo esc_url((string) $order['permalink']); ?>"><?php
|
||||||
|
echo (class_exists('\AKSRT\Core\DomainModel') && $ptype === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE)
|
||||||
|
? esc_html__('去内容', 'aksrt')
|
||||||
|
: esc_html__('商品页', 'aksrt');
|
||||||
|
?></a>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -358,6 +358,21 @@ $pending_count = (int) ($stats['pending'] ?? 0);
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Order detail modal -->
|
||||||
|
<div class="aksrt-modal" data-aksrt-order-detail-modal hidden>
|
||||||
|
<div class="aksrt-modal__overlay" data-aksrt-order-detail-close></div>
|
||||||
|
<div class="aksrt-modal__dialog aksrt-order-detail-dialog" role="dialog" aria-modal="true" aria-labelledby="aksrt-order-detail-title">
|
||||||
|
<div class="aksrt-modal__header">
|
||||||
|
<h3 id="aksrt-order-detail-title"><?php esc_html_e('订单详情', 'aksrt'); ?></h3>
|
||||||
|
<button type="button" class="aksrt-modal__close" data-aksrt-order-detail-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="aksrt-modal__body" data-aksrt-order-detail-body>
|
||||||
|
<p class="muted-2-color"><?php esc_html_e('加载中…', 'aksrt'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="aksrt-modal__footer" data-aksrt-order-detail-footer></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- After-sale modal -->
|
<!-- After-sale modal -->
|
||||||
<div class="aksrt-modal" data-aksrt-aftersale-modal hidden>
|
<div class="aksrt-modal" data-aksrt-aftersale-modal hidden>
|
||||||
<div class="aksrt-modal__overlay"></div>
|
<div class="aksrt-modal__overlay"></div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ trait OrderRepositoryCore {
|
|||||||
$note = sanitize_textarea_field($note_raw);
|
$note = sanitize_textarea_field($note_raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
$wpdb->insert($this->table(), [
|
$row = [
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'user_id' => (int) ($data['user_id'] ?? 0),
|
'user_id' => (int) ($data['user_id'] ?? 0),
|
||||||
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
|
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
|
||||||
@@ -53,9 +53,17 @@ trait OrderRepositoryCore {
|
|||||||
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
|
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
|
||||||
'created_at' => $data['created_at'] ?? current_time('mysql'),
|
'created_at' => $data['created_at'] ?? current_time('mysql'),
|
||||||
'paid_at' => $data['paid_at'] ?? null,
|
'paid_at' => $data['paid_at'] ?? null,
|
||||||
'expires_at' => $data['expires_at'] ?? null,
|
|
||||||
'note' => $note,
|
'note' => $note,
|
||||||
], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
];
|
||||||
|
$formats = ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
|
||||||
|
|
||||||
|
// Honour configured expiry when callers pass expires_at (shop checkout etc.).
|
||||||
|
if (array_key_exists('expires_at', $data) && $data['expires_at'] !== null && $data['expires_at'] !== '') {
|
||||||
|
$row['expires_at'] = (string) $data['expires_at'];
|
||||||
|
$formats[] = '%s';
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpdb->insert($this->table(), $row, $formats);
|
||||||
|
|
||||||
if ($wpdb->insert_id) {
|
if ($wpdb->insert_id) {
|
||||||
return $order_number;
|
return $order_number;
|
||||||
@@ -506,8 +514,36 @@ trait OrderRepositoryCore {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a unique order number using the admin-configured prefix.
|
||||||
|
*
|
||||||
|
* Column + major gateways (WeChat out_trade_no) cap at 32 chars, so the
|
||||||
|
* prefix is trimmed to leave room for {YmdHis}{4 digits} (=18).
|
||||||
|
*/
|
||||||
private function generate_order_number(): string {
|
private function generate_order_number(): string {
|
||||||
return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
$prefix = 'AKSRT';
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_order_settings')) {
|
||||||
|
$settings = \AKSRT\Settings\Settings::get_order_settings();
|
||||||
|
$configured = sanitize_text_field((string) ($settings['prefix'] ?? ''));
|
||||||
|
if ($configured !== '') {
|
||||||
|
$prefix = $configured;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only gateway-safe chars (REST + WeChat/Alipay out_trade_no).
|
||||||
|
$prefix = preg_replace('/[^A-Za-z0-9_-]/', '', $prefix);
|
||||||
|
if (!is_string($prefix) || $prefix === '') {
|
||||||
|
$prefix = 'AKSRT';
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||||
|
// Reserve 18 for body; leave ≥1 char of prefix so numbers stay human-readable.
|
||||||
|
$max_prefix = max(1, 32 - strlen($body));
|
||||||
|
if (strlen($prefix) > $max_prefix) {
|
||||||
|
$prefix = substr($prefix, 0, $max_prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $prefix . $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ class UserOrders {
|
|||||||
$can_receipt = $is_shop && $status === Order::STATUS_DELIVERED;
|
$can_receipt = $is_shop && $status === Order::STATUS_DELIVERED;
|
||||||
$can_review = $is_shop && $status === Order::STATUS_PENDING_REVIEW;
|
$can_review = $is_shop && $status === Order::STATUS_PENDING_REVIEW;
|
||||||
$can_rebuy = $is_shop && in_array($status, [Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW], true);
|
$can_rebuy = $is_shop && in_array($status, [Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW], true);
|
||||||
$can_view = $permalink !== '' && in_array($status, [
|
// Order detail modal is always available; product permalink is optional.
|
||||||
|
$can_view = true;
|
||||||
|
$can_view_product = $permalink !== '' && in_array($status, [
|
||||||
Order::STATUS_PAID,
|
Order::STATUS_PAID,
|
||||||
Order::STATUS_DELIVERED,
|
Order::STATUS_DELIVERED,
|
||||||
Order::STATUS_COMPLETED,
|
Order::STATUS_COMPLETED,
|
||||||
@@ -232,14 +234,16 @@ class UserOrders {
|
|||||||
'shipping_records' => $shipping_records,
|
'shipping_records' => $shipping_records,
|
||||||
'pay_url' => '#',
|
'pay_url' => '#',
|
||||||
'actions' => [
|
'actions' => [
|
||||||
'can_pay' => $can_pay,
|
'can_pay' => $can_pay,
|
||||||
'can_cancel' => $can_cancel,
|
'can_cancel' => $can_cancel,
|
||||||
'can_after_sale' => $can_after_sale,
|
'can_after_sale' => $can_after_sale,
|
||||||
'can_receipt' => $can_receipt,
|
'can_receipt' => $can_receipt,
|
||||||
'can_review' => $can_review,
|
'can_review' => $can_review,
|
||||||
'can_rebuy' => $can_rebuy,
|
'can_rebuy' => $can_rebuy,
|
||||||
'can_view' => $can_view,
|
'can_view' => $can_view,
|
||||||
|
'can_view_product' => $can_view_product,
|
||||||
],
|
],
|
||||||
|
'quantity' => $this->resolve_order_quantity($order_items, $meta),
|
||||||
'time_left' => $this->get_order_countdown($expires_at, $status),
|
'time_left' => $this->get_order_countdown($expires_at, $status),
|
||||||
'secondary_url' => $this->resolve_secondary_url($product_type),
|
'secondary_url' => $this->resolve_secondary_url($product_type),
|
||||||
'secondary_label' => $this->resolve_secondary_label($product_type),
|
'secondary_label' => $this->resolve_secondary_label($product_type),
|
||||||
@@ -534,4 +538,25 @@ class UserOrders {
|
|||||||
$left = strtotime($expires_at) - time();
|
$left = strtotime($expires_at) - time();
|
||||||
return max(0, (int) $left);
|
return max(0, (int) $left);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sum line quantities for display (shop multi-item or single meta item).
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $order_items
|
||||||
|
* @param array<string, mixed> $meta
|
||||||
|
*/
|
||||||
|
private function resolve_order_quantity(array $order_items, array $meta): int {
|
||||||
|
$qty = 0;
|
||||||
|
$source = !empty($order_items) ? $order_items : (is_array($meta['items'] ?? null) ? $meta['items'] : []);
|
||||||
|
foreach ($source as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$qty += max(1, (int) ($row['quantity'] ?? $row['qty'] ?? 1));
|
||||||
|
}
|
||||||
|
if ($qty <= 0) {
|
||||||
|
$qty = max(1, (int) ($meta['quantity'] ?? $meta['qty'] ?? 1));
|
||||||
|
}
|
||||||
|
return $qty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user