Import full product catalog

This commit is contained in:
Jacky Ser
2026-08-10 14:22:47 +08:00
parent 96d49f0a07
commit 09a99d5e48
173 changed files with 7838 additions and 27 deletions

View File

@@ -1,6 +1,11 @@
"use client";
import { FormEvent, useMemo, useState } from "react";
import {
CATALOG_COUNTS,
CATALOG_PRODUCTS,
type CatalogProduct,
} from "./catalog-data";
type Gear = "塑料齿" | "半金属齿" | "金属齿";
@@ -252,6 +257,36 @@ const APPLICATIONS = [
{ name: "工业设备", note: "控制机构与定制传动组件" },
];
const CATALOG_CATEGORIES = [
"全部产品",
"标准舵机",
"异形舵机",
"全金属舵机",
"大扭矩舵机",
"积木舵机",
"标准齿轮箱",
"翻盖电机",
];
const FEATURED_SPEC_ORDER = [
"堵转扭矩",
"额定电压",
"电压范围",
"信号类型",
"通讯方式",
"齿轮比",
"寿命",
"产品尺寸",
"外形尺寸",
];
function getFeaturedSpecs(product: CatalogProduct) {
return FEATURED_SPEC_ORDER
.filter((label) => product.specs[label])
.slice(0, 3)
.map((label) => [label, product.specs[label]] as const);
}
const DEFAULT_REQUIREMENTS: Requirements = {
application: "",
torque: 2,
@@ -333,10 +368,34 @@ export default function Home() {
const [showSampleForm, setShowSampleForm] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [leadId, setLeadId] = useState("");
const [catalogQuery, setCatalogQuery] = useState("");
const [catalogCategory, setCatalogCategory] = useState("全部产品");
const [catalogLimit, setCatalogLimit] = useState(24);
const [selectedCatalogProduct, setSelectedCatalogProduct] = useState<CatalogProduct | null>(null);
const matches = useMemo(() => getMatches(requirements), [requirements]);
const topMatches = matches.slice(0, 3);
const top = topMatches[0];
const filteredCatalog = useMemo(() => {
const query = catalogQuery.trim().toLowerCase();
return CATALOG_PRODUCTS.filter((product) => {
const matchesCategory =
catalogCategory === "全部产品" || product.category === catalogCategory;
if (!matchesCategory) return false;
if (!query) return true;
const haystack = [
product.name,
product.code,
product.category,
...Object.keys(product.specs),
...Object.values(product.specs),
]
.join(" ")
.toLowerCase();
return haystack.includes(query);
});
}, [catalogCategory, catalogQuery]);
const visibleCatalog = filteredCatalog.slice(0, catalogLimit);
const needsEngineer =
top.score < 60 ||
requirements.annualVolume === "50000件以上" ||
@@ -374,7 +433,7 @@ export default function Home() {
</a>
<nav aria-label="主导航">
<a href="#selector">AI选型</a>
<a href="#products"></a>
<a href="#products"></a>
<a href="#how"></a>
</nav>
<button className="header-cta" type="button" onClick={() => setStage(1)}>
@@ -400,8 +459,8 @@ export default function Home() {
<a href="#products" className="text-action"></a>
</div>
<div className="trust-row">
<span><b>13</b> </span>
<span><b>0.0924</b> kgf·cm</span>
<span><b>280</b> </span>
<span><b>0.09920</b> kgf·cm</span>
<span><b>48h</b> </span>
</div>
</div>
@@ -692,25 +751,171 @@ export default function Home() {
<section className="products-section" id="products">
<div className="section-heading">
<div>
<p className="eyebrow"><span /> </p>
<h2>24kgf·cm</h2>
<p className="eyebrow"><span /> · 2026.06</p>
<h2>280</h2>
</div>
<p>13</p>
<p>齿</p>
</div>
<div className="product-band">
{[
{ name: "微型轻量", range: "1.7g3.7g", note: "AI毛绒 · 微型机构", image: "/products/servo-2g.jpg" },
{ name: "通用标准", range: "9g17g", note: "玩具 · 教育 · 航模", image: "/products/servo-9g-hybrid.jpg" },
{ name: "中等负载", range: "25g37g", note: "机器人 · 工业机构", image: "/products/servo-37g-metal.jpg" },
].map((item) => (
<article key={item.name}>
<img src={item.image} alt={item.name} />
<div><span>{item.name}</span><h3>{item.range}</h3><p>{item.note}</p></div>
</article>
<div className="catalog-summary" aria-label="产品分类统计">
{CATALOG_CATEGORIES.slice(1).map((category) => (
<button
type="button"
key={category}
className={catalogCategory === category ? "active" : ""}
onClick={() => {
setCatalogCategory(category);
setCatalogLimit(24);
}}
>
<strong>{CATALOG_COUNTS[category]}</strong>
<span>{category}</span>
</button>
))}
</div>
<div className="catalog-toolbar">
<label className="catalog-search">
<span></span>
<input
value={catalogQuery}
onChange={(event) => {
setCatalogQuery(event.target.value);
setCatalogLimit(24);
}}
placeholder="搜索型号、编码、扭矩、协议或参数"
aria-label="搜索产品目录"
/>
{catalogQuery && (
<button type="button" onClick={() => setCatalogQuery("")} aria-label="清空搜索">×</button>
)}
</label>
<div className="category-pills" aria-label="产品类别筛选">
{CATALOG_CATEGORIES.map((category) => (
<button
type="button"
key={category}
className={catalogCategory === category ? "active" : ""}
onClick={() => {
setCatalogCategory(category);
setCatalogLimit(24);
}}
>{category}</button>
))}
</div>
<p className="catalog-result-count">
<strong>{filteredCatalog.length}</strong>
</p>
</div>
{visibleCatalog.length > 0 ? (
<div className="catalog-grid">
{visibleCatalog.map((product) => (
<article className="catalog-card" key={product.id}>
<button
type="button"
className="catalog-image-button"
onClick={() => setSelectedCatalogProduct(product)}
aria-label={`查看${product.name}详情`}
>
{product.image ? (
<img src={product.image} alt={product.name} loading="lazy" />
) : (
<span className="catalog-placeholder">GAUDELOT</span>
)}
<small>{product.category}</small>
</button>
<div className="catalog-card-body">
<p>{product.code}</p>
<h3>{product.name}</h3>
<dl>
{getFeaturedSpecs(product).map(([label, value]) => (
<div key={label}><dt>{label}</dt><dd>{value}</dd></div>
))}
</dl>
<button type="button" onClick={() => setSelectedCatalogProduct(product)}>
<span></span>
</button>
</div>
</article>
))}
</div>
) : (
<div className="catalog-empty">
<strong></strong>
<p></p>
<button type="button" onClick={() => { setCatalogQuery(""); setCatalogCategory("全部产品"); }}>
</button>
</div>
)}
{catalogLimit < filteredCatalog.length && (
<div className="load-more-wrap">
<button type="button" onClick={() => setCatalogLimit((current) => current + 24)}>
<span> {visibleCatalog.length} / {filteredCatalog.length}</span>
</button>
</div>
)}
</section>
{selectedCatalogProduct && (
<div
className="catalog-modal-backdrop"
role="presentation"
onMouseDown={() => setSelectedCatalogProduct(null)}
>
<section
className="catalog-modal"
role="dialog"
aria-modal="true"
aria-labelledby="catalog-modal-title"
onMouseDown={(event) => event.stopPropagation()}
>
<button
className="catalog-modal-close"
type="button"
onClick={() => setSelectedCatalogProduct(null)}
aria-label="关闭产品详情"
>×</button>
<div className="catalog-modal-product">
<div className="catalog-modal-image">
{selectedCatalogProduct.image ? (
<img src={selectedCatalogProduct.image} alt={selectedCatalogProduct.name} />
) : (
<span className="catalog-placeholder">GAUDELOT</span>
)}
</div>
<div>
<p className="step-kicker">{selectedCatalogProduct.category}</p>
<h2 id="catalog-modal-title">{selectedCatalogProduct.name}</h2>
<code>{selectedCatalogProduct.code}</code>
<p className="catalog-source-note"> {selectedCatalogProduct.page} </p>
</div>
</div>
<div className="catalog-spec-table">
{Object.entries(selectedCatalogProduct.specs).map(([label, value]) => (
<div key={label}><span>{label}</span><strong>{value}</strong></div>
))}
</div>
<div className="catalog-modal-actions">
<button
className="panel-primary"
type="button"
onClick={() => {
setSelectedCatalogProduct(null);
setStage(1);
window.scrollTo({ top: 0, behavior: "smooth" });
}}
>
AI继续确认需求 <span></span>
</button>
<p></p>
</div>
</section>
</div>
)}
<section className="how-section" id="how">
<div className="how-copy">
<p className="eyebrow light"><span /> </p>