feat(admin): product families api client and types
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# 产品族二期(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 Plus(auto-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-table(familyCode/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。
|
||||
Reference in New Issue
Block a user