按用户反馈调整上一批优化: - 左树撤回族组展开层(虚线层+重复名称行):改为一族只显示一行(排序最前的 代表商品),行内「N 个配置」徽标悬浮列出该族全部配置(名称/国家·分类/打开 按钮),不再有额外层级与重复名称 - 右树恢复配置入口:撤回「族已配置拦截」,本链接未单独建商品即显示「配置」 按钮(含族已配置情形,供调整归族/重新配置),拖拽路径同步解除拦截 - 国家模式拖拽排序回退为分组内简单排序(族组节点已不存在) - 文档同步更新徽标/入口语义 验证:admin typecheck + 22/22 + 构建绿;浏览器实测美国分组 25 行一族一行、 「8 个配置」徽标悬浮列表可打开、国内工厂深层成族链接显示「配置」按钮
212 lines
15 KiB
Markdown
212 lines
15 KiB
Markdown
# 产品中心
|
||
|
||
官网产品中心位于 `apps/website/app/pages/product-center.vue`,访问路径为 `/product-center`。
|
||
|
||
## 默认展示
|
||
|
||
页面首次加载时展示全部商品和全部国家,不默认选择分类、物流或工艺标签。每页默认请求 20 条商品,用户操作筛选项后再按真实业务 ID 查询。
|
||
|
||
## 筛选与分页
|
||
|
||
- 左侧分类树:选择父分类或子分类。
|
||
- 国家:点击国家胶囊筛选,点击“全部”取消国家筛选。
|
||
- 物流与工艺:按标签组展示下拉选项,每个分组内暂为单选,不同分组可各选择一项。
|
||
- 筛选菜单在选择选项或点击组件外部后自动关闭;已选 Tag 使用独立标签行。仅选择工艺时标签从最左侧开始;物流与工艺同时选择时按物流、工艺顺序与上方筛选项对应排列。
|
||
- 搜索:输入关键词后按回车或点击“搜索”。
|
||
- 商品卡:整卡链接到 `https://inkpod.vip/portal/detail/{商品ID}`,在新窗口打开。
|
||
- 公共商品接口的 `id` 来自 `originGood.sdsGoodId`,即 InkPOD 商品 ID;`goods.good_id` 仅作为本站数据库内部主键使用。
|
||
- URL 国家筛选:支持 `/product-center?countryId={后台国家ID}`,仅接受后台已配置的国家 ID。
|
||
- 无结果:展示设计稿空状态,可通过“查看全部商品”或“清空筛选”恢复完整列表。
|
||
- 加载中:展示两行共八个商品骨架卡,筛选栏保持可操作。
|
||
- 分页:支持页码、前后翻页、每页数量和页码跳转。
|
||
|
||
所有查询通过 `useProductCenter.ts` 调用 `/api/backend/*` 代理路由,不在页面中硬编码业务 ID。
|
||
|
||
## 后台配置联动
|
||
|
||
- 品类树、国家、Tag 与 Tag Group 均来自 NestJS `/public/*` 接口。
|
||
- 国家旗帜读取 `countryIcon`,品类图标读取 `categoryIcon`;前端不提供外部旗帜 CDN 或固定名称兜底。
|
||
- `/uploads/*` 与 `/assets/*` 等后台相对资源地址由 `useProductCenter.ts` 根据 `runtimeConfig.public.backendUrl` 转为完整 URL。
|
||
- 物流和工艺筛选分别由后台名称包含“物流”和“工艺”的 Tag Group 生成;“印刷位置”等其他分组仍可用于商品卡片标签,但不会自动成为顶部筛选项。
|
||
- Figma 基准图标位于 `apps/api/public/product-center/`,通过后端 `/assets/product-center/*` 提供。
|
||
- 更新现有数据库图标配置:`pnpm --filter @inkreach/api configure:product-center-icons`。脚本按中文名称幂等更新,不创建缺失的国家或品类。
|
||
|
||
## 响应式
|
||
|
||
桌面采用 240px 分类栏,页面左右保留 `24–56px` 自适应留白,分类栏可滚动但隐藏可见 scrollbar。商品网格以 240px 为最小卡片宽度自动增加或减少列数:1440px 基准宽度保持四列,1920px 可显示六列。国家筛选区按内容自然增高,不保留固定空白。
|
||
|
||
左侧品类栏不显示最左侧边线,一级品类之间保留 10px 间距;国家筛选区及物流、工艺、搜索工具区的底部分隔线从内容区向右延伸至页面边缘。
|
||
|
||
宽度低于 1000px 后分类栏变为抽屉;移动端商品网格降为两列或单列,国家筛选仅在自身区域横向滚动,不会撑宽页面。
|
||
|
||
## 多源合并商品(已废弃,由产品族替代)
|
||
|
||
> **2026-08-28 起,该机制已被「产品族」完全替代**:多链接合并由族承载(尺码/包装并集 +
|
||
> 五维价格矩阵),配置/编辑商品不再写入 `mergedOriginGoodIds`;`good_origin_goods` 表
|
||
> 只读保留(历史数据仍可命中详情),观察期后删除。以下为历史行为记录:
|
||
|
||
- 数据层:主源存 `goods.origin_good_id`,副源存中间表 `good_origin_goods`。
|
||
- 详情可达性:官网上通过**任一**关联原产品的 `sdsGoodId` 都能访问到该商品详情(族机制下
|
||
同样成立:族内任何成员链接的 sdsGoodId 均命中同一商品)。
|
||
|
||
> 该 Good 级合并能力保留可用;新一级的「产品族(SPU)合并」见下节,公开读路径接入族数据属于三期范围。
|
||
|
||
## 产品族(SPU 层,一期后端已上线)
|
||
|
||
设计文档:`docs/superpowers/specs/2026-08-28-product-family-merge-design.md`。
|
||
|
||
**模型**:SDS 叶子分类即产品模型(如分类 `DG001 180G纯棉T恤(JSA002)` 下 16 条链接),
|
||
族 = 同分类链接的合并体;链接名解析列(`国家(物流)品名-SKU-工艺[-仓库]`)提供
|
||
物流/工艺归因。例:DG001 族 = 16 链接 × 3 工艺 × 3 物流 × 9 尺码(S~XXXXXL 并集)× 20 颜色 ≈ 470 格价格矩阵,起价取光板(不打印)最低价。
|
||
|
||
**价格**:`价格 = f(尺码, 颜色, 物流, 工艺[含印花数量])`,全部从成员链接变体推导透传
|
||
(同格子多仓库取最低价,来源全保留);`family_price_overrides` 表支持按格人工改价,
|
||
删覆盖即恢复推导价。
|
||
|
||
**关键端点用法示例**(JWT):
|
||
|
||
```bash
|
||
# 1. 自动成族:先预览
|
||
curl -X POST /product-families/auto-group -H "Authorization: Bearer $T" \
|
||
-d '{"apply": false}'
|
||
# → { applied: 0, groups: [{ groupKey: "cat:8240", familyName: "DG001 180G纯棉T恤(JSA002)", memberCount: 16, ... }] }
|
||
|
||
# 2. 应用(幂等,可重复执行)
|
||
curl -X POST /product-families/auto-group -H "Authorization: Bearer $T" -d '{"apply": true}'
|
||
|
||
# 3. 族详情:成员 + 并集尺码表/包装 + 五维价格矩阵
|
||
curl /product-families/12 -H "Authorization: Bearer $T"
|
||
|
||
# 4. 人工改价(维度必须存在于族矩阵选项,否则 400 并列出非法键)
|
||
curl -X PUT /product-families/12/price-overrides -H "Authorization: Bearer $T" \
|
||
-d '{"items": [{"sizeId":"size_S","colorId":"color_blk","craft":"单面印花","logistics":"包邮","price": 23, "note": "促销"}]}'
|
||
|
||
# 5. 删覆盖恢复推导价
|
||
curl -X DELETE /product-families/12/price-overrides -H "Authorization: Bearer $T" \
|
||
-d '{"cells": [{"sizeId":"size_S","colorId":"color_blk","craft":"单面印花","logistics":"包邮"}]}'
|
||
|
||
# 6. 族内创建自定义商品(人工补链接,物流/工艺归因必填)
|
||
curl -X POST /product-families/12/members/custom -H "Authorization: Bearer $T" \
|
||
-d '{"goodName":"美国(海运)180g纯棉T恤-DG001-烫画","logisticsLabel":"海运","craftLabel":"烫画",
|
||
"variants":[{"sku":"C-M","sizeId":"size_M","sizeName":"M","colorId":"color_red","colorName":"红色","price": 33}]}'
|
||
```
|
||
|
||
**同步联动**:商品同步落库时刷新解析列;新链接与已有族**同分类唯一命中**时自动挂族
|
||
(多族/零族留给管理员;锁定族只置 `stale`);详情同步提交后异步重算受影响族
|
||
(进程内去重、幂等)。回填/修复脚本:
|
||
|
||
```bash
|
||
pnpm --filter @inkreach/api backfill:product-families
|
||
# → [1/2] 解析全部链接名 [2/2] 自动建族 [3/3] 全量族重算(幂等,可随时重跑)
|
||
```
|
||
|
||
**公开读路径(四期族化契约,默认开启)**:`PUBLIC_DETAIL_FROM_FAMILY=false` 可应急回退旧行为。
|
||
公开端点**以款(族)为一等公民**:
|
||
|
||
- `GET /public/goods`:**一族一条**(族内多条 Good 去重,代表行=排序第一条),
|
||
`goodId` = **族 ID**,`total` = 族数,分页作用于分组后;`price` = 族矩阵最低价(起价),
|
||
价格排序按族起价重排;**无族商品(自定义)完全不返回**(列表/首页/分类树/标签统计同理)。
|
||
- `GET /public/goods/:goodId`:`goodId` 即**族 ID**(唯一公开寻址键;旧 SDS 链接 ID 与
|
||
自定义商品 sdsGoodId 均 404)。响应为款级聚合:
|
||
|
||
- 公共信息取代表 Good(名称/主图/国家/分类/标签)+ 主链接(详情文案);
|
||
- `variants` = 全体族成员 ∪ 旧副源(过渡期),按 `(链接, 变体)` 再按 `颜色+尺码` 去重;
|
||
`mediaByColor` 同源;`options/media` 并入族成员 detail;
|
||
- `sizeChart` / `packageSpecs` = 族物化并集(空并集回退主链接合并结果);
|
||
- **族块**(默认输出):`family` 含并集尺码表/包装 + **严格五维价格矩阵** + 族起价:
|
||
|
||
```jsonc
|
||
{
|
||
"family": {
|
||
"familyId": "12", "familyCode": "DG015", "familyName": "DG015 180G纯棉T恤",
|
||
"sizes": [...], "colors": [...],
|
||
"printCounts": ["单面印花", "双面印花"], "crafts": ["烫画", "直喷", "不打印"], "logistics": ["包邮", "不包邮"],
|
||
"sizeChart": { /* 并集 */ }, "packageSpecs": { /* 并集 */ },
|
||
"priceMatrix": { /* rows 为 {sizeId, colorId, printCount, craft, logistics, price, manual, sources} */ },
|
||
"minPrice": "29.5"
|
||
}
|
||
}
|
||
```
|
||
|
||
矩阵维度来源是**链接级标签**(印花数量/印刷工艺/物流渠道三组;人工接管后按人工标签),
|
||
不再用原始链接名的第 3 段(`craftLabel` 含「直喷单面/光板不打印」等噪声)。名称派生规则已
|
||
覆盖裸「单面/双面」写法(如「直喷双面」→ 双面印花 + 直喷)。光板/不打印链接无印花面概念,
|
||
矩阵中印花数量维回退「单面印花」占位。前端本地按五维联动 `priceMatrix` 即可实时算价
|
||
(设计 D4)。
|
||
|
||
**边界声明**:价格区间筛选(minPrice/maxPrice)仍作用于链接 `goodPrice`(族内任一链接命中即
|
||
返回该族;SQL 层无法廉价解析族矩阵 JSON);`good_origin_goods` 转只读保留,观察期后另行删除。
|
||
|
||
**后台操作入口(族替代旧主源/副源,界面保持原有布局)**:
|
||
|
||
- 商品配置页布局不变(左树=官网商品、右树=原产品库分类平铺);配置弹窗保持原「合并同名」
|
||
勾选流程,提交时**静默**把勾选链接与主链接归入同一族(无族自动成族);族内**不区分主次**
|
||
(成员列表无设为主链接按钮,主链接仅作为详情数据源的内部实现);
|
||
- 编辑弹窗的「**关联原产品**」区块:显示族编码与链接数,成员行显示**原始链接名**(便于核对)
|
||
并可**逐个展开**——查看成员详情(SDS ID / 链接价格 / SKU 数 / 物流备注 / 工艺位置 / 仓库)、
|
||
**配置该链接的标签**(保存后人工接管;「恢复自动」回到按名称派生)、
|
||
**改价格**(尺码/颜色/价格 可编辑表格,写入族价格覆盖)、
|
||
**查看成员自己的详情**(商品详情 / 尺码表 / 包装规格 / SKU 四个页签,懒加载
|
||
`GET /origin-goods/:id`,SKU 表带 物流/工艺/印花数量 维度列;未同步时可在面板内直接同步);
|
||
SDS 商品不再在弹窗底部展示单一链接的旧详情区(自定义商品的编辑表单仍保留在底部);
|
||
- 成员列表下方有**族级并集视图**页签:尺码表 / 包装规格(族物化并集)与
|
||
**SKU(全族动态合并)**——SKU 即价格矩阵行,各链接变体按 物流×工艺×印花数量×尺码×颜色
|
||
归并(同组合取最低价,「链接数」列显示来源数,人工价带标签),无需额外请求;
|
||
- 编辑弹窗商品「名称」预填解析后的「品名 型号」,保存即采用解析名;右侧原产品库与配置弹窗
|
||
始终显示**原始链接名**;
|
||
- 配置弹窗「合并同名」候选为**全局同款链接**(按名称 3 段组键跨分类匹配,
|
||
含已配置链接),支持搜索过滤。
|
||
- 右树徽标语义(**族感知**,公开契约一族一款):**已配置 N**(绿,本链接已配置为官网商品,
|
||
N=国数)/ **已配置**(绿,本链接未单独建商品但所属族已配置)/ **成族**(蓝,已归族且全族
|
||
未配置)/ **未配置**(橙);「定位到官网商品」在自身或同族已配置时出现(按 主链接 → 同族 →
|
||
旧副源顺序定位并高亮),「配置」在本链接未单独建商品时可用(含族已配置的情形,供调整归族;
|
||
注意重复配置会产生官网不可见的冗余商品);
|
||
- 族分组头部计数与「仅未配置」筛选同样按族语义(族已配置 → 计入已配置)。
|
||
- **左树一族一行**(国家/品类模式):同款多条配置只显示排序最前的一条,名称旁
|
||
「N 个配置」徽标悬浮列出其余配置(可打开编辑),分组头部计数仍按全部商品统计。
|
||
- 人工改价/自动成族等族管理 API(`/product-families/*`)保留,供脚本或后续界面使用;
|
||
- 页面组件化:`GoodsView.vue` 拆出 `components/` 下的 GoodsEditDialog(编辑弹窗)、
|
||
GoodsConfigDialog(配置弹窗)、CustomGoodDialog(自定义商品)、TagFilterPopover(标签筛选弹层)。
|
||
|
||
> 注意:历史上每族多点「配置」会产生多个同名的官网商品(重复项可手动删除)。现在族已配置后
|
||
> 「配置」入口会被拦截;存量重复商品(如早期 DG120/AUTM003 测试期间产生的)仍建议清理。
|
||
|
||
**链接级标签(自动派生 + 人工修正,2026-08 规则改版)**:
|
||
|
||
- 标签与**产品链接一一对应**(每条链接因印花数量/工艺/物流不同而价格不同),
|
||
落库在链接级 `origin_good_tags` 表(`manual` 标记人工/派生);商品的自动组标签是其
|
||
**主链接标签的镜像**;
|
||
- 解析规则(`apps/api/src/product-families/auto-tag-rules.ts`,与 admin 端
|
||
`utils/origin-name.ts#deriveLinkTagNames` 同构):
|
||
- **印花数量**:名称含「双面印花」→ `双面印花`;否则含「单面印花」→ `单面印花`(组「印花数量」);
|
||
- **工艺**:名称含「直喷」→ `直喷`;含「不打印」或「光板」→ `不打印`(**光板即为不打印**,
|
||
组「印刷工艺」);都不含 → 默认 `烫画`;
|
||
- **物流**:含「不包邮」→ `不包邮`;否则含「包邮」→ `包邮`(组「物流渠道」,先判不包邮防子串误命中);
|
||
- 每次族重算/成员变更/商品创建更新时:未接管的 SDS 链接按名称刷新派生行;
|
||
**人工接管的链接(`tagsManual=true`)永不被覆盖**;商品镜像其链接的有效标签;
|
||
缺失的组与标签自动补建;
|
||
- **人工修正入口**:编辑弹窗成员行展开 → 修改标签 → 保存标签(全量替换为人工行,
|
||
`PUT /origin-goods/:id/tags`);「恢复自动」清掉人工行回到派生(`DELETE /origin-goods/:id/tags`);
|
||
- 其他分组(如风格类)不受影响,保持人工管理;自定义链接(无名称可解析)同样支持人工配置;
|
||
- 实测:`美国(包邮)…-DG001-单面印花` → `包邮 / 烫画 / 单面印花`;
|
||
`美国(不包邮光板)…-JSA002-不打印` → `不包邮 / 不打印 / 光板`;
|
||
`美国(不包邮)…-DG501-双面印花` → `不包邮 / 烫画 / 双面印花`。
|
||
|
||
**链接名称展示解析**:后台所有链接名展示位(左右树、族成员列表、配置/编辑弹窗、搜索候选)
|
||
统一解析为「品名 型号」(如 `美国(包邮)180g纯棉T恤成人款-DG001-单面印花` →
|
||
`180g纯棉T恤成人款 DG001`),悬停 tooltip 保留原始全名;搜索同时匹配原始名与解析名
|
||
(`utils/origin-name.ts#cleanLinkName`)。
|
||
|
||
|
||
|
||
|
||
## 验证
|
||
|
||
```bash
|
||
pnpm --filter @inkreach/website test --run
|
||
pnpm --filter @inkreach/website build
|
||
pnpm --filter @inkreach/website dev
|
||
pnpm --filter @inkreach/api test -- --runInBand
|
||
pnpm --filter @inkreach/api build
|
||
```
|