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

feat(frontend): 小程序商品页特价展示 + checkout 特价透传

Sisyphus Agent 4 дней назад
Родитель
Сommit
0fb5d12970

+ 170 - 6
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -18,13 +18,37 @@
         <view class="price-row">
           <view class="price-main">
             <text class="price">{{ formatPriceWithSymbol(displayPrice) }}</text>
-            <text v-if="hasMemberPrice && isMember" class="member-tag">会员价</text>
+            <text v-if="isSpecialDealRunning" class="special-tag">特价</text>
+            <text v-else-if="hasMemberPrice && isMember" class="member-tag">会员价</text>
           </view>
-          <view v-if="hasMemberPrice && isMember" class="price-original">
+          <view v-if="isSpecialDealRunning" class="price-original">
             <text class="original-label">原价</text>
             <text class="original-text">{{ formatPriceWithSymbol(originalPriceVal) }}</text>
           </view>
-          <view v-if="hasMemberPrice && !isMember" class="member-hint" @click="goUpgrade">加入会员享会员价 {{ formatPriceWithSymbol(memberPriceVal) }} ›</view>
+          <view v-else-if="hasMemberPrice && isMember" class="price-original">
+            <text class="original-label">原价</text>
+            <text class="original-text">{{ formatPriceWithSymbol(originalPriceVal) }}</text>
+          </view>
+          <view v-if="hasMemberPrice && !isMember && !isSpecialDealRunning" class="member-hint" @click="goUpgrade">加入会员享会员价 {{ formatPriceWithSymbol(memberPriceVal) }} ›</view>
+        </view>
+
+        <!-- 限时特价(扫码闪购)信息区 -->
+        <view v-if="showSpecialDealBlock" class="special-deal-block">
+          <view v-if="specialDealStatus === 'active'" class="special-deal-row">
+            <text class="special-deal-tag">限时特价</text>
+            <text class="special-deal-limit">限每人 {{ specialDeal.perUserLimit || 1 }} 件</text>
+            <text class="special-deal-remain">剩余 {{ specialDeal.remainingQuantity || 0 }} / {{ specialDeal.totalQuantity || 0 }}</text>
+          </view>
+          <view v-else-if="specialDealStatus === 'not_started'" class="special-deal-row">
+            <text class="special-deal-tag">限时特价</text>
+            <text class="special-deal-countdown">距开售还有 {{ countdownHours }} 小时</text>
+          </view>
+          <view v-else class="special-deal-row">
+            <text class="special-deal-status-text">活动已售罄 / 已结束</text>
+          </view>
+          <view v-if="specialDealStatus === 'active'" class="special-deal-progress">
+            <view class="special-deal-progress-bar" :style="{ width: (specialDeal.totalQuantity > 0 ? Math.min(100, Math.round(specialDeal.soldQuantity * 100 / specialDeal.totalQuantity)) : 0) + '%' }"></view>
+          </view>
         </view>
         <view v-if="product.giftLevelName" class="gift-tag">购买即送 {{ product.giftLevelName }} 会员</view>
         <!-- 可选赠品选择区 -->
@@ -153,7 +177,8 @@
 <script>
 import { BrandColors } from '@/utils/brand-colors.js'
 import config from '@/config.js'
