v0.10.21: 产品归档页 C1-C4(原站 /shop/ 模板 1398 + WPC 筛选 + WooCommerce 网格)
- 导入 archive 模板 post 1398(slug=shop-archive),/collection/ 用 ElementorRenderer 直出 hero/两列外壳,按 widget id 注入 ShopFilters/ShopProducts;新增 server function 解决客户端导航时 loader 打不开 db 的问题 - C1 hero:面包屑 Home / Shop(BreadcrumbWidget + vars.breadcrumb 上下文) - C2 筛选:12 组属性(term taxonomy 顺序,scripts/import-attribute-term-order.ts); 长期列表 5 项 + See more,Battery/Operating/Install 默认折叠;URL query 驱动, SoM 筛选实测 22 条;顶部 Filters 标题/已选 chips/Clear all - C3 网格:WooCommerce 归档 DOM(结果计数、orderby、24 卡 4 列、卡片 1:1 图/标题/ Read more);默认排序导入 products.meta.menu_order(scripts/import-product-menu-order.ts), 前三个商品与原站一致(DSOM-040R/110R/130N) - C4 分页:页码/箭头、当前页样式、page2 25–48 条验证 - 修复 Tailwind .columns-4 与 WooCommerce 网格类冲突;app.css 新增 shop 归档 scoped 样式 - 1436 实测:hero 52.8、shop 节 2906.7、筛选区 2798.7、网格 2523.3、卡 257.7×409、 分页 y2855,与原站逐项一致(±6px 内);bun test 11 pass;版本 0.10.21
This commit is contained in:
@@ -1,29 +1,30 @@
|
||||
/**
|
||||
* # 路由 产品列表 /collection/
|
||||
* 列出全部产品(无分类限定)+ 顶部分类卡片;带分页。
|
||||
* # 路由 产品列表 /collection/(原站 /shop/ 的等价页)
|
||||
* 原站 /shop/ 使用 WooCommerce 归档模板(elementor_library post 1398):
|
||||
* - a2ab15c:面包屑 hero(bg #F4F5F8)
|
||||
* - 65176b9:左侧 WPC 筛选(20% 列)+ 右侧 “IoT Products” 标题、排序、24 商品 4 列网格、分页
|
||||
* 本路由以 ElementorRenderer 直出该模板外壳,按 widget id 注入:
|
||||
* - 98d2008 → ShopFilters(属性复选筛选,URL query 驱动)
|
||||
* - d761846 → ShopProducts(WooCommerce 归档网格 + 排序 + 分页)
|
||||
* 模板缺失时降级为内置分类卡片 + 简单网格。
|
||||
*/
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { getLayoutData, Layout } from "~/components/website/Layout";
|
||||
import { ElementorRenderer, extractElementor } from "~/components/elementor/Renderer";
|
||||
import { ProductCard } from "~/components/website/ProductCard";
|
||||
import { Pagination } from "~/components/website/Pagination";
|
||||
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb";
|
||||
import { parseCollectionSearch, searchToFilters, type CollectionSearch } from "~/components/website/collection-shared";
|
||||
import { getProductCategoryTree, listProducts, type ProductListResult } from "~/server/data";
|
||||
import { ShopFilters } from "~/components/website/ShopFilters";
|
||||
import { ShopProducts, orderByToSort } from "~/components/website/ShopProducts";
|
||||
import { getFilterConfig, getProductCategoryTree, getShopTemplate, listProducts, type ProductListResult } from "~/server/data";
|
||||
import { useT } from "~/i18n";
|
||||
|
||||
export const Route = createFileRoute("/collection/")({
|
||||
validateSearch: parseCollectionSearch as (search: Record<string, unknown>) => CollectionSearch,
|
||||
loaderDeps: ({ search }) => ({ search }),
|
||||
loader: async ({ deps }) => {
|
||||
const [layout, cats, list] = await Promise.all([
|
||||
getLayoutData(),
|
||||
getProductCategoryTree().catch(() => []),
|
||||
listProducts({ page: deps.search.page, filters: searchToFilters(deps.search), pageSize: 24 }).catch(
|
||||
() => ({ items: [], total: 0, page: 1, pageSize: 24 }) as ProductListResult,
|
||||
),
|
||||
]);
|
||||
return { layout, cats, list };
|
||||
},
|
||||
loader: async ({ deps }) => loadCollection({ data: deps.search }),
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ title: "All Products | IoT Gateways & Devices | Dusun IoT" },
|
||||
@@ -33,12 +34,72 @@ export const Route = createFileRoute("/collection/")({
|
||||
component: CollectionIndexPage,
|
||||
});
|
||||
|
||||
/** # 函数 产品归档页数据加载(Server Function:客户端导航时由服务端执行,
|
||||
* 避免把 db/查询层打进浏览器包;返回纯 JSON) */
|
||||
const loadCollection = createServerFn({ method: "GET" })
|
||||
.validator((d: CollectionSearch) => d)
|
||||
.handler(async ({ data: search }) => {
|
||||
const filters = searchToFilters(search);
|
||||
const [layout, cats, list, shop, filterConfig] = await Promise.all([
|
||||
getLayoutData(),
|
||||
getProductCategoryTree().catch(() => []),
|
||||
listProducts({ page: search.page, filters, pageSize: 24, sort: orderByToSort(search.orderby) }).catch(
|
||||
() => ({ items: [], total: 0, page: 1, pageSize: 24 }) as ProductListResult,
|
||||
),
|
||||
getShopTemplate().catch(() => null),
|
||||
getFilterConfig().catch(() => []),
|
||||
]);
|
||||
// Elementor 文档是递归 JSON,TanStack 序列化校验无法展开;运行时为纯 JSON
|
||||
return { layout, cats, list, shop, filters, filterConfig, search } as any;
|
||||
});
|
||||
|
||||
/** # 类型 归档页 loader 数据 */
|
||||
type CollectionData = {
|
||||
layout: any;
|
||||
cats: { id: number; name: string; slug: string; parentId: number | null; count: number }[];
|
||||
list: ProductListResult;
|
||||
shop: { meta?: unknown } | null;
|
||||
filters: Record<string, string[]>;
|
||||
filterConfig: { name: string; slug: string; values: { value: string; slug: string; count: number }[] }[];
|
||||
search: CollectionSearch;
|
||||
};
|
||||
|
||||
/** # 组件 全部产品列表页 */
|
||||
function CollectionIndexPage() {
|
||||
const t = useT();
|
||||
const data = Route.useLoaderData();
|
||||
const shopDoc = extractElementor(data.shop?.meta);
|
||||
if (shopDoc) {
|
||||
return (
|
||||
<Layout layout={data.layout}>
|
||||
<ElementorRenderer
|
||||
doc={shopDoc}
|
||||
vars={{ breadcrumb: [{ label: "Home", url: "/" }, { label: "Shop" }] }}
|
||||
shortcodes={{
|
||||
"98d2008": (
|
||||
<ShopFilters config={data.filterConfig} selected={data.filters} total={data.list.total} search={data.search} />
|
||||
),
|
||||
"d761846": (
|
||||
<ShopProducts
|
||||
items={data.list.items}
|
||||
total={data.list.total}
|
||||
page={data.list.page}
|
||||
pageSize={data.list.pageSize}
|
||||
search={data.search}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
return <FallbackCollectionPage data={data} />;
|
||||
}
|
||||
|
||||
/** # 组件 降级列表页(归档模板缺失时) */
|
||||
function FallbackCollectionPage(props: { data: CollectionData }) {
|
||||
const t = useT();
|
||||
const data = props.data;
|
||||
const topCats = data.cats.filter((c) => !c.parentId && c.slug !== "uncategorized");
|
||||
const page = data.list.page;
|
||||
return (
|
||||
<Layout layout={data.layout}>
|
||||
<div className="site-container">
|
||||
@@ -55,14 +116,13 @@ function CollectionIndexPage() {
|
||||
<section className="mt-5 mb-8">
|
||||
<div className="grid grid-cols-2 gap-[18px] min-[960px]:grid-cols-4">
|
||||
{topCats.map((c) => (
|
||||
<Link key={c.id} to="/collection/$slug/" params={{ slug: c.slug }} className="block overflow-hidden rounded-lg border border-line bg-white text-brand transition-[box-shadow,transform] duration-200 hover:-translate-y-0.5 hover:shadow-[0_8px_24px_rgba(16,42,76,0.1)]">
|
||||
<Link key={c.id} to="/collection/$slug/" params={{ slug: c.slug }} className="block overflow-hidden rounded-lg border border-line bg-white text-brand transition-[box-shadow,transform] duration-200 hover:-translate-y-0.5">
|
||||
<div className="px-4 pt-3.5 pb-[18px]">
|
||||
<div className="mb-1.5 text-[15px] leading-[1.45] font-semibold">{c.name}</div>
|
||||
<div className="text-[13px] text-muted-foreground">{t("collection_products_count", { total: Number(c.count) })}</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{topCats.length === 0 ? <p className="py-[60px] text-center text-muted-foreground">Product categories are being updated.</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -70,14 +130,9 @@ function CollectionIndexPage() {
|
||||
{data.list.items.map((p) => (
|
||||
<ProductCard key={p.id} p={p} />
|
||||
))}
|
||||
{data.list.items.length === 0 ? <p className="py-[60px] text-center text-muted-foreground">No products found. They may still be syncing — please check back soon.</p> : null}
|
||||
{data.list.items.length === 0 ? <p className="py-[60px] text-center text-muted-foreground">No products found.</p> : null}
|
||||
</div>
|
||||
<Pagination
|
||||
page={page}
|
||||
total={data.list.total}
|
||||
pageSize={data.list.pageSize}
|
||||
makeHref={(p) => `/collection/?page=${p}`}
|
||||
/>
|
||||
<Pagination page={data.list.page} total={data.list.total} pageSize={data.list.pageSize} makeHref={(p) => `/collection/?page=${p}`} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user