Revert ecommerce catalog rematch

This commit is contained in:
Jacky Ser
2026-08-11 15:37:23 +08:00
parent b4d11e2f12
commit 6c37aa3d47
561 changed files with 7342 additions and 16435 deletions

File diff suppressed because it is too large Load Diff

93
app/catalog-image-map.ts Normal file
View File

@@ -0,0 +1,93 @@
import type { CatalogProduct } from "./catalog-data";
// The ecommerce image archive groups products by family rather than by the
// individual model code used in the PDF catalog. These are the corresponding
// family-level main images copied into public/catalog-products/source-main.
const SOURCE_MAIN_IMAGES = {
"1004": "/catalog-products/source-main/1004-main.png",
"258": "/catalog-products/source-main/258-main.jpg",
"259": "/catalog-products/source-main/259-main.jpg",
"266": "/catalog-products/source-main/266-main.jpg",
"267": "/catalog-products/source-main/267-main.jpg",
"268": "/catalog-products/source-main/268-main.jpg",
"270": "/catalog-products/source-main/270-main.jpg",
"271": "/catalog-products/source-main/271-main.jpg",
"272": "/catalog-products/source-main/272-main.jpg",
"273": "/catalog-products/source-main/273-main.jpg",
"274": "/catalog-products/source-main/274-main.jpg",
"275": "/catalog-products/source-main/275-main.jpg",
"276": "/catalog-products/source-main/276-main.jpg",
"308": "/catalog-products/source-main/308-main.png",
"309": "/catalog-products/source-main/309-main.png",
"310": "/catalog-products/source-main/310-main.png",
"311": "/catalog-products/source-main/311-main.png",
"313": "/catalog-products/source-main/313-main.png",
} as const;
type SourceImageKey = keyof typeof SOURCE_MAIN_IMAGES;
function hasWeight(name: string, grams: string) {
return new RegExp(`${grams}\\s*(?:g|克)`, "i").test(name);
}
function getGearSeries(name: string) {
if (/半金属/.test(name)) return "semi";
if (/全金属|金属/.test(name)) return "metal";
if (/塑胶|塑料/.test(name)) return "plastic";
return null;
}
function getSmallServoImage(name: string): string | null {
if (hasWeight(name, "1\\.7")) return SOURCE_MAIN_IMAGES["1004"];
const gearSeries = getGearSeries(name);
if (hasWeight(name, "2") && gearSeries === "plastic") return SOURCE_MAIN_IMAGES["272"];
if (hasWeight(name, "3\\.7") && gearSeries === "plastic") return SOURCE_MAIN_IMAGES["271"];
if (hasWeight(name, "9")) {
if (gearSeries === "metal") return SOURCE_MAIN_IMAGES["266"];
if (gearSeries === "semi") return SOURCE_MAIN_IMAGES["259"];
if (gearSeries === "plastic") return SOURCE_MAIN_IMAGES["258"];
}
if (hasWeight(name, "17")) {
if (gearSeries === "metal") return SOURCE_MAIN_IMAGES["274"];
if (gearSeries === "plastic") return SOURCE_MAIN_IMAGES["273"];
}
if (hasWeight(name, "25")) {
if (gearSeries === "metal") return SOURCE_MAIN_IMAGES["276"];
if (gearSeries === "plastic") return SOURCE_MAIN_IMAGES["275"];
}
if (hasWeight(name, "37")) {
if (gearSeries === "metal") return SOURCE_MAIN_IMAGES["270"];
if (gearSeries === "semi") return SOURCE_MAIN_IMAGES["268"];
if (gearSeries === "plastic") return SOURCE_MAIN_IMAGES["267"];
}
return null;
}
function getBrickServoImage(name: string): string | null {
if (name.includes("3x3x3")) return SOURCE_MAIN_IMAGES["311"];
if (name.includes("3x3x7")) return SOURCE_MAIN_IMAGES["310"];
if (name.includes("3x7x5")) return SOURCE_MAIN_IMAGES["309"];
if (name.includes("6x6x5")) return SOURCE_MAIN_IMAGES["308"];
if (name.includes("11x5x4")) return SOURCE_MAIN_IMAGES["313"];
return null;
}
export function getCatalogProductImage(product: CatalogProduct) {
if (product.category === "积木舵机") {
return getBrickServoImage(product.name) ?? product.image;
}
if (product.category === "标准舵机" || product.category === "异形舵机") {
return getSmallServoImage(product.name) ?? product.image;
}
return product.image;
}
export const CATALOG_SOURCE_IMAGE_KEYS: SourceImageKey[] = Object.keys(SOURCE_MAIN_IMAGES) as SourceImageKey[];

View File

