2026-08-04-product-sku-selector-design.md 7.3 KB

商品 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 接口获取

// /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 }
    ]
  }
]

组件状态

data() {
  return {
    product: {},
    specGroups: [],           // 从 specMap 获取
    selectedSpecs: {},        // { groupId: optionId }
    selectedSku: null,        // 匹配到的完整 SKU
    skuList: [],              // 从 specMap 解析出的所有 SKU 映射
    isMember: false           // 会员状态
  }
}

SKU 匹配逻辑

// 从 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 处理逻辑)
  • 订单详情页无改动