Browse Source

feat(miniapp): 优惠券积分兑换入口+商品详情兑换区块+发券提示+会员权益展示

Xiaogang Liao 1 month ago
parent
commit
765e19ecde

+ 112 - 2
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -53,6 +53,19 @@
         </view>
       </view>
 
+      <!-- 积分兑换优惠券 -->
+      <view class="coupon-exchange-section" v-if="productCoupons.length > 0">
+        <text class="section-title">积分兑换优惠券</text>
+        <view class="coupon-item" v-for="(item, index) in productCoupons" :key="getCouponKey(item, index)">
+          <view class="coupon-info">
+            <text class="coupon-name">{{ item.name }}</text>
+            <text class="coupon-desc">{{ getCouponDesc(item) }}</text>
+          </view>
+          <view class="coupon-price">{{ item.pointsPrice }}积分</view>
+          <button class="coupon-exchange-btn" @click="doExchangeCoupon(item)">兑换</button>
+        </view>
+      </view>
+
       <view class="intro-section" v-if="product.intro">
         <text class="section-title">商品介绍</text>
         <mp-html :content="product.intro" />
@@ -90,7 +103,7 @@
 
 <script>
 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, getProductCoupons, exchangeCoupon } 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'
@@ -110,7 +123,8 @@ export default {
       specGroups: [],
       selectedSpecs: {},
       selectedSku: null,
-      isMember: false
+      isMember: false,
+      productCoupons: []
     }
   },
   computed: {
@@ -250,6 +264,7 @@ export default {
             '/pages/discover-detail/product-detail/product-detail?id=' + that.product.id
           )
           that.loadSkuData(id)
+          that.loadProductCoupons(parseInt(id))
         }
       }).catch(function() {
         that.loading = false
@@ -270,6 +285,50 @@ export default {
       })
     },
 
