Files
gdm-Servo-model/app/page.tsx
2026-08-11 09:20:37 +08:00

1039 lines
46 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.
"use client";
import { FormEvent, useMemo, useState } from "react";
import {
CATALOG_COUNTS,
CATALOG_PRODUCTS,
type CatalogProduct,
} from "./catalog-data";
type Requirements = {
application: string;
category: string;
torque: number;
voltage: number;
weightLimit: number;
angle: string;
control: string;
gear: string;
annualVolume: string;
firstOrderQuantity: string;
currentModel: string;
otherRequirements: string;
painPoint: string;
};
const APPLICATIONS = [
{ name: "AI毛绒", note: "头部、耳朵、手臂等轻量动作" },
{ name: "智能玩具", note: "互动机构、表情和行走结构" },
{ name: "教育/积木", note: "STEAM套件与积木动力模块" },
{ name: "微型机器人", note: "关节、夹爪和小型机械臂" },
{ name: "云台/航模", note: "飞行控制、云台与模型机构" },
{ name: "工业设备", note: "控制机构与定制传动组件" },
];
const PRIMARY_CATALOG_CATEGORIES = [
"标准舵机",
"异形舵机",
"全金属舵机",
"大扭矩舵机",
"积木舵机",
"标准齿轮箱",
"翻盖电机",
];
const CATALOG_FILTER_CATEGORIES = ["全部产品", ...PRIMARY_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 SERVO_CATEGORIES = ["标准舵机", "异形舵机", "全金属舵机", "大扭矩舵机", "积木舵机"];
const SILENT_SERVO_CATEGORY = "静音舵机";
const CUSTOM_SERVO_CATEGORY = "非标准舵机定制";
const TORQUE_MIN = 0.09;
const TORQUE_MAX = 920;
const TORQUE_PRESETS = [0.1, 1, 5, 20, 100, 300, 920];
const APPLICATION_CATEGORY_MAP: Record<string, string[]> = {
AI毛绒: ["标准舵机", "异形舵机"],
: ["标准舵机", "异形舵机", "积木舵机"],
"教育/积木": ["积木舵机", "标准舵机"],
: ["标准舵机", "异形舵机", "全金属舵机"],
"云台/航模": ["标准舵机", "全金属舵机"],
: ["全金属舵机", "大扭矩舵机", "异形舵机"],
};
function parseNumbers(value = "") {
return (value.match(/\d+(?:\.\d+)?/g) ?? []).map(Number);
}
function isSilentServo(product: CatalogProduct) {
return SERVO_CATEGORIES.includes(product.category) &&
[product.name, ...Object.values(product.specs)].join(" ").includes("静音");
}
const SILENT_SERVO_COUNT = CATALOG_PRODUCTS.filter(isSilentServo).length;
function getCategoryCount(category: string) {
return category === SILENT_SERVO_CATEGORY ? SILENT_SERVO_COUNT : CATALOG_COUNTS[category];
}
function inferWeight(product: CatalogProduct) {
const listedWeight = parseNumbers(product.specs["重量"])[0];
if (Number.isFinite(listedWeight)) return listedWeight;
if (product.category !== "标准舵机") return null;
const namedWeight = product.name.match(/(\d+(?:\.\d+)?)\s*(?:g|克)/i);
return namedWeight ? Number(namedWeight[1]) : null;
}
function classifyGear(value = "") {
const hasPlastic = /塑胶|塑料/.test(value);
const hasMetal = /金属|铜|粉末冶金|滚齿/.test(value);
if (hasPlastic && hasMetal) return "混合齿";
if (hasMetal) return "全金属齿";
if (hasPlastic) return "塑胶齿";
return "未标注";
}
function controlMatches(control: string, productControl: string) {
if (control === "不限") return true;
const value = productControl.toUpperCase();
if (control === "PWM/三线") return value.includes("PWM") || /3线|3PIN/.test(value);
if (control === "串口/TTL") return value.includes("TTL") || value.includes("串口");
if (control === "二/四/五线") return /[245]线|[245]PIN/.test(value);
return value.includes(control.toUpperCase());
}
function angleMatches(angle: string, productAngle: string) {
if (angle === "不限") return true;
if (!productAngle) return false;
const firstAngle = parseNumbers(productAngle)[0] ?? 0;
if (angle === "135°") return firstAngle > 0 && firstAngle <= 135;
if (angle === "180°") return firstAngle > 135 && firstAngle <= 180;
if (angle === "270330°") return firstAngle >= 270 && firstAngle < 360;
return productAngle.includes("多圈") || firstAngle >= 360;
}
function formatTorque(value: number) {
if (value < 1) return value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
if (value < 100) return value.toFixed(1).replace(/\.0$/, "");
return Math.round(value).toString();
}
function torqueToSlider(value: number) {
return ((Math.log(value) - Math.log(TORQUE_MIN)) / (Math.log(TORQUE_MAX) - Math.log(TORQUE_MIN))) * 100;
}
function sliderToTorque(position: number) {
const value = Math.exp(
Math.log(TORQUE_MIN) + (position / 100) * (Math.log(TORQUE_MAX) - Math.log(TORQUE_MIN)),
);
if (value < 1) return Number(value.toFixed(2));
if (value < 100) return Number(value.toFixed(1));
return Math.round(value);
}
const SELECTOR_PRODUCTS = CATALOG_PRODUCTS.filter((product) =>
SERVO_CATEGORIES.includes(product.category),
).map((product) => {
const voltageValues = parseNumbers(product.specs["电压范围"]);
const ratedVoltage = product.ratedVoltage ?? 0;
const control = [
product.specs["信号类型"],
product.specs["通讯方式"],
product.specs["端子连接线"],
product.specs["连接线类型"],
].filter(Boolean).join(" · ");
return {
...product,
torque: product.torqueValue ?? 0,
voltageMin: voltageValues[0] ?? ratedVoltage,
voltageMax: voltageValues.at(-1) ?? ratedVoltage,
weight: inferWeight(product),
isSilent: isSilentServo(product),
gear: classifyGear(product.specs["齿轮材质"]),
angle: product.specs["可操作角度"] ?? product.specs["操作角度范围"] ?? "",
control,
speed: product.specs["空载速度"] ?? product.specs["转动速度"] ?? "",
size: product.specs["产品尺寸"] ?? product.specs["外形尺寸"] ?? "",
};
});
const RATED_VOLTAGES = [...new Set(
SELECTOR_PRODUCTS
.map((product) => product.ratedVoltage)
.filter((voltage): voltage is number => voltage !== null),
)].sort((a, b) => a - b);
const DEFAULT_REQUIREMENTS: Requirements = {
application: "",
category: "全部舵机",
torque: 2,
voltage: 5,
weightLimit: 0,
angle: "不限",
control: "不限",
gear: "不限",
annualVolume: "500050000件",
firstOrderQuantity: "",
currentModel: "",
otherRequirements: "",
painPoint: "交期",
};
function getMatches(requirements: Requirements) {
return SELECTOR_PRODUCTS.map((product) => {
let score = 20;
const reasons: string[] = [];
const categoryOk = requirements.category === "全部舵机" ||
requirements.category === product.category ||
(requirements.category === SILENT_SERVO_CATEGORY && product.isSilent);
const torqueOk = product.torque >= requirements.torque;
const voltageOk = requirements.voltage === product.ratedVoltage;
const weightOk =
requirements.weightLimit === 0 || product.weight === null || product.weight <= requirements.weightLimit;
const gearOk = requirements.gear === "不限" || requirements.gear === product.gear;
const angleOk = angleMatches(requirements.angle, product.angle);
const controlOk = controlMatches(requirements.control, product.control);
const coreCompatible = categoryOk && torqueOk && voltageOk && weightOk && gearOk && angleOk && controlOk;
if (categoryOk) score += requirements.category === "全部舵机" ? 6 : 16;
else score -= 120;
if (APPLICATION_CATEGORY_MAP[requirements.application]?.includes(product.category)) {
score += 12;
reasons.push(`适合${requirements.application}场景`);
}
if (torqueOk) {
const ratio = product.torque / requirements.torque;
score += Math.max(5, 28 - Math.log10(Math.max(1, ratio)) * 9);
const margin = Math.round((product.torque / requirements.torque - 1) * 100);
reasons.push(`扭矩满足,约有${Math.max(0, margin)}%余量`);
} else {
score -= 120;
}
if (voltageOk) {
score += 22;
reasons.push(`${requirements.voltage}V额定电压匹配`);
} else {
score -= 90;
}
if (requirements.weightLimit > 0) {
if (product.weight === null) score -= 6;
else if (weightOk) {
score += 10;
reasons.push(`重量${product.weight}g符合上限`);
} else score -= 45;
}
if (requirements.gear !== "不限") score += gearOk ? 10 : -40;
if (requirements.angle !== "不限") score += angleOk ? 10 : -40;
if (requirements.control !== "不限") score += controlOk ? 12 : -50;
if (requirements.painPoint === "成本" && product.gear === "塑料齿") score += 8;
if (requirements.painPoint === "耐久" && product.gear === "全金属齿") score += 8;
if (requirements.painPoint === "体积" && product.weight !== null && product.weight <= 9) score += 8;
return {
product,
score,
coreCompatible,
confidence: Math.max(48, Math.min(97, Math.round(score))),
reasons: reasons.slice(0, 3),
};
}).sort((a, b) => Number(b.coreCompatible) - Number(a.coreCompatible) || b.score - a.score);
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="metric">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
export default function Home() {
const [stage, setStage] = useState(0);
const [requirements, setRequirements] = useState(DEFAULT_REQUIREMENTS);
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 compatibleCount = matches.filter((match) => match.coreCompatible).length;
const filteredCatalog = useMemo(() => {
const query = catalogQuery.trim().toLowerCase();
return CATALOG_PRODUCTS.filter((product) => {
const matchesCategory =
catalogCategory === "全部产品" ||
product.category === catalogCategory ||
(catalogCategory === SILENT_SERVO_CATEGORY && isSilentServo(product));
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?.coreCompatible ||
requirements.annualVolume === "50000件以上" ||
Number(requirements.firstOrderQuantity) >= 1000 ||
requirements.application === "工业设备";
function update<K extends keyof Requirements>(key: K, value: Requirements[K]) {
setRequirements((current) => ({ ...current, [key]: value }));
}
function reset() {
setStage(1);
setRequirements(DEFAULT_REQUIREMENTS);
setShowSampleForm(false);
setSubmitted(false);
}
function submitLead(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const id = `GDL-${new Date().toISOString().slice(5, 10).replace("-", "")}-${String(
Date.now(),
).slice(-4)}`;
setLeadId(id);
setSubmitted(true);
}
return (
<main>
<header className="site-header">
<a className="brand" href="#top" aria-label="高徳乐 AI选型首页">
<span className="brand-mark">G</span>
<span>
<strong>GAUDELOT</strong>
<small> · </small>
</span>
</a>
<nav aria-label="主导航">
<a href="#selector">AI选型</a>
<a href="#products"></a>
<a href="#how"></a>
</nav>
<button className="header-cta" type="button" onClick={() => setStage(1)}>
<span></span>
</button>
</header>
<section className="hero" id="top">
<div className="hero-copy">
<div className="eyebrow"><span /> AI · </div>
<h1>
3<br />
<em></em>
</h1>
<p className="hero-lead">
AI从高徳乐完整舵机目录中生成候选方案
</p>
<div className="hero-actions">
<button className="primary-action" type="button" onClick={() => setStage(1)}>
<span></span>
</button>
<a href="#products" className="text-action"></a>
</div>
<div className="trust-row">
<span><b>280</b> </span>
<span><b>0.09920</b> kgf·cm</span>
<span><b>48h</b> </span>
</div>
</div>
<div className="selector-shell" id="selector">
<div className="selector-topbar">
<div>
<span className="live-dot" /> AI
</div>
<span className="secure-note"></span>
</div>
{stage === 0 && (
<div className="welcome-panel">
<div className="assistant-avatar">AI</div>
<p className="assistant-label"></p>
<h2></h2>
<p>3</p>
<button className="panel-primary" type="button" onClick={() => setStage(1)}>
<span>3</span>
</button>
<div className="privacy-note"> </div>
</div>
)}
{stage > 0 && stage < 4 && (
<div className="question-panel">
<div className="progress-wrap" aria-label={`选型进度 ${stage}/3`}>
<div className="progress-labels">
<span></span>
<strong>{stage} / 3</strong>
</div>
<div className="progress-track"><i style={{ width: `${stage * 33.33}%` }} /></div>
</div>
{stage === 1 && (
<div className="question-step">
<p className="step-kicker"> · 使</p>
<h2></h2>
<p className="step-help"></p>
<div className="choice-grid">
{APPLICATIONS.map((item) => (
<button
type="button"
key={item.name}
className={`choice-card ${requirements.application === item.name ? "selected" : ""}`}
onClick={() => update("application", item.name)}
>
<b>{item.name}</b>
<small>{item.note}</small>
<span className="choice-check"></span>
</button>
))}
</div>
</div>
)}
{stage === 2 && (
<div className="question-step">
<p className="step-kicker"> · </p>
<h2></h2>
<p className="step-help">2430.09920 kgf·cm</p>
<div className="two-fields compact-fields">
<label className="select-field">
<span></span>
<select
value={requirements.category}
onChange={(event) => update("category", event.target.value)}
>
<option value="全部舵机">243</option>
<optgroup label="官方产品系列">
{SERVO_CATEGORIES.map((category) => (
<option key={category} value={category}>
{category}{getCategoryCount(category)}
</option>
))}
</optgroup>
<optgroup label="特色筛选与定制">
{[SILENT_SERVO_CATEGORY, CUSTOM_SERVO_CATEGORY].map((category) => (
<option key={category} value={category}>
{category === CUSTOM_SERVO_CATEGORY
? `${category}(联系我们)`
: `${category}${getCategoryCount(category)}款)`}
</option>
))}
</optgroup>
</select>
</label>
<label className="select-field">
<span></span>
<select
value={requirements.voltage}
onChange={(event) => update("voltage", Number(event.target.value))}
>
{RATED_VOLTAGES.map((voltage) => (
<option key={voltage} value={voltage}>{voltage.toFixed(1)}V</option>
))}
</select>
</label>
</div>
<div className="field-stack">
<div className="field-label">
<label htmlFor="torque"></label>
<span className="torque-entry">
<input
aria-label="最低堵转扭矩精确值"
type="number"
min={TORQUE_MIN}
max={TORQUE_MAX}
step="0.01"
value={requirements.torque}
onChange={(event) => update(
"torque",
Math.min(TORQUE_MAX, Math.max(TORQUE_MIN, Number(event.target.value) || TORQUE_MIN)),
)}
/>
<strong>kgf·cm</strong>
</span>
</div>
<input
id="torque"
className="range-input"
type="range"
min="0"
max="100"
step="0.1"
value={torqueToSlider(requirements.torque)}
onChange={(event) => update("torque", sliderToTorque(Number(event.target.value)))}
/>
<div className="range-scale"><span>0.09</span><span>1</span><span>10</span><span>100</span><span>920</span></div>
<div className="torque-presets" aria-label="常用扭矩快捷值">
{TORQUE_PRESETS.map((torque) => (
<button
key={torque}
type="button"
className={requirements.torque === torque ? "active" : ""}
onClick={() => update("torque", torque)}
>{formatTorque(torque)}</button>
))}
</div>
</div>
<div className="parameter-grid">
<label className="select-field">
<span></span>
<select
value={requirements.weightLimit}
onChange={(event) => update("weightLimit", Number(event.target.value))}
>
<option value={0}></option>
{[2, 5, 10, 20, 40, 100, 250, 500, 1000, 2000].map((weight) => (
<option key={weight} value={weight}>{weight}g以内</option>
))}
</select>
</label>
<label className="select-field">
<span></span>
<select
value={requirements.angle}
onChange={(event) => update("angle", event.target.value)}
>
<option></option>
<option>135°</option>
<option>180°</option>
<option>270330°</option>
<option>360°/</option>
</select>
</label>
<label className="select-field">
<span> / </span>
<select
value={requirements.control}
onChange={(event) => update("control", event.target.value)}
>
<option></option>
<option>PWM/线</option>
<option>/TTL</option>
<option>RS485</option>
<option>CAN</option>
<option>SBUS</option>
<option>//线</option>
</select>
</label>
<label className="select-field">
<span>齿</span>
<select
value={requirements.gear}
onChange={(event) => update("gear", event.target.value)}
>
<option></option>
<option>齿</option>
<option>齿</option>
<option>齿</option>
</select>
</label>
</div>
</div>
)}
{stage === 3 && (
<div className="question-step">
<p className="step-kicker"> · </p>
<h2></h2>
<p className="step-help"></p>
<div className="two-fields">
<label className="select-field">
<span></span>
<select
value={requirements.annualVolume}
onChange={(event) => update("annualVolume", event.target.value)}
>
<option>500</option>
<option>5005000</option>
<option>500050000</option>
<option>50000</option>
</select>
</label>
<label className="select-field">
<span></span>
<input
className="parameter-input"
type="number"
min="1"
step="1"
inputMode="numeric"
value={requirements.firstOrderQuantity}
onChange={(event) => update("firstOrderQuantity", event.target.value)}
placeholder="例如500"
/>
</label>
</div>
<div className="two-fields">
<label className="select-field">
<span></span>
<select
value={requirements.painPoint}
onChange={(event) => update("painPoint", event.target.value)}
>
<option></option>
<option></option>
<option></option>
<option></option>
<option></option>
<option></option>
</select>
</label>
<label className="select-field">
<span>使</span>
<input
className="parameter-input"
value={requirements.currentModel}
onChange={(event) => update("currentModel", event.target.value)}
placeholder="例如SG90 / DS-E001"
/>
</label>
</div>
<label className="text-field project-notes">
<span></span>
<textarea
value={requirements.otherRequirements}
onChange={(event) => update("otherRequirements", event.target.value)}
placeholder="例如:防水等级、噪音、寿命、认证、安装尺寸、线长或交期要求"
/>
</label>
</div>
)}
<div className="panel-nav">
<button className="back-button" type="button" onClick={() => setStage(stage - 1)}> </button>
<button
className="next-button"
type="button"
disabled={stage === 1 && !requirements.application}
onClick={() => setStage(stage + 1)}
>
{stage === 3 ? "生成选型方案" : "继续"} <span></span>
</button>
</div>
</div>
)}
{stage === 4 && requirements.category === CUSTOM_SERVO_CATEGORY && (
<div className="result-panel custom-result-panel">
<div className="result-heading">
<div>
<p className="step-kicker"></p>
<h2></h2>
</div>
<button type="button" className="restart-button" onClick={reset}></button>
</div>
<div className="custom-result-card">
<div className="custom-result-mark">ODM</div>
<div>
<span></span>
<h3></h3>
<p>线</p>
</div>
<div className="metric-grid custom-requirement-grid">
<Metric label="最低堵转扭矩" value={`${formatTorque(requirements.torque)} kgf·cm`} />
<Metric label="额定电压" value={`${requirements.voltage.toFixed(1)}V`} />
<Metric label="操作角度" value={requirements.angle} />
<Metric label="控制 / 通讯" value={requirements.control} />
<Metric label="首次采购数量" value={requirements.firstOrderQuantity ? `${requirements.firstOrderQuantity}` : "待确认"} />
<Metric label="年度用量" value={requirements.annualVolume} />
</div>
{requirements.otherRequirements && (
<p className="custom-notes"><strong></strong>{requirements.otherRequirements}</p>
)}
</div>
{!showSampleForm && (
<div className="result-actions">
<button className="panel-primary" type="button" onClick={() => setShowSampleForm(true)}>
· <span></span>
</button>
<button className="outline-button" type="button" onClick={() => window.print()}> / </button>
</div>
)}
{showSampleForm && !submitted && (
<form className="sample-form custom-contact-form" onSubmit={submitLead}>
<div>
<p className="step-kicker"></p>
<h3></h3>
<p>CRM并通知工程师跟进</p>
</div>
<div className="form-grid">
<label><span></span><input required name="company" placeholder="请输入公司名称" /></label>
<label><span></span><input required name="name" placeholder="姓名" /></label>
<label><span></span><input required name="phone" inputMode="tel" placeholder="用于方案确认" /></label>
<label><span></span><input name="email" type="email" placeholder="接收定制方案" /></label>
</div>
<label className="full-field">
<span></span>
<textarea name="notes" defaultValue={requirements.otherRequirements} placeholder="请补充结构图、安装空间、认证或交期要求" />
</label>
<button className="panel-primary" type="submit"> <span></span></button>
<small className="demo-disclaimer"></small>
</form>
)}
{submitted && (
<div className="success-card">
<div className="success-icon"></div>
<p className="step-kicker"></p>
<h3> {leadId}</h3>
<p>线</p>
<button type="button" className="outline-button" onClick={reset}></button>
</div>
)}
</div>
)}
{stage === 4 && requirements.category !== CUSTOM_SERVO_CATEGORY && top && (
<div className="result-panel">
<div className="result-heading">
<div>
<p className="step-kicker">AI </p>
<h2>{compatibleCount > 0 ? `找到 ${compatibleCount} 个匹配型号` : "暂无完全匹配型号"}</h2>
</div>
<button type="button" className="restart-button" onClick={reset}></button>
</div>
<div className="best-match">
<div className="match-image-wrap">
<img src={top.product.image} alt={top.product.name} />
<span className="match-badge">{top.coreCompatible ? "首选推荐" : "待复核候选"}</span>
</div>
<div className="match-content">
<div className="match-score"><span></span><strong>{top.confidence}%</strong></div>
<p className="product-code">{top.product.code}</p>
<h3>{top.product.name}</h3>
<p className="product-subtitle">{top.product.category} · {top.product.size || "尺寸待复核"}</p>
<div className="metric-grid">
<Metric label="堵转扭矩" value={`${formatTorque(top.product.torque)} kgf·cm`} />
<Metric label="电压范围" value={`${top.product.voltageMin}${top.product.voltageMax}V`} />
<Metric label="空载速度" value={top.product.speed || "目录未标注"} />
<Metric label="操作角度" value={top.product.angle || "目录未标注"} />
<Metric label="控制 / 通讯" value={top.product.control || "目录未标注"} />
<Metric label="齿轮材质" value={top.product.gear} />
</div>
<ul className="reason-list">
{top.reasons.map((reason) => <li key={reason}> {reason}</li>)}
</ul>
<div className="source-line">
<span></span>
<strong>{top.product.category}</strong>
<small></small>
</div>
</div>
</div>
{needsEngineer && (
<div className="engineer-alert">
<span></span>
<p>{compatibleCount === 0
? "当前条件没有完全匹配的目录型号,建议调整电压、扭矩余量或发起定制评估。"
: "你的项目包含高用量、工业场景或边界参数,系统已标记为优先人工复核。"}</p>
</div>
)}
<div className="alternative-list">
{topMatches.slice(1).map((match) => (
<div className="alternative-card" key={match.product.id}>
<img src={match.product.image} alt={match.product.name} />
<div>
<span>{match.confidence}% </span>
<h4>{match.product.name}</h4>
<p>{formatTorque(match.product.torque)} kgf·cm · {match.product.category} · {match.product.gear}</p>
</div>
</div>
))}
</div>
{!showSampleForm && (
<div className="result-actions">
<button className="panel-primary" type="button" onClick={() => setShowSampleForm(true)}>
<span></span>
</button>
<button className="outline-button" type="button" onClick={() => window.print()}> / </button>
</div>
)}
{showSampleForm && !submitted && (
<form className="sample-form" onSubmit={submitLead}>
<div>
<p className="step-kicker"></p>
<h3></h3>
<p>线CRM</p>
</div>
<div className="form-grid">
<label><span></span><input required name="company" placeholder="请输入公司名称" /></label>
<label><span></span><input required name="name" placeholder="姓名" /></label>
<label><span></span><input required name="phone" inputMode="tel" placeholder="用于样品确认" /></label>
<label><span></span><select name="quantity"><option>12</option><option>35</option><option>610</option></select></label>
</div>
<label className="full-field"><span></span><textarea name="notes" placeholder="安装尺寸、线长、角度、控制协议或测试时间要求" /></label>
<button className="panel-primary" type="submit"> <span></span></button>
<small className="demo-disclaimer"></small>
</form>
)}
{submitted && (
<div className="success-card">
<div className="success-icon"></div>
<p className="step-kicker">线</p>
<h3> {leadId}</h3>
<p>{top.product.name}线CRM并触发技术复核</p>
<button type="button" className="outline-button" onClick={reset}></button>
</div>
)}
</div>
)}
</div>
</section>
<section className="capability-strip" aria-label="高徳乐能力">
<div><strong>2</strong><span></span></div>
<div><strong>137</strong><span></span></div>
<div><strong>1/</strong><span>线</span></div>
<div><strong>IATF 16949</strong><span></span></div>
</section>
<section className="products-section" id="products">
<div className="section-heading">
<div>
<p className="eyebrow"><span /> · 2026.06</p>
<h2>280</h2>
</div>
<p>280</p>
</div>
<div className="catalog-summary" aria-label="产品分类统计">
{PRIMARY_CATALOG_CATEGORIES.map((category) => (
<button
type="button"
key={category}
className={catalogCategory === category ? "active" : ""}
onClick={() => {
setCatalogCategory(category);
setCatalogLimit(24);
}}
>
<strong>{getCategoryCount(category)}</strong>
<span>{category}</span>
</button>
))}
</div>
<p className="catalog-category-note"> · 280 · 11</p>
<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_FILTER_CATEGORIES.map((category) => (
<button
type="button"
key={category}
className={`${catalogCategory === category ? "active" : ""} ${category === SILENT_SERVO_CATEGORY ? "feature-filter" : ""}`}
onClick={() => {
setCatalogCategory(category);
setCatalogLimit(24);
}}
>{category === SILENT_SERVO_CATEGORY ? `${category} · ${SILENT_SERVO_COUNT}` : 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>
<span className="catalog-zoom-hint"></span>
</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>
<h2><br /></h2>
<p>AI负责需求采集线</p>
<button className="primary-action light-button" type="button" onClick={() => { setStage(1); window.scrollTo({ top: 0, behavior: "smooth" }); }}>
<span></span>
</button>
</div>
<div className="flow-list">
<div><b>01</b><span><strong></strong><small></small></span></div>
<div><b>02</b><span><strong>AI生成候选方案</strong><small></small></span></div>
<div><b>03</b><span><strong></strong><small>线</small></span></div>
<div><b>04</b><span><strong></strong><small></small></span></div>
</div>
</section>
<footer>
<div className="brand footer-brand">
<span className="brand-mark">G</span>
<span><strong>GAUDELOT</strong><small> · </small></span>
</div>
<p>AI选型结果仅供初步筛选</p>
<span> · V1</span>
</footer>
</main>
);
}