-import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getMyMembership } from '@/utils/api.js'
+import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getMyMembership, getSpecialPriceInfo } from '@/utils/api.js'
+import { parseDate } from '@/utils/format.js'
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import RelatedContent from '@/components/RelatedContent.vue'
@@ -187,7 +212,12 @@ export default {
       // 会员身份(实时调 getMyMembership,FREE=非会员)
       isMember: false,
       memberLevel: 'FREE',
-      bundleItems: []
+      bundleItems: [],
+      // 限时特价(扫码闪购):scene=id={productId}&sp={dealId}
+      specialDealId: null,
+      specialDeal: null,
+      specialDealStatus: 'invalid',
+      specialDealCountdownTimer: null
     }
   },
   computed: {
@@ -289,18 +319,42 @@ export default {
       return true
     },
     displayPrice: function() {
+      // 特价活动(进行中/未开始)展示特价,覆盖会员价
+      if (this.specialDeal && (this.specialDealStatus === 'active' || this.specialDealStatus === 'not_started')) {
+        return this.specialDeal.specialPrice || 0
+      }
       // 实际支付价(按会员身份)
       return this.actualPrice
     },
+    // 特价进行中/未开始(决定价格行的特价标签与划线原价展示)
+    isSpecialDealRunning: function() {
+      return this.specialDealStatus === 'active' || this.specialDealStatus === 'not_started'
+    },
+    // 特价信息区是否展示(进行中/未开始/已售罄)
+    showSpecialDealBlock: function() {
+      return this.specialDealStatus === 'active' || this.specialDealStatus === 'not_started' || this.specialDealStatus === 'sold_out'
+    },
+    // 距开售剩小时数(not_started 倒计时展示,向上取整)
+    countdownHours: function() {
+      if (!this.specialDeal || !this.specialDeal.startTime) return 0
+      var start = parseDate(this.specialDeal.startTime)
+      if (!start) return 0
+      var diff = start.getTime() - Date.now()
+      if (diff <= 0) return 0
+      return Math.ceil(diff / 3600000)
+    },
     displayStock: function() {
       if (this.selectedSku) return this.selectedSku.stock || 0
       if (this.hasSku) return 0
       return this.product.stock || 0
     },
     buyButtonDisabled: function() {
+      // 特价未开始时禁用购买按钮(到点自动刷新进入 active)
+      if (this.specialDealStatus === 'not_started') return true
       return false
     },
     buyButtonText: function() {
+      if (this.specialDealStatus === 'not_started') return '距开售还有 ' + this.countdownHours + ' 小时'
       return '立即购买'
     },
     // 展平 giftConfigs 内所有赠品项(后端按当前等级返回,通常单配置)
@@ -331,6 +385,11 @@ export default {
         if (idMatch && idMatch[1]) pid = idMatch[1]
         var refMatch = sceneDecoded.match(/ref=([^&]+)/)
         if (refMatch && refMatch[1]) this.autoBindInviteCode(refMatch[1])
+        // 限时特价(扫码闪购):scene 含 sp=dealId 时记录活动 ID
+        var spMatch = sceneDecoded.match(/sp=(\d+)/)
+        if (spMatch && spMatch[1]) {
+          this.specialDealId = parseInt(spMatch[1], 10)
+        }
       } catch (e) {}
     }
     if (pid) {
@@ -338,10 +397,20 @@ export default {
     }
     // 实时查询会员身份,用于会员价展示和实付价计算
     this.loadMembership()
+    // 特价入口:加载活动状态(倒计时/进行中/已售罄)
+    if (this.specialDealId) {
+      this.loadSpecialDealInfo()
+    }
   },
   onShow() {
     this.fontScale = getFontScale()
   },
+  onUnload() {
+    if (this.specialDealCountdownTimer) {
+      clearInterval(this.specialDealCountdownTimer)
+      this.specialDealCountdownTimer = null
+    }
+  },
   methods: {
     loadMembership: function() {
       var that = this
@@ -364,6 +433,45 @@ export default {
         that.memberLevel = 'FREE'
       })
     },