@@ -242,7 +242,7 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
.section-heading > p { max-width: 480px; margin: 0; color: #6e7a8e; line-height: 1.8; font-size: 13px; }
.catalog-summary {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-columns: repeat(7, 1fr);
gap: 9px;
margin-bottom: 22px;
}

View File

@@ -11,17 +11,17 @@ export async function generateMetadata(): Promise<Metadata> {
return {
metadataBase,
title: "高徳乐 AI 舵机选型助手",
description: "高徳乐商城536款产品资料支持产品参数搜索、AI舵机初选与样品申请。",
description: "高徳乐280个产品型号完整目录支持搜索筛选、AI舵机初选与样品申请。",
openGraph: {
title: "高徳乐 AI 舵机选型助手",
description: "536款商城产品完整目录。3分钟找到适合你的舵机AI初选工程师复核。",
description: "280个型号完整目录。3分钟找到适合你的舵机AI初选工程师复核。",
images: [{ url: new URL("/og.png", metadataBase).toString(), width: 1200, height: 630 }],
type: "website",
},
twitter: {
card: "summary_large_image",
title: "高徳乐 AI 舵机选型助手",
description: "高徳乐商城产品资料与 AI 舵机选型助手。",
description: "3分钟找到适合你的舵机。",
images: [new URL("/og.png", metadataBase).toString()],
},
};

View File

@@ -3,9 +3,15 @@
import { FormEvent, useMemo, useState } from "react";
import {
CATALOG_COUNTS,
CATALOG_PRODUCTS,
CATALOG_PRODUCTS as RAW_CATALOG_PRODUCTS,
type CatalogProduct,
} from "./catalog-data";
import { getCatalogProductImage } from "./catalog-image-map";
const CATALOG_PRODUCTS = RAW_CATALOG_PRODUCTS.map((product) => ({
...product,
image: getCatalogProductImage(product),
}));
type Requirements = {
application: string;
@@ -34,10 +40,12 @@ const APPLICATIONS = [
const PRIMARY_CATALOG_CATEGORIES = [
"标准舵机",
"异形舵机",
"全金属舵机",
"大扭矩舵机",
"积木舵机",
"舵盘配件",
"标准齿轮箱",
"精密齿轮配件",
"翻盖电机",
];
const CATALOG_FILTER_CATEGORIES = ["全部产品", ...PRIMARY_CATALOG_CATEGORIES, "静音舵机"];
@@ -52,13 +60,6 @@ const FEATURED_SPEC_ORDER = [
"寿命",
"产品尺寸",
"外形尺寸",
"空载速度",
"材质",
"模数",
"齿数",
"孔径",
"重量",
"参考单价",
];
function getFeaturedSpecs(product: CatalogProduct) {
@@ -68,32 +69,20 @@ function getFeaturedSpecs(product: CatalogProduct) {
.map((label) => [label, product.specs[label]] as const);
}
const SERVO_CATEGORIES = ["标准舵机", "积木舵机"];
const SERVO_CATEGORIES = ["标准舵机", "异形舵机", "全金属舵机", "大扭矩舵机", "积木舵机"];
const SILENT_SERVO_CATEGORY = "静音舵机";
const CUSTOM_SERVO_CATEGORY = "非标准舵机定制";
const TORQUE_MIN = 0.09;
const TORQUE_MAX = Math.max(
TORQUE_MIN,
...CATALOG_PRODUCTS.filter((product) => SERVO_CATEGORIES.includes(product.category))
.map((product) => product.torqueValue ?? 0),
);
const TORQUE_PRESETS = [...new Set([
0.1,
1,
5,
20,
100,
300,
TORQUE_MAX,
].filter((value) => value <= TORQUE_MAX))];
const TORQUE_MAX = 920;
const TORQUE_PRESETS = [0.1, 1, 5, 20, 100, 300, 920];
const APPLICATION_CATEGORY_MAP: Record<string, string[]> = {
AI毛绒: ["标准舵机"],
: ["标准舵机", "积木舵机"],
AI毛绒: ["标准舵机", "异形舵机"],
: ["标准舵机", "异形舵机", "积木舵机"],
"教育/积木": ["积木舵机", "标准舵机"],
: ["标准舵机", "积木舵机"],
"云台/航模": ["标准舵机"],
: ["标准舵机", "积木舵机"],
: ["标准舵机", "异形舵机", "全金属舵机"],
"云台/航模": ["标准舵机", "全金属舵机"],
: ["全金属舵机", "大扭矩舵机", "异形舵机"],
};
function parseNumbers(value = "") {
@@ -108,7 +97,7 @@ function isSilentServo(product: CatalogProduct) {
const SILENT_SERVO_COUNT = CATALOG_PRODUCTS.filter(isSilentServo).length;
function getCategoryCount(category: string) {
return category === SILENT_SERVO_CATEGORY ? SILENT_SERVO_COUNT : CATALOG_COUNTS[category] ?? 0;
return category === SILENT_SERVO_CATEGORY ? SILENT_SERVO_COUNT : CATALOG_COUNTS[category];
}
type ProductScale = {
@@ -225,10 +214,10 @@ const SELECTOR_PRODUCTS = CATALOG_PRODUCTS.filter((product) =>
weight: inferWeight(product),
isSilent: isSilentServo(product),
gear: classifyGear(product.specs["齿轮材质"]),
angle: product.specs["可操作角度"] ?? "",
angle: product.specs["可操作角度"] ?? product.specs["操作角度范围"] ?? "",
control,
speed: product.specs["空载速度"] ?? "",
size: product.specs["产品尺寸"] ?? "",
speed: product.specs["空载速度"] ?? product.specs["转动速度"] ?? "",
size: product.specs["产品尺寸"] ?? product.specs["外形尺寸"] ?? "",
};
});
@@ -360,8 +349,6 @@ export default function Home() {
product.name,
product.code,
product.category,
product.subCategory,
product.sourceCategory,
...Object.keys(product.specs),
...Object.values(product.specs),
]
@@ -442,8 +429,8 @@ export default function Home() {
<a href="#products" className="text-action"></a>
</div>
<div className="trust-row">
<span><b>{CATALOG_PRODUCTS.length}</b> </span>
<span><b>{formatTorque(TORQUE_MIN)}{formatTorque(TORQUE_MAX)}</b> kgf·cm</span>
<span><b>280</b> </span>
<span><b>0.09920</b> kgf·cm</span>
<span><b>48h</b> </span>
</div>
</div>
@@ -505,7 +492,7 @@ export default function Home() {
<div className="question-step">
<p className="step-kicker"> · </p>
<h2></h2>
<p className="step-help">{SELECTOR_PRODUCTS.length}{TORQUE_MIN}{TORQUE_MAX} kgf·cm</p>
<p className="step-help">2430.09920 kgf·cm</p>
<div className="two-fields compact-fields">
<label className="select-field">
<span></span>
@@ -513,7 +500,7 @@ export default function Home() {
value={requirements.category}
onChange={(event) => update("category", event.target.value)}
>
<option value="全部舵机">{SELECTOR_PRODUCTS.length}</option>
<option value="全部舵机">243</option>
<optgroup label="官方产品系列">
{SERVO_CATEGORIES.map((category) => (
<option key={category} value={category}>
@@ -573,11 +560,7 @@ export default function Home() {
value={torqueToSlider(requirements.torque)}
onChange={(event) => update("torque", sliderToTorque(Number(event.target.value)))}
/>
<div className="range-scale">
{[TORQUE_MIN, 1, 10, 100, TORQUE_MAX]
.filter((value, index, values) => value <= TORQUE_MAX && values.indexOf(value) === index)
.map((value) => <span key={value}>{formatTorque(value)}</span>)}
</div>
<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
@@ -837,7 +820,7 @@ export default function Home() {
{match.reasons.slice(0, 2).map((reason) => <li key={reason}> {reason}</li>)}
</ul>
<div className="source-line">
<span></span>
<span></span>
<strong>{match.product.category}</strong>
<small></small>
</div>
@@ -908,9 +891,9 @@ export default function Home() {
<div className="section-heading">
<div>
<p className="eyebrow"><span /> · 2026.06</p>
<h2>{CATALOG_PRODUCTS.length}</h2>
<h2>280</h2>
</div>
<p></p>
<p>280</p>
</div>
<div className="catalog-summary" aria-label="产品分类统计">
@@ -929,7 +912,7 @@ export default function Home() {
</button>
))}
</div>
<p className="catalog-category-note"> {CATALOG_PRODUCTS.length} 齿齿</p>
<p className="catalog-category-note"> · 280 · 11</p>
<div className="catalog-toolbar">
<label className="catalog-search">
@@ -1006,7 +989,7 @@ export default function Home() {
) : (
<span className="catalog-placeholder">GAUDELOT</span>
)}
<small>{product.subCategory || product.category}</small>
<small>{product.category}</small>
<span className="catalog-zoom-hint"></span>
</button>
<div className="catalog-card-body">
@@ -1049,8 +1032,6 @@ export default function Home() {
role="presentation"
onMouseDown={() => setSelectedCatalogProduct(null)}
>
{/* Stop a click inside the dialog from closing the backdrop. */}
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<section
className="catalog-modal"
role="dialog"
@@ -1076,7 +1057,7 @@ export default function Home() {
<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.sourceId} · {selectedCatalogProduct.sourceCategory}</p>
<p className="catalog-source-note"> {selectedCatalogProduct.page} </p>
</div>
</div>
<div className="catalog-spec-table">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Some files were not shown because too many files have changed in this diff Show More