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

feat(coupon-share): 优惠券分享转赠 T03 前端——share-claim 落地页+券列表分享入口+分享记录tab

Agnes Agent 2 часов назад
Родитель
Сommit
9dc097eab7

+ 11 - 0
cfc-frontend/pages.json

@@ -2045,6 +2045,17 @@
           }
           }
         }
         }
       ]
       ]
+    },
+    {
+      "root": "pages/coupon",
+      "pages": [
+        {
+          "path": "share-claim",
+          "style": {
+            "navigationBarTitleText": "好友送你一张会员券"
+          }
+        }
+      ]
     }
     }
   ],
   ],
   "globalStyle": {
   "globalStyle": {

+ 375 - 0
cfc-frontend/pages/coupon/share-claim.vue

@@ -0,0 +1,375 @@
+<template>
+  <view class="page" :style="{ '--font-scale': fontScale }">
+    <!-- 登录浮层(全局 login-sheet 组件,mixin 提供 openLoginSheet) -->
+    <login-sheet ref="loginSheet" @success="onSheetLoginSuccess" />
+
+    <view v-if="loading" class="state-box">
+      <text class="state-text">加载中...</text>
+    </view>
+
+    <block v-else>
+      <!-- 标题:正常态 / 失效态 -->
+      <view class="hero">
+        <text class="hero-emoji">🎁</text>
+        <text class="hero-text">{{ isInvalid ? '分享已失效' : '好友送你一张会员券' }}</text>
+      </view>
+
+      <!-- 券卡片(token 无效无 detail,不渲染卡片) -->
+      <view v-if="detail" class="coupon-card" :class="isInvalid ? 'coupon-card-disabled' : ''">
+        <view class="card-left">
+          <text class="card-value">{{ formatValue(detail) }}</text>
+          <text class="card-type-label">{{ typeLabel(detail.couponType) }}</text>
+        </view>
+        <view class="card-right">
+          <text class="card-name">{{ detail.couponName }}</text>
+          <text class="card-condition">{{ conditionText(detail.minSpend) }}</text>
+          <text class="card-scope">会员专享</text>
+          <text class="card-expiry">有效期至 {{ formatDate(detail.validUntil) }}</text>
+        </view>
+      </view>
+
+      <!-- 赠送人信息(正常态展示) -->
+      <view v-if="!isInvalid && detail && detail.giverNickname" class="giver-row">
+        <image v-if="detail.giverAvatar" class="giver-avatar" :src="detail.giverAvatar" mode="aspectFill"></image>
+        <view v-else class="giver-avatar giver-avatar-default">{{ giverInitial }}</view>
+        <text class="giver-name">赠送人:{{ detail.giverNickname }}</text>
+      </view>
+
+      <!-- 正常态:确认领取 -->
+      <view v-if="!isInvalid" class="action-area">
+        <button class="claim-btn" :disabled="claiming" @click="onConfirmClaim">{{ claiming ? '领取中...' : '确认领取' }}</button>
+        <text class="claim-tip">领取后可用于购买会员</text>
+      </view>
+
+      <!-- 失效态:提示 + 知道了 -->
+      <view v-else class="action-area">
+        <text class="invalid-tip">⚠ {{ invalidText }}</text>
+        <button class="know-btn" @click="onKnow">知道了</button>
+      </view>
+    </block>
+  </view>
+</template>
+
+<script>
+import { getCouponShareDetail, claimCouponShare } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
+import fontScaleMixin from '@/mixins/font-scale.js'
+
+export default {
+  mixins: [fontScaleMixin],
+  data() {
+    return {
+      shareToken: '',
+      loading: true,
+      detail: null,
+      loadError: '',
+      claimError: '',
+      claiming: false
+    }
+  },
+  computed: {
+    isInvalid: function() {
+      if (this.loading) return false
+      if (this.loadError || this.claimError) return true
+      if (!this.detail) return true
+      if (this.detail.status !== 'PENDING') return true
+      if (this.detail.claimable !== true) return true
+      return false
+    },
+    invalidText: function() {
+      if (this.loadError) return this.loadError
+      if (this.claimError) return this.claimError
+      if (!this.detail) return '分享不存在或已失效'
+      var status = this.detail.status || ''
+      if (status === 'CLAIMED') return '该分享已被领取'
+      if (status === 'REVOKED') return '该分享已撤回'
+      if (status === 'EXPIRED') return '该分享已过期'
+      if (this.detail.claimable !== true) return '优惠券已失效'
+      return '分享不存在或已失效'
+    },
+    giverInitial: function() {
+      var name = this.detail && this.detail.giverNickname
+      if (!name) return '友'
+      return name.substring(0, 1)
+    }
+  },
+  onLoad(options) {
+    this.shareToken = (options && options.shareToken) || ''
+    this.loadDetail()
+  },
+  methods: {
+    loadDetail() {
+      var self = this
+      if (!this.shareToken) {
+        this.loadError = '分享不存在或已失效'
+        this.loading = false
+        return
+      }
+      this.loading = true
+      this.loadError = ''
+      this.claimError = ''
+      getCouponShareDetail(this.shareToken).then(function(res) {
+        self.detail = (res && res.data) || null
+      }).catch(function(e) {
+        self.loadError = (e && e.message) || '分享不存在或已失效'
+      }).then(function() {
+        self.loading = false
+      })
+    },
+    onConfirmClaim() {
+      if (this.claiming || this.isInvalid) return
+      // 未登录先弹登录浮层,登录成功后经 onLoginSuccess 自动继续领取
+      if (!uni.getStorageSync('token')) {
+        var self = this
+        this.openLoginSheet({ onSuccess: function() { self.doClaim() } })
+        return
+      }
+      this.doClaim()
+    },
+    // 登录浮层登录成功回调(login-sheet mixin onSheetLoginSuccess 调用)
+    onLoginSuccess() {
+      this.doClaim()
+    },
+    doClaim() {
+      var self = this
+      if (this.claiming || !this.shareToken) return
+      this.claiming = true
+      claimCouponShare(this.shareToken).then(function(res) {
+        if (res && res.code === 200) {
+          uni.showToast({ title: '领取成功', icon: 'success' })
+          setTimeout(function() {
+            uni.redirectTo({ url: '/pages/profile-extra/coupons' })
+          }, 1200)
+        } else {
+          self.claimError = (res && res.message) || '领取失败'
+        }
+      }).catch(function(e) {
+        self.claimError = (e && e.message) || '领取失败'
+      }).then(function() {
+        self.claiming = false
+      })
+    },
+    onKnow() {
+      var pages = getCurrentPages()
+      if (pages.length > 1) {
+        uni.navigateBack({ delta: 1 })
+      } else {
+        uni.switchTab({ url: '/pages/index-home/index' })
+      }
+    },
+    formatValue(detail) {
+      var type = detail.couponType
+      var value = detail.couponValue
+      if (type === 'DISCOUNT' || type === 'PERCENT') {
+        return (value ? (value / 100).toFixed(1) : '9.0') + '折'
+      }
+      return '¥' + (value ? (value / 100).toFixed(2) : '0.00')
+    },
+    typeLabel(type) {
+      if (type === 'FIXED') return '满减券'
+      if (type === 'DISCOUNT' || type === 'PERCENT') return '折扣券'
+      if (type === 'CASH') return '无门槛券'
+      return '会员券'
+    },
+    conditionText(minSpend) {
+      if (!minSpend || minSpend <= 0) return '无门槛'
+      return '满' + (minSpend / 100).toFixed(2) + '元可用'
+    },
+    formatDate(date) {
+      if (!date) return '长期有效'
+      var d = parseDate(date)
+      if (!d) return String(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;
+  padding: 40rpx $uni-space-page;
+  box-sizing: border-box;
+}
+
+/* ===== 加载态 ===== */
+.state-box {
+  display: flex;
+  justify-content: center;
+  padding-top: 240rpx;
+}
+.state-text {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+}
+
+/* ===== 标题 ===== */
+.hero {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 24rpx 0 40rpx;
+}
+.hero-emoji {
+  font-size: calc(40rpx * var(--font-scale, 1));
+  margin-right: 12rpx;
+}
+.hero-text {
+  font-size: calc(40rpx * var(--font-scale, 1));
+  font-weight: bold;
+  color: $facade-text-strong;
+}
+
+/* ===== 券卡片 ===== */
+.coupon-card {
+  display: flex;
+  background: $app-surface;
+  border-radius: 16rpx;
+  margin-bottom: 24rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.coupon-card-disabled {
+  opacity: 0.55;
+}
+.card-left {
+  width: 220rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+  padding: $uni-space-lg 12rpx;
+  flex-shrink: 0;
+}
+.coupon-card-disabled .card-left {
+  background: #c0c0c0;
+}
+.card-value {
+  font-size: calc(44rpx * 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.85);
+  margin-top: 8rpx;
+}
+.card-right {
+  flex: 1;
+  padding: 28rpx 24rpx;
+  display: flex;
+  flex-direction: column;
+}
+.card-name {
+  font-size: calc(30rpx * var(--font-scale, 1));
+  font-weight: 600;
+  color: $facade-text-strong;
+  display: block;
+  margin-bottom: 10rpx;
+  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: 8rpx;
+}
+.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: 8rpx;
+  align-self: flex-start;
+}
+.card-expiry {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $facade-text-ghost-light;
+  display: block;
+}
+
+/* ===== 赠送人 ===== */
+.giver-row {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 8rpx 0 32rpx;
+}
+.giver-avatar {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+  background: $brand-primary-light-2;
+}
+.giver-avatar-default {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: $app-surface;
+  font-size: calc(30rpx * var(--font-scale, 1));
+}
+.giver-name {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  color: $facade-text-strong;
+}
+
+/* ===== 操作区 ===== */
+.action-area {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 32rpx;
+}
+.claim-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+  color: $app-surface;
+  font-size: calc(32rpx * var(--font-scale, 1));
+  font-weight: 600;
+  border: none;
+}
+.claim-btn::after {
+  border: none;
+}
+.claim-btn[disabled] {
+  opacity: 0.7;
+}
+.claim-tip {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+  margin-top: 20rpx;
+}
+.invalid-tip {
+  font-size: calc(26rpx * var(--font-scale, 1));
+  color: $status-danger-2;
+  display: block;
+  margin-bottom: 32rpx;
+  text-align: center;
+}
+.know-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  background: $facade-fill;
+  color: $facade-text-strong;
+  font-size: calc(32rpx * var(--font-scale, 1));
+  border: none;
+}
+.know-btn::after {
+  border: none;
+}
+</style>

+ 75 - 5
cfc-frontend/pages/membership/gift-coupon.vue

@@ -65,7 +65,7 @@
     </scroll-view>
     </scroll-view>
 
 
     <!-- 赠送记录 -->
     <!-- 赠送记录 -->
-    <scroll-view v-else scroll-y class="content" refresher-enabled
+    <scroll-view v-else-if="activeTab === 1" scroll-y class="content" refresher-enabled
       :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
       :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
       <view v-if="sentList.length === 0" class="empty-state">
       <view v-if="sentList.length === 0" class="empty-state">
         <text class="empty-icon">🎁</text>
         <text class="empty-icon">🎁</text>
@@ -84,6 +84,26 @@
         <button v-if="item.status === 'GIFTED'" class="revoke-btn" @click="doRevoke(item)">撤回</button>
         <button v-if="item.status === 'GIFTED'" class="revoke-btn" @click="doRevoke(item)">撤回</button>
       </view>
       </view>
     </scroll-view>
     </scroll-view>
+
+    <!-- 分享记录(P1-2,分享转赠通道) -->
+    <scroll-view v-else scroll-y class="content" refresher-enabled
+      :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
+      <view v-if="shareList.length === 0" class="empty-state">
+        <text class="empty-icon">🎁</text>
+        <text class="empty-text">还没有分享记录</text>
+      </view>
+      <view v-for="item in shareList" :key="item.shareToken" class="sent-item">
+        <view class="sent-row">
+          <text class="sent-name">{{ item.couponName }}</text>
+          <text class="sent-status" :class="'sh-' + item.status">{{ shareStatusLabel(item.status) }}</text>
+        </view>
+        <view class="sent-row sub">
+          <text>有效期至 {{ formatDate(item.expireAt) }}</text>
+          <text v-if="item.status === 'CLAIMED' && item.claimNickname">由 {{ item.claimNickname }} 领取</text>
+        </view>
+        <button v-if="item.status === 'PENDING'" class="revoke-btn" @click="doRevokeShare(item)">撤回</button>
+      </view>
+    </scroll-view>
   </view>
   </view>
 </template>
 </template>
 
 
@@ -93,13 +113,16 @@ import {
   getGiftableCoupons,
   getGiftableCoupons,
   giftCoupon,
   giftCoupon,
   revokeGiftCoupon,
   revokeGiftCoupon,
-  getSentGifts
+  getSentGifts,
+  getCouponShareList,
+  revokeCouponShare
 } from '../../utils/api.js'
 } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
 
 
 export default {
 export default {
   data() {
   data() {
     return {
     return {
-      tabs: [{ name: '赠送会员券' }, { name: '赠送记录' }],
+      tabs: [{ name: '赠送会员券' }, { name: '赠送记录' }, { name: '分享记录' }],
       activeTab: 0,
       activeTab: 0,
       recipients: [],
       recipients: [],
       coupons: [],
       coupons: [],
@@ -108,6 +131,7 @@ export default {
       remark: '',
       remark: '',
       sending: false,
       sending: false,
       sentList: [],
       sentList: [],
+      shareList: [],
       refreshing: false
       refreshing: false
     }
     }
   },
   },
@@ -138,6 +162,7 @@ export default {
         self.refreshing = false
         self.refreshing = false
       })
       })
       self.loadSent()
       self.loadSent()
+      self.loadShares()
     },
     },
     loadSent: function() {
     loadSent: function() {
       var self = this
       var self = this
@@ -147,11 +172,22 @@ export default {
         console.error('加载赠送记录失败', e)
         console.error('加载赠送记录失败', e)
       })
       })
     },
     },