+    loadProductCoupons: function(productId) {
+      if (!productId) return
+      var that = this
+      getProductCoupons({ productId: productId }).then(function(res) {
+        if (res.code === 200) {
+          that.productCoupons = res.data || []
+        }
+      }).catch(function() {
+        // 兑换券加载失败不阻塞页面
+      })
+    },
+
+    getCouponKey: function(item, index) {
+      return item.id + '-' + index
+    },
+
+    getCouponDesc: function(item) {
+      if (item.type === 'DISCOUNT') {
+        return '折扣券 ' + (item.discountRate / 100).toFixed(1) + '折'
+      }
+      return '立减 ' + (item.value / 100).toFixed(2) + '元'
+    },
+
+    doExchangeCoupon: function(item) {
+      var that = this
+      uni.showModal({
+        title: '确认兑换',
+        content: '消耗 ' + item.pointsPrice + ' 积分兑换「' + item.name + '」?',
+        success: function(r) {
+          if (!r.confirm) return
+          exchangeCoupon({ couponId: item.id }).then(function(res) {
+            if (res.code === 200) {
+              uni.showToast({ title: '兑换成功', icon: 'success' })
+              that.loadProductCoupons(that.product.id)
+            } else {
+              uni.showToast({ title: res.message || '兑换失败', icon: 'none' })
+            }
+          }).catch(function(e) {
+            uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
+          })
+        }
+      })
+    },
+
     checkMemberStatus: function() {
       var that = this
       getMyMembership().then(function(res) {
@@ -642,6 +701,57 @@ export default {
   font-size: 24rpx;
   color: #F97316;
 }
+/* 积分兑换优惠券 */
+.coupon-exchange-section {
+  background: #fff;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+.coupon-item {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.coupon-item:last-child {
+  border-bottom: none;
+}
+.coupon-info {
+  flex: 1;
+  min-width: 0;
+}
+.coupon-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: bold;
+  display: block;
+}
+.coupon-desc {
+  font-size: 24rpx;
+  color: #F97316;
+  margin-top: 6rpx;
+  display: block;
+}
+.coupon-price {
+  font-size: 26rpx;
+  color: #F97316;
+  font-weight: bold;
+  margin-right: 24rpx;
+}
+.coupon-exchange-btn {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 26rpx;
+  padding: 0 32rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  border-radius: 32rpx;
+  border: none;
+  margin: 0;
+}
+.coupon-exchange-btn::after {
+  border: none;
+}
 .member-price-row {
   margin-top: 6rpx;
 }

+ 2 - 2
cfc-frontend/pages/family/add-member.vue

@@ -241,8 +241,8 @@ payload.isCesarean = this.form.isCesarean
 }
 var res = await addFamilyMember(payload)
 if (res.code === 200) {
-uni.showToast({ title: '添加成功', icon: 'success' })
-setTimeout(function() { uni.navigateBack() }, 1500)
+uni.showToast({ title: '添加成功,已发放优惠券', icon: 'none', duration: 2000 })
+setTimeout(function() { uni.navigateBack() }, 1600)
 } else {
 uni.showToast({ title: res.message || '添加失败', icon: 'none' })
 }

+ 68 - 1
cfc-frontend/pages/membership/upgrade.vue

@@ -132,6 +132,15 @@
         </view>
       </view>
 
+      <!-- 开通赠券规则 -->
+      <view class="join-rules-section" v-if="joinRules.length > 0">
+        <text class="section-title">开通赠券</text>
+        <view class="join-rule-item" v-for="(rule, idx) in joinRules" :key="getJoinRuleKey(rule, idx)">
+          <text class="join-rule-text">加入{{ getLevelName(rule.grantLevelCode) }}会员,赠送{{ rule.grantQuantity || 1 }}张「{{ rule.name }}」</text>
+          <text class="join-rule-desc">{{ getCouponDesc(rule) }}</text>
+        </view>
+      </view>
+
       <!-- 升级记录 -->
       <view class="records-section" v-if="records.length > 0">
         <text class="section-title">升级记录</text>
@@ -184,7 +193,7 @@
 </template>
 
 <script>
-import { getMyMembership, getMembershipPlans, getUpgradeRecords, upgradeMember, createOrder, getCouponList, requestVirtualPayment } from '../../utils/api.js'
+import { getMyMembership, getMembershipPlans, getUpgradeRecords, upgradeMember, createOrder, getCouponList, requestVirtualPayment, getJoinCouponRules } from '../../utils/api.js'
 
 export default {
   data() {
@@ -214,6 +223,7 @@ export default {
       ],
       selectedPeriod: 'yearly',
       selectedLevel: 'FAMILY',
+      joinRules: [],
     }
   },
   computed: {
@@ -240,8 +250,37 @@ export default {
   },
   onLoad() {
     this.loadData()
+    this.loadJoinRules()
   },
   methods: {
+    loadJoinRules() {
+      var that = this
+      getJoinCouponRules().then(function(res) {
+        if (res.code === 200) {
+          that.joinRules = res.data || []
+        }
+      }).catch(function(e) {
+        console.error('加载开通赠券规则失败', e)
+      })
+    },
+    getLevelName(code) {
+      var map = {
+        FREE: '免费版',
+        FAMILY: '家庭会员',
+        PREMIUM: '高级会员'
+      }
+      return map[code] || code || ''
+    },
+    getJoinRuleKey(rule, idx) {
+      return rule.id + '-' + idx
+    },
+    getCouponDesc(item) {
+      if (!item) return ''
+      if (item.type === 'DISCOUNT') {
+        return '折扣券 ' + (item.discountRate / 100).toFixed(1) + '折'
+      }
+      return '立减 ' + (item.value / 100).toFixed(2) + '元'
+    },
     async loadData() {
       try {
         const membershipRes = await getMyMembership()
@@ -739,6 +778,34 @@ export default {
   color: #F97316;
 }
 
+/* ===== 开通赠券 ===== */
+.join-rules-section {
+  background: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 4rpx 24rpx rgba(249, 115, 22, 0.08);
+}
+.join-rule-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #FED7AA;
+}
+.join-rule-item:last-child {
+  border-bottom: none;
+}
+.join-rule-text {
+  flex: 1;
+  font-size: 26rpx;
+  color: #1E293B;
+}
+.join-rule-desc {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 600;
+}
+
 /* ===== 升级记录 ===== */
 .records-section {
   background: #FFFFFF;

+ 76 - 3
cfc-frontend/pages/profile-extra/coupons.vue

@@ -20,7 +20,33 @@
       :refresher-triggered="refreshing"
       @refresherrefresh="onRefresh"
     >
-      <view v-if="loading" class="loading-wrap">
+      <view v-if="tabIndex === 3" class="coupon-list">
+        <view
+          v-for="(item, index) in exchangeableList"
+          :key="getCouponKey(item, index)"
+          class="coupon-card"
+        >
+          <view class="card-left">
+            <text class="card-value">{{ item.pointsPrice }}</text>
+            <text class="card-type-label">积分</text>
+          </view>
+          <view class="card-right">
+            <view class="card-header">
+              <text class="card-name">{{ item.name }}</text>
+            </view>
+            <text class="card-condition">{{ getCouponDesc(item) }}</text>
+            <text class="card-scope">{{ getScopeText(item.applicableTo) }}</text>
+            <text class="card-expiry">有效期至 {{ formatDate(item.validUntil) }}</text>
+            <button class="claim-btn" @click="doExchange(item)">兑换</button>
+          </view>
+        </view>
+        <view v-if="!loading && exchangeableList.length === 0" class="empty-state">
+          <text class="empty-icon">🎫</text>
+          <text class="empty-text">暂无可用兑换的优惠券</text>
+          <text class="empty-hint">积分可以在商城中兑换优惠券</text>
+        </view>
+      </view>
+      <view v-else-if="loading" class="loading-wrap">
         <text class="loading-text">加载中...</text>
       </view>
       <view v-else-if="filteredList.length === 0" class="empty-state">
@@ -60,7 +86,7 @@
 </template>
 
 <script>
-import { getCouponList, claimCoupon } from '../../utils/api.js'
+import { getCouponList, claimCoupon, getExchangeableCoupons, exchangeCoupon } from '../../utils/api.js'
 
 export default {
   data() {
@@ -68,10 +94,12 @@ export default {
       tabs: [
         { name: '可使用', status: 'AVAILABLE' },
         { name: '已使用', status: 'USED' },
-        { name: '已过期', status: 'EXPIRED' }
+        { name: '已过期', status: 'EXPIRED' },
+        { name: '可兑换' }
       ],
       tabIndex: 0,
       coupons: [],
+      exchangeableList: [],
       loading: false,
       refreshing: false
     }
@@ -107,10 +135,12 @@ export default {
   },
   onShow() {
     this.loadCoupons()
+    this.loadExchangeable()
   },
   onPullDownRefresh() {
     this.refreshing = true
     this.loadCoupons()
+    this.loadExchangeable()
   },
   methods: {
     async loadCoupons() {
@@ -133,6 +163,49 @@ export default {
     onScrollToLower() {},
     switchTab(idx) {
       this.tabIndex = idx
+      if (idx === 3) {
+        this.loadExchangeable()
+      }
+    },
+    loadExchangeable() {
+      getExchangeableCoupons().then(res => {
+        if (res.code === 200) {
+          this.exchangeableList = res.data || []
+        }
+      }).catch(e => {
+        console.error('加载可兑换优惠券失败', e)
+      })
+    },
+    getCouponKey(item, index) {
+      return item.id + '-' + index
+    },
+    getCouponDesc(item) {
+      if (item.type === 'DISCOUNT') {
+        return '折扣券 ' + (item.discountRate / 100).toFixed(1) + '折'
+      }
+      return '立减 ' + (item.value / 100).toFixed(2) + '元'
+    },
+    doExchange(item) {
+      var self = this
+      uni.showModal({
+        title: '确认兑换',
+        content: '消耗 ' + item.pointsPrice + ' 积分兑换「' + item.name + '」?',
+        success: (r) => {
+          if (r.confirm) {
+            exchangeCoupon({ couponId: item.id }).then(res => {
+              if (res.code === 200) {
+                uni.showToast({ title: '兑换成功', icon: 'success' })
+                self.loadExchangeable()
+                self.loadCoupons()
+              } else {
+                uni.showToast({ title: res.message || '兑换失败', icon: 'none' })
+              }
+            }).catch(e => {
+              uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
+            })
+          }
+        }
+      })
     },
     canClaim(coupon) {
       return this.tabIndex === 0 && (!coupon.status || coupon.status === 'AVAILABLE')

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

@@ -2065,6 +2065,22 @@ export const claimCoupon = (couponId) => {
   return request('/api/coupon/claim', 'POST', { couponId })
 }
 
+export const getExchangeableCoupons = () => {
+  return request('/api/coupon/exchangeable', 'POST', {})
+}
+
+export const getProductCoupons = (data) => {
+  return request('/api/coupon/product', 'POST', data)
+}
+
+export const exchangeCoupon = (data) => {
+  return request('/api/coupon/exchange', 'POST', data)
+}
+
+export const getJoinCouponRules = () => {
+  return request('/api/coupon/join-rules', 'POST', {})
+}
+
 // ===== 新手任务(Onboarding Rewards) =====
 export const getOnboardingProgress = () => {
   return request('/api/onboarding/progress', 'POST')