v0.10.5 首页 H2 产品线入口 ×6:ImageBoxWidget 重构为 Elementor 忠实 DOM + slim 白名单补齐 + H2 scoped CSS(post-18 对齐,桌面/移动/hover 实测一致)

This commit is contained in:
BigCode
2026-11-01 00:15:29 +08:00
commit 5b83e71ad9
128 changed files with 20008 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* # 路由 产品列表 /collection/
* 列出全部产品(无分类限定)+ 顶部分类卡片;带分页。
*/
import { createFileRoute, Link } 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, type CollectionSearch } from "~/components/website/collection-shared";
import { getProductCategoryTree, 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 };
},
head: () => ({
meta: [
{ title: "All Products | IoT Gateways & Devices | Dusun IoT" },
{ name: "description", content: "Browse all Dusun IoT products: industrial IoT gateways, single board computers, wireless modules and smart IoT devices." },
],
}),
component: CollectionIndexPage,
});
/** # 组件 全部产品列表页 */
function CollectionIndexPage() {
const t = useT();
const data = Route.useLoaderData();
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">
<Breadcrumb aria-label="Breadcrumb">
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink to="/">{t("nav_home")}</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbPage>{t("nav_products")}</BreadcrumbPage></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<h1 className="text-[28px]">{t("nav_all_products")}</h1>
<p className="text-muted-foreground">{t("collection_products_count", { total: data.list.total })}</p>
<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)]">
<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>
<div className="grid grid-cols-2 gap-[18px] min-[960px]:grid-cols-4">
{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}
</div>
<Pagination
page={page}
total={data.list.total}
pageSize={data.list.pageSize}
makeHref={(p) => `/collection/?page=${p}`}
/>
</div>
</Layout>
);
}