Procházet zdrojové kódy

feat(frontend): 文章/商品/活动详情页增加分享功能 + 邀请码自动绑定

Xiaogang Liao před 1 měsícem
rodič
revize
7bfe55b81f

+ 113 - 0
cfc-frontend/components/share-mixin.js

@@ -0,0 +1,113 @@
+/**
+ * share-mixin — 统一分享逻辑
+ * 处理:邀请码获取、分享配置、自动绑定邀请码
+ *
+ * 使用方式:
+ *   mixins: [shareMixin],
+ *   mounted/after data load:
+ *     this.setShareInfo('推荐阅读: ' + title, '/pages/article-center/article-detail?id=' + id)
+ */
+import { getReferralCode, bindReferral } from '../utils/api.js'
+
+export default {
+  data() {
+    return {
+      referralCode: '',
+      shareTitle: '',
+      sharePath: ''
+    }
+  },
+  onLoad(options) {
+    // 自动绑定:从分享链接进入时,绑定邀请码
+    if (options && options.inviteCode) {
+      this.autoBindInviteCode(options.inviteCode)
+    }
+  },
+  methods: {
+    /**
+     * 设置分享信息(由各页面在数据加载完成后调用)
+     */
+    setShareInfo(title, path) {
+      this.shareTitle = title
+      this.sharePath = path
+      this.loadReferralCode()
+    },
+
+    /**
+     * 获取自己的邀请码
+     */
+    async loadReferralCode() {
+      if (this.referralCode) return
+      try {
+        var res = await getReferralCode()
+        if (res && res.code === 200 && res.data) {
+          this.referralCode = res.data.referralCode || res.data.code || ''
+        }
+      } catch (e) {
+        console.log('获取邀请码失败', e)
+      }
+    },
+
+    /**
+     * 自动绑定分享者的邀请码
+     * 规则:如果用户已有邀请人,则忽略此邀请码
+     */
+    async autoBindInviteCode(code) {
+      // 未登录时存 storage,登录后由 login 页面处理
+      if (!uni.getStorageSync('token')) {
+        uni.setStorageSync('inviteCode', code)
+        return
+      }
+
+      // 已绑定过则跳过
+      var bound = uni.getStorageSync('boundInviteCode')
+      if (bound === code) return
+
+      try {
+        var res = await bindReferral(code)
+        if (res && res.code === 200) {
+          uni.setStorageSync('boundInviteCode', code)
+          console.log('邀请码绑定成功', code)
+        } else if (res && res.code === 400) {
+          // 后端返回 400 表示已有邀请人,记录此 code 避免重复请求
+          uni.setStorageSync('boundInviteCode', code)
+          console.log('用户已有邀请人,跳过绑定', code)
+        }
+      } catch (e) {
+        console.log('邀请码绑定失败', e)
+      }
+    },
+
+    /**
+     * 拼接完整分享路径
+     */
+    getSharePath() {
+      if (!this.sharePath) return ''
+      if (this.referralCode) {
+        var sep = this.sharePath.indexOf('?') === -1 ? '?' : '&'
+        return this.sharePath + sep + 'inviteCode=' + this.referralCode
+      }
+      return this.sharePath
+    },
+
+    /**
+     * 分享按钮点击(用于统计等)
+     */
+    onShareTap() {
+      console.log('分享按钮点击')
+    }
+  },
+  onShareAppMessage() {
+    if (!this.shareTitle || !this.sharePath) {
+      return {
+        title: '浠艾福 — 给全家的一站式幸福提案',
+        path: '/pages/index/index'
+      }
+    }
+    return {
+      title: this.shareTitle,
+      path: this.getSharePath(),
+      imageUrl: ''
+    }
+  }
+}

+ 11 - 0
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -79,6 +79,10 @@
           <text class="bottom-vendor" v-if="activity.vendorName">{{ activity.vendorName }}</text>
         </view>
         <view class="action-btns">
+          <button class="share-btn" open-type="share" @click="onShareTap">
+            <text class="share-btn-icon">📤</text>
+            <text class="share-btn-text">分享</text>
+          </button>
           <!-- 报名按钮(仅 requireRegistration=1 时显示) -->
           <button
             v-if="activity.requireRegistration === 1 && !registrationStatus"
@@ -196,8 +200,10 @@
 
 <script>
 import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback } from '@/utils/api.js'
