From f3db6c89cc9a92eb7c715c3e5e063cde40fc07da Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 12 Jul 2026 10:38:02 +0800 Subject: [PATCH] feat(shop): complete storefront archive, seller edit, revenue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- assets/css/frontend/product-archive.css | 13 + assets/js/order-center.js | 43 +++- assets/js/product-archive.js | 172 ++++++++++--- assets/js/product-purchase.js | 50 +++- assets/js/revenue.js | 114 +++++++++ ...op-controller-frontend-flow-operations.php | 184 ++++++++++++++ ...min-shop-controller-product-operations.php | 8 + .../class-aksrt-admin-shop-controller.php | 26 +- inc/classes/class-aksrt-settings.php | 4 + inc/classes/class-aksrt-shop.php | 14 ++ .../shop-traits/trait-aksrt-shop-part-1.php | 103 ++++++-- inc/helpers/aksrt-transactions.php | 17 +- templates/pages/page-orders.php | 4 +- templates/pages/page-product-edit.php | 42 ++++ templates/pages/page-revenue.php | 235 +++++++++++------- templates/shop/product-cat.php | 117 ++++++--- templates/shop/product-tag.php | 126 +++++++--- 17 files changed, 1024 insertions(+), 248 deletions(-) create mode 100644 assets/js/revenue.js diff --git a/assets/css/frontend/product-archive.css b/assets/css/frontend/product-archive.css index 81230196..91ae9662 100644 --- a/assets/css/frontend/product-archive.css +++ b/assets/css/frontend/product-archive.css @@ -719,3 +719,16 @@ .aksrt-product-edit__actions .btn-ghost:hover { border-color: var(--color-primary, var(--color-primary)); } + +.aksrt-product-archive__toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px} +.aksrt-product-archive__sort{display:inline-flex;padding:8px 14px;border-radius:999px;background:var(--surface-1,#fff);color:var(--color-ink-3,#5c6370);text-decoration:none;font-weight:600;font-size:14px;box-shadow:var(--shadow-sm)} +.aksrt-product-archive__sort.is-active,.aksrt-product-archive__sort:hover{background:var(--color-primary,#f04494);color:#fff} +.aksrt-product-archive__load-more{display:flex;justify-content:center;margin:20px 0} +.aksrt-revenue-page__layout{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin:20px 0} +.aksrt-revenue-page__panel{padding:18px;border-radius:16px;background:var(--surface-1,#fff);box-shadow:var(--shadow-sm)} +.aksrt-revenue-page__withdraw-form label{display:flex;flex-direction:column;gap:6px;margin-bottom:12px} +.aksrt-revenue-page__withdraw-form input,.aksrt-revenue-page__withdraw-form select,.aksrt-revenue-page__withdraw-form textarea{width:100%;padding:10px 12px;border:1px solid var(--color-line-soft,rgba(0,0,0,.08));border-radius:10px} +.aksrt-revenue-page__item{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-line-soft,rgba(0,0,0,.06))} +.aksrt-revenue-page__item .is-in{color:#27ae60;font-weight:700} +.aksrt-revenue-page__item .is-out{color:#e74c3c;font-weight:700} +.aksrt-product-editor__hint{font-size:13px;color:var(--color-ink-4,#8a909c);margin:0 0 10px} diff --git a/assets/js/order-center.js b/assets/js/order-center.js index e5f2a2bb..0116e4e9 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -280,16 +280,47 @@ return; } - // Review — jump to product if possible + // Review submit modal var reviewBtn = event.target.closest("[data-aksrt-order-review]"); if (reviewBtn) { + var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review"); 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("请到商品详情页评价"); + var productId = 0; + if (card) { + var firstItem = card.querySelector("[data-product-id]"); + if (firstItem) productId = Number(firstItem.getAttribute("data-product-id") || 0); } + if (!productId) { + // try parse from item title block dataset if present + var any = card ? card.querySelector(".aksrt-orders-page__card-item") : null; + if (any && any.getAttribute("data-product-id")) { + productId = Number(any.getAttribute("data-product-id") || 0); + } + } + if (!reviewOrder) return; + var content = window.prompt("请输入评价内容(可选)", "好评!"); + if (content === null) return; + var rating = window.prompt("商品评分 1-5", "5"); + var r = Math.max(1, Math.min(5, parseInt(rating || "5", 10) || 5)); + reviewBtn.disabled = true; + request("/shop/frontend/orders/" + reviewOrder + "/review", { + product_id: productId, + content: content || "好评!", + rating_product: r, + rating_service: r, + rating_shipping: r + }).then(function (result) { + reviewBtn.disabled = false; + if (!result || !result.success) { + showToast((result && result.message) || "评价失败"); + return; + } + showToast(result.message || "评价已提交"); + updateCardStatus(findOrderCard(reviewOrder), "completed", "已完成"); + }).catch(function () { + reviewBtn.disabled = false; + showToast("网络错误"); + }); return; } diff --git a/assets/js/product-archive.js b/assets/js/product-archive.js index 17a3d994..74a54470 100644 --- a/assets/js/product-archive.js +++ b/assets/js/product-archive.js @@ -1,44 +1,152 @@ /** - * AKSRT Product Archive — 商品归档页交互 - * - * 处理商品分类/标签归档页的筛选、排序和加载更多。 + * Product archive — sort + AJAX load more. * * @package AKSRT */ - (function () { - 'use strict'; + "use strict"; - var grid = document.getElementById('aksrt-product-grid'); - if (!grid) return; + var root = document.querySelector("[data-aksrt-product-archive]"); + if (!root) return; - // 商品卡片懒加载图片观察器 - if ('IntersectionObserver' in window) { - var imgObserver = new IntersectionObserver(function (entries) { - entries.forEach(function (entry) { - if (entry.isIntersecting) { - var img = entry.target; - if (img.dataset.src) { - img.src = img.dataset.src; - img.removeAttribute('data-src'); - } - imgObserver.unobserve(img); - } - }); - }, { rootMargin: '200px' }); + function restBase() { + var site = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; + return site + "/wp-json/aksrt/v1"; + } - grid.querySelectorAll('img[data-src]').forEach(function (img) { - imgObserver.observe(img); - }); + function toast(msg) { + if (typeof window.aksrtToast === "function") { + window.aksrtToast(msg); + return; + } + if (window.console) console.log(msg); + } + + var grid = root.querySelector("[data-aksrt-product-grid], #aksrt-product-grid"); + var countEl = root.querySelector("[data-aksrt-archive-count]"); + var loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]"); + var state = { + taxonomy: root.getAttribute("data-taxonomy") || "", + termId: Number(root.getAttribute("data-term-id") || 0), + orderby: root.getAttribute("data-orderby") || "date", + page: Number(root.getAttribute("data-page") || 1), + maxPages: Number(root.getAttribute("data-max-pages") || 1), + paging: root.getAttribute("data-paging") || "number", + ias: root.getAttribute("data-ias") === "1", + iasMax: Number(root.getAttribute("data-ias-max") || 5), + loading: false, + autoLoads: 0, + }; + + function fetchArchive(page, orderby, replace) { + if (state.loading) return Promise.resolve(); + state.loading = true; + if (loadMoreBtn) { + loadMoreBtn.disabled = true; + loadMoreBtn.textContent = "加载中..."; } - // 商品卡片 hover 效果增强 - grid.querySelectorAll('.aksrt-product-card').forEach(function (card) { - card.addEventListener('mouseenter', function () { - this.classList.add('aksrt-product-card--hover'); + var qs = new URLSearchParams(); + qs.set("taxonomy", state.taxonomy); + qs.set("term_id", String(state.termId)); + qs.set("page", String(page)); + qs.set("orderby", orderby || state.orderby); + + return fetch(restBase() + "/shop/frontend/products/archive?" + qs.toString(), { + method: "GET", + credentials: "same-origin", + headers: { + "X-WP-Nonce": (window.aksrtData && aksrtData.restNonce) || "", + }, + }) + .then(function (r) { return r.json(); }) + .then(function (result) { + state.loading = false; + if (!result || !result.success || !result.data) { + toast((result && result.message) || "加载失败"); + if (loadMoreBtn) { + loadMoreBtn.disabled = false; + loadMoreBtn.textContent = "加载更多"; + } + return; + } + var data = result.data; + state.page = Number(data.page || page); + state.maxPages = Number(data.max_pages || 1); + state.orderby = data.orderby || state.orderby; + root.setAttribute("data-page", String(state.page)); + root.setAttribute("data-max-pages", String(state.maxPages)); + root.setAttribute("data-orderby", state.orderby); + + if (countEl && typeof data.total !== "undefined") { + countEl.textContent = "共 " + data.total + " 件商品"; + } + + if (grid) { + if (replace) { + grid.innerHTML = data.html || ""; + } else if (data.html) { + grid.insertAdjacentHTML("beforeend", data.html); + } + } + + root.querySelectorAll("[data-aksrt-shop-orderby]").forEach(function (a) { + a.classList.toggle("is-active", a.getAttribute("data-aksrt-shop-orderby") === state.orderby); }); - card.addEventListener('mouseleave', function () { - this.classList.remove('aksrt-product-card--hover'); - }); - }); + + if (loadMoreBtn) { + var hasMore = state.page < state.maxPages; + loadMoreBtn.style.display = hasMore ? "" : "none"; + loadMoreBtn.disabled = false; + loadMoreBtn.textContent = "加载更多"; + } + }) + .catch(function () { + state.loading = false; + if (loadMoreBtn) { + loadMoreBtn.disabled = false; + loadMoreBtn.textContent = "加载更多"; + } + toast("网络错误"); + }); + } + + root.addEventListener("click", function (e) { + var sort = e.target.closest("[data-aksrt-shop-orderby]"); + if (sort && root.contains(sort)) { + e.preventDefault(); + var orderby = sort.getAttribute("data-aksrt-shop-orderby") || "date"; + if (orderby === state.orderby && state.paging === "ajax") return; + state.orderby = orderby; + state.autoLoads = 0; + try { + var u = new URL(window.location.href); + u.searchParams.set("shop_orderby", orderby); + u.searchParams.delete("paged"); + window.history.replaceState({}, "", u.toString()); + } catch (err) {} + fetchArchive(1, orderby, true); + return; + } + + var more = e.target.closest("[data-aksrt-shop-load-more]"); + if (more && root.contains(more)) { + e.preventDefault(); + if (state.page >= state.maxPages) return; + fetchArchive(state.page + 1, state.orderby, false); + } + }); + + if (state.paging === "ajax" && state.ias && "IntersectionObserver" in window && loadMoreBtn) { + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (!entry.isIntersecting) return; + if (state.loading || state.page >= state.maxPages) return; + if (state.iasMax > 0 && state.autoLoads >= state.iasMax) return; + state.autoLoads += 1; + fetchArchive(state.page + 1, state.orderby, false); + }); + }, { rootMargin: "240px" }); + io.observe(loadMoreBtn); + } })(); diff --git a/assets/js/product-purchase.js b/assets/js/product-purchase.js index 778cdb7b..9dea3012 100644 --- a/assets/js/product-purchase.js +++ b/assets/js/product-purchase.js @@ -320,25 +320,43 @@ 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(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(/\r?\n/) - .map(function (line) { - return line.trim(); - }) + .split(String.fromCharCode(10)) + .map(function (line) { return line.replace(/\r/g, "").trim(); }) .filter(Boolean); var payload = { @@ -351,9 +369,7 @@ categories: selectedMultiValues(form.querySelector('select[name="categories[]"]')), tags: String(formData.get("tags") || "") .split(",") - .map(function (tag) { - return tag.trim(); - }) + .map(function (tag) { return tag.trim(); }) .filter(Boolean), image: String(formData.get("image") || "").trim(), gallery: gallery, @@ -361,6 +377,14 @@ 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"; diff --git a/assets/js/revenue.js b/assets/js/revenue.js new file mode 100644 index 00000000..e687f0a9 --- /dev/null +++ b/assets/js/revenue.js @@ -0,0 +1,114 @@ +/** + * Creator revenue / withdraw form. + * + * @package AKSRT + */ +(function () { + "use strict"; + + var page = document.querySelector("[data-aksrt-revenue-page]"); + if (!page || !window.aksrtData) return; + + 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(); + }); + } + + var form = page.querySelector("[data-aksrt-withdraw-form]"); + var msg = page.querySelector("[data-aksrt-withdraw-msg]"); + var balanceEl = page.querySelector("[data-aksrt-revenue-balance]"); + var list = page.querySelector("[data-aksrt-withdraw-list]"); + + if (!form) return; + + form.addEventListener("submit", function (e) { + e.preventDefault(); + var fd = new FormData(form); + var amount = Number(fd.get("amount") || 0); + var method = String(fd.get("method") || "alipay"); + var account = String(fd.get("account_info") || "").trim(); + var min = Number(page.getAttribute("data-min-amount") || 50); + if (amount < min) { + if (msg) { + msg.hidden = false; + msg.textContent = "最低提现金额为 " + min; + } + return; + } + if (!account) { + if (msg) { + msg.hidden = false; + msg.textContent = "请填写收款账户信息"; + } + return; + } + + var btn = form.querySelector('button[type="submit"]'); + if (btn) { + btn.disabled = true; + btn.textContent = "提交中..."; + } + + rest("/shop/frontend/revenue/withdraw", "POST", { + amount: amount, + method: method, + account_info: account, + }) + .then(function (result) { + if (btn) { + btn.disabled = false; + btn.textContent = "提交提现申请"; + } + if (!result || !result.success) { + if (msg) { + msg.hidden = false; + msg.textContent = (result && result.message) || "提现失败"; + } + return; + } + if (msg) { + msg.hidden = false; + msg.textContent = result.message || "提现申请已提交"; + } + if (balanceEl && result.data && typeof result.data.balance !== "undefined") { + balanceEl.textContent = "¥" + Number(result.data.balance).toFixed(2); + } + if (list && result.data) { + var item = document.createElement("div"); + item.className = "aksrt-revenue-page__item"; + item.innerHTML = + "
¥" + + Number(result.data.amount || amount).toFixed(2) + + "" + + method + + "
pending
"; + list.insertBefore(item, list.firstChild); + } + form.reset(); + }) + .catch(function () { + if (btn) { + btn.disabled = false; + btn.textContent = "提交提现申请"; + } + if (msg) { + msg.hidden = false; + msg.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 dcc4d640..d84894f7 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 @@ -354,6 +354,190 @@ trait AKSRT_Admin_Shop_Controller_Frontend_Flow_Operations { return $this->success(['message' => __('已设为默认地址', 'aksrt')]); } + public function get_frontend_product_archive(WP_REST_Request $request): WP_REST_Response { + if (!class_exists('AKSRT_Shop')) { + return $this->error('商城服务不可用', 500); + } + + $taxonomy = sanitize_key((string) ($request->get_param('taxonomy') ?? '')); + $term_id = (int) ($request->get_param('term_id') ?? 0); + $page = max(1, (int) ($request->get_param('page') ?? 1)); + $orderby = sanitize_key((string) ($request->get_param('orderby') ?? '')); + + $view = AKSRT_Shop::get_instance()->get_product_archive_view([ + 'taxonomy' => $taxonomy, + 'term_id' => $term_id, + 'paged' => $page, + 'orderby' => $orderby, + ]); + + $html = ''; + if (!empty($view['products']) && is_array($view['products'])) { + ob_start(); + foreach ($view['products'] as $product) { + $this->render_product_card_html($product, $view); + } + $html = (string) ob_get_clean(); + } + + return $this->success([ + 'data' => array_merge($view, [ + 'html' => $html, + 'has_more' => (int) ($view['page'] ?? 1) < (int) ($view['max_pages'] ?? 1), + ]), + ]); + } + + private function render_product_card_html(array $product, array $view = []): void { + $sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; + $mode = (string) ($sales_cfg['show'] ?? 'show'); + $min_sales = max(0, (int) ($sales_cfg['show_min'] ?? 0)); + $sales = (int) ($product['sales'] ?? 0); + $can_show_sales = false; + if ($mode === 'show') { + $can_show_sales = $sales > 0; + } elseif ($mode === 'threshold') { + $can_show_sales = $sales >= $min_sales; + } + + $permalink = (string) ($product['permalink'] ?? '#'); + $title = (string) ($product['title'] ?? ''); + $image = (string) ($product['image'] ?? ''); + $price = (float) ($product['price'] ?? 0); + $excerpt = (string) ($product['excerpt'] ?? ''); + ?> +
+ + +
+ <?php echo esc_attr($title); ?> +
+ +
+ +
+ +
+

+ +

+ + +
+
+
+ error('收益服务不可用', 500); + } + $view = aksrt_get_revenue_page_view(get_current_user_id()); + $withdrawals = []; + if (class_exists('AKSRT_Withdrawal')) { + $rows = AKSRT_Withdrawal::get_instance()->get_user_withdrawals(get_current_user_id(), ['limit' => 20]); + foreach ($rows as $row) { + $withdrawals[] = [ + 'id' => (int) ($row['id'] ?? 0), + 'amount' => (float) ($row['amount'] ?? 0), + 'status' => (string) ($row['status'] ?? ''), + 'method' => (string) ($row['method'] ?? ''), + 'created_at' => (string) ($row['created_at'] ?? ''), + 'note' => (string) ($row['note'] ?? ''), + ]; + } + } + $settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_withdrawal_settings() : []; + return $this->success([ + 'data' => array_merge($view, [ + 'withdrawals' => $withdrawals, + 'settings' => [ + 'min_amount' => (float) ($settings['min_amount'] ?? 50), + 'service_charge_rate' => (float) ($settings['service_charge_rate'] ?? 0), + 'text' => (string) ($settings['text'] ?? ''), + ], + ]), + ]); + } + + public function create_frontend_withdraw(WP_REST_Request $request): WP_REST_Response { + if (!class_exists('AKSRT_Withdrawal') || !class_exists('AKSRT_Credits')) { + return $this->error('提现服务不可用', 500); + } + + $params = $this->get_json_params($request); + $amount = (float) ($params['amount'] ?? 0); + $method = sanitize_key((string) ($params['method'] ?? 'alipay')); + $account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? '')); + + $settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_withdrawal_settings() : []; + $min = (float) ($settings['min_amount'] ?? 50); + if ($amount < $min) { + return $this->error(sprintf(__('最低提现金额为 %s', 'aksrt'), $min)); + } + if ($account === '') { + return $this->error(__('请填写收款账户信息', 'aksrt')); + } + + $user_id = get_current_user_id(); + $balance = (float) AKSRT_Credits::balance($user_id); + if ($amount > $balance) { + return $this->error(__('可提现余额不足', 'aksrt')); + } + + $cost = (int) round($amount); + if ($cost <= 0) { + return $this->error(__('提现金额无效', 'aksrt')); + } + $deducted = AKSRT_Credits::deduct($user_id, $cost, 'withdraw', ['type' => 'withdraw'], __('用户提现申请', 'aksrt')); + if (is_wp_error($deducted)) { + return $this->error($deducted->get_error_message()); + } + + $id = AKSRT_Withdrawal::get_instance()->create($user_id, $amount, $method, $account); + if ($id <= 0) { + AKSRT_Credits::add($user_id, $cost, 'withdraw_refund', ['type' => 'withdraw'], __('提现失败退回', 'aksrt')); + return $this->error(__('提现申请创建失败', 'aksrt'), 500); + } + + return $this->success([ + 'message' => __('提现申请已提交', 'aksrt'), + 'data' => [ + 'id' => $id, + 'amount' => $amount, + 'balance' => (float) AKSRT_Credits::balance($user_id), + ], + ], 201); + } + + public function submit_frontend_product_review(WP_REST_Request $request): WP_REST_Response { + if (!class_exists('AKSRT_Product_Review')) { + return $this->error('评价服务不可用', 500); + } + $params = $this->get_json_params($request); + $order_number = sanitize_text_field((string) $request->get_param('order_number')); + $product_id = (int) ($params['product_id'] ?? 0); + $result = AKSRT_Product_Review::get_instance()->submit_review( + get_current_user_id(), + $order_number, + $product_id, + $params + ); + if (empty($result['success'])) { + return $this->error($result['message'] ?? __('评价失败', 'aksrt')); + } + return $this->success([ + 'message' => __('评价已提交', 'aksrt'), + 'data' => $result, + ], 201); + } + public function create_frontend_product(WP_REST_Request $request): WP_REST_Response { $params = $this->get_json_params($request); $params['author_id'] = get_current_user_id(); diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php index 3f149a9f..7abf8ae6 100644 --- a/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php +++ b/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php @@ -333,6 +333,10 @@ trait AKSRT_Admin_Shop_Controller_Product_Operations { update_post_meta($post_id, '_aksrt_product_image', $image); } + if (isset($params['options']) && is_array($params['options']) && class_exists('AKSRT_Shop')) { + AKSRT_Shop::get_instance()->save_product_options((int) $post_id, $params['options']); + } + return $this->success([ 'message' => __('商品已创建', 'aksrt'), 'data' => ['id' => $post_id], @@ -484,6 +488,10 @@ trait AKSRT_Admin_Shop_Controller_Product_Operations { update_post_meta($id, '_aksrt_product_image', esc_url_raw((string) $params['image'])); } + if (isset($params['options']) && is_array($params['options']) && class_exists('AKSRT_Shop')) { + AKSRT_Shop::get_instance()->save_product_options($id, $params['options']); + } + return $this->success([ 'message' => __('商品已更新', 'aksrt'), ]); diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller.php b/inc/classes/admin/class-aksrt-admin-shop-controller.php index 1112cfd3..4ea2c1ce 100644 --- a/inc/classes/admin/class-aksrt-admin-shop-controller.php +++ b/inc/classes/admin/class-aksrt-admin-shop-controller.php @@ -352,7 +352,31 @@ 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/products/(?P\d+)/favorite', [ + register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/archive', [ + 'methods' => 'GET', + 'callback' => [$this, 'get_frontend_product_archive'], + 'permission_callback' => '__return_true', + ]); + + register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue', [ + 'methods' => 'GET', + 'callback' => [$this, 'get_frontend_revenue'], + 'permission_callback' => [$this, 'check_logged_in_permission'], + ]); + + register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue/withdraw', [ + 'methods' => 'POST', + 'callback' => [$this, 'create_frontend_withdraw'], + 'permission_callback' => [$this, 'check_logged_in_permission'], + ]); + + register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9]+)/review', [ + 'methods' => 'POST', + 'callback' => [$this, 'submit_frontend_product_review'], + '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'], diff --git a/inc/classes/class-aksrt-settings.php b/inc/classes/class-aksrt-settings.php index 674de9d0..a93d9ead 100644 --- a/inc/classes/class-aksrt-settings.php +++ b/inc/classes/class-aksrt-settings.php @@ -1000,6 +1000,10 @@ class AKSRT_Settings { return AKSRT_Settings_Post_Display::get_comment_display_settings(); } + public static function get_withdrawal_settings(): array { + return AKSRT_Settings_Payment_Runtime::get_withdrawal_settings(); + } + /** * pre_get_posts: apply post_list_per_page on archive/tag/category/search/home pages. * diff --git a/inc/classes/class-aksrt-shop.php b/inc/classes/class-aksrt-shop.php index f5f2756a..71378645 100644 --- a/inc/classes/class-aksrt-shop.php +++ b/inc/classes/class-aksrt-shop.php @@ -152,6 +152,11 @@ class AKSRT_Shop { 'after_sale_policy' => '', 'support_refund' => true, 'status' => 'draft', + 'options' => [], + 'pay_modo' => '0', + 'start_price' => 0, + 'product_video' => [], + 'product_params' => '', ], 'taxonomy' => [ 'categories' => $this->product_service()->get_term_options('aksrt_product_cat'), @@ -181,6 +186,10 @@ class AKSRT_Shop { } $view['is_edit'] = true; + $videos = get_post_meta($product_id, '_aksrt_product_video', true); + if (!is_array($videos)) { + $videos = []; + } $view['product'] = [ 'id' => $product_id, 'title' => (string) $product->post_title, @@ -197,6 +206,11 @@ class AKSRT_Shop { 'after_sale_policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true), 'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0', 'status' => (string) get_post_status($product_id), + 'options' => method_exists($this, 'get_product_options') ? $this->get_product_options($product_id) : [], + 'pay_modo' => sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0', + 'start_price' => (float) get_post_meta($product_id, '_aksrt_start_price', true), + 'product_video' => array_values(array_filter(array_map('esc_url_raw', $videos))), + 'product_params' => (string) get_post_meta($product_id, '_aksrt_product_params', true), ]; return $view; diff --git a/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php b/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php index af68b558..38fa26f3 100644 --- a/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php +++ b/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php @@ -46,48 +46,111 @@ trait AKSRT_Shop_Part_1 { public function get_product_archive_view(array $args = []): array { $taxonomy = sanitize_key((string) ($args['taxonomy'] ?? '')); $term_id = (int) ($args['term_id'] ?? 0); - $paged = max(1, (int) ($args['paged'] ?? 1)); - $per_page = 12; + $paged = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1)); - if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true)) { - return ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0]; + $shop_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_shop_settings() : []; + $list = is_array($shop_settings['list'] ?? null) ? $shop_settings['list'] : []; + $per_page = max(4, min(100, (int) ($args['per_page'] ?? ($list['count'] ?? 12)))); + $orderby = sanitize_key((string) ($args['orderby'] ?? ($list['orderby'] ?? 'date'))); + if (!in_array($orderby, ['date', 'sales', 'price_asc', 'price_desc', 'comments', 'modified'], true)) { + $orderby = 'date'; + } + $paging = sanitize_key((string) ($args['paging'] ?? ($list['paging'] ?? 'number'))); + if (!in_array($paging, ['number', 'ajax'], true)) { + $paging = 'number'; + } + $ias_s = array_key_exists('ias_s', $args) ? !empty($args['ias_s']) : !empty($list['ias_s']); + $ias_max = max(1, min(50, (int) ($args['ias_max'] ?? ($list['ias_max'] ?? 5)))); + $style = sanitize_key((string) ($list['style'] ?? 'default')); + + $empty = [ + 'products' => [], + 'term' => null, + 'total' => 0, + 'max_pages' => 0, + 'page' => $paged, + 'orderby' => $orderby, + 'paging' => $paging, + 'ias_s' => $ias_s, + 'ias_max' => $ias_max, + 'per_page' => $per_page, + 'style' => $style, + 'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [], + ]; + + if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) { + return $empty; } - $term = get_term($term_id, $taxonomy); - if (is_wp_error($term) || !$term) { - return ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0]; - } - - $query = new WP_Query([ + $query_args = [ 'post_type' => 'aksrt_product', 'posts_per_page' => $per_page, 'paged' => $paged, 'post_status' => 'publish', - 'tax_query' => [[ + 'order' => 'DESC', + 'orderby' => 'date', + ]; + + $term = null; + if ($taxonomy !== '' && $term_id > 0) { + $term = get_term($term_id, $taxonomy); + if (is_wp_error($term) || !$term) { + return $empty; + } + $query_args['tax_query'] = [[ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_id, - ]], - ]); + ]]; + } + if ($orderby === 'sales') { + $query_args['meta_key'] = '_aksrt_sales'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + } elseif ($orderby === 'price_asc') { + $query_args['meta_key'] = '_aksrt_price'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'ASC'; + } elseif ($orderby === 'price_desc') { + $query_args['meta_key'] = '_aksrt_price'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + } elseif ($orderby === 'comments') { + $query_args['orderby'] = 'comment_count'; + $query_args['order'] = 'DESC'; + } elseif ($orderby === 'modified') { + $query_args['orderby'] = 'modified'; + $query_args['order'] = 'DESC'; + } + + $query = new WP_Query($query_args); $products = []; while ($query->have_posts()) { $query->the_post(); $products[] = $this->build_product_card_view(get_the_ID()); } - wp_reset_postdata(); return [ 'products' => $products, - 'term' => [ - 'id' => $term->term_id, - 'name' => $term->name, - 'description' => $term->description, - 'count' => $term->count, - ], + 'term' => $term ? [ + 'id' => (int) $term->term_id, + 'name' => (string) $term->name, + 'description' => (string) $term->description, + 'count' => (int) $term->count, + 'taxonomy' => $taxonomy, + ] : null, 'total' => (int) $query->found_posts, 'max_pages' => (int) $query->max_num_pages, + 'page' => $paged, + 'orderby' => $orderby, + 'paging' => $paging, + 'ias_s' => $ias_s, + 'ias_max' => $ias_max, + 'per_page' => $per_page, + 'style' => $style, + 'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [], ]; } diff --git a/inc/helpers/aksrt-transactions.php b/inc/helpers/aksrt-transactions.php index 93b2cd46..98827170 100644 --- a/inc/helpers/aksrt-transactions.php +++ b/inc/helpers/aksrt-transactions.php @@ -82,12 +82,13 @@ function aksrt_get_revenue_page_view(int $user_id): array { if (class_exists('AKSRT_Revenue')) { $revenues = AKSRT_Revenue::get_instance()->get_user_revenues($user_id, ['limit' => 200]); foreach ($revenues as $rev) { - $total_earned += (float) ($rev->amount ?? 0); + $row = is_array($rev) ? $rev : (array) $rev; + $total_earned += (float) ($row['amount'] ?? 0); $records[] = [ 'type' => 'earning', - 'title' => (string) ($rev->description ?? __('创作分成', 'aksrt')), - 'amount' => (float) ($rev->amount ?? 0), - 'created_at' => (string) ($rev->created_at ?? ''), + 'title' => (string) ($row['description'] ?? $row['type'] ?? __('创作分成', 'aksrt')), + 'amount' => (float) ($row['amount'] ?? 0), + 'created_at' => (string) ($row['created_at'] ?? ''), ]; } } @@ -95,12 +96,14 @@ function aksrt_get_revenue_page_view(int $user_id): array { if (class_exists('AKSRT_Withdrawal')) { $withdrawals = AKSRT_Withdrawal::get_instance()->get_user_withdrawals($user_id, ['limit' => 200]); foreach ($withdrawals as $w) { - $total_withdrawn += (float) ($w->amount ?? 0); + $wrow = is_array($w) ? $w : (array) $w; + $total_withdrawn += (float) ($wrow['amount'] ?? 0); $records[] = [ 'type' => 'withdrawal', 'title' => __('提现', 'aksrt'), - 'amount' => (float) ($w->amount ?? 0), - 'created_at' => (string) ($w->created_at ?? ''), + 'amount' => (float) ($wrow['amount'] ?? 0), + 'created_at' => (string) ($wrow['created_at'] ?? ''), + 'status' => (string) ($wrow['status'] ?? ''), ]; } } diff --git a/templates/pages/page-orders.php b/templates/pages/page-orders.php index f6417be5..70c172eb 100644 --- a/templates/pages/page-orders.php +++ b/templates/pages/page-orders.php @@ -92,7 +92,7 @@ $tabs = [
-
+
@@ -109,7 +109,7 @@ $tabs = [
-
+
¥
diff --git a/templates/pages/page-product-edit.php b/templates/pages/page-product-edit.php index 736ccb55..c4897d9a 100644 --- a/templates/pages/page-product-edit.php +++ b/templates/pages/page-product-edit.php @@ -91,7 +91,49 @@ $product = (array) ($edit_view['product'] ?? []);
+
+

+

+ +
+ + +
+ + +
+