From 433b7cb3a68b2f7494a8dd4ade9db9eb25637e0d Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 18 Jul 2026 16:42:52 +0800 Subject: [PATCH] feat(shop): category filter bar + sort/load-more 1:1 UX Port zibll hierarchical cat tabs, multi-metric sort with price toggle, skeleton/empty states, and REST archive payload (header/html/paginate) while reusing product-card template for AJAX HTML. --- assets/css/frontend/product-archive.css | 20 ++ assets/js/product-archive.js | 239 ++++++++++++++---- .../ShopControllerFrontendFlowOperations.php | 111 ++++---- src/AKSRT/Shop/Part1.php | 173 ++++++++++++- src/AKSRT/Shop/ProductReview.php | 3 +- templates/parts/shop/product-card.php | 2 +- templates/parts/shop/product-cat-filter.php | 149 +++++++++++ templates/shop/product-cat.php | 192 ++++++++------ templates/shop/product-tag.php | 212 ++++++++++------ 9 files changed, 828 insertions(+), 273 deletions(-) create mode 100644 templates/parts/shop/product-cat-filter.php diff --git a/assets/css/frontend/product-archive.css b/assets/css/frontend/product-archive.css index fdc26368..8ae89bc3 100644 --- a/assets/css/frontend/product-archive.css +++ b/assets/css/frontend/product-archive.css @@ -733,3 +733,23 @@ .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} + +/* ===== zibll-parity cat filter / skeleton ===== */ +.aksrt-shop-cat-filter.shop-cat-filter{margin-top:14px} +.shop-cat-filter-tab{display:flex;gap:10px;overflow-x:auto;list-style:none;margin:0 0 10px;padding:0 0 4px;-webkit-overflow-scrolling:touch} +.shop-cat-filter-tab li{flex:0 0 auto} +.shop-cat-filter-tab a, +.shop-cat-filter-tab span, +.shop-cat-filter-child a, +.shop-cat-filter-child span{display:inline-flex;align-items:center;padding:6px 12px;border-radius:999px;font-size:13px;font-weight:600;text-decoration:none;color:var(--color-ink-2,#5c6370);background:var(--surface-4,#f3f4f6)} +.shop-cat-filter-tab .active span, +.shop-cat-filter-child .focus-color, +.shop-cat-filter-orderby .focus-color{color:var(--color-primary,#f04494);background:color-mix(in srgb,var(--color-primary,#f04494) 12%, #fff)} +.shop-cat-filter-child{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px} +.shop-cat-filter-orderby{gap:8px} +.shop-cat-filter-orderby .opacity5{opacity:.55;margin-right:4px;font-size:13px} +.aksrt-product-archive__skeleton{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;padding:8px 0 16px} +.aksrt-product-card--placeholder{min-height:220px;border-radius:12px;background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb 37%,#f3f4f6 63%);background-size:400% 100%;animation:aksrtShopSk 1.2s ease infinite} +@keyframes aksrtShopSk{0%{background-position:100% 0}100%{background-position:0 0}} +@media (max-width:992px){.aksrt-product-archive__skeleton{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media (max-width:560px){.aksrt-product-archive__skeleton{grid-template-columns:1fr}} diff --git a/assets/js/product-archive.js b/assets/js/product-archive.js index 74a54470..7f387b44 100644 --- a/assets/js/product-archive.js +++ b/assets/js/product-archive.js @@ -1,5 +1,7 @@ /** - * Product archive — sort + AJAX load more. + * Product archive — cat filter + sort + AJAX load more (zibll ajax-next UX). + * + * Uses REST /shop/frontend/products/archive and optional AksrtAjaxList events. * * @package AKSRT */ @@ -10,7 +12,7 @@ if (!root) return; function restBase() { - var site = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; + var site = window.aksrtData && aksrtData.siteUrl ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; return site + "/wp-json/aksrt/v1"; } @@ -23,12 +25,17 @@ } var grid = root.querySelector("[data-aksrt-product-grid], #aksrt-product-grid"); + var pager = root.querySelector("[data-aksrt-ajaxpager], .ajaxpager"); var countEl = root.querySelector("[data-aksrt-archive-count]"); + var headerHost = root.querySelector("[data-aksrt-ajax-header]"); var loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]"); + var loader = root.querySelector("[data-aksrt-ajax-loader], .post_ajax_loader"); + var state = { taxonomy: root.getAttribute("data-taxonomy") || "", termId: Number(root.getAttribute("data-term-id") || 0), orderby: root.getAttribute("data-orderby") || "date", + order: (root.getAttribute("data-order") || "DESC").toUpperCase(), page: Number(root.getAttribute("data-page") || 1), maxPages: Number(root.getAttribute("data-max-pages") || 1), paging: root.getAttribute("data-paging") || "number", @@ -38,115 +45,251 @@ autoLoads: 0, }; - function fetchArchive(page, orderby, replace) { + function showLoader(on) { + if (loader) { + loader.hidden = !on; + loader.style.display = on ? "" : "none"; + loader.classList.toggle("is-loading", !!on); + } + if (pager) pager.classList.toggle("is-ajax-loading", !!on); + } + + function updateLoadMore(hasMore) { + var pag = root.querySelector(".ajax-pag"); + if (loadMoreBtn) { + loadMoreBtn.style.display = hasMore ? "" : "none"; + loadMoreBtn.disabled = false; + loadMoreBtn.textContent = hasMore ? "加载更多" : "没有更多了"; + if (!hasMore) loadMoreBtn.classList.add("ajax-nomore"); + else loadMoreBtn.classList.remove("ajax-nomore"); + } + if (pag) pag.style.display = hasMore ? "" : "none"; + } + + function syncOrderbyActive() { + root.querySelectorAll("[data-aksrt-shop-orderby]").forEach(function (a) { + var key = a.getAttribute("data-aksrt-shop-orderby") || ""; + var active = key === state.orderby; + a.classList.toggle("is-active", active); + a.classList.toggle("focus-color", active && key === "price"); + }); + } + + function fetchArchive(page, orderby, order, replace, termId) { if (state.loading) return Promise.resolve(); state.loading = true; + showLoader(true); if (loadMoreBtn) { loadMoreBtn.disabled = true; - loadMoreBtn.textContent = "加载中..."; + loadMoreBtn.textContent = "加载中…"; } var qs = new URLSearchParams(); qs.set("taxonomy", state.taxonomy); - qs.set("term_id", String(state.termId)); + qs.set("term_id", String(typeof termId === "number" ? termId : state.termId)); qs.set("page", String(page)); qs.set("orderby", orderby || state.orderby); + if ((orderby || state.orderby) === "price") { + qs.set("order", order || state.order || "ASC"); + } + + var container = pager || grid || root; + try { + container.dispatchEvent( + new CustomEvent("post_ajax.before", { + bubbles: true, + detail: { page: page, orderby: orderby || state.orderby, replace: !!replace }, + }) + ); + } catch (e0) {} return fetch(restBase() + "/shop/frontend/products/archive?" + qs.toString(), { method: "GET", credentials: "same-origin", headers: { + Accept: "application/json", "X-WP-Nonce": (window.aksrtData && aksrtData.restNonce) || "", + "X-AKSRT-Ajax": "list", }, }) - .then(function (r) { return r.json(); }) + .then(function (r) { + return r.json(); + }) .then(function (result) { state.loading = false; + showLoader(false); if (!result || !result.success || !result.data) { toast((result && result.message) || "加载失败"); - if (loadMoreBtn) { - loadMoreBtn.disabled = false; - loadMoreBtn.textContent = "加载更多"; - } + updateLoadMore(state.page < state.maxPages); return; } var data = result.data; state.page = Number(data.page || page); state.maxPages = Number(data.max_pages || 1); state.orderby = data.orderby || state.orderby; + state.order = String(data.order || state.order || "DESC").toUpperCase(); + if (typeof termId === "number" && termId > 0) { + state.termId = termId; + } + if (data.term && data.term.id) { + state.termId = Number(data.term.id); + } root.setAttribute("data-page", String(state.page)); root.setAttribute("data-max-pages", String(state.maxPages)); root.setAttribute("data-orderby", state.orderby); + root.setAttribute("data-order", state.order); + root.setAttribute("data-term-id", String(state.termId)); if (countEl && typeof data.total !== "undefined") { countEl.textContent = "共 " + data.total + " 件商品"; } + var titleEl = root.querySelector("[data-aksrt-archive-title]"); + if (titleEl && data.term && data.term.name) { + titleEl.textContent = data.term.name; + } + var descEl = root.querySelector("[data-aksrt-archive-desc]"); + if (descEl && data.term) { + descEl.textContent = data.term.description || ""; + descEl.style.display = data.term.description ? "" : "none"; + } - if (grid) { - if (replace) { - grid.innerHTML = data.html || ""; - } else if (data.html) { - grid.insertAdjacentHTML("beforeend", data.html); + if (headerHost && data.header_html) { + // Keep title/count; replace filter widget if present. + var existingFilter = headerHost.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter"); + var tmp = document.createElement("div"); + tmp.innerHTML = data.header_html; + var nextFilter = tmp.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter") || tmp.firstElementChild; + if (existingFilter && nextFilter) { + existingFilter.replaceWith(nextFilter); + } else if (!existingFilter && nextFilter) { + headerHost.appendChild(nextFilter); } } - root.querySelectorAll("[data-aksrt-shop-orderby]").forEach(function (a) { - a.classList.toggle("is-active", a.getAttribute("data-aksrt-shop-orderby") === state.orderby); - }); - - if (loadMoreBtn) { - var hasMore = state.page < state.maxPages; - loadMoreBtn.style.display = hasMore ? "" : "none"; - loadMoreBtn.disabled = false; - loadMoreBtn.textContent = "加载更多"; + var html = data.html || data.empty_html || ""; + if (grid) { + if (replace) { + grid.innerHTML = html; + } else if (html) { + grid.insertAdjacentHTML("beforeend", html); + } } + + if (data.paginate_html) { + var oldPag = root.querySelector(".ajax-pag"); + var wrap = document.createElement("div"); + wrap.innerHTML = data.paginate_html; + var nextPag = wrap.firstElementChild; + if (oldPag && nextPag) oldPag.replaceWith(nextPag); + else if (!oldPag && nextPag && pager) pager.appendChild(nextPag); + loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]"); + } + + syncOrderbyActive(); + updateLoadMore(!!data.has_more || state.page < state.maxPages); + + try { + container.dispatchEvent( + new CustomEvent("post_ajax.ed", { + bubbles: true, + detail: { page: state.page, replace: !!replace, data: data }, + }) + ); + document.dispatchEvent( + new CustomEvent("aksrt:posts:rendered", { + bubbles: true, + detail: { container: grid, replace: !!replace }, + }) + ); + } catch (e1) {} }) .catch(function () { state.loading = false; - if (loadMoreBtn) { - loadMoreBtn.disabled = false; - loadMoreBtn.textContent = "加载更多"; - } + showLoader(false); + updateLoadMore(state.page < state.maxPages); toast("网络错误"); }); } + function pushUrl(orderby, order, termUrl) { + try { + var u = termUrl ? new URL(termUrl, window.location.origin) : new URL(window.location.href); + if (orderby) u.searchParams.set("orderby", orderby); + else u.searchParams.delete("orderby"); + u.searchParams.delete("shop_orderby"); + if (orderby === "price" && order) u.searchParams.set("order", order); + else u.searchParams.delete("order"); + u.searchParams.delete("paged"); + window.history.replaceState({}, "", u.toString()); + } catch (err) {} + } + root.addEventListener("click", function (e) { - var sort = e.target.closest("[data-aksrt-shop-orderby]"); - if (sort && root.contains(sort)) { + // Category / sort links with ajax-next — intercept for REST (richer payload). + var termLink = e.target.closest("[data-aksrt-shop-term].ajax-next, a.ajax-next[data-aksrt-shop-term]"); + if (termLink && root.contains(termLink)) { e.preventDefault(); + e.stopPropagation(); + var tid = Number(termLink.getAttribute("data-aksrt-shop-term") || 0); + var href = termLink.getAttribute("href") || ""; + state.autoLoads = 0; + if (tid > 0) state.termId = tid; + pushUrl(state.orderby, state.order, href); + var rt = termLink.getAttribute("route-title"); + if (rt) { + try { + document.title = rt; + } catch (e2) {} + } + fetchArchive(1, state.orderby, state.order, true, tid); + return; + } + + var sort = e.target.closest("[data-aksrt-shop-orderby]"); + if (sort && root.contains(sort) && sort.tagName === "A") { + e.preventDefault(); + e.stopPropagation(); var orderby = sort.getAttribute("data-aksrt-shop-orderby") || "date"; - if (orderby === state.orderby && state.paging === "ajax") return; + var nextOrder = sort.getAttribute("data-aksrt-shop-order") || ""; + if (orderby === "price") { + if (state.orderby === "price" && nextOrder) { + state.order = nextOrder.toUpperCase(); + } else if (state.orderby !== "price") { + state.order = "ASC"; + } else { + state.order = state.order === "ASC" ? "DESC" : "ASC"; + } + } + if (orderby === state.orderby && orderby !== "price" && 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); + pushUrl(orderby, state.order); + fetchArchive(1, orderby, state.order, true); return; } var more = e.target.closest("[data-aksrt-shop-load-more]"); if (more && root.contains(more)) { e.preventDefault(); + e.stopPropagation(); if (state.page >= state.maxPages) return; - fetchArchive(state.page + 1, state.orderby, false); + fetchArchive(state.page + 1, state.orderby, state.order, 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" }); + 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, state.order, false); + }); + }, + { rootMargin: "240px" } + ); io.observe(loadMoreBtn); } })(); diff --git a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php index 18de487e..1eed7dd9 100644 --- a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php +++ b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php @@ -363,75 +363,92 @@ trait ShopControllerFrontendFlowOperations { $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') ?? '')); + $order = sanitize_key((string) ($request->get_param('order') ?? '')); $view = \AKSRT\Shop\Shop::get_instance()->get_product_archive_view([ 'taxonomy' => $taxonomy, 'term_id' => $term_id, 'paged' => $page, 'orderby' => $orderby, + 'order' => $order, ]); + $sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; $html = ''; if (!empty($view['products']) && is_array($view['products'])) { ob_start(); foreach ($view['products'] as $product) { - $this->render_product_card_html($product, $view); + if (function_exists('get_template_part')) { + get_template_part('templates/parts/shop/product', 'card', [ + 'product' => $product, + 'sales_cfg' => $sales_cfg, + ]); + } } $html = (string) ob_get_clean(); } + $empty_html = ''; + if ($html === '' && function_exists('aksrt_get_ajax_null')) { + $empty_html = aksrt_get_ajax_null(__('内容空空如也', 'aksrt')); + } elseif ($html === '') { + $empty_html = '

' . esc_html__('内容空空如也', 'aksrt') . '

'; + } + + // Header HTML for cat filter replace (when filter present). + $header_html = ''; + if (!empty($view['filter']) && is_array($view['filter'])) { + ob_start(); + $this->render_product_cat_filter_html($view); + $header_html = (string) ob_get_clean(); + } + + $paginate_html = ''; + $page_n = (int) ($view['page'] ?? 1); + $max_pages = (int) ($view['max_pages'] ?? 1); + $has_more = $page_n < $max_pages; + if ($has_more && (string) ($view['paging'] ?? '') === 'ajax') { + $next_url = add_query_arg([ + 'taxonomy' => $taxonomy, + 'term_id' => $term_id, + 'page' => $page_n + 1, + 'orderby' => (string) ($view['orderby'] ?? 'date'), + 'order' => (string) ($view['order'] ?? ''), + ], rest_url('aksrt/v1/shop/frontend/products/archive')); + if (function_exists('aksrt_get_ajax_next_paginate')) { + $paginate_html = aksrt_get_ajax_next_paginate( + (int) ($view['total'] ?? 0), + $page_n, + max(1, (int) ($view['per_page'] ?? 12)), + $next_url + ); + } + } + return $this->success([ 'data' => array_merge($view, [ - 'html' => $html, - 'has_more' => (int) ($view['page'] ?? 1) < (int) ($view['max_pages'] ?? 1), + 'html' => $html !== '' ? $html : $empty_html, + 'empty_html' => $empty_html, + 'header_html' => $header_html, + 'paginate_html' => $paginate_html, + 'has_more' => $has_more, + 'title' => !empty($view['term']['name']) + ? (string) $view['term']['name'] . ' - ' . wp_strip_all_tags(get_bloginfo('name')) + : '', ]), ]); } - 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); ?> -
- -
- -
- -
-

- -

- - -
-
-
- 0, 'page' => $paged, 'orderby' => $orderby, + 'order' => $order, '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'] : [], + 'filter' => null, ]; if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) { @@ -93,7 +124,7 @@ trait Part1 { 'posts_per_page' => $per_page, 'paged' => $paged, 'post_status' => 'publish', - 'order' => 'DESC', + 'order' => $order ?: 'DESC', 'orderby' => 'date', ]; @@ -114,16 +145,21 @@ trait Part1 { $query_args['meta_key'] = '_aksrt_sales'; $query_args['orderby'] = 'meta_value_num'; $query_args['order'] = 'DESC'; - } elseif ($orderby === 'price_asc') { + } elseif ($orderby === 'price') { $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['order'] = $order === 'ASC' ? 'ASC' : 'DESC'; + } elseif ($orderby === 'favorite_count') { + $query_args['meta_key'] = '_aksrt_favorite_count'; $query_args['orderby'] = 'meta_value_num'; $query_args['order'] = 'DESC'; - } elseif ($orderby === 'comments') { - $query_args['orderby'] = 'comment_count'; + } elseif ($orderby === 'views') { + $query_args['meta_key'] = '_aksrt_views'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + } elseif ($orderby === 'score') { + $query_args['meta_key'] = '_aksrt_score_average'; + $query_args['orderby'] = 'meta_value_num'; $query_args['order'] = 'DESC'; } elseif ($orderby === 'modified') { $query_args['orderby'] = 'modified'; @@ -138,6 +174,11 @@ trait Part1 { } wp_reset_postdata(); + $filter = null; + if ($taxonomy === 'aksrt_product_cat' && $term) { + $filter = $this->build_product_cat_filter((int) $term->term_id, $orderby, $order); + } + return [ 'products' => $products, 'term' => $term ? [ @@ -146,17 +187,129 @@ trait Part1 { 'description' => (string) $term->description, 'count' => (int) $term->count, 'taxonomy' => $taxonomy, + 'parent' => (int) $term->parent, + 'url' => get_term_link($term), ] : null, 'total' => (int) $query->found_posts, 'max_pages' => (int) $query->max_num_pages, 'page' => $paged, 'orderby' => $orderby, + 'order' => $order, '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'] : [], + 'filter' => $filter, + ]; + } + + /** + * Hierarchical cat filter data (zibll zib_shop_get_cat_filter). + * + * @return array{siblings:array,children:array,parent_id:int,mode:string} + */ + public function build_product_cat_filter(int $cat_id, string $orderby = 'date', string $order = 'DESC'): array { + $taxonomy = 'aksrt_product_cat'; + $cat = get_term($cat_id, $taxonomy); + if (!$cat || is_wp_error($cat)) { + return ['siblings' => [], 'children' => [], 'parent_id' => 0, 'mode' => 'flat', 'orderby' => $orderby, 'order' => $order]; + } + + $map_term = static function ($t) use ($orderby, $order): array { + $url = get_term_link($t); + if (!is_wp_error($url) && $orderby) { + $url = add_query_arg(array_filter([ + 'orderby' => $orderby, + 'order' => $orderby === 'price' ? $order : null, + ]), $url); + } + return [ + 'id' => (int) $t->term_id, + 'name' => (string) $t->name, + 'url' => is_wp_error($url) ? '' : (string) $url, + 'count' => (int) $t->count, + ]; + }; + + $parent_id = (int) $cat->parent; + $children = get_terms([ + 'taxonomy' => $taxonomy, + 'parent' => $cat_id, + 'hide_empty' => false, + ]); + if (is_wp_error($children)) { + $children = []; + } + + if ($parent_id > 0) { + $siblings = get_terms([ + 'taxonomy' => $taxonomy, + 'parent' => $parent_id, + 'hide_empty' => false, + ]); + } else { + $siblings = get_terms([ + 'taxonomy' => $taxonomy, + 'parent' => 0, + 'hide_empty' => false, + ]); + } + if (is_wp_error($siblings)) { + $siblings = []; + } + + $mode = 'siblings'; + $parent_terms = []; + if (empty($children) && $parent_id > 0) { + $parent_term = get_term($parent_id, $taxonomy); + if ($parent_term && !is_wp_error($parent_term) && (int) $parent_term->parent > 0) { + $parent_terms = get_terms([ + 'taxonomy' => $taxonomy, + 'parent' => (int) $parent_term->parent, + 'hide_empty' => false, + ]); + if (is_wp_error($parent_terms)) { + $parent_terms = []; + } + $mode = 'parent_siblings'; + } elseif ($parent_id > 0) { + $parent_terms = get_terms([ + 'taxonomy' => $taxonomy, + 'parent' => 0, + 'hide_empty' => false, + ]); + if (is_wp_error($parent_terms)) { + $parent_terms = []; + } + $mode = 'parent_siblings'; + } + } elseif (!empty($children)) { + $mode = 'with_children'; + } + + $parent_url = ''; + if ($parent_id > 0) { + $plink = get_term_link($parent_id, $taxonomy); + if (!is_wp_error($plink)) { + $parent_url = add_query_arg(array_filter([ + 'orderby' => $orderby, + 'order' => $orderby === 'price' ? $order : null, + ]), $plink); + } + } + + return [ + 'mode' => $mode, + 'current_id' => $cat_id, + 'parent_id' => $parent_id, + 'parent_url' => $parent_url, + 'siblings' => array_map($map_term, empty($parent_terms) ? $siblings : $parent_terms), + 'children' => array_map($map_term, $mode === 'with_children' ? $children : ($mode === 'parent_siblings' ? $siblings : [])), + 'child_tabs' => array_map($map_term, $children), + 'orderby' => $orderby, + 'order' => $order, ]; } diff --git a/src/AKSRT/Shop/ProductReview.php b/src/AKSRT/Shop/ProductReview.php index 4a6b2115..e5d8ca40 100644 --- a/src/AKSRT/Shop/ProductReview.php +++ b/src/AKSRT/Shop/ProductReview.php @@ -85,7 +85,8 @@ class ProductReview { update_comment_meta($comment_id, '_aksrt_review_images', $images); } - $this->refresh_product_scores($product_id); + $scores = $this->refresh_product_scores($product_id); + update_post_meta($product_id, '_aksrt_score_average', (float) ($scores['average'] ?? $average)); if (class_exists('\AKSRT\Payment\OrderRepository')) { $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); diff --git a/templates/parts/shop/product-card.php b/templates/parts/shop/product-card.php index 37c435e1..26fc3a5f 100644 --- a/templates/parts/shop/product-card.php +++ b/templates/parts/shop/product-card.php @@ -34,7 +34,7 @@ $image = (string) ($product['image'] ?? ''); $excerpt = (string) ($product['excerpt'] ?? ''); $price = (float) ($product['price'] ?? 0); ?> -
+
diff --git a/templates/parts/shop/product-cat-filter.php b/templates/parts/shop/product-cat-filter.php new file mode 100644 index 00000000..e191e21b --- /dev/null +++ b/templates/parts/shop/product-cat-filter.php @@ -0,0 +1,149 @@ + __('最新', 'aksrt'), + 'views' => __('最热', 'aksrt'), + 'favorite_count' => __('收藏', 'aksrt'), + 'score' => __('评分', 'aksrt'), + 'sales' => __('销量', 'aksrt'), +]; + +$base_term_url = !empty($term['url']) ? (string) $term['url'] : ''; +if ($base_term_url === '' && $current_id > 0) { + $link = get_term_link($current_id, 'aksrt_product_cat'); + $base_term_url = is_wp_error($link) ? '' : (string) $link; +} + +$blog = wp_strip_all_tags(get_bloginfo('name')); +?> +
+ + + + + +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + +
+ + +
+ + $label) : + if ($orderby === $key) : ?> + + $key], $base_term_url) : add_query_arg(['orderby' => $key]); + ?> + + + + 'price', 'order' => $next_order], $base_term_url) + : add_query_arg(['orderby' => 'price', 'order' => $next_order]); + ?> + + + + 'price'], $base_term_url) + : add_query_arg(['orderby' => 'price']); + ?> + + + + +
+
diff --git a/templates/shop/product-cat.php b/templates/shop/product-cat.php index 9bc3853d..d15133fa 100644 --- a/templates/shop/product-cat.php +++ b/templates/shop/product-cat.php @@ -1,6 +1,6 @@ get_product_archive_view([ - 'taxonomy' => 'aksrt_product_cat', - 'term_id' => $term ? (int) $term->term_id : 0, - 'paged' => $paged, - 'orderby' => $orderby, -]) : ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0, 'orderby' => 'date', 'paging' => 'number']; +$orderby = ''; +$order = ''; +if (isset($_GET['orderby'])) { + $orderby = sanitize_key(wp_unslash((string) $_GET['orderby'])); +} elseif (isset($_GET['shop_orderby'])) { + $orderby = sanitize_key(wp_unslash((string) $_GET['shop_orderby'])); +} +if (isset($_GET['order'])) { + $order = sanitize_key(wp_unslash((string) $_GET['order'])); +} + +$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null; +$view = $shop ? $shop->get_product_archive_view([ + 'taxonomy' => 'aksrt_product_cat', + 'term_id' => $term ? (int) $term->term_id : 0, + 'paged' => $paged, + 'orderby' => $orderby, + 'order' => $order, +]) : ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0, 'orderby' => 'date', 'order' => 'DESC', 'paging' => 'number']; $products = $view['products'] ?? []; $total = (int) ($view['total'] ?? 0); @@ -26,95 +37,108 @@ $max_pages = (int) ($view['max_pages'] ?? 0); $term_name = $term->name ?? ''; $term_desc = $term->description ?? ''; $active_orderby = (string) ($view['orderby'] ?? 'date'); +$active_order = strtoupper((string) ($view['order'] ?? 'DESC')); $paging = (string) ($view['paging'] ?? 'number'); $ias_s = !empty($view['ias_s']); $ias_max = (int) ($view['ias_max'] ?? 5); $style = (string) ($view['style'] ?? 'default'); $sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; - -$order_options = [ - 'date' => __('最新', 'aksrt'), - 'sales' => __('销量', 'aksrt'), - 'price_asc' => __('价格升序', 'aksrt'), - 'price_desc' => __('价格降序', 'aksrt'), - 'comments' => __('评价', 'aksrt'), -]; +$filter = is_array($view['filter'] ?? null) ? $view['filter'] : null; ?>
- + -
+
-
-
-