+    // 加载限时特价活动信息(扫码闪购入口;normal 状态回落原价)
+    loadSpecialDealInfo: function() {
+      var that = this
+      if (!this.specialDealId) return
+      getSpecialPriceInfo(this.specialDealId).then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          that.specialDeal = res.data
+          that.specialDealStatus = that.computeSpecialDealStatus(res.data.runStatus)
+        } else {
+          that.specialDeal = null
+          that.specialDealStatus = 'invalid'
+        }
+        that.refreshSpecialDealTimer()
+      }).catch(function() {
+        that.specialDeal = null
+        that.specialDealStatus = 'invalid'
+        that.refreshSpecialDealTimer()
+      })
+    },
+    // 倒计时管理:仅 not_started 每秒刷新(到点自动变 active/ended);其余状态清除定时器
+    refreshSpecialDealTimer: function() {
+      if (this.specialDealStatus === 'not_started') {
+        if (this.specialDealCountdownTimer) return
+        this.specialDealCountdownTimer = setInterval(this.loadSpecialDealInfo, 1000)
+      } else {
+        if (this.specialDealCountdownTimer) {
+          clearInterval(this.specialDealCountdownTimer)
+          this.specialDealCountdownTimer = null
+        }
+      }
+    },
+    // runStatus 归一化:ACTIVE/NOT_STARTED/SOLD_OUT 保留,其余(ENDED/DISABLED/DRAFT/INVALID)回落原价
+    computeSpecialDealStatus: function(runStatus) {
+      if (!runStatus) return 'invalid'
+      if (runStatus === 'ACTIVE') return 'active'
+      if (runStatus === 'NOT_STARTED') return 'not_started'
+      if (runStatus === 'SOLD_OUT') return 'sold_out'
+      return 'invalid'
+    },
     getImageUrl: function(path) {
       if (!path) return '/static/default-product.png'
       if (path.startsWith('http') || path.startsWith('/static/')) return path
@@ -589,7 +697,13 @@ export default {
         this.openLoginSheet()
         return
       }
-      var price = this.actualPrice
+      // 限时特价(扫码闪购):仅活动进行中携带 dealId 走特价路径。
+      // 先清除再写入,保证普通购买不残留旧 dealId(特价仅扫码入口生效,后端亦校验商品/活动一致性)
+      uni.removeStorageSync('checkoutSpecialDealId')
+      if (this.specialDealId && this.specialDealStatus === 'active') {
+        uni.setStorageSync('checkoutSpecialDealId', this.specialDealId)
+      }
+      var price = this.displayPrice
       var productId = this.product.id
       // Linked SKU: 使用关联商品 ID
       if (this.selectedSku && this.selectedSku.linkedProductId) {
@@ -800,6 +914,56 @@ export default {
   border-radius: 8rpx;
   align-self: flex-start;
 }
+.special-tag {
+  margin-left: 8rpx;
+  font-size: 20rpx;
+  color: #fff;
+  background: $brand-primary;
+  border-radius: 6rpx;
+  padding: 2rpx 10rpx;
+}
+.special-deal-block {
+  margin-top: 16rpx;
+  padding: 16rpx 20rpx;
+  background: $uni-primary-pale;
+  border: 1rpx solid $brand-primary-tint-2;
+  border-radius: 16rpx;
+}
+.special-deal-row {
+  display: flex;
+  align-items: center;
+}
+.special-deal-tag {
+  font-size: 22rpx;
+  font-weight: bold;
+  color: #fff;
+  background: $brand-primary;
+  border-radius: 8rpx;
+  padding: 2rpx 12rpx;
+  margin-right: 16rpx;
+}
+.special-deal-limit,
+.special-deal-countdown,
+.special-deal-remain,
+.special-deal-status-text {
+  font-size: 24rpx;
+  color: $brand-primary;
+}
+.special-deal-remain {
+  margin-left: 16rpx;
+}
+.special-deal-progress {
+  margin-top: 12rpx;
+  height: 12rpx;
+  background: $brand-primary-tint-2;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.special-deal-progress-bar {
+  height: 100%;
+  background: linear-gradient(90deg, $brand-primary, #fb923c);
+  border-radius: 6rpx;
+}
 .gift-tag {
   display: inline-block;
   margin-top: 12rpx;

+ 45 - 3
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -239,7 +239,7 @@
       </view>
 
       <!-- Coupon Section -->
-      <view class="section coupon-section" @click="openCouponPicker">
+      <view v-if="!isSpecialCouponDisabled" class="section coupon-section" @click="openCouponPicker">
         <view class="info-row">
           <text class="info-label">优惠券</text>
           <view class="coupon-selected" v-if="selectedCoupon">
@@ -484,7 +484,7 @@
 <script>
 import { BrandColors } from '@/utils/brand-colors.js'
 import config from '@/config.js'
-import { getCouponList, getCouponCheckoutList, addressList, getProductRequiredFields, getMyMembership, getGiftMembershipProgress, productDetail, getFamilyPlatformBalance, exchangeCouponByCf, shopDiscountCalculate } from '../../../utils/api.js'
+import { getCouponList, getCouponCheckoutList, addressList, getProductRequiredFields, getMyMembership, getGiftMembershipProgress, productDetail, getFamilyPlatformBalance, exchangeCouponByCf, shopDiscountCalculate, getSpecialPriceInfo } from '../../../utils/api.js'
 import { parseDate } from '@/utils/format.js'
 import CountdownTag from '@/pages/shop/components/countdown-tag.vue'
 import HandSignature from '@/components/hand-signature/hand-signature.vue'
@@ -534,7 +534,10 @@ export default {
       giftProgress: null,   // 赠会员累计进度
       discountInfo: null,
       giftPromo: null,       // 送会员一口价促销信息
-      taskId: null          // 购买任务打卡:透传至支付页
+      taskId: null,          // 购买任务打卡:透传至支付页
+      // 限时特价(扫码闪购):由 product-detail 经 storage 一次性带入,onLoad 消费后即清除
+      checkoutSpecialDealId: null,
+      specialDeal: null
     }
   },
   computed: {
@@ -570,6 +573,12 @@ export default {
       if (!this.giftPromo || this.giftPromo.active !== true) return 0
       return this.giftPromo.discount || 0
     },
+    // 限时特价(扫码闪购):特价单且活动禁止优惠券时隐藏优惠券选择区。
+    // 活动信息未返回(加载中/失败)时默认视为不可用券(特价单保守处理)。
+    isSpecialCouponDisabled() {
+      if (!this.checkoutSpecialDealId) return false
+      return !this.specialDeal || this.specialDeal.allowCoupon === 0
+    },
     giftLevelNames() {
       var names = []
       for (var i = 0; i < this.items.length; i++) {
@@ -669,6 +678,13 @@ export default {
     this.loadDeliveryConfig()
     this.loadDiscount()
     this.loadGiftProgress()
+    // 限时特价(扫码闪购):一次性消费 storage key —— 防止普通购物车入口误挂特价单。
+    // 仅产品详情扫码进 checkout 场景才会携带此 key;其他入口(购物车、商品直接加购)不写该 key。
+    this.checkoutSpecialDealId = uni.getStorageSync('checkoutSpecialDealId') || null
+    uni.removeStorageSync('checkoutSpecialDealId')
+    if (this.checkoutSpecialDealId) {
+      this.loadCheckoutSpecialDeal()
+    }
     this._onLoadFired = true
   },
   onShow() {
@@ -947,6 +963,29 @@ export default {
         }
       }).catch(function() {})
     },
+    // 限时特价(扫码闪购):拉取活动信息并校验一次性消费的 dealId。
+    // 仅当活动进行中且与订单商品一致才按特价单处理,否则回落普通订单(后端同样拒绝无效特价单)。
+    loadCheckoutSpecialDeal() {
+      var that = this
+      getSpecialPriceInfo(this.checkoutSpecialDealId).then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          var deal = res.data
+          if (deal.runStatus === 'ACTIVE' && that.items && that.items.length === 1
+              && that.items[0].productId === deal.productId) {
+            that.specialDeal = deal
+          } else {
+            that.checkoutSpecialDealId = null
+            that.specialDeal = null
+          }
+        } else {
+          that.checkoutSpecialDealId = null
+          that.specialDeal = null
+        }
+      }).catch(function() {
+        that.checkoutSpecialDealId = null
+        that.specialDeal = null
+      })
+    },
     onDeliveryMethodChange: function(e) {
       this.deliveryMethod = parseInt(e.detail.value)
       this.selectedPickupLocation = null
@@ -1061,6 +1100,8 @@ export default {
           pointsUsed: that.pointsUsed,
           discountAmount: (that.discountInfo ? that.discountInfo.discountAmount : 0) + that.giftPromoDiscount,
           couponId: that.selectedCoupon ? that.selectedCoupon.id : null,
+          // 特价单透传(无则传 null):后端 createSpecialDealOrder 校验活动后按特价结算
+          specialDealId: that.checkoutSpecialDealId || null,
           deliveryMethod: that.deliveryMethod,
           pickupLocationId: that.selectedPickupLocation ? that.selectedPickupLocation.id : null,
           timeSlotId: that.selectedTimeSlot ? that.selectedTimeSlot.id : null,
@@ -1078,6 +1119,7 @@ export default {
             uni.removeStorageSync('checkoutItems')
             uni.removeStorageSync('checkoutGiftItemIds')
             uni.removeStorageSync('checkoutGiftPromo')
+            uni.removeStorageSync('checkoutSpecialDealId')
             var moneyAmount = order.moneyAmount != null ? order.moneyAmount : order.totalAmount
             var pointsUsed = order.pointsUsed || 0
             setTimeout(function() {

+ 5 - 0
cfc-frontend/utils/api.js

@@ -1662,6 +1662,11 @@ export const productDetail = (params) => {
   return request('/api/product/detail', 'POST', params)
 }
 
+// 限时特价活动信息(公开,扫码闪购,scene 含 sp=dealId 时进入商品页携带)
+export const getSpecialPriceInfo = (dealId) => {
+  return request('/api/special-price/info', 'POST', { dealId })
+}
+
 // 商品规格映射(公开)
 export const productSpecMap = (params) => {
   return request('/api/product/spec/map', 'POST', params)