feat(shop): wire storefront buy path to server cart

Connect product options/add-cart, server cart checkout payloads,
favorite/rebuy/receipt REST, and order-center actions without
localStorage dual-read or hardcoded cart/orders URL hacks.
This commit is contained in:
kate522 committed 2026-07-12 09:15:45 +08:00
1 parent 2ed3ffe317
commit 50e3db85bb
6 files changed
+621 -304

No files matched your search

+373 -231
View File
@@ -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 = "网络错误,请稍后重试。";
});
});
})();