' . esc_html__('内容空空如也', 'aksrt') . '
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') . '
' . esc_html__('内容空空如也', 'aksrt') . '
' . esc_html__('内容空空如也', 'aksrt') . '