Files
wordpress-theme-aksrt/assets/js/product-purchase.js
T
kate522 f3db6c89cc feat(shop): complete storefront archive, seller edit, revenue
Wire product cat/tag archives to settings-driven sort and AJAX paging,
seller product options/video/params editing, revenue withdraw form and
order review submit — finishing remaining shop frontend gaps after buy-path.
2026-07-12 10:38:02 +08:00

408 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Product purchase page — options selector + server cart.
*
* @package AKSRT
*/
(function () {
"use strict";
function restBase() {
var root = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : "";
return root + "/wp-json/aksrt/v1";
}
function restNonce() {
return (window.aksrtData && aksrtData.restNonce) || "";
}
function toast(msg, type) {
if (typeof window.aksrtToast === "function") {
window.aksrtToast(msg, type || "info");
return;
}
if (window.aksrt && typeof aksrt.notyf === "function") {
aksrt.notyf(msg, { type: type || "info" });
return;
}
window.alert(msg);
}
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);
}
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);
}
function parseOptionsText(text) {
var groups = [];
String(text || "").split(String.fromCharCode(10)).forEach(function (line) {
line = line.replace(/\r/g, "").trim();
if (!line) return;
var parts = line.split("|");
if (parts.length < 2) return;
var name = parts[0].trim();
var values = [];
parts.slice(1).join("|").split(",").forEach(function (chunk) {
chunk = chunk.trim();
if (!chunk) return;
var segs = chunk.split(":");
var label = (segs[0] || "").trim();
if (!label) return;
values.push({ label: label, price_change: Number(segs[1] || 0), image: "" });
});
if (name && values.length) {
groups.push({ name: name, view_mode: "list", values: values });
}
});
return groups;
}
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(String.fromCharCode(10))
.map(function (line) { return line.replace(/\r/g, "").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",
options: parseOptionsText(formData.get("options_text") || ""),
start_price: Number(formData.get("start_price") || 0),
pay_modo: String(formData.get("pay_modo") || "0"),
product_params: String(formData.get("product_params") || ""),
product_video: String(formData.get("product_video") || "")
.split(String.fromCharCode(10))
.map(function (line) { return line.replace(/\r/g, "").trim(); })
.filter(Boolean)
};
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 = "网络错误,请稍后重试。";
});
});
})();