# 商品 SKU 选择器 实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 在商品详情页添加 SKU 规格选择功能,让用户在加购/下单前选择规格,会员价联动展示
**架构:** 使用现有 `/api/product/spec/map` 接口获取规格分组数据,在详情页 `info-section` 和 `intro-section` 之间插入规格选择器,选择后同步更新价格/库存,加购/下单时传递 `skuId` 和 `specDesc`
**技术栈:** uni-app Vue 2 Options API,微信小程序
**设计文档:** `docs/superpowers/specs/2026-08-04-product-sku-selector-design.md`
---
### 任务 1:product-detail.vue — 数据层改造
**文件:**
- 修改:`cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`
- [ ] **步骤 1:添加 imports 和 data 属性**
在 `import` 中添加 `productSpecMap` 和 `getMyMembership`:
```js
import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getMyMembership } from '@/utils/api.js'
```
在 `data()` 中添加:
```js
data() {
return {
product: {},
loading: false,
showPoster: false,
posterQrCode: '',
posterDimension: '',
posterInviteText: '',
// SKU 选择器状态
specGroups: [], // 规格分组数据
selectedSpecs: {}, // { groupId: optionId }
selectedSku: null, // 匹配到的完整 SKU
isMember: false // 会员状态
}
}
```
- [ ] **步骤 2:添加计算属性**
在 `computed` 中添加:
```js
computed: {
// ... 现有计算属性 ...
hasSku: function() {
return this.specGroups && this.specGroups.length > 0
},
allSpecsSelected: function() {
if (!this.hasSku) return true
for (var i = 0; i < this.specGroups.length; i++) {
if (!this.selectedSpecs[this.specGroups[i].groupId]) return false
}
return true
},
displayPrice: function() {
// 有 SKU 且已选 → 显示 SKU 价格
if (this.selectedSku) return this.selectedSku.price
// 有 SKU 但未选 → 显示产品默认价格
if (this.hasSku) return this.product.price || 0
// 无 SKU → 显示产品价格
return this.product.price || 0
},
displayMemberPrice: function() {
if (this.selectedSku) {
// 如果 SKU 有 memberPrice,用;否则用产品的 memberPrice
return this.product.memberPrice || 0
}
return this.product.memberPrice || 0
},
displayStock: function() {
if (this.selectedSku) return this.selectedSku.stock || 0
if (this.hasSku) return 0
return this.product.stock || 0
},
buyButtonDisabled: function() {
return this.hasSku && !this.allSpecsSelected
},
buyButtonText: function() {
if (this.hasSku && !this.allSpecsSelected) return '请选择完整规格'
return '立即购买'
}
}
```
- [ ] **步骤 3:修改 loadDetail 方法**
修改 `loadDetail` 在加载商品详情后获取规格数据:
```js
loadDetail: function(id) {
this.loading = true
var that = this
productDetail({ id: parseInt(id) }).then(function(res) {
that.loading = false
if (res.code === 200 && res.data) {
that.product = res.data
that.setShareInfo(
that.product.name,
'/pages/discover-detail/product-detail/product-detail?id=' + that.product.id
)
// 加载 SKU 规格数据
that.loadSkuData(id)
}
}).catch(function() {
that.loading = false
})
}
```
- [ ] **步骤 4:添加 loadSkuData 和 checkMemberStatus 方法**
在 `methods` 中添加:
```js
loadSkuData: function(productId) {
var that = this
productSpecMap({ productIds: [parseInt(productId)] }).then(function(res) {
if (res.code === 200 && res.data) {
var map = res.data
var groups = map[productId]
if (groups && groups.length > 0) {
that.specGroups = groups
}
}
}).catch(function() {
// 规格加载失败不阻塞页面
})
},
checkMemberStatus: function() {
var that = this
getMyMembership().then(function(res) {
if (res.data && res.data.memberLevel) {
that.isMember = res.data.memberLevel !== 'FREE'
}
}).catch(function() {})
}
```
在 `onLoad` 末尾调用 `checkMemberStatus`:
```js
onLoad: function(options) {
// ... 现有代码 ...
if (pid) {
this.loadDetail(pid)
this.checkMemberStatus()
}
}
```
- [ ] **步骤 5:添加规格选择方法**
在 `methods` 中添加:
```js
selectSpec: function(groupId, option) {
// 切换选中状态
if (this.selectedSpecs[groupId] === option.id) {
// 点击已选中的 → 取消选中
delete this.selectedSpecs[groupId]
this.selectedSku = null
} else {
this.selectedSpecs[groupId] = option.id
this.matchSku()
}
// 触发视图更新
this.$forceUpdate()
},
matchSku: function() {
// 检查是否所有组都已选
for (var i = 0; i < this.specGroups.length; i++) {
if (!this.selectedSpecs[this.specGroups[i].groupId]) {
this.selectedSku = null
return
}
}
// 取所有选中选项的 skuId 的交集
var commonSkuIds = null
for (var j = 0; j < this.specGroups.length; j++) {
var group = this.specGroups[j]
var selectedOptionId = this.selectedSpecs[group.groupId]
var skuIds = []
for (var k = 0; k < group.options.length; k++) {
if (group.options[k].id === selectedOptionId) {
skuIds.push(group.options[k].skuId)
}
}
if (commonSkuIds === null) {
commonSkuIds = skuIds
} else {
// 取交集
var intersection = []
for (var m = 0; m < commonSkuIds.length; m++) {
for (var n = 0; n < skuIds.length; n++) {
if (commonSkuIds[m] === skuIds[n]) {
intersection.push(commonSkuIds[m])
break
}
}
}
commonSkuIds = intersection
}
}
if (commonSkuIds && commonSkuIds.length === 1) {
// 找到匹配的 SKU,从 specMap 数据中取价格和库存
var matchedSkuId = commonSkuIds[0]
// 从任一组的选项中获取完整的 SKU 信息
for (var p = 0; p < this.specGroups.length; p++) {
var opts = this.specGroups[p].options
for (var q = 0; q < opts.length; q++) {
if (opts[q].skuId === matchedSkuId) {
this.selectedSku = {
id: matchedSkuId,
price: opts[q].price,
stock: opts[q].stock
}
return
}
}
}
}
this.selectedSku = null
},
buildSpecDesc: function() {
if (!this.selectedSku) return ''
var desc = ''
for (var i = 0; i < this.specGroups.length; i++) {
var group = this.specGroups[i]
var optionId = this.selectedSpecs[group.groupId]
var optionName = ''
for (var j = 0; j < group.options.length; j++) {
if (group.options[j].id === optionId) {
optionName = group.options[j].name
break
}
}
if (desc) desc += '; '
desc += group.groupName + ':' + optionName
}
return desc
}
```
### 任务 2:product-detail.vue — 模板层改造
**文件:**
- 修改:`cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`
- [ ] **步骤 1:修改价格展示区**
将 `info-section` 中的 price-row 替换为:
```html
{{ formatPriceWithSymbol(displayPrice) }}
{{ formatPriceWithSymbol(displayPrice) }}
会员价 {{ formatPriceWithSymbol(displayMemberPrice) }} → 点击升级
会员价 {{ formatPriceWithSymbol(displayMemberPrice) }}
```
- [ ] **步骤 2:添加 SKU 选择器模板**
在 `info-section` 结束标签 `` 之后、`intro-section` 之前插入:
```html
{{ group.groupName }}
{{ option.name }}
已选: {{ buildSpecDesc() }}
```
- [ ] **步骤 3:修改底部栏联动**
将 `bottom-bar` 中的价格和按钮替换为:
```html
{{ formatPriceWithSymbol(displayPrice) }}
会员{{ formatPriceWithSymbol(displayMemberPrice) }}
会员{{ formatPriceWithSymbol(displayMemberPrice) }}
```
- [ ] **步骤 4:添加样式**
在 `