+    loadShares: function() {
+      var self = this
+      getCouponShareList().then(function(res) {
+        self.shareList = (res && res.data) || []
+      }).catch(function(e) {
+        console.error('加载分享记录失败', e)
+      })
+    },
     onRefresh: function() {
     onRefresh: function() {
       this.loadAll()
       this.loadAll()
     },
     },
     switchTab: function(idx) {
     switchTab: function(idx) {
       this.activeTab = idx
       this.activeTab = idx
+      if (idx === 2) {
+        this.loadShares()
+      }
     },
     },
     pickRecipient: function(r) {
     pickRecipient: function(r) {
       this.selectedRecipient = r
       this.selectedRecipient = r
@@ -201,6 +237,36 @@ export default {
         }
         }
       })
       })
     },
     },
+    // 撤回分享(仅 PENDING 态):分享记录 tab
+    doRevokeShare: function(item) {
+      var self = this
+      uni.showModal({
+        title: '撤回分享',
+        content: '确定撤回「' + item.couponName + '」的分享吗?撤回后好友将无法领取。',
+        success: function(m) {
+          if (!m.confirm) return
+          revokeCouponShare(item.shareToken).then(function(res) {
+            if (res && res.code === 200) {
+              uni.showToast({ title: '已撤回', icon: 'success' })
+              self.loadShares()
+            } else {
+              uni.showToast({ title: (res && res.message) || '撤回失败', icon: 'none' })
+            }
+          }).catch(function(e) {
+            uni.showToast({ title: (e && e.message) || '撤回失败', icon: 'none' })
+            console.error(e)
+          })
+        }
+      })
+    },
+    // 分享记录状态文案(PENDING/CLAIMED/REVOKED/EXPIRED)
+    shareStatusLabel: function(status) {
+      if (status === 'PENDING') return '待领取'
+      if (status === 'CLAIMED') return '已领取'
+      if (status === 'REVOKED') return '已撤回'
+      if (status === 'EXPIRED') return '已过期'
+      return status || ''
+    },
     formatValue: function(c) {
     formatValue: function(c) {
       if (c.type === 'DISCOUNT') {
       if (c.type === 'DISCOUNT') {
         return (c.discountRate ? (c.discountRate / 100).toFixed(1) : '9.0') + '折'
         return (c.discountRate ? (c.discountRate / 100).toFixed(1) : '9.0') + '折'
@@ -218,8 +284,8 @@ export default {
     },
     },
     formatDate: function(d) {
     formatDate: function(d) {
       if (!d) return '长期有效'
       if (!d) return '长期有效'
-      var dt = new Date(d)
-      if (isNaN(dt.getTime())) return String(d)
+      var dt = parseDate(d)
+      if (!dt) return String(d)
       var m = dt.getMonth() + 1
       var m = dt.getMonth() + 1
       var day = dt.getDate()
       var day = dt.getDate()
       return dt.getFullYear() + '-' + (m < 10 ? '0' + m : m) + '-' + (day < 10 ? '0' + day : day)
       return dt.getFullYear() + '-' + (m < 10 ? '0' + m : m) + '-' + (day < 10 ? '0' + day : day)
@@ -433,6 +499,10 @@ export default {
 .st-GIFTED { color: #ff6b35; }
 .st-GIFTED { color: #ff6b35; }
 .st-USED { color: #999; }
 .st-USED { color: #999; }
 .st-REVOKED { color: #bbb; }
 .st-REVOKED { color: #bbb; }
+.sh-PENDING { color: #ff6b35; }
+.sh-CLAIMED { color: #52c41a; }
+.sh-REVOKED { color: #bbb; }
+.sh-EXPIRED { color: #bbb; }
 .sent-remark {
 .sent-remark {
   font-size: 24rpx;
   font-size: 24rpx;
   color: #888;
   color: #888;

+ 74 - 2
cfc-frontend/pages/profile-extra/coupons.vue

@@ -81,6 +81,8 @@
             <view v-if="tabIndex === 0 && (!coupon.status || coupon.status === 'AVAILABLE')" class="coupon-actions">
             <view v-if="tabIndex === 0 && (!coupon.status || coupon.status === 'AVAILABLE')" class="coupon-actions">
               <button class="use-btn" @click="useCoupon(coupon)">使用</button>
               <button class="use-btn" @click="useCoupon(coupon)">使用</button>
               <button class="gift-btn" @click="giftCoupon(coupon)">转赠</button>
               <button class="gift-btn" @click="giftCoupon(coupon)">转赠</button>
+              <button v-if="shareTokenMap[coupon.id]" class="share-btn" open-type="share" @tap="setShareTarget(coupon)">发送给好友</button>
+              <button v-else class="share-btn" @click="prepareShare(coupon)">分享</button>
             </view>
             </view>
           </view>
           </view>
         </view>
         </view>
@@ -90,7 +92,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { getMyCoupons, getExchangeableCoupons, exchangeCouponByCf, getMyMembership, productDetail } from '../../utils/api.js'
+import { getMyCoupons, getExchangeableCoupons, exchangeCouponByCf, getMyMembership, productDetail, createCouponShare } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 
@@ -109,7 +111,9 @@ export default {
       exchangeableList: [],
       exchangeableList: [],
       loading: false,
       loading: false,
       refreshing: false,
       refreshing: false,
-      isMember: false
+      isMember: false,
+      shareTokenMap: {},
+      shareTarget: null
     }
     }
   },
   },
   computed: {
   computed: {
@@ -151,6 +155,26 @@ export default {
     this.loadCoupons()
     this.loadCoupons()
     this.loadExchangeable()
     this.loadExchangeable()
   },
   },
+  // 分享转赠(两段式):shareToken 预备在 shareTokenMap 后,onShareAppMessage 同步返回带 token 的 path
+  onShareAppMessage() {
+    var target = this.shareTarget
+    var token = ''
+    if (target && target.id) {
+      token = this.shareTokenMap[target.id] || ''
+    }
+    if (token) {
+      return {
+        title: '送你一张「' + target.name + '」会员券,快来领取',
+        path: '/pages/coupon/share-claim?shareToken=' + token,
+        imageUrl: ''
+      }
+    }
+    return {
+      title: '我有一张会员券想送给你,快来领取!',
+      path: '/pages/profile-extra/coupons',
+      imageUrl: ''
+    }
+  },
   methods: {
   methods: {
     async loadCoupons() {
     async loadCoupons() {
       this.loading = true
       this.loading = true
@@ -277,6 +301,34 @@ export default {
     giftCoupon(coupon) {
     giftCoupon(coupon) {
       uni.navigateTo({ url: '/pages/membership/gift-coupon' })
       uni.navigateTo({ url: '/pages/membership/gift-coupon' })
     },
     },
+    // 分享转赠第一步:点「分享」→ 异步 create 拿 token(幂等:已有 PENDING 返回同一 token)→ 切换为「发送给好友」
+    prepareShare(coupon) {
+      var self = this
+      if (this.shareTokenMap[coupon.id]) {
+        this.shareTarget = coupon
+        return
+      }
+      uni.showLoading({ title: '生成中...' })
+      createCouponShare(coupon.id).then(function(res) {
+        if (res && res.code === 200 && res.data && res.data.shareToken) {
+          var map = Object.assign({}, self.shareTokenMap)
+          map[coupon.id] = res.data.shareToken
+          self.shareTokenMap = map
+          self.shareTarget = coupon
+        } else {
+          uni.showToast({ title: (res && res.message) || '分享失败', icon: 'none' })
+        }
+      }).catch(function(e) {
+        uni.showToast({ title: (e && e.message) || '分享失败,请重试', icon: 'none' })
+        console.error('创建分享失败', e)
+      }).then(function() {
+        uni.hideLoading()
+      })
+    },
+    // 分享转赠第二步:点击「发送给好友」(open-type=share)时记录当前分享目标,供 onShareAppMessage 取 token
+    setShareTarget(coupon) {
+      this.shareTarget = coupon
+    },
     getConditionText(minSpend) {
     getConditionText(minSpend) {
       if (!minSpend || minSpend <= 0) return '无门槛'
       if (!minSpend || minSpend <= 0) return '无门槛'
       return '满' + (minSpend / 100).toFixed(2) + '元可用'
       return '满' + (minSpend / 100).toFixed(2) + '元可用'
@@ -618,6 +670,26 @@ export default {
   opacity: 0.85;
   opacity: 0.85;
 }
 }
 
 
+.share-btn {
+  background: $app-surface;
+  color: $brand-primary;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  padding: 10rpx 24rpx;
+  border-radius: 24rpx;
+  border: 2rpx solid $brand-primary-light-2;
+  line-height: 1.4;
+  height: auto;
+  margin: 0;
+}
+
+.share-btn::after {
+  border: none;
+}
+
+.share-btn:active {
+  opacity: 0.85;
+}
+
 .gift-entry {
 .gift-entry {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

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

@@ -2730,6 +2730,23 @@ export const getSentGifts = () => {
   return request('/api/coupon/gift/sent', 'POST', {})
   return request('/api/coupon/gift/sent', 'POST', {})
 }
 }
 
 
+// ===== 会员券分享转赠(ARCH-CFC-COUPON-SHARE-GIFT-001 §5,契约登记见 API_REFERENCE §4.32) =====
+export const createCouponShare = (familyCouponId) => {
+  return request('/api/coupon/gift/share/create', 'POST', { familyCouponId })
+}
+export const getCouponShareDetail = (shareToken) => {
+  return request('/api/coupon/gift/share/detail', 'POST', { shareToken })
+}
+export const claimCouponShare = (shareToken) => {
+  return request('/api/coupon/gift/share/claim', 'POST', { shareToken })
+}
+export const revokeCouponShare = (shareToken) => {
+  return request('/api/coupon/gift/share/revoke', 'POST', { shareToken })
+}
+export const getCouponShareList = () => {
+  return request('/api/coupon/gift/share/list', 'POST', {})
+}
+
 // ===== 新手任务(Onboarding Rewards) =====
 // ===== 新手任务(Onboarding Rewards) =====
 export const getOnboardingProgress = () => {
 export const getOnboardingProgress = () => {
   return request('/api/onboarding/progress', 'POST')
   return request('/api/onboarding/progress', 'POST')