Files
inkreach-official-website/plans/feature/product-family-admin-feature.md
T

148 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 产品族二期(admin 前端)实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** admin 后台落地产品族管理界面:新增「产品族」标签页(列表/详情/成员管理/自定义成员/人工改价/自动成族),GoodsView 原产品树接入族信息。
**Architecture:** 新建 `views/product-family/` 目录按职责拆组件(不复制 GoodsView 巨型单文件模式);API 走新增 `src/api/product-families.ts`;GoodsView 仅做两处增量(兄弟勾选按族匹配、树节点族徽标)。依赖一期后端 `/product-families/*` 端点(已上线 develop)。
**Tech Stack:** Vue 3 `<script setup>` + Element Plusauto-import+ vitest(纯逻辑 TDD;仓库无 @vue/test-utils,组件以 vue-tsc + vite build 验证)。
**范围约定:** Good 配置表单的 familyId 字段属三期(后端 `Good.familyId` 未上线),本期不做;现有 Good 级合并(mergedOriginGoodIds)保持原样。
---
### Task 1: 类型与 API client
**Files:**
- Modify: `apps/admin/src/types/index.ts`OriginGoodsTreeNode + 族字段;新增 ProductFamily 系列)
- Create: `apps/admin/src/api/product-families.ts`
- [ ] types`OriginGoodsTreeNode` 增加 `familyId: string | null; familyName: string | null; familyCode: string | null; familyStale: boolean | null`;新增:
```ts
export interface ProductFamilyMember {
id: string; sdsGoodId: string; goodName: string; goodImage: string | null
source: 'SDS' | 'CUSTOM'; delisted: boolean
skuCode: string | null; logisticsLabel: string | null; craftLabel: string | null; warehouseLabel: string | null
}
export interface FamilyPriceOverrideRow {
id: string; sizeId: string; colorId: string; craft: string; logistics: string
price: string; note: string | null; derivedPrice: string | null; diff: string | null
}
export interface ProductFamily {
id: string; familyCode: string | null; familyName: string; familyImage: string | null
countryId: string | null; categoryId: string | null; primaryOriginGoodId: string | null
autoManaged: boolean; stale: boolean
detail: Record<string, unknown> | null
sizeChart: { rows?: Array<Record<string, unknown>> } | null
packageSpecs: { rows?: Array<Record<string, unknown>> } | null
priceMatrix: {
sizes: Array<{ key: string; name: string | null }>
colors: Array<{ key: string; name: string | null; hex: string | null; imageUrl: string | null }>
crafts: string[]; logistics: string[]
rows: Array<{ sizeId: string; sizeName: string | null; colorId: string; colorName: string | null
craft: string; logistics: string; price: string; manual: boolean
sources: Array<{ sdsGoodId: string; sdsVariantId: string; price: string }> }>
} | null
originGoods: ProductFamilyMember[]
priceOverrides: FamilyPriceOverrideRow[]
_count: { originGoods: number; priceOverrides: number }
}
export interface AutoGroupPreviewGroup {
groupKey: string; familyName: string; familyCode: string | null
memberCount: number; sampleNames: string[]
}
export interface CustomMemberVariantInput { sku: string; sizeId?: string | null; sizeName?: string | null
colorId?: string | null; colorName?: string | null; colorHex?: string | null; imageUrl?: string | null; price: number }
```
- [ ] `src/api/product-families.ts`
```ts
import request from './request'
import type { PaginatedResult, ProductFamily, AutoGroupPreviewGroup } from '@/types'
export const productFamiliesApi = {
list: (params: { keyword?: string; page?: number; pageSize?: number }) =>
request.get<any, PaginatedResult<ProductFamily>>('/product-families', { params }),
detail: (id: string) => request.get<any, ProductFamily>(`/product-families/${id}`),
create: (data: { familyName: string; familyCode?: string; familyImage?: string; originGoodIds?: string[]; primaryOriginGoodId?: string }) =>
request.post<any, ProductFamily>('/product-families', data),
patch: (id: string, data: Record<string, unknown>) =>
request.patch<any, ProductFamily>(`/product-families/${id}`, data),
autoGroup: (apply: boolean) =>
request.post<any, { applied: number; groups: AutoGroupPreviewGroup[] }>('/product-families/auto-group', { apply }),
updateMembers: (id: string, data: { addOriginGoodIds?: string[]; removeOriginGoodIds?: string[] }) =>
request.post<any, ProductFamily>(`/product-families/${id}/members`, data),
createCustomMember: (id: string, data: Record<string, unknown>) =>
request.post<any, unknown>(`/product-families/${id}/members/custom`, data),
recompute: (id: string) => request.post<any, ProductFamily>(`/product-families/${id}/recompute`),
listOverrides: (id: string) =>
request.get<any, { items: FamilyPriceOverrideRow[] }>(`/product-families/${id}/price-overrides`),
putOverrides: (id: string, items: Array<Record<string, unknown>>) =>
request.put<any, { items: FamilyPriceOverrideRow[] }>(`/product-families/${id}/price-overrides`, { items }),
deleteOverrides: (id: string, cells: Array<Record<string, unknown>>) =>
request.delete<any, { items: FamilyPriceOverrideRow[] }>(`/product-families/${id}/price-overrides`, { data: { cells } }),
}
```
`request` 封装与 goods.ts 一致;delete 带 body 用 `{ data }`。)
- [ ] `pnpm --filter @inkreach/admin test` 通过(现有 spec 不受影响)→ Commit `feat(admin): product families api client and types`
### Task 2: 族匹配工具(TDD
**Files:**
- Create: `apps/admin/src/utils/family-match.ts` + `family-match.spec.ts`
- [ ] 先写失败测试:`sameFamily(a, b)` —— 两节点 `familyId` 相等且非空 → true;任一 familyId 为空 → 回退 `sameOriginGroup(goodName)`;均空 false。
- [ ] 实现(3 行逻辑,复用 `sameOriginGroup`)。
- [ ] 测试通过 → Commit `feat(admin): family match util`
### Task 3: 产品族标签页组件
**Files:**
- Create: `apps/admin/src/views/product-family/FamilyView.vue`
- Create: `apps/admin/src/views/product-family/FamilyDetailDrawer.vue`
- Create: `apps/admin/src/views/product-family/AutoGroupDialog.vue`
- Create: `apps/admin/src/views/product-family/PriceOverridePanel.vue`
- Create: `apps/admin/src/views/product-family/CustomMemberDialog.vue`
- Modify: `apps/admin/src/views/product-management/ProductManagementView.vue`tabs 增加 `{ name: 'families', label: '产品族', comp: FamilyView }`icon 用 `Files`
- [ ] **FamilyView**:顶部工具栏(关键词搜索、`自动成族`按钮开 AutoGroupDialog、`新建族`简易弹窗)+ el-tablefamilyCode/familyName/成员数/覆盖数/autoManaged 标签/stale 红点/更新时间/操作列 `详情`+ el-pagination;行点击开 FamilyDetailDrawer`@reload` 重新拉列表)。
- [ ] **FamilyDetailDrawer**el-drawer size 70%):
- 头部:familyName/familyCode/stale 标记/autoManaged 开关(`patch`/`重算`按钮(`recompute`);
- canonical 编辑:名称/编码/主图(复用 ImageUpload)/主链接(成员下拉选择)→ `patch`
- 成员表:goodName/物流/工艺/仓库/来源/操作`移除``updateMembers remove`);
- 添加成员:输入 origin_good_id 列表(el-select remote 搜索用 `originGoodsApi.getOriginGoodsList` keyword)→ `updateMembers add`
- `新建自定义成员`按钮开 CustomMemberDialog`价格管理`折叠区嵌入 PriceOverridePanel
- 矩阵摘要:sizes/crafts/logistics 数量 + 起价(rows 最低 price)。
- [ ] **AutoGroupDialog**:打开先 `autoGroup(false)` 预览(表格:族名/编码/成员数/示例名);`确认建族`按钮调 `autoGroup(true)``@done` 刷新。
- [ ] **PriceOverridePanel**:按 (craft × logistics) el-tabs;表格行=尺码、列=颜色(或平铺行),单元格显示矩阵价 + manual 高亮 + 行内编辑(el-input-number)→ 失焦 `putOverrides` 单格;顶部已设覆盖列表(推导价/差额/删除恢复)。
- [ ] **CustomMemberDialog**:表单 goodName/goodImage/物流*/工艺*/skuCode/仓库 + 变体动态行(sku/sizeId/sizeName/colorId/colorName/price)→ `createCustomMember`
- [ ] `pnpm --filter @inkreach/admin build`vue-tsc + vite)通过。
- [ ] Commit `feat(admin): product family management tab`
### Task 4: GoodsView 接入族信息
**Files:**
- Modify: `apps/admin/src/views/goods/GoodsView.vue`
- [ ] L411 兄弟自动勾选:`sameOriginGroup(...)` 换成 `sameFamily(ogNode, s)`(familyId 优先、名称回退)——`collectSiblings` 收集的节点已带 familyId 字段(tree 接口已返回)。
- [ ] 右侧树节点 label`familyCode` 存在时以 el-tag 小徽标展示在名称后;`familyStale` 时加橙点提示。
- [ ] 构建通过 + `pnpm --filter @inkreach/admin test` 全绿。
- [ ] Commit `feat(admin): goods view family badges and sibling matching`
### Task 5: 验证与文档
- [ ] `pnpm --filter @inkreach/admin test``pnpm --filter @inkreach/admin build``pnpm --filter @inkreach/api test` 全绿。
- [ ] 起后端 + admin dev,浏览器冒烟(登录 → 产品族 tab → 列表 → 详情 → 自动成族预览 → 改价)——如环境不可用则以 curl 走 vite 代理替代。
- [ ] 更新 `docs/references/structs.md`(admin 章节目录树 + 新页面)与 `docs/references/product-center.md`(后台操作路径)。
- [ ] Commit `docs: admin product family references`
## Self-Review
- 设计 §11.2 四组件全覆盖(FamilyList 并入 FamilyView 表格,粒度合并不改语义);§11.3 两处增量落地(Good 表单 familyId 明确延后三期并在范围约定声明);§11.4 client ✓。
- 组件无测试设施属仓库现状,以 vue-tsc/build 兜底并在计划头部声明;纯逻辑(family-match)走 TDD。