|
|
@@ -0,0 +1,583 @@
|
|
|
+# 商品 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
|
|
|
+<view class="price-row">
|
|
|
+ <text class="price">{{ formatPriceWithSymbol(displayPrice) }}</text>
|
|
|
+ <view v-if="displayMemberPrice && displayMemberPrice > 0 && displayMemberPrice !== displayPrice" class="member-price-row">
|
|
|
+ <text v-if="isMember" class="member-price-original">
|
|
|
+ <text class="original-price">{{ formatPriceWithSymbol(displayPrice) }}</text>
|
|
|
+ </text>
|
|
|
+ <text v-if="!isMember" class="member-price-hint" @click="goUpgrade">
|
|
|
+ 会员价 {{ formatPriceWithSymbol(displayMemberPrice) }} → 点击升级
|
|
|
+ </text>
|
|
|
+ <text v-if="isMember" class="member-price-current">
|
|
|
+ 会员价 {{ formatPriceWithSymbol(displayMemberPrice) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:添加 SKU 选择器模板**
|
|
|
+
|
|
|
+在 `info-section` 结束标签 `</view>` 之后、`intro-section` 之前插入:
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- SKU 选择器 -->
|
|
|
+<view v-if="specGroups.length > 0" class="sku-section">
|
|
|
+ <view v-for="group in specGroups" :key="group.groupId" class="spec-group">
|
|
|
+ <text class="spec-group-name">{{ group.groupName }}</text>
|
|
|
+ <view class="spec-options">
|
|
|
+ <view
|
|
|
+ v-for="option in group.options"
|
|
|
+ :key="option.id"
|
|
|
+ :class="['spec-option', getSpecOptionClass(group.groupId, option)]"
|
|
|
+ @click="selectSpec(group.groupId, option)"
|
|
|
+ >
|
|
|
+ <text class="spec-option-text">{{ option.name }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-if="selectedSku" class="selected-summary">
|
|
|
+ <text class="selected-text">已选: {{ buildSpecDesc() }}</text>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:修改底部栏联动**
|
|
|
+
|
|
|
+将 `bottom-bar` 中的价格和按钮替换为:
|
|
|
+
|
|
|
+```html
|
|
|
+<view class="bottom-bar">
|
|
|
+ <view class="price-info">
|
|
|
+ <text class="bottom-price">{{ formatPriceWithSymbol(displayPrice) }}</text>
|
|
|
+ <text v-if="displayMemberPrice && displayMemberPrice > 0 && !isMember" class="bottom-member">会员{{ formatPriceWithSymbol(displayMemberPrice) }}</text>
|
|
|
+ <text v-if="displayMemberPrice && displayMemberPrice > 0 && isMember" class="bottom-member bottom-member-highlight">会员{{ formatPriceWithSymbol(displayMemberPrice) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="action-btns">
|
|
|
+ <view class="action-extra">
|
|
|
+ <button class="btn-share" @click="onShareProduct">🖼</button>
|
|
|
+ </view>
|
|
|
+ <view class="action-extra">
|
|
|
+ <button class="btn-cart" :class="buyButtonDisabled ? 'btn-disabled' : ''" :disabled="buyButtonDisabled" @click="onAddToCart">{{ buyButtonDisabled ? '请选规格' : '加入购物车' }}</button>
|
|
|
+ </view>
|
|
|
+ <button class="btn-buy" :class="buyButtonDisabled ? 'btn-disabled' : ''" :disabled="buyButtonDisabled" @click="onBuy">{{ buyButtonText }}</button>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:添加样式**
|
|
|
+
|
|
|
+在 `<style scoped>` 中添加:
|
|
|
+
|
|
|
+```css
|
|
|
+/* SKU 选择器 */
|
|
|
+.sku-section {
|
|
|
+ background: #fff;
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.spec-group {
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.spec-group:last-child {
|
|
|
+ margin-bottom: 10rpx;
|
|
|
+}
|
|
|
+.spec-group-name {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.spec-options {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+.spec-option {
|
|
|
+ padding: 12rpx 28rpx;
|
|
|
+ border: 2rpx solid #ddd;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.spec-option.spec-option-selected {
|
|
|
+ border-color: #F97316;
|
|
|
+ background: #F97316;
|
|
|
+}
|
|
|
+.spec-option.spec-option-selected .spec-option-text {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.spec-option.spec-option-disabled {
|
|
|
+ border-color: #eee;
|
|
|
+ background: #f9f9f9;
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+.spec-option-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.selected-summary {
|
|
|
+ margin-top: 10rpx;
|
|
|
+ padding-top: 14rpx;
|
|
|
+ border-top: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.selected-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #F97316;
|
|
|
+}
|
|
|
+
|
|
|
+/* 会员价 */
|
|
|
+.member-price-row {
|
|
|
+ margin-top: 6rpx;
|
|
|
+}
|
|
|
+.member-price-hint {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #F97316;
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+.member-price-original {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.member-price-current {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #F97316;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+.original-price {
|
|
|
+ text-decoration: line-through;
|
|
|
+}
|
|
|
+.bottom-member-highlight {
|
|
|
+ color: #F97316 !important;
|
|
|
+}
|
|
|
+.btn-disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:添加 getSpecOptionClass 方法**
|
|
|
+
|
|
|
+在 `methods` 中添加:
|
|
|
+
|
|
|
+```js
|
|
|
+getSpecOptionClass: function(groupId, option) {
|
|
|
+ var isSelected = this.selectedSpecs[groupId] === option.id
|
|
|
+ var isDisabled = !option.enabled || (option.stock !== undefined && option.stock <= 0)
|
|
|
+ if (isSelected) return 'spec-option spec-option-selected'
|
|
|
+ if (isDisabled) return 'spec-option spec-option-disabled'
|
|
|
+ return 'spec-option'
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 6:添加 goUpgrade 方法**
|
|
|
+
|
|
|
+在 `methods` 中添加:
|
|
|
+
|
|
|
+```js
|
|
|
+goUpgrade: function() {
|
|
|
+ uni.navigateTo({ url: '/pages/membership/upgrade' })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 3:product-detail.vue — 修改加购和购买逻辑
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:修改 onAddToCart 传 skuId**
|
|
|
+
|
|
|
+```js
|
|
|
+onAddToCart: function() {
|
|
|
+ if (this.buyButtonDisabled) {
|
|
|
+ uni.showToast({ title: '请选择完整规格', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var userId = uni.getStorageSync('userId')
|
|
|
+ if (!userId) {
|
|
|
+ uni.navigateTo({ url: '/pages/login/login' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.showLoading({ title: '加入购物车...' })
|
|
|
+ var params = {
|
|
|
+ userId: userId,
|
|
|
+ productId: this.product.id,
|
|
|
+ quantity: 1
|
|
|
+ }
|
|
|
+ if (this.selectedSku) {
|
|
|
+ params.skuId = this.selectedSku.id
|
|
|
+ }
|
|
|
+ cartAdd(params).then(function(res) {
|
|
|
+ uni.hideLoading()
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '已加入购物车', icon: 'success' })
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '加入购物车失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '网络异常', icon: 'none' })
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:修改 onBuy 传 skuId 和 specDesc**
|
|
|
+
|
|
|
+```js
|
|
|
+onBuy: function() {
|
|
|
+ if (this.buyButtonDisabled) {
|
|
|
+ uni.showToast({ title: '请选择完整规格', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var userId = uni.getStorageSync('userId')
|
|
|
+ if (!userId) {
|
|
|
+ uni.navigateTo({ url: '/pages/login/login' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var price = this.selectedSku ? this.selectedSku.price : this.product.price
|
|
|
+ var productData = {
|
|
|
+ productId: this.product.id,
|
|
|
+ productName: this.product.name,
|
|
|
+ coverImage: this.product.coverImage,
|
|
|
+ unitPrice: price,
|
|
|
+ quantity: 1
|
|
|
+ }
|
|
|
+ if (this.selectedSku) {
|
|
|
+ productData.skuId = this.selectedSku.id
|
|
|
+ productData.specDesc = this.buildSpecDesc()
|
|
|
+ }
|
|
|
+ uni.setStorageSync('checkoutItems', [productData])
|
|
|
+ uni.navigateTo({
|
|
|
+ url: '/pages/shop/checkout/checkout?from=product&id=' + this.product.id
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 4:cart.vue — 结算时传递 skuId 和 specDesc
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/shop/cart/cart.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:修改 goCheckout 传 skuId 和 specDesc**
|
|
|
+
|
|
|
+```js
|
|
|
+goCheckout: function() {
|
|
|
+ if (this.selectedCount === 0) {
|
|
|
+ uni.showToast({ title: '请选择商品', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var selected = []
|
|
|
+ for (var i = 0; i < this.items.length; i++) {
|
|
|
+ if (this.items[i].checked) {
|
|
|
+ var item = {
|
|
|
+ productId: this.items[i].productId,
|
|
|
+ productName: this.items[i].productName,
|
|
|
+ coverImage: this.items[i].coverImage,
|
|
|
+ unitPrice: this.items[i].unitPrice,
|
|
|
+ quantity: this.items[i].quantity
|
|
|
+ }
|
|
|
+ // 传递 SKU 信息
|
|
|
+ if (this.items[i].skuId) {
|
|
|
+ item.skuId = this.items[i].skuId
|
|
|
+ }
|
|
|
+ if (this.items[i].specDesc) {
|
|
|
+ item.specDesc = this.items[i].specDesc
|
|
|
+ }
|
|
|
+ selected.push(item)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ uni.setStorageSync('checkoutItems', selected)
|
|
|
+ uni.navigateTo({ url: '/pages/shop/checkout/checkout' })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 5:验证
|
|
|
+
|
|
|
+- [ ] **步骤 1:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /app/cfc/cfc-frontend && npm run build:mp-weixin
|
|
|
+```
|
|
|
+
|
|
|
+预期:无编译错误,dist 目录生成成功
|
|
|
+
|
|
|
+- [ ] **步骤 2:代码审查**
|
|
|
+
|
|
|
+检查以下要点:
|
|
|
+1. specMap 返回空时,SKU 选择器不显示,不影响无 SKU 商品
|
|
|
+2. 未选完整规格时,按钮显示"请选择完整规格"并禁用
|
|
|
+3. 会员价展示逻辑正确(会员/非会员区分)
|
|
|
+4. 加购时传 skuId
|
|
|
+5. 购买时传 skuId 和 specDesc
|
|
|
+6. 购物车结算时传 skuId 和 specDesc
|
|
|
+7. 无可选链、CSS Grid 等微信小程序禁止语法
|