- -

- -

-
+
+
+

+ +

+ +

- + +
- -
- $product, - 'sales_cfg' => $sales_cfg, - ]); - endforeach; ?> -
+
+ - 1) : ?> - -
- -
- - - - - -
- -

-
- -
+
+ + $product, + 'sales_cfg' => $sales_cfg, + ]); + endforeach; ?> + +

' . esc_html__('内容空空如也', 'aksrt') . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + +
+ + 1 && $paging === 'ajax') : ?> +
+
+ +
+
+ 1) : ?> + + +
+
get_product_archive_view([ - 'taxonomy' => 'aksrt_product_tag', - 'term_id' => $term ? (int) $term->term_id : 0, - 'paged' => $paged, - 'orderby' => $orderby, -]) : ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0, 'orderby' => 'date', 'paging' => 'number']; +$orderby = ''; +$order = ''; +if (isset($_GET['orderby'])) { + $orderby = sanitize_key(wp_unslash((string) $_GET['orderby'])); +} elseif (isset($_GET['shop_orderby'])) { + $orderby = sanitize_key(wp_unslash((string) $_GET['shop_orderby'])); +} +if (isset($_GET['order'])) { + $order = sanitize_key(wp_unslash((string) $_GET['order'])); +} + +$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null; +$view = $shop ? $shop->get_product_archive_view([ + 'taxonomy' => 'aksrt_product_tag', + 'term_id' => $term ? (int) $term->term_id : 0, + 'paged' => $paged, + 'orderby' => $orderby, + 'order' => $order, +]) : ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0, 'orderby' => 'date', 'order' => 'DESC', 'paging' => 'number']; $products = $view['products'] ?? []; $total = (int) ($view['total'] ?? 0); @@ -26,95 +37,132 @@ $max_pages = (int) ($view['max_pages'] ?? 0); $term_name = $term->name ?? ''; $term_desc = $term->description ?? ''; $active_orderby = (string) ($view['orderby'] ?? 'date'); +$active_order = strtoupper((string) ($view['order'] ?? 'DESC')); $paging = (string) ($view['paging'] ?? 'number'); $ias_s = !empty($view['ias_s']); $ias_max = (int) ($view['ias_max'] ?? 5); $style = (string) ($view['style'] ?? 'default'); $sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; -$order_options = [ - 'date' => __('最新', 'aksrt'), - 'sales' => __('销量', 'aksrt'), - 'price_asc' => __('价格升序', 'aksrt'), - 'price_desc' => __('价格降序', 'aksrt'), - 'comments' => __('评价', 'aksrt'), +$order_labels = [ + 'date' => __('最新', 'aksrt'), + 'views' => __('最热', 'aksrt'), + 'favorite_count' => __('收藏', 'aksrt'), + 'score' => __('评分', 'aksrt'), + 'sales' => __('销量', 'aksrt'), ]; +$base_url = $term && !is_wp_error(get_term_link($term)) ? get_term_link($term) : ''; ?>
- + -
+
-
-
-

- -

- -

-
+
+
+

+ +

+ +

- +
+ + $label) : + if ($active_orderby === $key) : ?> + + $key], $base_url) : add_query_arg(['orderby' => $key]); + ?> + + + + 'price', 'order' => $next_order], $base_url) : add_query_arg(['orderby' => 'price', 'order' => $next_order]); + ?> + + + + 'price'], $base_url) : add_query_arg(['orderby' => 'price']); + ?> + + + + +
+
- -
- $product, - 'sales_cfg' => $sales_cfg, - ]); - endforeach; ?> -
+
+ - 1) : ?> - -
- -
- - - - - -
- -

-
- -
+
+ + $product, + 'sales_cfg' => $sales_cfg, + ]); + endforeach; ?> + +

' . esc_html__('内容空空如也', 'aksrt') . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + +
+ + 1 && $paging === 'ajax') : ?> +
+
+ +
+
+ 1) : ?> + + +
+