Просмотр исходного кода

feat: 商品详情页新增SKU规格选择器

- 支持多属性规格选择(颜色/尺码等)

- 会员价联动展示(非会员可点击升级)

- 未选规格时按钮禁用提示

- 加购/购买时传递 skuId 和 specDesc

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 3 месяцев назад
Родитель
Сommit
3ee061680e
1 измененных файлов с 303 добавлено и 29 удалено
  1. 303 29
      cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

+ 303 - 29
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -14,14 +14,18 @@
           <text class="product-name">{{ product.name }}</text>
         </view>
         <view class="price-row">
-          <text class="price">{{ formatPriceWithSymbol(product.price) }}</text>
-          <text v-if="product.memberPrice" class="member-price">会员价 {{ formatPriceWithSymbol(product.memberPrice) }}</text>
+          <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-hint" @click="goUpgrade">会员价 {{ formatPriceWithSymbol(displayMemberPrice) }}  → 点击升级</text>
+            <text v-if="isMember" class="member-price-current">会员价 {{ formatPriceWithSymbol(displayMemberPrice) }}</text>
+            <text v-if="isMember" class="member-price-original">原价 {{ formatPriceWithSymbol(displayPrice) }}</text>
+          </view>
         </view>
         <view v-if="product.giftLevelName" class="gift-tag">购买即送 {{ product.giftLevelName }} 会员</view>
         <view class="meta-row">
           <text class="meta-item" v-if="product.productType">{{ typeLabel }}</text>
           <text class="meta-item" v-if="product.domain">{{ domainLabel }}</text>
-          <text class="meta-item">库存 {{ product.stock || 0 }}</text>
+          <text class="meta-item">库存 {{ displayStock }}</text>
         </view>
         <view class="vendor-row">
           <text class="vendor-label">供应商:</text>
@@ -29,6 +33,26 @@
         </view>
       </view>
 
+      <!-- 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="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>
+
       <view class="intro-section" v-if="product.intro">
         <text class="section-title">商品介绍</text>
         <mp-html :content="product.intro" />
@@ -41,17 +65,18 @@
 
       <view class="bottom-bar">
         <view class="price-info">
-          <text class="bottom-price">{{ formatPriceWithSymbol(product.price) }}</text>
-          <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
+          <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" @click="onAddToCart">加入购物车</button>
+            <button class="btn-cart" :class="buyButtonDisabled ? 'btn-disabled' : ''" :disabled="buyButtonDisabled" @click="onAddToCart">{{ buyButtonDisabled ? '请选规格' : '加入购物车' }}</button>
           </view>
-          <button class="btn-buy" @click="onBuy">立即购买</button>
+          <button class="btn-buy" :class="buyButtonDisabled ? 'btn-disabled' : ''" :disabled="buyButtonDisabled" @click="onBuy">{{ buyButtonText }}</button>
         </view>
       </view>
     </scroll-view>
@@ -65,7 +90,7 @@
 
 <script>
 import config from '@/config.js'
-import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode } from '@/utils/api.js'
+import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getMyMembership } from '@/utils/api.js'
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import shareMixin from '../../../components/share-mixin.js'
@@ -80,7 +105,12 @@ export default {
       showPoster: false,
       posterQrCode: '',
       posterDimension: '',
-      posterInviteText: ''
+      posterInviteText: '',
+      // SKU 选择器状态
+      specGroups: [],
+      selectedSpecs: {},
+      selectedSku: null,
+      isMember: false
     }
   },
   computed: {
@@ -144,6 +174,39 @@ export default {
         images = images.concat(this.product.imageList)
       }
       return images.length ? images : ['/static/default-product.png']
+    },
+    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() {
+      if (this.selectedSku) return this.selectedSku.price
+      if (this.hasSku) return this.product.price || 0
+      return this.product.price || 0
+    },
+    displayMemberPrice: function() {
+      if (this.selectedSku) {
+        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 '立即购买'
     }
   },
   onLoad(options) {
@@ -159,6 +222,7 @@ export default {
     }
     if (pid) {
       this.loadDetail(pid)
+      this.checkMemberStatus()
     }
   },
   methods: {
@@ -174,61 +238,192 @@ export default {
         images[idx] = '/static/default-product.png'
       }
     },
-    loadDetail(id) {
+    loadDetail: function(id) {
       this.loading = true
-      productDetail({ id: parseInt(id) }).then(res => {
-        this.loading = false
+      var that = this
+      productDetail({ id: parseInt(id) }).then(function(res) {
+        that.loading = false
         if (res.code === 200 && res.data) {
-          this.product = res.data
-          this.setShareInfo(
-            this.product.name,
-            '/pages/discover-detail/product-detail/product-detail?id=' + this.product.id
+          that.product = res.data
+          that.setShareInfo(
+            that.product.name,
+            '/pages/discover-detail/product-detail/product-detail?id=' + that.product.id
           )
+          that.loadSkuData(id)
         }
-      }).catch(() => {
-        this.loading = false
+      }).catch(function() {
+        that.loading = false
+      })
+    },
+    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() {
+        // 规格加载失败不阻塞页面
       })
     },
-    onBuy() {
-      const userId = uni.getStorageSync('userId')
+
+    checkMemberStatus: function() {
+      var that = this
+      getMyMembership().then(function(res) {
+        if (res.data && res.data.memberLevel) {
+          that.isMember = res.data.memberLevel !== 'FREE'
+        }
+      }).catch(function() {})
+    },
+
+    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
+        }
+      }
+      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) {
+        var matchedSkuId = commonSkuIds[0]
+        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
+    },
+
+    getSpecOptionClass: function(groupId, option) {
+      if (this.selectedSpecs[groupId] === option.id) return 'spec-option spec-option-selected'
+      if (!option.enabled || (option.stock !== undefined && option.stock <= 0)) return 'spec-option spec-option-disabled'
+      return 'spec-option'
+    },
+
+    goUpgrade: function() {
+      uni.navigateTo({ url: '/pages/membership/upgrade' })
+    },
+
+    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
       }
-      // 跳转结算页面,传递商品信息
-      const price = this.product.price
-      const productData = {
+      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
       }
-      // 先存储到 storage,checkout 页面会读取
+      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
       })
     },
-    onAddToCart() {
-      const userId = uni.getStorageSync('userId')
+    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: '加入购物车...' })
-      cartAdd({
+      var params = {
         userId: userId,
         productId: this.product.id,
         quantity: 1
-      }).then(res => {
+      }
+      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(() => {
+      }).catch(function() {
         uni.hideLoading()
         uni.showToast({ title: '网络异常', icon: 'none' })
       })
@@ -392,6 +587,85 @@ export default {
   line-height: 1.6;
   display: block;
 }
+/* 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-selected {
+  border-color: #F97316;
+  background: #F97316;
+}
+.spec-option-selected .spec-option-text {
+  color: #fff;
+}
+.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;
+  text-decoration: line-through;
+}
+.member-price-current {
+  font-size: 22rpx;
+  color: #F97316;
+  font-weight: bold;
+}
+.bottom-member-highlight {
+  color: #F97316 !important;
+}
+.btn-disabled {
+  opacity: 0.5;
+}
 .bottom-bar {
   position: fixed;
   bottom: 0;