diff --git a/assets/js/cart-checkout.js b/assets/js/cart-checkout.js index 4be54728..80ede97a 100644 --- a/assets/js/cart-checkout.js +++ b/assets/js/cart-checkout.js @@ -6,7 +6,28 @@ return; } - var CART_KEY = "aksrt_cart"; + function restBase() { + return ((aksrtData.siteUrl || "").replace(/\/$/, "")) + "/wp-json/aksrt/v1"; + } + function rest(path, method, body) { + var opts = { + method: method || "GET", + credentials: "same-origin", + headers: { "X-WP-Nonce": aksrtData.restNonce || "" } + }; + if (body !== undefined) { + opts.headers["Content-Type"] = "application/json"; + opts.body = JSON.stringify(body); + } + return fetch(restBase() + path, opts).then(function (r) { return r.json(); }); + } + function ordersUrl() { + return aksrtData.ordersUrl || (aksrtData.routes && aksrtData.routes.orders) || "/"; + } + function shopHomeUrl() { + return (aksrtData.routes && aksrtData.routes.home) || aksrtData.siteUrl || "/"; + } + var cartItems = []; var currencySymbol = page.getAttribute("data-currency-symbol") || "¥"; var itemsEl = page.querySelector("[data-aksrt-cart-items]"); var subtotalEl = page.querySelector("[data-aksrt-summary-subtotal]"); @@ -20,18 +41,8 @@ var previewState = null; var searchParams = new URLSearchParams(window.location.search); - function getCart() { - try { - var parsed = JSON.parse(localStorage.getItem(CART_KEY) || "[]"); - return Array.isArray(parsed) ? parsed : []; - } catch (error) { - return []; - } - } - - function setCart(items) { - localStorage.setItem(CART_KEY, JSON.stringify(items)); - } + function getCart() { return cartItems; } + function setCart(items) { cartItems = Array.isArray(items) ? items : []; } function money(value) { return currencySymbol + Number(value || 0).toFixed(2); @@ -46,7 +57,8 @@ var items = getCart().map(function (item) { return { product_id: Number(item.product_id || item.id || 0), - quantity: Number(item.qty || item.quantity || 1) + quantity: Number(item.qty || item.quantity || 1), + options_string: item.options_string || "" }; }).filter(function (item) { return item.product_id > 0; @@ -69,7 +81,7 @@ '
' + '' + "

购物车为空,去商品页添加想购买的内容。

" + - '继续浏览' + + '继续浏览' + "
"; subtotalEl.textContent = money(0); shippingEl.textContent = money(0); @@ -84,15 +96,19 @@ var html = ""; items.forEach(function (item, index) { var qty = Number(item.qty || item.quantity || 1); - var price = Number(item.price || 0); - var lineTotal = qty * price; + var price = Number(item.unit_price != null ? item.unit_price : (item.price != null ? item.price : (item.product && item.product.price) || 0)); + var lineTotal = Number(item.line_total != null ? item.line_total : qty * price); total += lineTotal; + var title = item.title || (item.product && item.product.title) || "商品"; + var image = item.image || (item.product && item.product.image) || ""; + var opts = item.options_label || item.options_string || ""; html += '' + - '
' + - (item.image ? '' : '
') + + '
' + + (image ? '' : '
') + '
' + - '

' + (item.title || "商品") + "

" + + '

' + title + "

" + + (opts ? "" + opts + "" : "") + '

' + money(price) + "

" + "
" + '
' + @@ -124,18 +140,7 @@ createBtn.disabled = !data; } - function request(path, body) { - return fetch("/wp-json/aksrt/v1" + path, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-WP-Nonce": aksrtData.restNonce - }, - body: JSON.stringify(body || {}) - }).then(function (response) { - return response.json(); - }); - } + function request(path, body) { return rest(path, "POST", body || {}); } function previewCheckout() { var payload = getPayload(); @@ -183,12 +188,11 @@ return; } - localStorage.removeItem(CART_KEY); var orderNumber = (result.data && result.data.order_number) || ""; - var ordersUrl = window.location.pathname.replace("cart", "orders") + "?order_status=pending"; + var nextOrdersUrl = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending"; if (!orderNumber) { - window.location.href = ordersUrl; + window.location.href = nextOrdersUrl; return; } @@ -201,10 +205,10 @@ return; } // 二维码 / 余额 / 失败:统一回订单中心处理(含 ?payment 提示)。 - window.location.href = ordersUrl; + window.location.href = nextOrdersUrl; }) .catch(function () { - window.location.href = ordersUrl; + window.location.href = nextOrdersUrl; }); }) .catch(function () { @@ -217,31 +221,36 @@ var actionButton = event.target.closest("[data-qty-action]"); if (actionButton) { var itemEl = actionButton.closest("[data-cart-index]"); - var index = Number(itemEl.getAttribute("data-cart-index")); - var items = getCart(); - if (!items[index]) { + if (!itemEl) return; + var productId = Number(itemEl.getAttribute("data-product-id") || 0); + var optionsString = itemEl.getAttribute("data-options-string") || ""; + var action = actionButton.getAttribute("data-qty-action"); + var path = "/shop/frontend/cart/add"; + var body = { product_id: productId, options_string: optionsString, count: 1 }; + if (action === "dec") { + path = "/shop/frontend/cart/reduce"; + } else if (action === "remove") { + path = "/shop/frontend/cart/remove"; + body = { product_id: productId, options_string: optionsString }; + } else if (action !== "inc") { return; } - - var action = actionButton.getAttribute("data-qty-action"); - if (action === "inc") { - items[index].qty = Number(items[index].qty || 1) + 1; - } else if (action === "dec") { - items[index].qty = Math.max(1, Number(items[index].qty || 1) - 1); - } else if (action === "remove") { - items.splice(index, 1); - } - - setCart(items); - setSummary(null); - renderCart(); + rest(path, "POST", body).then(function (result) { + if (!result || !result.success) { + window.alert((result && result.message) || "操作失败"); + return; + } + setSummary(null); + loadServerCart(); + }); return; } if (event.target.closest("[data-aksrt-clear-cart]")) { - localStorage.removeItem(CART_KEY); - setSummary(null); - renderCart(); + rest("/shop/frontend/cart/empty", "POST", {}).then(function () { + setSummary(null); + loadServerCart(); + }); return; } @@ -257,10 +266,28 @@ var resumeButton = event.target.closest("[data-aksrt-resume-order]"); if (resumeButton) { - window.location.href = window.location.pathname.replace("cart", "orders") + "?order_status=pending"; + window.location.href = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending"; } }); + function loadServerCart() { + return rest("/shop/frontend/cart", "GET").then(function (result) { + var items = (result && result.success && result.data && result.data.items) ? result.data.items : []; + items = items.map(function (it) { + it.qty = it.quantity || it.qty || 1; + it.price = it.unit_price != null ? it.unit_price : (it.product && it.product.price) || 0; + it.title = it.title || (it.product && it.product.title) || "商品"; + it.image = it.image || (it.product && it.product.image) || ""; + return it; + }); + setCart(items); + renderCart(); + }).catch(function () { + setCart([]); + renderCart(); + }); + } + var couponInput = page.querySelector("[data-aksrt-coupon-code]"); if (couponInput && searchParams.has("coupon")) { couponInput.value = searchParams.get("coupon") || ""; @@ -300,7 +327,7 @@ } function refreshAddressList() { - fetch("/wp-json/aksrt/v1/shop/frontend/address", { + fetch(restBase() + "/shop/frontend/address", { headers: { "X-WP-Nonce": aksrtData.restNonce } }) .then(function (r) { return r.json(); }) @@ -389,9 +416,9 @@ if (deleteBtn) { var deleteId = deleteBtn.getAttribute("data-aksrt-address-delete"); if (!confirm("确定删除该地址吗?")) return; - request("/shop/frontend/address/" + deleteId, {}).then(function (result) { - if (!result.success) { - alert(result.message || "删除失败。"); + rest("/shop/frontend/address/" + deleteId, "DELETE").then(function (result) { + if (!result || !result.success) { + alert((result && result.message) || "删除失败。"); return; } refreshAddressList(); @@ -452,5 +479,5 @@ }); } - renderCart(); + loadServerCart(); })(); diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 38015b06..e5f2a2bb 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -46,19 +46,27 @@ } /* ── REST helper ── */ - function request(path, body) { - return fetch("/wp-json/aksrt/v1" + path, { - method: "POST", + function restRoot() { + return ((aksrtData.siteUrl || "").replace(/\/$/, "")) + "/wp-json/aksrt/v1"; + } + + function request(path, body, method) { + return fetch(restRoot() + path, { + method: method || "POST", headers: { "Content-Type": "application/json", "X-WP-Nonce": aksrtData.restNonce }, - body: JSON.stringify(body || {}) + body: body === undefined ? undefined : JSON.stringify(body || {}) }).then(function (response) { return response.json(); }); } + function cartUrl() { + return aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart) || "/"; + } + /* ── 扫码支付弹层(微信 Native code_url 等)── */ function showPaymentQr(content, orderNumber) { var existing = document.getElementById("aksrt-pay-qr-overlay"); @@ -118,12 +126,12 @@ function updateCardStatus(card, newStatus, statusLabel) { if (!card) return; - var badge = card.querySelector(".aksrt-orders-page__item-badge"); + 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]"); + 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"; } @@ -216,6 +224,75 @@ /* ── Event delegation ── */ page.addEventListener("click", function (event) { + // 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("/shop/frontend/orders/" + receiptOrder + "/confirm-receipt", {}) + .then(function (result) { + receiptBtn.disabled = false; + if (!result || !result.success) { + showToast((result && result.message) || "确认收货失败"); + return; + } + showToast("已确认收货"); + updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成"); + }) + .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("/shop/frontend/orders/" + 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 — jump to product if possible + var reviewBtn = event.target.closest("[data-aksrt-order-review]"); + if (reviewBtn) { + var card = reviewBtn.closest("[data-aksrt-order-card]"); + var productLink = card ? card.querySelector("a[href*='product']") : null; + if (productLink && productLink.href) { + window.location.href = productLink.href + (productLink.href.indexOf("#") >= 0 ? "" : "#reviews"); + } else { + showToast("请到商品详情页评价"); + } + return; + } + // Pay var payButton = event.target.closest("[data-aksrt-order-pay]"); if (payButton) { diff --git a/assets/js/product-purchase.js b/assets/js/product-purchase.js index 2d805b5a..778cdb7b 100644 --- a/assets/js/product-purchase.js +++ b/assets/js/product-purchase.js @@ -1,241 +1,383 @@ +/** + * Product purchase page — options selector + server cart. + * + * @package AKSRT + */ (function () { - "use strict"; + "use strict"; - function updateCart(product, quantity, replace) { - var key = "aksrt_cart"; - var current; + function restBase() { + var root = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; + return root + "/wp-json/aksrt/v1"; + } - try { - current = JSON.parse(localStorage.getItem(key) || "[]"); - if (!Array.isArray(current)) { - current = []; - } - } catch (error) { - current = []; - } + function restNonce() { + return (window.aksrtData && aksrtData.restNonce) || ""; + } - var index = current.findIndex(function (item) { - return Number(item.product_id || item.id || 0) === Number(product.product_id); - }); - - if (index >= 0) { - current[index].qty = replace ? quantity : Number(current[index].qty || 1) + quantity; - } else { - current.push({ - product_id: product.product_id, - id: product.product_id, - title: product.title, - image: product.image, - price: product.price, - qty: quantity - }); - } - - localStorage.setItem(key, JSON.stringify(current)); + function toast(msg, type) { + if (typeof window.aksrtToast === "function") { + window.aksrtToast(msg, type || "info"); + return; } - - var productPage = document.querySelector("[data-aksrt-product-page]"); - if (productPage) { - var quantityInput = productPage.querySelector("[data-aksrt-quantity]"); - var productId = Number(productPage.getAttribute("data-product-id") || 0); - var titleEl = productPage.querySelector(".aksrt-product-page__title"); - var imageEl = productPage.querySelector(".aksrt-product-page__primary-image img"); - var priceEl = productPage.querySelector(".aksrt-product-page__price"); - var couponEl = productPage.querySelector("[data-aksrt-product-coupon]"); - - productPage.addEventListener("click", function (event) { - var qtyBtn = event.target.closest("[data-aksrt-qty-action]"); - if (qtyBtn && quantityInput) { - var action = qtyBtn.getAttribute("data-aksrt-qty-action"); - var current = Number(quantityInput.value || 1); - var max = Number(quantityInput.getAttribute("max") || 999); - quantityInput.value = action === "inc" ? Math.min(max, current + 1) : Math.max(1, current - 1); - return; - } - - if (event.target.closest("[data-aksrt-add-cart]")) { - updateCart({ - product_id: productId, - title: titleEl ? titleEl.textContent.trim() : "商品", - image: imageEl ? imageEl.getAttribute("src") : "", - price: Number((priceEl ? priceEl.textContent : "0").replace(/[^\d.]/g, "")) - }, Number(quantityInput ? quantityInput.value : 1), false); - if (typeof window.aksrtToast === 'function') { window.aksrtToast("已加入购物车。", 'success'); } - else { window.alert("已加入购物车。"); } - return; - } - - if (event.target.closest("[data-aksrt-buy-now]")) { - updateCart({ - product_id: productId, - title: titleEl ? titleEl.textContent.trim() : "商品", - image: imageEl ? imageEl.getAttribute("src") : "", - price: Number((priceEl ? priceEl.textContent : "0").replace(/[^\d.]/g, "")) - }, Number(quantityInput ? quantityInput.value : 1), true); - - var cartUrl = new URL(aksrtData.cartUrl || window.location.href, window.location.origin); - if (couponEl && couponEl.value) { - cartUrl.searchParams.set("coupon", couponEl.value); - } - window.location.href = cartUrl.toString(); - } - }); - - productPage.addEventListener("click", function (event) { - var thumb = event.target.closest("[data-aksrt-product-thumb]"); - var primaryImage = productPage.querySelector(".aksrt-product-page__primary-image img"); - if (thumb && primaryImage) { - primaryImage.setAttribute("src", thumb.getAttribute("data-aksrt-product-thumb")); - } - }); - - // SKU 规格选择器 - var skuSelector = productPage.querySelector("[data-aksrt-sku-selector]"); - if (skuSelector) { - var selectedSpecs = {}; - var skuInfo = productPage.querySelector("[data-aksrt-sku-info]"); - var skuPrice = skuInfo ? skuInfo.querySelector(".aksrt-product-page__sku-price") : null; - var skuStock = skuInfo ? skuInfo.querySelector(".aksrt-product-page__sku-stock") : null; - var skus = []; - - try { - var skuData = skuSelector.getAttribute("data-aksrt-skus"); - if (skuData) skus = JSON.parse(skuData); - } catch (e) { /* ignore */ } - - skuSelector.addEventListener("click", function (event) { - var btn = event.target.closest("[data-aksrt-spec]"); - if (!btn) return; - - var specIndex = btn.getAttribute("data-aksrt-spec"); - var specVal = btn.getAttribute("data-aksrt-spec-val"); - - // 切换选中状态 - var siblings = skuSelector.querySelectorAll('[data-aksrt-spec="' + specIndex + '"]'); - siblings.forEach(function (s) { s.classList.remove("is-active"); }); - btn.classList.add("is-active"); - - selectedSpecs[specIndex] = specVal; - - // 查找匹配的 SKU - var matchedSku = findMatchingSku(skus, selectedSpecs); - if (matchedSku && skuInfo) { - skuInfo.hidden = false; - if (skuPrice) skuPrice.textContent = "¥" + Number(matchedSku.price || 0).toFixed(2); - if (skuStock) skuStock.textContent = "库存: " + (matchedSku.stock || 0); - - // 更新主价格显示 - if (priceEl) priceEl.textContent = "¥" + Number(matchedSku.price || 0).toFixed(2); - - // 更新库存限制 - if (quantityInput) { - quantityInput.setAttribute("max", String(matchedSku.stock || 999)); - if (Number(quantityInput.value) > (matchedSku.stock || 999)) { - quantityInput.value = matchedSku.stock || 1; - } - } - - // 更新主图 - if (matchedSku.image && primaryImage) { - primaryImage.setAttribute("src", matchedSku.image); - } - } else if (skuInfo) { - skuInfo.hidden = true; - } - }); - - function findMatchingSku(skus, specs) { - if (!skus.length) return null; - var specKeys = Object.keys(specs); - if (specKeys.length === 0) return null; - - return skus.find(function (sku) { - if (!sku.specs) return false; - return specKeys.every(function (key) { - return sku.specs[key] === specs[key]; - }); - }) || null; - } - } + if (window.aksrt && typeof aksrt.notyf === "function") { + aksrt.notyf(msg, { type: type || "info" }); + return; } + window.alert(msg); + } - var editor = document.querySelector("[data-aksrt-product-editor]"); - if (!editor || !window.aksrtData) { - return; + function rest(path, method, body) { + var opts = { + method: method || "GET", + credentials: "same-origin", + headers: { + "X-WP-Nonce": restNonce(), + }, + }; + if (body !== undefined) { + opts.headers["Content-Type"] = "application/json"; + opts.body = JSON.stringify(body); } - - var form = editor.querySelector("[data-aksrt-product-form]"); - var messageEl = editor.querySelector("[data-aksrt-product-editor-message]"); - - function selectedMultiValues(select) { - if (!select) { - return []; - } - - return Array.prototype.slice.call(select.options) - .filter(function (option) { - return option.selected; - }) - .map(function (option) { - return Number(option.value || 0); - }) - .filter(Boolean); - } - - form.addEventListener("submit", function (event) { - event.preventDefault(); - - var formData = new FormData(form); - var productId = Number(editor.getAttribute("data-product-id") || 0); - var gallery = String(formData.get("gallery") || "") - .split(/\r?\n/) - .map(function (line) { return line.trim(); }) - .filter(Boolean); - - var payload = { - title: String(formData.get("title") || "").trim(), - price: Number(formData.get("price") || 0), - stock: Number(formData.get("stock") || 0), - excerpt: String(formData.get("excerpt") || ""), - content: String(formData.get("content") || ""), - status: String(formData.get("status") || "draft"), - categories: selectedMultiValues(form.querySelector('select[name="categories[]"]')), - tags: String(formData.get("tags") || "") - .split(",") - .map(function (tag) { return tag.trim(); }) - .filter(Boolean), - image: String(formData.get("image") || "").trim(), - gallery: gallery, - shipping_mode: String(formData.get("shipping_mode") || "express"), - shipping_fee: Number(formData.get("shipping_fee") || 0), - after_sale_policy: String(formData.get("after_sale_policy") || ""), - support_refund: formData.get("support_refund") === "1" - }; - - var path = productId > 0 ? "/shop/frontend/products/" + productId : "/shop/frontend/products"; - messageEl.textContent = "正在保存商品..."; - - fetch("/wp-json/aksrt/v1" + path, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-WP-Nonce": aksrtData.restNonce - }, - body: JSON.stringify(payload) - }) - .then(function (response) { return response.json(); }) - .then(function (result) { - if (!result.success) { - messageEl.textContent = result.message || "保存失败。"; - return; - } - - var centerUrl = aksrtData.userCenterUrl || "/"; - messageEl.textContent = "商品已保存,正在返回用户中心..."; - window.location.href = centerUrl; - }) - .catch(function () { - messageEl.textContent = "网络错误,请稍后重试。"; - }); + return fetch(restBase() + path, opts).then(function (r) { + return r.json(); }); + } + + function money(value, payModo) { + var n = Number(value || 0).toFixed(2); + return payModo === "points" ? n : "¥" + n; + } + + /* ── Product page ── */ + var productPage = document.querySelector("[data-aksrt-product-page]"); + if (productPage) { + var quantityInput = productPage.querySelector("[data-aksrt-quantity]"); + var productId = Number(productPage.getAttribute("data-product-id") || 0); + var titleEl = productPage.querySelector(".aksrt-product-page__title"); + var imageEl = productPage.querySelector(".aksrt-product-page__primary-image img"); + var priceEl = productPage.querySelector("[data-aksrt-dynamic-price], .aksrt-product-page__price"); + var stockEl = productPage.querySelector("[data-aksrt-stock-display]"); + var couponEl = productPage.querySelector("[data-aksrt-product-coupon]"); + var optionsRoot = productPage.querySelector("[data-aksrt-options-selector]"); + var optionResult = productPage.querySelector("[data-aksrt-option-result]"); + var selectedLabelsEl = productPage.querySelector("[data-aksrt-selected-labels]"); + var resolvedPriceEl = productPage.querySelector("[data-aksrt-resolved-price]"); + var resolvedStockEl = productPage.querySelector("[data-aksrt-resolved-stock]"); + var payModo = productPage.getAttribute("data-pay-modo") || "0"; + var basePrice = Number(productPage.getAttribute("data-base-price") || 0); + var baseStock = Number(productPage.getAttribute("data-base-stock") || 0); + var selectedByGroup = {}; + var groupCount = optionsRoot + ? optionsRoot.querySelectorAll(".aksrt-product-page__option-group").length + : 0; + + function currentOptionsKey() { + if (!groupCount) return ""; + var labels = []; + for (var i = 0; i < groupCount; i++) { + if (!selectedByGroup[i]) return null; + labels.push(selectedByGroup[i].label); + } + return labels.join("|"); + } + + function resolveSelection() { + if (!groupCount) { + return { + key: "", + labels: "", + price: basePrice, + stock: baseStock, + image: "", + }; + } + var key = currentOptionsKey(); + if (key === null) return null; + + var price = basePrice; + var image = ""; + var labels = []; + for (var i = 0; i < groupCount; i++) { + var sel = selectedByGroup[i]; + labels.push(sel.label); + price += Number(sel.priceChange || 0); + if (sel.image) image = sel.image; + } + return { + key: key, + labels: labels.join(" / "), + price: Math.max(0, price), + stock: baseStock, + image: image, + }; + } + + function paintSelection() { + var resolved = resolveSelection(); + if (!resolved) { + if (optionResult) optionResult.hidden = true; + return; + } + if (priceEl) { + if (groupCount && productPage.getAttribute("data-price-range") === "1" && currentOptionsKey() === null) { + /* keep range until complete selection */ + } else { + priceEl.textContent = money(resolved.price, payModo) + (payModo === "points" ? "" : ""); + if (payModo === "points" && priceEl.querySelector("small") === null) { + /* leave as-is */ + } + } + } + if (stockEl && resolved.stock > -1) { + stockEl.textContent = String(resolved.stock); + } + if (quantityInput && resolved.stock > 0) { + quantityInput.setAttribute("max", String(resolved.stock)); + if (Number(quantityInput.value) > resolved.stock) { + quantityInput.value = String(resolved.stock); + } + } + if (imageEl && resolved.image) { + imageEl.setAttribute("src", resolved.image); + } + if (optionResult) { + optionResult.hidden = false; + if (selectedLabelsEl) selectedLabelsEl.textContent = resolved.labels; + if (resolvedPriceEl) resolvedPriceEl.textContent = money(resolved.price, payModo); + if (resolvedStockEl) { + resolvedStockEl.textContent = + resolved.stock <= -1 ? "库存充足" : "库存 " + resolved.stock; + } + } + } + + if (optionsRoot) { + optionsRoot.addEventListener("click", function (event) { + var btn = event.target.closest("[data-option-group]"); + if (!btn || !optionsRoot.contains(btn)) return; + var group = btn.getAttribute("data-option-group"); + var label = btn.getAttribute("data-option-label") || ""; + var priceChange = Number(btn.getAttribute("data-price-change") || 0); + var image = btn.getAttribute("data-option-image") || ""; + + optionsRoot + .querySelectorAll('[data-option-group="' + group + '"]') + .forEach(function (el) { + el.classList.remove("is-active"); + }); + btn.classList.add("is-active"); + selectedByGroup[group] = { label: label, priceChange: priceChange, image: image }; + paintSelection(); + }); + } + + function ensureOptionsSelected() { + if (!groupCount) return true; + if (currentOptionsKey() === null) { + toast("请先选择商品规格", "warning"); + return false; + } + return true; + } + + function addToServerCart(replaceQty) { + if (!window.aksrtData || !aksrtData.isLoggedIn) { + window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/"; + return; + } + if (!ensureOptionsSelected()) return; + + var qty = Number(quantityInput ? quantityInput.value : 1) || 1; + var optionsString = currentOptionsKey() || ""; + var body = { + product_id: productId, + options_string: optionsString, + count: qty, + }; + + rest("/shop/frontend/cart/add", "POST", body) + .then(function (result) { + if (!result || !result.success) { + toast((result && (result.message || result.error)) || "加入购物车失败", "error"); + return; + } + toast("已加入购物车", "success"); + if (replaceQty) { + var cartUrl = new URL( + (window.aksrtData && (aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart))) || "/", + window.location.origin + ); + if (couponEl && couponEl.value) { + cartUrl.searchParams.set("coupon", couponEl.value); + } + window.location.href = cartUrl.toString(); + } + }) + .catch(function () { + toast("网络错误,请稍后重试", "error"); + }); + } + + productPage.addEventListener("click", function (event) { + var qtyBtn = event.target.closest("[data-aksrt-qty-action]"); + if (qtyBtn && quantityInput) { + var action = qtyBtn.getAttribute("data-aksrt-qty-action"); + var current = Number(quantityInput.value || 1); + var max = Number(quantityInput.getAttribute("max") || 999); + quantityInput.value = action === "inc" ? Math.min(max, current + 1) : Math.max(1, current - 1); + return; + } + + if (event.target.closest("[data-aksrt-add-cart]")) { + addToServerCart(false); + return; + } + + if (event.target.closest("[data-aksrt-buy-now]")) { + addToServerCart(true); + return; + } + + var thumb = event.target.closest("[data-aksrt-product-thumb]"); + if (thumb && imageEl) { + imageEl.setAttribute("src", thumb.getAttribute("data-aksrt-product-thumb")); + return; + } + + var videoBtn = event.target.closest("[data-aksrt-product-video]"); + if (videoBtn) { + var url = videoBtn.getAttribute("data-aksrt-product-video"); + if (url) window.open(url, "_blank", "noopener"); + return; + } + + var favBtn = event.target.closest("[data-aksrt-toggle-fav]"); + if (favBtn) { + if (!window.aksrtData || !aksrtData.isLoggedIn) { + window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/"; + return; + } + var pid = Number(favBtn.getAttribute("data-aksrt-toggle-fav") || productId); + rest("/shop/frontend/products/" + pid + "/favorite", "POST", {}) + .then(function (result) { + if (!result || !result.success) { + toast((result && result.message) || "操作失败", "error"); + return; + } + var active = !!(result.data && result.data.is_favorite); + productPage.querySelectorAll("[data-aksrt-toggle-fav]").forEach(function (btn) { + btn.classList.toggle("is-active", active); + }); + toast((result.message) || (active ? "已收藏" : "已取消收藏"), "success"); + }) + .catch(function () { + toast("网络错误", "error"); + }); + return; + } + + var tabBtn = event.target.closest("[data-aksrt-product-tab]"); + if (tabBtn) { + var tab = tabBtn.getAttribute("data-aksrt-product-tab"); + productPage.querySelectorAll("[data-aksrt-product-tab]").forEach(function (b) { + b.classList.toggle("is-active", b === tabBtn); + }); + productPage.querySelectorAll("[data-aksrt-product-panel]").forEach(function (panel) { + panel.hidden = panel.getAttribute("data-aksrt-product-panel") !== tab; + }); + return; + } + + if (event.target.closest("[data-aksrt-show-params]")) { + var paramsModal = productPage.querySelector("[data-aksrt-params-modal]"); + if (paramsModal) paramsModal.hidden = false; + return; + } + if (event.target.closest("[data-aksrt-show-services]")) { + var servicesModal = productPage.querySelector("[data-aksrt-services-modal]"); + if (servicesModal) servicesModal.hidden = false; + return; + } + if (event.target.closest("[data-aksrt-close-modal]")) { + var modal = event.target.closest("[data-aksrt-params-modal], [data-aksrt-services-modal], .aksrt-modal"); + if (modal) modal.hidden = true; + } + }); + } + + /* ── Product editor (seller) ── */ + var editor = document.querySelector("[data-aksrt-product-editor]"); + if (!editor || !window.aksrtData) { + return; + } + + var form = editor.querySelector("[data-aksrt-product-form]"); + var messageEl = editor.querySelector("[data-aksrt-product-editor-message]"); + if (!form) return; + + function selectedMultiValues(select) { + if (!select) return []; + return Array.prototype.slice + .call(select.options) + .filter(function (option) { + return option.selected; + }) + .map(function (option) { + return Number(option.value || 0); + }) + .filter(Boolean); + } + + form.addEventListener("submit", function (event) { + event.preventDefault(); + + var formData = new FormData(form); + var productId = Number(editor.getAttribute("data-product-id") || 0); + var gallery = String(formData.get("gallery") || "") + .split(/\r?\n/) + .map(function (line) { + return line.trim(); + }) + .filter(Boolean); + + var payload = { + title: String(formData.get("title") || "").trim(), + price: Number(formData.get("price") || 0), + stock: Number(formData.get("stock") || 0), + excerpt: String(formData.get("excerpt") || ""), + content: String(formData.get("content") || ""), + status: String(formData.get("status") || "draft"), + categories: selectedMultiValues(form.querySelector('select[name="categories[]"]')), + tags: String(formData.get("tags") || "") + .split(",") + .map(function (tag) { + return tag.trim(); + }) + .filter(Boolean), + image: String(formData.get("image") || "").trim(), + gallery: gallery, + shipping_mode: String(formData.get("shipping_mode") || "express"), + shipping_fee: Number(formData.get("shipping_fee") || 0), + after_sale_policy: String(formData.get("after_sale_policy") || ""), + support_refund: formData.get("support_refund") === "1", + }; + + var path = productId > 0 ? "/shop/frontend/products/" + productId : "/shop/frontend/products"; + if (messageEl) messageEl.textContent = "正在保存商品..."; + + rest(path, "POST", payload) + .then(function (result) { + if (!result.success) { + if (messageEl) messageEl.textContent = result.message || "保存失败。"; + return; + } + var centerUrl = aksrtData.userCenterUrl || "/"; + if (messageEl) messageEl.textContent = "商品已保存,正在返回用户中心..."; + window.location.href = centerUrl; + }) + .catch(function () { + if (messageEl) messageEl.textContent = "网络错误,请稍后重试。"; + }); + }); })(); diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php index 63430a09..dcc4d640 100644 --- a/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php +++ b/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php @@ -130,16 +130,37 @@ trait AKSRT_Admin_Shop_Controller_Frontend_Flow_Operations { $items = []; foreach ($cart as $pid => $opts_map) { - $product = $shop->build_product_card_view((int) $pid); + $product_id = (int) $pid; + $product = $shop->build_product_card_view($product_id); if (empty($product) || !is_array($opts_map)) { continue; } foreach ($opts_map as $opt_str => $qty) { + $options_string = $opt_str === '_default' ? '' : (string) $opt_str; + $unit_price = method_exists($shop, 'resolve_option_price') + ? (float) $shop->resolve_option_price($product_id, $options_string !== '' ? $options_string : '_default') + : (float) ($product['price'] ?? 0); + $options_label = method_exists($shop, 'resolve_option_labels') + ? (string) $shop->resolve_option_labels($product_id, $options_string !== '' ? $options_string : '_default') + : ''; + $option_image = method_exists($shop, 'get_option_image') + ? (string) $shop->get_option_image($product_id, $options_string !== '' ? $options_string : '_default') + : ''; + $stock = method_exists($shop, 'resolve_option_stock') + ? (int) $shop->resolve_option_stock($product_id, $options_string !== '' ? $options_string : '_default') + : (int) ($product['stock'] ?? 0); + $items[] = [ - 'product_id' => (int) $pid, - 'options_string' => $opt_str === '_default' ? '' : $opt_str, - 'quantity' => (int) $qty, - 'product' => $product, + 'product_id' => $product_id, + 'options_string' => $options_string, + 'options_label' => $options_label, + 'quantity' => (int) $qty, + 'unit_price' => $unit_price, + 'line_total' => round($unit_price * (int) $qty, 2), + 'stock' => $stock, + 'image' => $option_image !== '' ? $option_image : (string) ($product['image'] ?? ''), + 'title' => (string) ($product['title'] ?? ''), + 'product' => $product, ]; } } @@ -219,6 +240,40 @@ trait AKSRT_Admin_Shop_Controller_Frontend_Flow_Operations { return $this->success(['data' => $result]); } + public function toggle_frontend_product_favorite(WP_REST_Request $request): WP_REST_Response { + if (!class_exists('AKSRT_Shop')) { + return $this->error('商城服务不可用', 500); + } + + $product_id = (int) $request->get_param('id'); + $result = AKSRT_Shop::get_instance()->toggle_favorite(get_current_user_id(), $product_id); + if (empty($result['success'])) { + return $this->error(__('收藏操作失败', 'aksrt')); + } + + return $this->success([ + 'message' => !empty($result['is_favorite']) ? __('已收藏', 'aksrt') : __('已取消收藏', 'aksrt'), + 'data' => $result, + ]); + } + + public function rebuy_frontend_order(WP_REST_Request $request): WP_REST_Response { + if (!class_exists('AKSRT_Shop')) { + return $this->error('商城服务不可用', 500); + } + + $order_number = sanitize_text_field((string) $request->get_param('order_number')); + $result = AKSRT_Shop::get_instance()->re_purchase(get_current_user_id(), $order_number); + if (empty($result['success'])) { + return $this->error($result['message'] ?? __('再次购买失败', 'aksrt')); + } + + return $this->success([ + 'message' => __('商品已加入购物车', 'aksrt'), + 'data' => $result, + ]); + } + public function get_frontend_addresses(): WP_REST_Response { if (!class_exists('AKSRT_Shop')) { return $this->success(['data' => ['addresses' => []]]); diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller.php b/inc/classes/admin/class-aksrt-admin-shop-controller.php index 2e0d9ab8..1112cfd3 100644 --- a/inc/classes/admin/class-aksrt-admin-shop-controller.php +++ b/inc/classes/admin/class-aksrt-admin-shop-controller.php @@ -346,6 +346,18 @@ class AKSRT_Admin_Shop_Controller extends AKSRT_Admin_Controller_Base { 'permission_callback' => [$this, 'check_logged_in_permission'], ]); + register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9]+)/rebuy', [ + 'methods' => 'POST', + 'callback' => [$this, 'rebuy_frontend_order'], + 'permission_callback' => [$this, 'check_logged_in_permission'], + ]); + + register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P\d+)/favorite', [ + 'methods' => 'POST', + 'callback' => [$this, 'toggle_frontend_product_favorite'], + 'permission_callback' => [$this, 'check_logged_in_permission'], + ]); + register_rest_route(self::API_NAMESPACE, '/shop/after-sale/reasons', [ 'methods' => 'GET', 'callback' => [$this, 'get_after_sale_reasons'], diff --git a/templates/pages/page-product.php b/templates/pages/page-product.php index cf40925b..745b2676 100644 --- a/templates/pages/page-product.php +++ b/templates/pages/page-product.php @@ -46,6 +46,10 @@ $author = $author_id > 0 ? get_userdata($author_id) : null; data-aksrt-product-page data-product-id="" data-options='' + data-base-price=" 0 ? $product['start_price'] : ($product['price'] ?? 0))); ?>" + data-base-stock="" + data-pay-modo="" + data-price-range="" >