+import shareMixin from '../../components/share-mixin.js'
 
 export default {
+  mixins: [shareMixin],
   data() {
     return {
       activityId: null,
@@ -261,6 +267,7 @@ export default {
         self.loading = false
         if (res && res.code === 200 && res.data) {
           self.activity = res.data
+          self.setShareInfo('推荐活动: ' + (res.data.title || ''), '/pages/activity/activity-detail/activity-detail?id=' + id)
           self.parseFeedbackDimensions()
           self.loadFeedbackStatus()
           // Load registration status if logged in and activity requires registration
@@ -864,4 +871,8 @@ export default {
   border-radius: 40rpx; padding: 20rpx; font-size: 28rpx;
   text-align: center;
 }
+.share-btn { height: 64rpx; line-height: 64rpx; background: #f5f5f5; color: #666; font-size: 24rpx; border-radius: 32rpx; padding: 0 20rpx; display: flex; align-items: center; border: none; flex-shrink: 0; }
+.share-btn::after { border: none; }
+.share-btn-icon { font-size: 28rpx; margin-right: 4rpx; }
+.share-btn-text { font-size: 24rpx; }
 </style>

+ 11 - 0
cfc-frontend/pages/article-center/article-detail.vue

@@ -51,6 +51,10 @@
 
       <!-- 阅读完成 -->
       <view v-if="!showQuiz && !showResult" class="detail-footer">
+        <button class="share-btn" open-type="share" @click="onShareTap">
+          <text class="share-btn-icon">📤</text>
+          <text class="share-btn-text">分享</text>
+        </button>
         <button
           class="read-btn"
           @click="onReadComplete"
@@ -109,8 +113,10 @@
 
 <script>
 import { getArticleDetail, getAiQuestions, submitAnswers, recordArticleRead } from '@/utils/api.js'
+import shareMixin from '../../components/share-mixin.js'
 
 export default {
+  mixins: [shareMixin],
   data() {
     return {
       articleId: '',
@@ -164,6 +170,7 @@ export default {
         var res = await getArticleDetail({ id: id })
         if (res.code === 200 && res.data) {
           this.article = res.data
+          this.setShareInfo('推荐阅读: ' + (res.data.title || ''), '/pages/article-center/article-detail?id=' + id)
         } else {
           this.error = true
           this.errorMsg = '文章不存在或无权限查看'
@@ -370,6 +377,10 @@ export default {
 .detail-footer { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 30rpx; display: flex; align-items: center; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.06); z-index: 10; }
 .read-btn { flex: 1; height: 80rpx; line-height: 80rpx; background: linear-gradient(135deg, #5B9BD5, #3A7CC4); color: #fff; font-size: 30rpx; font-weight: bold; border-radius: 40rpx; text-align: center; border: none; }
 .read-btn::after { border: none; }
+.share-btn { height: 72rpx; line-height: 72rpx; background: #f5f5f5; color: #666; font-size: 24rpx; border-radius: 36rpx; padding: 0 24rpx; margin-right: 16rpx; display: flex; align-items: center; border: none; flex-shrink: 0; }
+.share-btn::after { border: none; }
+.share-btn-icon { font-size: 28rpx; margin-right: 6rpx; }
+.share-btn-text { font-size: 24rpx; }
 /* 答题区 */
 .quiz-container { padding: 30rpx; }
 .quiz-header { text-align: center; margin-bottom: 40rpx; }

+ 11 - 0
cfc-frontend/pages/discover/product-detail/product-detail.vue

@@ -75,6 +75,10 @@
           <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         <view class="action-btns">
+          <button class="share-btn" open-type="share" @click="onShareTap">
+            <text class="share-btn-icon">📤</text>
+            <text class="share-btn-text">分享</text>
+          </button>
           <view class="cart-icon-wrap" @click="onAddToCart">
             <text class="cart-icon">🛒</text>
             <text v-if="cartCount > 0" class="cart-badge">{{ cartCount > 99 ? '99+' : cartCount }}</text>
@@ -93,8 +97,10 @@
 <script>
 import config from '@/config.js'
 import { productDetail, getMyMembership } from '@/utils/api.js'
+import shareMixin from '../../components/share-mixin.js'
 
 export default {
+  mixins: [shareMixin],
   data() {
     return {
       product: {},
@@ -181,6 +187,7 @@ export default {
         that.loading = false
         if (res.code === 200 && res.data) {
           that.product = res.data
+          that.setShareInfo('推荐商品: ' + (res.data.name || ''), '/pages/discover/product-detail/product-detail?id=' + id)
           that.loadSpecs(id)
           that.loadSuppliers(id)
         }
@@ -593,4 +600,8 @@ export default {
   border-radius: 16rpx;
   padding: 0 6rpx;
 }
+.share-btn { height: 64rpx; line-height: 64rpx; background: #f5f5f5; color: #666; font-size: 24rpx; border-radius: 32rpx; padding: 0 20rpx; margin-right: 12rpx; display: flex; align-items: center; border: none; flex-shrink: 0; }
+.share-btn::after { border: none; }
+.share-btn-icon { font-size: 28rpx; margin-right: 4rpx; }
+.share-btn-text { font-size: 24rpx; }
 </style>

+ 19 - 1
cfc-frontend/pages/login/login.vue

@@ -55,7 +55,7 @@
 </template>
 
 <script>
-import { wechatLogin, checkTeamInviteStatus, joinTeamByInvite } from '../../utils/api.js'
+import { wechatLogin, checkTeamInviteStatus, joinTeamByInvite, bindReferral } from '../../utils/api.js'
 
 export default {
   data() {
@@ -181,10 +181,28 @@ export default {
 
       if (inviteCode && inviteType === 'team') {
         this.handleTeamInviteFlow(inviteCode)
+      } else if (inviteCode) {
+        // 非 team 邀请码(如分享文章/商品/活动的邀请码),绑定推荐关系
+        this.handleReferralBind(inviteCode)
       } else {
         this.navigateToHome()
       }
     },
+    async handleReferralBind(inviteCode) {
+      try {
+        var res = await bindReferral(inviteCode)
+        if (res && res.code === 200) {
+          console.log('登录后绑定邀请码成功', inviteCode)
+        } else if (res && res.code === 400) {
+          console.log('用户已有邀请人,跳过绑定')
+        }
+      } catch (e) {
+        console.log('邀请码绑定失败', e)
+      }
+      uni.removeStorageSync('inviteCode')
+      uni.removeStorageSync('inviteType')
+      this.navigateToHome()
+    },
     async handleTeamInviteFlow(inviteCode) {
       try {
         const res = await checkTeamInviteStatus(inviteCode)