# 商品 SKU 选择器设计 ## 概述 在商品详情页添加 SKU(规格)选择功能,让用户在加购或下单前选择商品规格。后端和购物车/结算链路已支持 SKU,主要工作在前端。 ## 涉及文件 | 文件 | 改动 | |------|------| | `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue` | 新增 SKU 选择器 UI + 修改加购/购买逻辑 | | `cfc-frontend/pages/shop/cart/cart.vue` | 结算时传递 skuId + specDesc | | `cfc-frontend/utils/api.js` | 已有 `productSpecMap`,无需新增 | ## 数据流 ### 加载时 ``` loadDetail(id) ├── productDetail({ id }) → 商品基本信息 └── productSpecMap({ productIds: [id] }) → 规格分组 ``` ### 规格选择时 ``` 用户选择规格选项 → 记录 selectedSpecs: { groupId → optionId } → 遍历 SKU 列表匹配当前选择组合 → 找到匹配 SKU → 更新价格/库存/图片 → 未完全选择 → 提示"请选择完整规格" ``` ### 提交时 ``` onAddToCart() → cartAdd({ userId, productId, skuId, quantity: 1 }) onBuy() → checkoutItems = [{ productId, productName, coverImage, skuId, specDesc, unitPrice, quantity }] → 存入 storage → 跳转 checkout goCheckout() (cart.vue) → checkoutItems = [{ ..., skuId, specDesc }] → 存入 storage → 跳转 checkout ``` ## 数据结构 ### 从 specMap 接口获取 ```js // /api/product/spec/map // 入参: { productIds: [123] } // 出参: { "123": [ ...groups ] } specGroups: [ { groupId: 1, groupName: '颜色', options: [ { id: 101, name: '红色', skuId: 1, price: 10000, stock: 10, enabled: 1 }, { id: 102, name: '蓝色', skuId: 2, price: 12000, stock: 5, enabled: 1 } ] }, { groupId: 2, groupName: '尺码', options: [ { id: 201, name: 'M', skuId: 1, price: 10000, stock: 10, enabled: 1 }, { id: 202, name: 'L', skuId: 2, price: 12000, stock: 5, enabled: 1 } ] } ] ``` ### 组件状态 ```js data() { return { product: {}, specGroups: [], // 从 specMap 获取 selectedSpecs: {}, // { groupId: optionId } selectedSku: null, // 匹配到的完整 SKU skuList: [], // 从 specMap 解析出的所有 SKU 映射 isMember: false // 会员状态 } } ``` ### SKU 匹配逻辑 ```js // 从 specMap 数据中构建 SKU 映射 // 每个 option 都带有 skuId,同一 skuId 跨 group 出现 // 当选完所有组后,任一选项的 skuId 即为匹配结果 matchSku: function() { var groups = this.specGroups var selected = this.selectedSpecs // 检查是否所有组都已选 for (var i = 0; i < groups.length; i++) { if (!selected[groups[i].groupId]) return null } // 取第一个选中选项的 skuId(同组所有选项的 skuId 指向同一 SKU) var firstGroup = groups[0] var firstOptionId = selected[firstGroup.groupId] var skuId = null for (var j = 0; j < firstGroup.options.length; j++) { if (firstGroup.options[j].id === firstOptionId) { skuId = firstGroup.options[j].skuId break } } return skuId } ``` ## UI 设计 ### 页面布局 ``` ┌─────────────────────────────────┐ │ [轮播图] │ ├─────────────────────────────────┤ │ 商品名称 │ │ ¥100.00 │ ← 主价格(大号橙色) │ 会员价 ¥80.00 → 点击升级 │ ← 非会员: 小字灰色,可点击 │ ─ 或 ─ │ │ ¥80.00 │ ← 会员: 主价格显示会员价 │ ¥100.00 │ ← 会员: 原价带删除线 │ │ │ 库存 50 件 │ │ 供应商:XXX │ ├─────────────────────────────────┤ │ 颜色 │ ← 规格组名 │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ ■红色 │ │ 蓝色 │ │ 黑色 │ │ ← 选中态/未选态/不可选态 │ └──────┘ └──────┘ └──────┘ │ │ │ │ 尺码 │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ S │ │ ■M │ │ L │ │ │ └──────┘ └──────┘ └──────┘ │ │ │ │ 已选: 红色 / M │ ├─────────────────────────────────┤ │ 商品介绍... │ ├─────────────────────────────────┤ │ 商品详情... │ ├─────────────────────────────────┤ │ ¥100.00 [🖼][加入购物车][立即购买] │ └─────────────────────────────────┘ ``` ### 规格按钮状态 | 状态 | 样式 | 条件 | |------|------|------| | 未选 | 白底灰边框 #ddd | 默认 | | 选中 | 橙底白字 #F97316 | 已点击选中 | | 不可选 | 灰色+删除线 | 对应 SKU 库存为 0 或未启用 | ### 价格联动 - **无 SKU 的商品**:不显示 SKU 选择器,保持现有行为 - **有 SKU 但未选完整**:底部栏显示"请选择完整规格",按钮禁用 - **有 SKU 且已选完整**:显示 SKU 价格,按钮启用 - **非会员**:主价格显示原价,小字显示会员价(可点击跳转 `/pages/membership/upgrade`) - **会员**:主价格显示会员价,小字显示原价(带删除线) ## 修改点汇总 ### 1. product-detail.vue **新增导入:** `productSpecMap`, `getMyMembership` **新增 data:** - `specGroups: []` - `selectedSpecs: {}` - `selectedSku: null` - `isMember: false` **loadDetail 中补充:** 调用 `productSpecMap` 获取规格数据 **新增模板:** 在 `info-section` 和 `intro-section` 之间插入规格选择区域 **修改 onAddToCart:** 传 `skuId` **修改 onBuy:** 传 `skuId` 和 `specDesc` **新增方法:** - `selectSpec(groupId, option)` — 选择规格 - `matchSku()` — 匹配当前选择的 SKU - `buildSpecDesc()` — 构建规格描述文本 - `checkMemberStatus()` — 检查会员状态 ### 2. cart.vue **修改 goCheckout:** 传 `skuId` 和 `specDesc` 到 `checkoutItems` ## 边界情况 | 场景 | 行为 | |------|------| | 商品无 SKU(specMap 返回空) | 不显示 SKU 选择器,保持现有行为 | | 部分规格选项库存为 0 | 该选项显示为灰色不可选 | | 选择组合后无对应 SKU | 提示"该规格组合暂无库存" | | 用户未选规格直接点加购 | 提示"请选择完整规格" | | 非会员查看会员价 | 会员价小字显示,可点击跳转升级页 | | 会员查看 | 主价格显示会员价,原价带删除线 | ## 不涉及 - 后端无改动(已有完整 SKU 支持) - 结算页无改动(已有 `skuId` 处理逻辑) - 订单详情页无改动