八雷
BALEY 八雷吸顶音箱

BALEY 八雷吸顶音箱

关于我们介绍信息

官网侧适配 CMS 编辑器动态产品/轮播图能力
背景
网站管理器(CMS 编辑器,端口 3002)已实现以下能力,官网(端口 3001)需要相应适配:

编辑器可动态新增/删除产品组:创建 products.items.{slug}.title/category/description/image/overview/specs 等 6 个字段,删除时走硬删除进回收站
编辑器可动态新增/删除轮播图:创建 home.carousel.slides.{n}.eyebrow/title/description/cta/ctaHref/image 等 6 个字段,序号自动递增
产品图片字段类型改为 images(多图):存储格式为 JSON 数组字符串,如 '["/uploads/a.jpg","/uploads/b.jpg"]'
产品标题、分类、描述等均为 CMS 动态内容
官网现状问题
官网多处硬编码,导致编辑器增删产品/轮播图后官网无法正确显示:

文件 硬编码内容 影响
src/app/[locale]/(marketing)/products/product-data.ts PRODUCT_KEYS = ["flagship","amplifier","driver","ceiling"] 新增产品不显示
同上 PRODUCTS 数组固定 4 项 + 写死 categoryKey 新增产品无法分类筛选
同上 PRODUCT_DETAILS 为每个 key 硬编码 specs/views 新增产品详情页无规格
src/app/[locale]/(marketing)/products/[productId]/page.tsx generateStaticParams 只生成 4 个静态路径 新增产品详情页 404
src/app/[locale]/(marketing)/page.tsx 第 63 行 [0,1,2,3].map(...) 轮播图固定 4 张 增删轮播图不生效
CMS 接口数据格式说明
产品内容接口

Plain Text

GET http://localhost:3002/api/cms/content/{siteSlug}/products?locale=zh
返回结构:


JSON

{
"siteSlug": "baley-official",
"page": "products",
"locale": "zh",
"content": {
"products.title": "...",
"products.description": "...",
"products.viewDetail": "...",
"products.items.flagship.title": "旗舰落地音箱",
"products.items.flagship.category": "ceiling",
"products.items.flagship.description": "...",
"products.items.flagship.image": "[\"/uploads/a.jpg\",\"/uploads/b.jpg\"]",
"products.detail.flagship.overview": "...",
"products.specs.flagship": "频率响应|20Hz-22kHz\n灵敏度|89dB\n...",
"products.items.subwoofer.title": "新品低音炮",
...
},
"generatedAt": "..."
}
关键点:

产品 slug 是动态的(flagship/amplifier/driver/ceiling/subwoofer/...),由编辑器用户输入
image 字段是 JSON 数组字符串(多图),需用 pickCmsImages 解析
category 字段值可能是任意字符串(如 ceiling/amplifier/subwoofer)
specs 字段是 label|value\nlabel|value 格式(已有 parseSpecs 函数处理)
首页轮播图接口

Plain Text

GET http://localhost:3002/api/cms/content/{siteSlug}/home?locale=zh
content 中包含 home.carousel.slides.{i}.image/title/description/cta/ctaHref/eyebrow,序号 i 从 0 开始连续递增,数量不固定。

需要实现的功能
功能 1:轮播图动态数量(优先级高,改动小)
文件:src/app/[locale]/(marketing)/page.tsx

改动:第 63 行 [0, 1, 2, 3].map(...) 改为从 CMS 内容动态检测 slide 数量:


TypeScript

// 检测 CMS 中实际存在的轮播图序号
const maxSlides = 20; // 上限保护
const carouselIndices: number[] = [];
for (let i = 0; i < maxSlides; i++) {
const hasImage = pickCmsImage(homeContent, `home.carousel.slides.${i}.image`, "") !== "";
const hasTitle = pickCmsValue(homeContent, `home.carousel.slides.${i}.title`, "") !== "";
if (hasImage || hasTitle) {
carouselIndices.push(i);
}
}
const effectiveIndices = carouselIndices.length > 0 ? carouselIndices : [0, 1, 2, 3];

const carouselSlides: Slide[] = effectiveIndices.map((i) => {
// ... 原有 map 逻辑不变
});
验收:编辑器删除第 3 张轮播图后,官网只显示 3 张;编辑器新增第 5 张,官网显示 5 张。

功能 2:产品列表动态化(优先级高,改动较大)
目标:产品列表页和首页产品展示区从 CMS 动态发现产品,不再依赖硬编码的 PRODUCT_KEYS。

步骤 A:在 src/lib/cms/types.ts 新增工具函数 discoverProductSlugs:


TypeScript

/**
* 从 CMS content 中发现所有产品 slug
* 通过扫描 products.items.{slug}.title 的 key 提取 slug
*/
export const discoverProductSlugs = (content: CmsContentMap): string[] => {
const slugs = new Set<string>();
for (const key of Object.keys(content)) {
const m = key.match(/^products.items.([^.]+).title$/);
if (m && m[1]) {
// 过滤掉空 title(未配置的产品)
const v = content[key];
if (typeof v === "string" && v.trim().length > 0) {
slugs.add(m[1]);
}
}
}
return [...slugs];
};
在 src/lib/cms/index.ts 导出该函数。

步骤 B:修改 src/app/[locale]/(marketing)/products/products-grid.tsx,不再从 PRODUCTS 数组遍历,改为接收动态 slug 列表:


TypeScript

