From 596493a68686d037263b47333e697b7066fbdd42 Mon Sep 17 00:00:00 2001 From: Jacky Ser Date: Mon, 10 Aug 2026 16:21:49 +0800 Subject: [PATCH] Expand selector to full servo catalog --- app/globals.css | 25 +- app/page.tsx | 595 ++++++++++++++++++++++-------------------------- 2 files changed, 297 insertions(+), 323 deletions(-) diff --git a/app/globals.css b/app/globals.css index 1d8aa1e..408995c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -137,17 +137,25 @@ a { color: inherit; text-decoration: none; } .back-button, .restart-button { padding: 11px 2px; border: 0; color: #798599; background: transparent; cursor: pointer; font-size: 12px; } .next-button { min-width: 150px; padding: 13px 20px; } .next-button:disabled { cursor: not-allowed; opacity: .35; transform: none; box-shadow: none; } -.field-stack { padding: 18px 18px 15px; border-radius: 12px; background: #f6f8fc; } -.field-label { display: flex; justify-content: space-between; color: #657187; font-size: 12px; } +.field-stack { margin-top: 13px; padding: 15px 16px 13px; border-radius: 12px; background: #f6f8fc; } +.field-label { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: #657187; font-size: 12px; } .field-label strong { color: var(--blue); font-size: 15px; } -.range-input { width: 100%; margin: 24px 0 6px; accent-color: var(--blue); } +.torque-entry { display: flex; align-items: center; gap: 6px; } +.torque-entry input { width: 86px; height: 34px; padding: 0 9px; text-align: right; font-weight: 750; } +.torque-entry strong { font-size: 10px; white-space: nowrap; } +.range-input { width: 100%; margin: 16px 0 5px; accent-color: var(--blue); } .range-scale { display: flex; justify-content: space-between; color: #9aa3b1; font-size: 9px; } +.torque-presets { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin-top: 10px; } +.torque-presets button { min-width: 0; padding: 6px 2px; border: 1px solid #d8e0eb; border-radius: 7px; color: #657187; background: white; cursor: pointer; font-size: 9px; } +.torque-presets button.active { border-color: var(--blue); color: var(--blue); background: var(--blue-soft); font-weight: 750; } .two-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 15px; } +.compact-fields { margin-top: 0; } +.parameter-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; margin-top: 13px; } .select-field, .text-field { display: grid; gap: 7px; } .select-field span, .text-field span, .field-title, .sample-form label span { color: #68758a; font-size: 11px; font-weight: 650; } select, input, textarea { outline: none; border: 1px solid #dbe2ec; border-radius: 9px; color: var(--ink); background: white; } select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(24,94,232,.1); } -.select-field select { height: 46px; padding: 0 12px; } +.select-field select, .parameter-input { width: 100%; height: 43px; padding: 0 12px; } .text-field { margin-top: 13px; } .text-field input { height: 44px; padding: 0 13px; } .field-group { margin: 17px 0 13px; } @@ -159,7 +167,7 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado .result-heading { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 18px; } .best-match { display: grid; grid-template-columns: 180px 1fr; border: 1px solid #cfdaf0; border-radius: 15px; overflow: hidden; box-shadow: 0 8px 24px rgba(38,70,125,.08); } .match-image-wrap { position: relative; min-height: 270px; background: #eff4fb; } -.match-image-wrap img { width: 100%; height: 100%; object-fit: cover; } +.match-image-wrap img { width: 100%; height: 100%; object-fit: contain; mix-blend-mode: multiply; } .match-badge { position: absolute; left: 12px; top: 12px; padding: 6px 9px; border-radius: 7px; color: white; background: var(--orange); font-size: 9px; font-weight: 800; } .match-content { position: relative; padding: 19px 19px 16px; } .match-score { position: absolute; right: 16px; top: 15px; display: grid; text-align: right; color: #7c889a; font-size: 9px; } @@ -176,6 +184,10 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado .price-line span { color: #7c8798; font-size: 9px; } .price-line strong { color: #e55726; font-size: 20px; } .price-line small { color: #9aa4b3; font-size: 8px; } +.source-line { display: flex; align-items: center; gap: 7px; padding-top: 9px; border-top: 1px solid #edf0f4; } +.source-line span { color: #7c8798; font-size: 9px; } +.source-line strong { color: var(--blue); font-size: 10px; } +.source-line small { margin-left: auto; color: #9aa4b3; font-size: 8px; } .engineer-alert { display: flex; align-items: center; gap: 10px; margin-top: 10px; padding: 10px 12px; border: 1px solid #f0d29e; border-radius: 9px; background: #fff9ef; } .engineer-alert span { flex: 0 0 auto; padding: 4px 7px; border-radius: 5px; color: #9b5a0a; background: #ffe7bc; font-size: 8px; font-weight: 800; } .engineer-alert p { margin: 0; color: #815e2f; font-size: 9px; } @@ -439,7 +451,8 @@ footer > span { color: #9aa4b2; font-size: 10px; } .selector-shell { border-radius: 16px; } .question-panel, .result-panel { padding: 20px 16px; } .welcome-panel { padding: 55px 20px 35px; } - .choice-grid, .two-fields, .form-grid, .alternative-list { grid-template-columns: 1fr; } + .choice-grid, .two-fields, .parameter-grid, .form-grid, .alternative-list { grid-template-columns: 1fr; } + .torque-presets { grid-template-columns: repeat(4, 1fr); } .best-match { grid-template-columns: 1fr; } .match-image-wrap { height: 240px; } .capability-strip { grid-template-columns: 1fr 1fr; gap: 24px 0; padding: 28px 16px; } diff --git a/app/page.tsx b/app/page.tsx index d9788db..7f77c5e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,247 +7,20 @@ import { type CatalogProduct, } from "./catalog-data"; -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; + category: string; torque: number; voltage: number; weightLimit: number; - gear: "不限" | Gear; + angle: string; + control: string; + gear: string; 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: "互动机构、表情和行走结构" }, @@ -287,11 +60,112 @@ function getFeaturedSpecs(product: CatalogProduct) { .map((label) => [label, product.specs[label]] as const); } +const SERVO_CATEGORIES = ["标准舵机", "异形舵机", "全金属舵机", "大扭矩舵机", "积木舵机"]; +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 = { + AI毛绒: ["标准舵机", "异形舵机"], + 智能玩具: ["标准舵机", "异形舵机", "积木舵机"], + "教育/积木": ["积木舵机", "标准舵机"], + 微型机器人: ["标准舵机", "异形舵机", "全金属舵机"], + "云台/航模": ["标准舵机", "全金属舵机"], + 工业设备: ["全金属舵机", "大扭矩舵机", "异形舵机"], +}; + +function parseNumbers(value = "") { + return (value.match(/\d+(?:\.\d+)?/g) ?? []).map(Number); +} + +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 === "270–330°") 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), + gear: classifyGear(product.specs["齿轮材质"]), + angle: product.specs["可操作角度"] ?? product.specs["操作角度范围"] ?? "", + control, + speed: product.specs["空载速度"] ?? product.specs["转动速度"] ?? "", + size: product.specs["产品尺寸"] ?? product.specs["外形尺寸"] ?? "", + }; +}); + const DEFAULT_REQUIREMENTS: Requirements = { application: "", + category: "全部舵机", torque: 2, voltage: 5, - weightLimit: 40, + weightLimit: 0, + angle: "不限", + control: "不限", gear: "不限", annualVolume: "5000–50000件", currentModel: "", @@ -299,58 +173,69 @@ const DEFAULT_REQUIREMENTS: Requirements = { }; function getMatches(requirements: Requirements) { - return PRODUCTS.map((product) => { - let score = 34; + return SELECTOR_PRODUCTS.map((product) => { + let score = 20; const reasons: string[] = []; + const categoryOk = + requirements.category === "全部舵机" || requirements.category === product.category; + const torqueOk = product.torque >= requirements.torque; + const voltageOk = + requirements.voltage >= product.voltageMin && requirements.voltage <= product.voltageMax; + 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 (product.applications.includes(requirements.application)) { - score += 18; + 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 (product.torque >= requirements.torque) { - score += 25; + 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 -= 70; + score -= 120; } - if ( - requirements.voltage >= product.voltageMin && - requirements.voltage <= product.voltageMax - ) { - score += 14; + if (voltageOk) { + score += 22; reasons.push(`${requirements.voltage}V供电兼容`); } else { - score -= 35; + score -= 90; } - if (product.weight <= requirements.weightLimit) { - score += 10; - reasons.push(`重量不超过${requirements.weightLimit}g`); - } else { - score -= 45; + 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 === "不限" || 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.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 <= 9) score += 8; + if (requirements.painPoint === "耐久" && product.gear === "全金属齿") score += 8; + if (requirements.painPoint === "体积" && product.weight !== null && product.weight <= 9) score += 8; return { product, score, - confidence: Math.max(52, Math.min(96, score)), + coreCompatible, + confidence: Math.max(48, Math.min(97, Math.round(score))), reasons: reasons.slice(0, 3), }; - }).sort((a, b) => b.score - a.score); + }).sort((a, b) => Number(b.coreCompatible) - Number(a.coreCompatible) || b.score - a.score); } function Metric({ label, value }: { label: string; value: string }) { @@ -376,6 +261,7 @@ export default function Home() { 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) => { @@ -397,7 +283,7 @@ export default function Home() { }, [catalogCategory, catalogQuery]); const visibleCatalog = filteredCatalog.slice(0, catalogLimit); const needsEngineer = - top.score < 60 || + !top?.coreCompatible || requirements.annualVolume === "50000件以上" || requirements.application === "工业设备"; @@ -449,7 +335,7 @@ export default function Home() { 适合你的舵机

- 告诉我们应用、扭矩与结构限制。AI从高徳乐标准产品中生成候选方案, + 告诉我们应用、扭矩与结构限制。AI从高徳乐完整舵机目录中生成候选方案, 同步准备样品与替代选型摘要。

@@ -521,50 +407,134 @@ export default function Home() { {stage === 2 && (

第二步 · 核心参数

-

需要多大的输出能力?

-

不确定时可填写估算值,工程师会在送样前复核。

-
-