Files
dusuniot/src/routes/shop.tsx
T

119 lines
5.3 KiB
TypeScript
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.
/**
* # 路由 /shop/ 全部产品列表页
* 原站 /shop/ 形态:左栏属性筛选(复选框,URL search params 驱动)+ 主区
* 「IoT Products / Showing x–y of z results」+ 产品网格 + 分页。
*/
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { getLayoutData, Layout } from "~/components/website/Layout";
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, filtersToSearch, type CollectionSearch } from "~/components/website/collection-shared";
import { getFilterConfig, listProducts, type FilterState, type ProductListResult } from "~/server/data";
import { useT } from "~/i18n";
export const Route = createFileRoute("/shop")({
validateSearch: parseCollectionSearch as (search: Record<string, unknown>) => CollectionSearch,
loaderDeps: ({ search }) => ({ search }),
loader: async ({ deps }) => {
const [layout, filterConfig] = await Promise.all([
getLayoutData(),
getFilterConfig().catch(() => []),
]);
const list = await listProducts({
filters: searchToFilters(deps.search),
page: deps.search.page,
pageSize: 24,
}).catch(() => ({ items: [], total: 0, page: 1, pageSize: 24 }) as ProductListResult);
return { layout, filterConfig, list };
},
head: () => ({
meta: [
{ title: "All Products - DusunIoT" },
{ name: "description", content: "Browse all Dusun IoT gateways and modules." },
],
}),
component: ShopPage,
});
/** # 函数 组件 /shop/ 页面 */
function ShopPage() {
const t = useT();
const data = Route.useLoaderData();
const search = Route.useSearch();
const router = useRouter();
const page = data.list.page;
const from = (page - 1) * data.list.pageSize + (data.list.items.length ? 1 : 0);
const to = (page - 1) * data.list.pageSize + data.list.items.length;
/** # 函数 更新筛选:切换某属性值后 navigate(重置到第 1 页) */
function toggleFilter(attrSlug: string, valueSlug: string) {
const filters: FilterState = searchToFilters(search);
const cur = filters[attrSlug] || [];
const next = cur.includes(valueSlug) ? cur.filter((v) => v !== valueSlug) : [...cur, valueSlug];
if (next.length) filters[attrSlug] = next;
else delete filters[attrSlug];
router.navigate({ to: "/shop/", search: filtersToSearch(filters, 1) });
}
return (
<Layout layout={data.layout}>
<div className="site-container">
<Breadcrumb aria-label="Breadcrumb">
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink to="/">{t("nav_home")}</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbPage>{t("nav_shop")}</BreadcrumbPage></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="grid grid-cols-1 items-start gap-7 min-[900px]:grid-cols-[250px_1fr]">
{/* 左栏属性筛选(与原站 /shop/ 一致:仅属性过滤器,无分类树) */}
<aside>
{data.filterConfig.map((f) => (
<div className="mb-[18px] rounded-lg border border-line bg-soft p-4" key={f.slug}>
<h3 className="m-0 mb-2.5 text-[15px]">{f.name}</h3>
<ul className="m-0 list-none p-0">
{f.values.map((v) => {
const checked = (searchToFilters(search)[f.slug] || []).includes(v.slug);
return (
<li key={v.slug} className="mb-1.5">
<label className="flex cursor-pointer items-center gap-2">
<input type="checkbox" checked={checked} onChange={() => toggleFilter(f.slug, v.slug)} />
<span className="flex-1">{v.value}</span>
<span className="text-xs text-muted-foreground">({Number(v.count)})</span>
</label>
</li>
);
})}
{f.values.length === 0 ? <li className="text-muted-foreground">{t("collection_no_options")}</li> : null}
</ul>
</div>
))}
</aside>
<div>
<h1 className="mt-0 text-[26px]">{t("shop_iot_products")}</h1>
<p className="text-muted-foreground">
{data.list.total > 0 ? t("shop_showing", { from, to, total: data.list.total }) : t("shop_no_results")}
</p>
<div className="grid grid-cols-2 gap-[18px] min-[960px]:grid-cols-4">
{data.list.items.map((p) => (
<ProductCard key={p.id} p={p} />
))}
</div>
{data.list.items.length === 0 ? (
<p className="py-[60px] text-center text-muted-foreground">No products found with the selected filters.</p>
) : null}
<Pagination
page={page}
total={data.list.total}
pageSize={data.list.pageSize}
makeHref={(n) => (n > 1 ? `/shop/?page=${n}` : "/shop/")}
/>
</div>
</div>
</div>
</Layout>
);
}