interface ProductsGridProps {
cmsContent?: CmsContentMap;
productSlugs: string[]; // 新增:动态产品 slug 列表
}

const ProductsGrid = ({ cmsContent = {}, productSlugs }: ProductsGridProps) => {
// 用 productSlugs 替代原来的 PRODUCTS 遍历
// category 从 CMS 读取:pickCmsValue(cmsContent, `products.items.${slug}.category`, "other")
// 分类筛选改为动态收集所有 category 值
};
步骤 C:修改 src/app/[locale]/(marketing)/products/page.tsx,调用 discoverProductSlugs 并传给 ProductsGrid:


TypeScript

const content = await getCmsContent({...}, "products", locale);
const productSlugs = discoverProductSlugs(content);
// 传给 ProductsGrid
<ProductsGrid cmsContent={content} productSlugs={productSlugs} />
步骤 D:修改 src/components/global/home-products.tsx,同样用 discoverProductSlugs 替代 PRODUCTS 数组。

步骤 E:分类筛选动态化。CATEGORY_ORDER 改为从 CMS content 动态收集:


TypeScript

// 从所有产品的 category 字段动态收集分类
const categoriesInUse = new Set<string>();
productSlugs.forEach((slug) => {
const cat = pickCmsValue(cmsContent, `products.items.${slug}.category`, "");
if (cat) categoriesInUse.add(cat);
});
分类标签的国际化走 t(\categories.${cat}`)`,未命中则直接显示 cat 原文。

验收:编辑器新增产品 subwoofer 后,官网产品列表页显示该产品;删除某产品后官网不再显示。

功能 3:产品详情页动态路由(优先级高)
文件:src/app/[locale]/(marketing)/products/[productId]/page.tsx

改动 A:generateStaticParams 改为动态:


TypeScript

export async function generateStaticParams() {
const content = await getCmsContent(
{ baseUrl: process.env.CMS_BASE_URL ?? "", siteSlug: process.env.CMS_SITE_SLUG ?? "" },
"products",
"zh"
);
const slugs = discoverProductSlugs(content);
// 兜底:CMS 不可用时返回硬编码 4 个
const finalSlugs = slugs.length > 0 ? slugs : ["flagship", "amplifier", "driver", "ceiling"];
return finalSlugs.map((productId) => ({ productId }));
}
改动 B:resolveProduct 放宽校验,不再限制为固定 4 个 key:


TypeScript

const resolveProduct = (raw: string): string | undefined => {
// 接受任何非空字符串作为产品 slug(实际是否存在由 CMS content 决定)
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
};
改动 C:PRODUCT_DETAILS 改为兜底数据。当 CMS 没有某产品的 specs 时,用通用占位:


TypeScript

// 保留 PRODUCT_DETAILS 作为兜底(specs/overview 的 fallback)
// 但 views 改为根据 CMS 图片数量动态决定
const productImages = pickCmsImages(content, `products.items.${productId}.image`);
const views = productImages.length > 0
? productImages.map((_, i) => `视图 ${i + 1}`)
: ["正面", "侧面", "顶部"]; // 兜底
验收:编辑器新增产品 subwoofer 后,访问 /zh/products/subwoofer 能正常显示详情页(不 404)。

功能 4:多图显示(已完成部分,需全面应用)
现状:pickCmsImage 已改为调用 pickCmsImages 取首张(上一轮已改),产品详情页 page.tsx 已用 pickCmsImages。

待确认:产品列表卡片 product-card.tsx 和首页 home-products.tsx 是否也需要显示多图?目前列表卡片只显示单张封面,符合列表场景,可保持不变。

兜底策略
所有动态化改动必须保证 CMS 不可用时官网仍能正常工作:

discoverProductSlugs 返回空数组时,回退到硬编码的 ["flagship","amplifier","driver","ceiling"]
pickCmsValue / pickCmsImage 的 fallback 参数保持原有 messages 翻译值
PRODUCTS / PRODUCT_DETAILS / CATEGORY_ORDER 常量保留,作为兜底数据源
验收命令

PowerShell

pnpm exec tsc --noEmit
pnpm lint
pnpm build
验收场景
编辑器新增产品 subwoofer(slug + 中文名 + 图片 + 分类),发布后:
官网产品列表页显示该产品卡片
官网首页产品展示区显示该产品
访问 /zh/products/subwoofer 显示详情页(含多图轮播)
分类筛选器出现 subwoofer 所属分类
编辑器删除产品 ceiling,发布后:
官网产品列表不再显示 ceiling
访问 /zh/products/ceiling 返回 404
编辑器新增第 5 张轮播图,发布后官网首页显示 5 张
编辑器删除第 3 张轮播图,发布后官网首页显示 3 张(序号不重排,按实际存在的显示)
CMS 服务不可用时,官网仍正常显示(用硬编码兜底数据)
注意事项
不要破坏现有的 ISR 缓存机制(revalidateTag("cms-content"))
generateStaticParams 在 build 时执行,需保证 CMS 可达或兜底数据有效
产品图片用 unoptimized 属性(已配置,避免 Next.js 图片压缩导致模糊)
多图字段用 pickCmsImages(返回 string[]),单图字段用 pickCmsImage(返回首张 string)
官网工作目录:D:\BALEY 八雷\10-官网\自己写\linkify-main
官网 dev 启动:pnpm dev -- -p 3001(注意:不要在 dev 运行时删除 .next 目录)