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

feat(coupon): C3 用户端新增领券中心页 coupon-center 并接入空状态跳转

【COUPON-C3】补齐用户端独立「领券中心」页面(P1 用户触达):
- cfc-frontend/pages/profile-extra/coupon-center.vue 新建领券中心页:
  调 /api/coupon/list 拉取可取券列表 + /api/coupon/claim 领取(couponId=券模板id)
- pages.json 在 pages/profile-extra 分包注册 coupon-center(标题:领券中心)
- coupons.vue 空状态文案改为可点击跳转真实领券中心页(goToCouponCenter)

核验:mvn compile BUILD SUCCESS(1958 源文件);verify-vue-compile PASS(模板/样式/脚本 2/2);
audit-mp-antipatterns ERROR 0;cfc-web vue-cli-service build DONE(exit 0)。
E2E Test Bot 2 недель назад
Родитель
Сommit
30bb2255d7

+ 6 - 0
cfc-frontend/pages.json

@@ -1782,6 +1782,12 @@
             "navigationBarTitleText": "我的优惠券"
           }
         },
+        {
+          "path": "coupon-center",
+          "style": {
+            "navigationBarTitleText": "领券中心"
+          }
+        },
         {
           "path": "gates",
           "style": {

+ 281 - 0
cfc-frontend/pages/profile-extra/coupon-center.vue

@@ -0,0 +1,281 @@
+<template>
+  <view class="page" :style="{ '--font-scale': fontScale }">
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <view v-else-if="couponList.length === 0" class="empty-state">
+      <text class="empty-icon">🎫</text>
+      <text class="empty-text">暂无可领取的优惠券</text>
+      <text class="empty-hint">平台发放的优惠券将在这里展示,敬请期待</text>
+    </view>
+    <scroll-view v-else scroll-y class="coupon-scroll">
+      <view class="coupon-list">
+        <view v-for="item in couponList" :key="item.id" class="coupon-card">
+          <view class="card-left">
+            <text class="card-value">{{ formatPriceWithSymbol(item.value) }}</text>
+            <text class="card-type-label">{{ getTypeLabel(item.type) }}</text>
+          </view>
+          <view class="card-right">
+            <view class="card-header">
+              <text class="card-name">{{ item.name }}</text>
+            </view>
+            <text class="card-condition">{{ getConditionText(item) }}</text>
+            <text class="card-scope">{{ getScopeText(item.applicableTo) }}</text>
+            <text class="card-expiry">有效期至 {{ formatDate(item.validUntil) }}</text>
+            <button class="claim-btn" @click="handleClaim(item)">立即领取</button>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { getCouponList, claimCoupon } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
+import fontScaleMixin from '@/mixins/font-scale.js'
+
+export default {
+  mixins: [fontScaleMixin],
+  data() {
+    return {
+      couponList: [],
+      loading: false,
+      claiming: false
+    }
+  },
+  onLoad() {
+    this.loadCoupons()
+    this._onLoadFired = true
+  },
+  onShow() {
+    // 首次进入由 onLoad 加载,避免 onLoad/onShow 双发;返回本页时刷新
+    if (this._onLoadFired) {
+      this._onLoadFired = false
+    } else {
+      this.loadCoupons()
+    }
+  },
+  methods: {
+    loadCoupons() {
+      var self = this
+      this.loading = true
+      getCouponList().then(function(res) {
+        if (res && res.code === 200) {
+          self.couponList = res.data || []
+        } else {
+          self.couponList = []
+        }
+      }).catch(function(e) {
+        console.error('加载领券中心失败', e)
+        self.couponList = []
+      }).then(function() {
+        self.loading = false
+      })
+    },
+    handleClaim(item) {
+      if (this.claiming) return
+      var self = this
+      uni.showModal({
+        title: '确认领取',
+        content: '确认领取「' + (item.name || '') + '」优惠券?',
+        success: function(r) {
+          if (!r.confirm) return
+          self.claiming = true
+          // /api/coupon/claim 契约:couponId 为券模板 id(list 返回的 couponId 字段)
+          claimCoupon(item.couponId || item.id).then(function(res) {
+            self.claiming = false
+            if (res.code === 200) {
+              uni.showToast({ title: res.message || '领取成功', icon: 'success' })
+              self.loadCoupons()
+            } else {
+              uni.showToast({ title: res.message || '领取失败', icon: 'none' })
+            }
+          }).catch(function(e) {
+            self.claiming = false
+            uni.showToast({ title: (e && e.message) || '领取失败', icon: 'none' })
+          })
+        }
+      })
+    },
+    getTypeLabel(type) {
+      if (type === 'FIXED') return '满减券'
+      if (type === 'CASH') return '无门槛券'
+      if (type === 'DISCOUNT') return '折扣券'
+      return type || ''
+    },
+    getConditionText(item) {
+      if (item.type === 'DISCOUNT') {
+        return '折扣券 ' + ((item.discountRate || 0) / 100).toFixed(1) + '折'
+      }
+      if (item.minSpend && item.minSpend > 0) {
+        return '满' + (item.minSpend / 100).toFixed(2) + '元可用'
+      }
+      return '无门槛'
+    },
+    getScopeText(applicableTo) {
+      if (applicableTo === 'ALL') return '全品类适用'
+      if (applicableTo === 'MEMBERSHIP') return '会员专享'
+      return applicableTo || '全品类适用'
+    },
+    formatDate(date) {
+      if (!date) return ''
+      var d = parseDate(date)
+      if (!d) return date
+      var y = d.getFullYear()
+      var m = String(d.getMonth() + 1).padStart(2, '0')
+      var day = String(d.getDate()).padStart(2, '0')
+      return y + '.' + m + '.' + day
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.page {
+  min-height: 100vh;
+  background: $facade-fill-2;
+}
+
+.coupon-scroll {
+  height: 100vh;
+}
+
+.loading-wrap {
+  display: flex;
+  justify-content: center;
+  padding-top: 200rpx;
+}
+
+.loading-text {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+
+.empty-icon {
+  font-size: calc(80rpx * var(--font-scale, 1));
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+}
+
+.empty-hint {
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $facade-text-ghost;
+  margin-top: 12rpx;
+}
+
+.coupon-list {
+  padding: 20rpx;
+}
+
+.coupon-card {
+  display: flex;
+  background: $app-surface;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+
+.card-left {
+  width: 200rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+  padding: 30rpx 12rpx;
+  flex-shrink: 0;
+}
+
+.card-value {
+  font-size: calc(40rpx * var(--font-scale, 1));
+  font-weight: bold;
+  color: $app-surface;
+}
+
+.card-type-label {
+  font-size: calc(20rpx * var(--font-scale, 1));
+  color: rgba(255, 255, 255, 0.8);
+  margin-top: 8rpx;
+}
+
+.card-right {
+  flex: 1;
+  padding: 24rpx 24rpx 20rpx;
+  position: relative;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10rpx;
+}
+
+.card-name {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  font-weight: 600;
+  color: $facade-text-strong;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.card-condition {
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $brand-primary;
+  display: block;
+  margin-bottom: 6rpx;
+}
+
+.card-scope {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $uni-info;
+  background: $tint-sky-ghost;
+  display: inline-block;
+  padding: 2rpx 12rpx;
+  border-radius: 8rpx;
+  margin-bottom: 6rpx;
+}
+
+.card-expiry {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $facade-text-ghost-light;
+  display: block;
+}
+
+.claim-btn {
+  position: absolute;
+  right: 24rpx;
+  bottom: 20rpx;
+  background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+  color: $app-surface;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  padding: 10rpx 28rpx;
+  border-radius: 28rpx;
+  border: none;
+  line-height: 1.4;
+  height: auto;
+}
+
+.claim-btn::after {
+  border: none;
+}
+
+.claim-btn:active {
+  opacity: 0.85;
+}
+</style>

+ 12 - 1
cfc-frontend/pages/profile-extra/coupons.vue

@@ -52,7 +52,7 @@
       <view v-else-if="filteredList.length === 0" class="empty-state">
         <text class="empty-icon">🎫</text>
         <text class="empty-text">{{ tabIndex === 0 ? '暂无可用优惠券' : tabIndex === 1 ? '暂无已使用优惠券' : '暂无已过期优惠券' }}</text>
-        <text class="empty-hint" v-if="tabIndex === 0">去领券中心看看有哪些优惠吧</text>
+        <view class="empty-hint empty-hint-link" v-if="tabIndex === 0" @click="goToCouponCenter">去领券中心看看有哪些优惠吧 ›</view>
       </view>
       <view v-else class="coupon-list">
         <view
@@ -276,6 +276,9 @@ export default {
     },
     goToMembership() {
       uni.navigateTo({ url: '/pages/membership/index' })
+    },
+    goToCouponCenter() {
+      uni.navigateTo({ url: '/pages/profile-extra/coupon-center' })
     }
   }
 }
@@ -360,6 +363,14 @@ export default {
   margin-top: 12rpx;
 }
 
+.empty-hint-link {
+  color: $brand-primary;
+}
+
+.empty-hint-link:active {
+  opacity: 0.8;
+}
+
 .coupon-list {
   padding: 20rpx;
 }