"use client"; import { FormEvent, useEffect, useMemo, useState } from "react"; import { CATALOG_COUNTS, CATALOG_PRODUCTS as RAW_CATALOG_PRODUCTS, type CatalogProduct, } from "./catalog-data"; import { STEERING_WHEEL_ACCESSORIES } from "./catalog-accessories"; import { PRECISION_GEAR_PRODUCTS, PRECISION_GEAR_SUBCATEGORIES } from "./precision-gear-data"; import { getCatalogProductDimensionImage, getCatalogProductImage } from "./catalog-image-map"; const CATALOG_PRODUCTS = [...RAW_CATALOG_PRODUCTS, ...STEERING_WHEEL_ACCESSORIES].map((product) => ({ ...product, image: getCatalogProductImage(product), })); 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; }; type CatalogVisualMode = "main" | "size"; 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 PRECISION_GEAR_CARD_SPEC_ORDER = [ "模数", "齿数", "材质", "外径(mm)", "长(mm)", "宽(mm)", "高(mm)", "孔径", "类别", ]; function getPrecisionGearCardSpecs(product: CatalogProduct) { return PRECISION_GEAR_CARD_SPEC_ORDER .filter((label) => product.specs[label]) .slice(0, 2) .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 = { 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 : category === "舵盘配件" ? STEERING_WHEEL_ACCESSORIES.length : CATALOG_COUNTS[category]; } type ProductScale = { key: string; label: string; unit: "g" | "kg"; value: number; }; function getProductScale(product: CatalogProduct): ProductScale | null { const kilogramMatch = product.name.match(/(\d+(?:\.\d+)?)\s*(?:kg|千克)/i); if (kilogramMatch) { const value = Number(kilogramMatch[1]); return { key: `kg:${value}`, label: `${value}kg`, unit: "kg", value }; } const gramMatch = product.name.match(/(\d+(?:\.\d+)?)\s*(?:g|克)/i); if (gramMatch) { const value = Number(gramMatch[1]); return { key: `g:${value}`, label: `${value}g`, unit: "g", value }; } return null; } const PRODUCT_SCALE_OPTIONS = [...CATALOG_PRODUCTS.reduce((options, product) => { const scale = getProductScale(product); if (!scale) return options; const current = options.get(scale.key); options.set(scale.key, { ...scale, count: (current?.count ?? 0) + 1 }); return options; }, new Map()).values()].sort( (a, b) => (a.unit === b.unit ? a.value - b.value : a.unit === "g" ? -1 : 1), ); const GRAM_SCALE_OPTIONS = PRODUCT_SCALE_OPTIONS.filter((option) => option.unit === "g"); const KILOGRAM_SCALE_OPTIONS = PRODUCT_SCALE_OPTIONS.filter((option) => option.unit === "kg"); const UNMARKED_SCALE_COUNT = CATALOG_PRODUCTS.length - PRODUCT_SCALE_OPTIONS.reduce( (total, option) => total + option.count, 0, ); 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), 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: "5000–50000件", 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 (
{label} {value}
); } type SaveProductRequest = { product: CatalogProduct; badge?: string; subtitle?: string; extraSpecs?: Array<[string, string]>; }; function getSaveProductSpecs(product: CatalogProduct, extraSpecs: Array<[string, string]> = []) { return [ ...extraSpecs.filter(([, value]) => value), ...Object.entries(product.specs).filter(([, value]) => value), ]; } function wrapCanvasText(context: CanvasRenderingContext2D, value: string, maxWidth: number, maxLines = 2) { const characters = Array.from(String(value)); const lines: string[] = []; let current = ""; for (const character of characters) { const next = current + character; if (current && context.measureText(next).width > maxWidth) { lines.push(current); current = character; if (lines.length === maxLines) break; } else { current = next; } } if (lines.length < maxLines && current) lines.push(current); if (lines.length === maxLines && characters.join("").length > lines.join("").length) { const last = lines[maxLines - 1] ?? ""; lines[maxLines - 1] = `${last.slice(0, Math.max(1, last.length - 1))}…`; } return lines; } function drawRoundedRect( context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number, fill: string, stroke?: string, ) { context.beginPath(); context.moveTo(x + radius, y); context.lineTo(x + width - radius, y); context.quadraticCurveTo(x + width, y, x + width, y + radius); context.lineTo(x + width, y + height - radius); context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); context.lineTo(x + radius, y + height); context.quadraticCurveTo(x, y + height, x, y + height - radius); context.lineTo(x, y + radius); context.quadraticCurveTo(x, y, x + radius, y); context.closePath(); context.fillStyle = fill; context.fill(); if (stroke) { context.strokeStyle = stroke; context.lineWidth = 2; context.stroke(); } } function loadProductCanvasImage(src: string) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error(`产品图片加载失败:${src}`)); image.src = src; }); } async function renderProductCardPng({ product, badge, subtitle, extraSpecs = [] }: SaveProductRequest) { const specs = getSaveProductSpecs(product, extraSpecs); const columnCount = 2; const rowCount = Math.max(1, Math.ceil(specs.length / columnCount)); const cardWidth = 1200; const padding = 52; const headerHeight = 152; const specCellHeight = 70; const specGap = 10; const mainHeight = Math.max(390, rowCount * specCellHeight + (rowCount - 1) * specGap + 28); const cardHeight = padding + headerHeight + 24 + mainHeight + 24 + 42 + padding; const canvas = document.createElement("canvas"); canvas.width = cardWidth; canvas.height = cardHeight; const context = canvas.getContext("2d"); if (!context) throw new Error("当前浏览器不支持图片生成"); context.fillStyle = "#eef3fa"; context.fillRect(0, 0, cardWidth, cardHeight); drawRoundedRect(context, 18, 18, cardWidth - 36, cardHeight - 36, 26, "#ffffff", "#d9e3f2"); context.fillStyle = "#185ee8"; context.fillRect(padding, padding, 110, 7); context.fillStyle = "#123267"; context.font = "800 25px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; context.fillText("GAUDELOT", padding, padding + 34); context.fillStyle = "#7b8ca8"; context.font = "600 15px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; context.fillText("高徳乐产品资料", padding + 145, padding + 33); if (badge) { const badgeWidth = Math.max(100, context.measureText(badge).width + 34); drawRoundedRect(context, cardWidth - padding - badgeWidth, padding + 8, badgeWidth, 32, 16, "#edf4ff"); context.fillStyle = "#185ee8"; context.font = "700 14px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; context.fillText(badge, cardWidth - padding - badgeWidth + 17, padding + 29); } context.fillStyle = "#111d34"; context.font = "800 30px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; const titleLines = wrapCanvasText(context, product.name, 760, 2); titleLines.forEach((line, index) => context.fillText(line, padding, padding + 82 + index * 36)); context.fillStyle = "#71809a"; context.font = "600 17px ui-monospace, SFMono-Regular, Menlo, monospace"; context.fillText(product.code, padding, padding + 145); if (subtitle) { context.fillStyle = "#8290a5"; context.font = "500 15px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; context.fillText(subtitle, padding + 240, padding + 145); } const mainY = padding + headerHeight + 24; const imageX = padding; const imageWidth = 430; const infoX = imageX + imageWidth + 24; const infoWidth = cardWidth - padding - infoX; drawRoundedRect(context, imageX, mainY, imageWidth, mainHeight, 18, "#f3f7fd", "#dce6f3"); drawRoundedRect(context, infoX, mainY, infoWidth, mainHeight, 18, "#f8faff", "#dce6f3"); try { if (!product.image) throw new Error("没有产品主图"); const image = await loadProductCanvasImage(product.image); const maxImageWidth = imageWidth - 34; const maxImageHeight = mainHeight - 34; const scale = Math.min(maxImageWidth / image.naturalWidth, maxImageHeight / image.naturalHeight); const imageWidthOnCanvas = image.naturalWidth * scale; const imageHeightOnCanvas = image.naturalHeight * scale; context.drawImage( image, imageX + (imageWidth - imageWidthOnCanvas) / 2, mainY + (mainHeight - imageHeightOnCanvas) / 2, imageWidthOnCanvas, imageHeightOnCanvas, ); } catch { context.fillStyle = "#9eb3d3"; context.font = "800 22px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; context.fillText("GAUDELOT", imageX + 132, mainY + mainHeight / 2); } context.fillStyle = "#6f809b"; context.font = "700 15px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; context.fillText("主要参数", infoX + 22, mainY + 31); const cellsTop = mainY + 48; const cellWidth = (infoWidth - 44 - specGap) / columnCount; specs.forEach(([label, value], index) => { const column = index % columnCount; const row = Math.floor(index / columnCount); const cellX = infoX + 22 + column * (cellWidth + specGap); const cellY = cellsTop + row * (specCellHeight + specGap); drawRoundedRect(context, cellX, cellY, cellWidth, specCellHeight, 10, "#ffffff", "#e4ebf4"); context.fillStyle = "#8b98ab"; context.font = "600 13px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; const labelLines = wrapCanvasText(context, label, cellWidth - 24, 1); context.fillText(labelLines[0] ?? label, cellX + 12, cellY + 22); context.fillStyle = "#15233d"; context.font = "800 16px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; const valueLines = wrapCanvasText(context, value, cellWidth - 24, 2); valueLines.forEach((line, lineIndex) => context.fillText(line, cellX + 12, cellY + 47 + lineIndex * 18)); }); context.fillStyle = "#8b98ab"; context.font = "500 13px -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif"; context.fillText("高徳乐官方产品目录 · 仅供选型参考,具体参数以正式文件为准", padding, cardHeight - padding - 11); return new Promise((resolve, reject) => { canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error("PNG生成失败"))), "image/png"); }); } export default function Home() { const [stage, setStage] = useState(0); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); 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 [catalogScale, setCatalogScale] = useState("all"); const [catalogLimit, setCatalogLimit] = useState(24); const [catalogToolbarCompact, setCatalogToolbarCompact] = useState(false); const [catalogToolbarFiltersOpen, setCatalogToolbarFiltersOpen] = useState(false); const [catalogToolbarHidden, setCatalogToolbarHidden] = useState(false); const [precisionGearQuery, setPrecisionGearQuery] = useState(""); const [precisionGearCategory, setPrecisionGearCategory] = useState("全部子类"); const [precisionGearLimit, setPrecisionGearLimit] = useState(24); const [precisionGearToolbarCompact, setPrecisionGearToolbarCompact] = useState(false); const [precisionGearFiltersOpen, setPrecisionGearFiltersOpen] = useState(false); const [selectedCatalogProduct, setSelectedCatalogProduct] = useState(null); const [catalogVisualMode, setCatalogVisualMode] = useState("main"); const [savingProductKey, setSavingProductKey] = useState(""); const [saveProductMessage, setSaveProductMessage] = useState(""); 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 productScale = getProductScale(product)?.key ?? "unmarked"; if (catalogScale !== "all" && productScale !== catalogScale) return false; 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, catalogScale]); const visibleCatalog = filteredCatalog.slice(0, catalogLimit); const filteredPrecisionGears = useMemo(() => { const query = precisionGearQuery.trim().toLowerCase(); return PRECISION_GEAR_PRODUCTS.filter((product) => { if (precisionGearCategory !== "全部子类" && product.subCategory !== precisionGearCategory) return false; if (!query) return true; const haystack = [ product.name, product.code, product.subCategory, ...Object.keys(product.specs), ...Object.values(product.specs), ].join(" ").toLowerCase(); return haystack.includes(query); }); }, [precisionGearCategory, precisionGearQuery]); const visiblePrecisionGears = filteredPrecisionGears.slice(0, precisionGearLimit); const needsEngineer = !top?.coreCompatible || requirements.annualVolume === "50000件以上" || Number(requirements.firstOrderQuantity) >= 1000 || requirements.application === "工业设备"; const selectedCatalogDimensionImage = selectedCatalogProduct ? getCatalogProductDimensionImage(selectedCatalogProduct) : null; useEffect(() => { function updateCatalogToolbarState() { const toolbar = document.querySelector(".catalog-toolbar"); const shouldCompact = window.innerWidth <= 720 && Boolean(toolbar) && toolbar.getBoundingClientRect().top <= 72 && window.scrollY > 0; setCatalogToolbarCompact((current) => (current === shouldCompact ? current : shouldCompact)); if (!shouldCompact) setCatalogToolbarFiltersOpen(false); const precisionHeading = document.querySelector(".precision-gear-catalog-heading"); const shouldHideCatalogToolbar = window.innerWidth <= 720 && Boolean(precisionHeading) && precisionHeading.getBoundingClientRect().top <= 72; setCatalogToolbarHidden((current) => (current === shouldHideCatalogToolbar ? current : shouldHideCatalogToolbar)); const precisionToolbar = document.querySelector(".precision-gear-controls"); const shouldCompactPrecision = window.innerWidth <= 720 && Boolean(precisionToolbar) && precisionToolbar.getBoundingClientRect().top <= 72 && window.scrollY > 0; setPrecisionGearToolbarCompact((current) => (current === shouldCompactPrecision ? current : shouldCompactPrecision)); if (!shouldCompactPrecision) setPrecisionGearFiltersOpen(false); } updateCatalogToolbarState(); window.addEventListener("scroll", updateCatalogToolbarState, { passive: true }); document.addEventListener("scroll", updateCatalogToolbarState, { passive: true, capture: true }); window.addEventListener("resize", updateCatalogToolbarState); return () => { window.removeEventListener("scroll", updateCatalogToolbarState); document.removeEventListener("scroll", updateCatalogToolbarState, true); window.removeEventListener("resize", updateCatalogToolbarState); }; }, []); function openCatalogProduct(product: CatalogProduct) { setSelectedCatalogProduct(product); setCatalogVisualMode("main"); } async function saveProductCard(request: SaveProductRequest) { if (savingProductKey) return; const saveKey = `${request.product.id}-${request.badge ?? "product"}`; setSavingProductKey(saveKey); setSaveProductMessage("正在生成产品图片…"); try { const blob = await renderProductCardPng(request); const safeName = `${request.product.code}-${request.product.name}` .replace(/[\\/:*?"<>|]/g, "-") .replace(/\s+/g, "-") .slice(0, 90); const fileName = `${safeName || request.product.id}.png`; const file = new File([blob], fileName, { type: "image/png" }); const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.maxTouchPoints > 0 && window.innerWidth <= 900); const canShareFile = typeof navigator.share === "function" && (!navigator.canShare || navigator.canShare({ files: [file] })); if (isMobile && canShareFile) { try { await navigator.share({ title: `${request.product.name} 产品资料`, text: `高徳乐${request.product.category} · ${request.product.code}`, files: [file], }); setSaveProductMessage("已打开系统分享面板"); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { setSaveProductMessage("已取消保存"); } else { throw error; } } } else { const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1000); setSaveProductMessage("PNG图片已下载"); } } catch (error) { console.error("保存产品参数图片失败", error); setSaveProductMessage("图片生成失败,请稍后重试"); } finally { setSavingProductKey(""); window.setTimeout(() => setSaveProductMessage(""), 2800); } } function startSelection() { setMobileMenuOpen(false); setStage(1); requestAnimationFrame(() => { document.getElementById("selector")?.scrollIntoView({ behavior: "smooth", block: "start" }); }); } function update(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) { event.preventDefault(); const id = `GDL-${new Date().toISOString().slice(5, 10).replace("-", "")}-${String( Date.now(), ).slice(-4)}`; setLeadId(id); setSubmitted(true); } return (
{saveProductMessage && (
{saveProductMessage}
)}
GAUDELOT 高德乐
AI 智能选型 · 工程师复核

3分钟,找到
适合你的舵机

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

查看能力范围
280 个产品型号 0.09–920 kgf·cm 48h 方案响应
AI 选型助手
仅作初选建议
{stage === 0 && (
AI

高徳乐选型助手

你好,我来帮你缩小选型范围。

回答3组问题,即可获得推荐型号、匹配理由和样品申请入口。

✓ 无需注册即可查看选型结果
)} {stage > 0 && stage < 4 && (
需求识别 {stage} / 3
{stage === 1 && (

第一步 · 使用场景

舵机将用在什么产品上?

选择最接近的一项,后续可以补充具体结构。

{APPLICATIONS.map((item) => ( ))}
)} {stage === 2 && (

第二步 · 核心参数

输入舵机的关键技术要求

已覆盖243款舵机:0.09–920 kgf·cm,并按官方目录额定电压精确筛选。

update( "torque", Math.min(TORQUE_MAX, Math.max(TORQUE_MIN, Number(event.target.value) || TORQUE_MIN)), )} /> kgf·cm
update("torque", sliderToTorque(Number(event.target.value)))} />
0.09110100920
{TORQUE_PRESETS.map((torque) => ( ))}
)} {stage === 3 && (

第三步 · 项目信息

最后,告诉我采购侧的偏好。

这些信息用于调整候选型号的优先级,并由工程师在送样前复核。