Files
gdm-Servo-model/app/page.tsx
2026-08-10 11:48:07 +08:00

742 lines
28 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";
type Gear = "塑料齿" | "半金属齿" | "金属齿";
type Product = {
id: string;
name: string;
subtitle: string;
weight: number;
torque: number;
speed: number;
voltageMin: number;
voltageMax: number;
gear: Gear;
price: number;
size: string;
image: string;
applications: string[];
code: string;
};
type Requirements = {
application: string;
torque: number;
voltage: number;
weightLimit: number;
gear: "不限" | Gear;
annualVolume: string;
currentModel: string;
painPoint: string;
};
const PRODUCTS: Product[] = [
{
id: "17g",
name: "1.7克超微型舵机",
subtitle: "极致轻量,适合微型结构",
weight: 1.7,
torque: 0.09,
speed: 0.12,
voltageMin: 2.7,
voltageMax: 5,
gear: "塑料齿",
price: 30,
size: "13.40 × 6.15 × 16.00 mm",
image: "/products/servo-17g.png",
applications: ["AI毛绒", "智能玩具", "微型机器人", "云台/航模"],
code: "4.A.01.A0060",
},
{
id: "2g",
name: "2克塑料齿舵机",
subtitle: "快速响应的微型执行方案",
weight: 2,
torque: 0.4,
speed: 0.08,
voltageMin: 3.5,
voltageMax: 6,
gear: "塑料齿",
price: 14.4,
size: "16.25 × 8.30 × 16.90 mm",
image: "/products/servo-2g.jpg",
applications: ["AI毛绒", "智能玩具", "微型机器人", "云台/航模"],
code: "4.A.01.A0004",
},
{
id: "37g",
name: "3.7克塑料齿舵机",
subtitle: "轻量玩具与科创教育通用",
weight: 3.7,
torque: 0.8,
speed: 0.09,
voltageMin: 3.5,
voltageMax: 6,
gear: "塑料齿",
price: 14.7,
size: "20.40 × 8.70 × 20.32 mm",
image: "/products/servo-37g.jpg",
applications: ["AI毛绒", "智能玩具", "教育/积木", "云台/航模"],
code: "4.A.01.A0003",
},
{
id: "9p",
name: "9克塑料齿舵机",
subtitle: "成本优先的标准轻载方案",
weight: 9,
torque: 2.1,
speed: 0.11,
voltageMin: 4.8,
voltageMax: 6,
gear: "塑料齿",
price: 13.1,
size: "23.00 × 12.36 × 28.50 mm",
image: "/products/servo-9g-plastic.jpg",
applications: ["智能玩具", "教育/积木", "微型机器人", "云台/航模"],
code: "4.A.01.A0009",
},
{
id: "9h",
name: "9克半金属齿舵机",
subtitle: "耐久与成本兼顾",
weight: 9,
torque: 3.2,
speed: 0.13,
voltageMin: 4.8,
voltageMax: 6,
gear: "半金属齿",
price: 18,
size: "23.00 × 12.36 × 28.50 mm",
image: "/products/servo-9g-hybrid.jpg",
applications: ["智能玩具", "教育/积木", "微型机器人", "云台/航模"],
code: "4.A.01.B0001",
},
{
id: "9m",
name: "9克全金属齿舵机",
subtitle: "高频使用与冲击负载优选",
weight: 9,
torque: 3.2,
speed: 0.13,
voltageMin: 4.8,
voltageMax: 6,
gear: "金属齿",
price: 28.7,
size: "23.00 × 12.36 × 28.50 mm",
image: "/products/servo-9g-metal.jpg",
applications: ["教育/积木", "微型机器人", "云台/航模", "工业设备"],
code: "4.A.01.B0026",
},
{
id: "17p",
name: "17克塑料齿舵机",
subtitle: "玩具机构的高性价比选择",
weight: 17,
torque: 5,
speed: 0.1,
voltageMin: 4.8,
voltageMax: 6,
gear: "塑料齿",
price: 14.9,
size: "28.60 × 13.00 × 31.00 mm",
image: "/products/servo-17g-plastic.jpg",
applications: ["智能玩具", "教育/积木", "微型机器人"],
code: "4.A.01.A0002",
},
{
id: "17m",
name: "17克金属齿舵机",
subtitle: "宽电压、高扭矩、耐冲击",
weight: 17,
torque: 8.8,
speed: 0.18,
voltageMin: 4.8,
voltageMax: 8.4,
gear: "金属齿",
price: 27.8,
size: "28.60 × 13.00 × 31.00 mm",
image: "/products/servo-17g-metal.jpg",
applications: ["智能玩具", "微型机器人", "云台/航模", "工业设备"],
code: "4.A.01.B0007",
},
{
id: "25p",
name: "25克塑料齿舵机",
subtitle: "中等负载的经济型方案",
weight: 25,
torque: 7.5,
speed: 0.15,
voltageMin: 4.8,
voltageMax: 6,
gear: "塑料齿",
price: 16.4,
size: "36.00 × 15.20 × 29.50 mm",
image: "/products/servo-25g-plastic.jpg",
applications: ["智能玩具", "教育/积木", "微型机器人"],
code: "4.A.01.A0001",
},
{
id: "25m",
name: "25克金属齿舵机",
subtitle: "中型机器人和高频机构",
weight: 25,
torque: 8.2,
speed: 0.15,
voltageMin: 4.8,
voltageMax: 8.4,
gear: "金属齿",
price: 44.7,
size: "36.00 × 15.20 × 29.50 mm",
image: "/products/servo-25g-metal.jpg",
applications: ["微型机器人", "云台/航模", "工业设备"],
code: "4.A.01.B0014",
},
{
id: "37p",
name: "37克塑料齿舵机",
subtitle: "标准尺寸、大负载经济型",
weight: 37,
torque: 6.8,
speed: 0.22,
voltageMin: 4.8,
voltageMax: 6,
gear: "塑料齿",
price: 18.4,
size: "40.85 × 20.15 × 39.15 mm",
image: "/products/servo-37g-plastic.jpg",
applications: ["智能玩具", "微型机器人", "工业设备"],
code: "4.A.01.B0015",
},
{
id: "37h",
name: "37克半金属齿舵机",
subtitle: "大扭矩与成本的平衡点",
weight: 37,
torque: 11.2,
speed: 0.26,
voltageMin: 4.8,
voltageMax: 6,
gear: "半金属齿",
price: 32.9,
size: "40.85 × 20.15 × 39.15 mm",
image: "/products/servo-37g-hybrid.jpg",
applications: ["微型机器人", "云台/航模", "工业设备"],
code: "4.A.01.B0005",
},
{
id: "37m",
name: "37克金属齿舵机",
subtitle: "24kgf·cm级强劲输出",
weight: 37,
torque: 24,
speed: 0.22,
voltageMin: 4.8,
voltageMax: 8.4,
gear: "金属齿",
price: 37.6,
size: "40.85 × 20.15 × 39.15 mm",
image: "/products/servo-37g-metal.jpg",
applications: ["机器人/机械臂", "云台/航模", "工业设备"],
code: "4.A.01.B0017",
},
];
const APPLICATIONS = [
{ name: "AI毛绒", note: "头部、耳朵、手臂等轻量动作" },
{ name: "智能玩具", note: "互动机构、表情和行走结构" },
{ name: "教育/积木", note: "STEAM套件与积木动力模块" },
{ name: "微型机器人", note: "关节、夹爪和小型机械臂" },
{ name: "云台/航模", note: "飞行控制、云台与模型机构" },
{ name: "工业设备", note: "控制机构与定制传动组件" },
];
const DEFAULT_REQUIREMENTS: Requirements = {
application: "",
torque: 2,
voltage: 5,
weightLimit: 40,
gear: "不限",
annualVolume: "500050000件",
currentModel: "",
painPoint: "交期",
};
function getMatches(requirements: Requirements) {
return PRODUCTS.map((product) => {
let score = 34;
const reasons: string[] = [];
if (product.applications.includes(requirements.application)) {
score += 18;
reasons.push(`适合${requirements.application}场景`);
}
if (product.torque >= requirements.torque) {
score += 25;
const margin = Math.round((product.torque / requirements.torque - 1) * 100);
reasons.push(`扭矩满足,约有${Math.max(0, margin)}%余量`);
} else {
score -= 70;
}
if (
requirements.voltage >= product.voltageMin &&
requirements.voltage <= product.voltageMax
) {
score += 14;
reasons.push(`${requirements.voltage}V供电兼容`);
} else {
score -= 35;
}
if (product.weight <= requirements.weightLimit) {
score += 10;
reasons.push(`重量不超过${requirements.weightLimit}g`);
} else {
score -= 45;
}
if (requirements.gear === "不限" || requirements.gear === product.gear) {
score += requirements.gear === "不限" ? 4 : 12;
if (requirements.gear !== "不限") reasons.push(`${product.gear}符合偏好`);
} else if (requirements.gear === "金属齿" && product.gear !== "金属齿") {
score -= 22;
}
if (requirements.painPoint === "成本" && product.gear === "塑料齿") score += 8;
if (requirements.painPoint === "耐久" && product.gear === "金属齿") score += 8;
if (requirements.painPoint === "体积" && product.weight <= 9) score += 8;
return {
product,
score,
confidence: Math.max(52, Math.min(96, score)),
reasons: reasons.slice(0, 3),
};
}).sort((a, b) => 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 matches = useMemo(() => getMatches(requirements), [requirements]);
const topMatches = matches.slice(0, 3);
const top = topMatches[0];
const needsEngineer =
top.score < 60 ||
requirements.annualVolume === "50000件以上" ||
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>13</b> </span>
<span><b>0.0924</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"></p>
<div className="field-stack">
<label className="field-label" htmlFor="torque">
<strong>{requirements.torque} kgf·cm</strong>
</label>
<input
id="torque"
className="range-input"
type="range"
min="0.1"
max="24"
step="0.1"
value={requirements.torque}
onChange={(event) => update("torque", Number(event.target.value))}
/>
<div className="range-scale"><span>0.1</span><span>5</span><span>10</span><span>15</span><span>24</span></div>
</div>
<div className="two-fields">
<label className="select-field">
<span></span>
<select
value={requirements.voltage}
onChange={(event) => update("voltage", Number(event.target.value))}
>
<option value={3.7}>3.7V</option>
<option value={4.2}>4.2V</option>
<option value={5}>5.0V</option>
<option value={6}>6.0V</option>
<option value={7.4}>7.4V</option>
<option value={8.4}>8.4V</option>
</select>
</label>
<label className="select-field">
<span></span>
<select
value={requirements.weightLimit}
onChange={(event) => update("weightLimit", Number(event.target.value))}
>
<option value={2}>2g以内</option>
<option value={4}>4g以内</option>
<option value={10}>10g以内</option>
<option value={20}>20g以内</option>
<option value={40}>40g以内</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="field-group">
<span className="field-title">齿</span>
<div className="pill-row">
{(["不限", "塑料齿", "半金属齿", "金属齿"] as const).map((gear) => (
<button
type="button"
key={gear}
className={requirements.gear === gear ? "active" : ""}
onClick={() => update("gear", gear)}
>{gear}</button>
))}
</div>
</div>
<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>
<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>
</div>
<label className="text-field">
<span>使</span>
<input
value={requirements.currentModel}
onChange={(event) => update("currentModel", event.target.value)}
placeholder="例如SG90 / DS-E001 / 自定义型号"
/>
</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 && top && (
<div className="result-panel">
<div className="result-heading">
<div>
<p className="step-kicker">AI </p>
<h2> {topMatches.length} </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"></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.subtitle}</p>
<div className="metric-grid">
<Metric label="堵转扭矩" value={`${top.product.torque} kgf·cm`} />
<Metric label="空载速度" value={`${top.product.speed}s/60°`} />
<Metric label="工作电压" value={`${top.product.voltageMin}${top.product.voltageMax}V`} />
<Metric label="齿轮材质" value={top.product.gear} />
</div>
<ul className="reason-list">
{top.reasons.map((reason) => <li key={reason}> {reason}</li>)}
</ul>
<div className="price-line">
<span></span>
<strong>¥{top.product.price.toFixed(1)}</strong>
<small>/ · </small>
</div>
</div>
</div>
{needsEngineer && (
<div className="engineer-alert">
<span></span>
<p></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>{match.product.torque} kgf·cm · {match.product.gear} · ¥{match.product.price.toFixed(1)}</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 /> </p>
<h2>24kgf·cm</h2>
</div>
<p>13</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>
</section>
<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>
);
}