Browse Source

refactor: move share QR to floating button on profile page
- remove invite share menu item from ProfileMenu
- remove share.vue page and its registration
- add floating QR code button in profile-main/profile.vue
- add share poster modal with QR code, copy and share actions
- add support for ref code on homepage onLoad

E2E Test Bot 1 day ago
parent
commit
b9a5cf59da

+ 0 - 6
cfc-frontend/pages.json

@@ -1802,12 +1802,6 @@
           "style": {
             "navigationBarTitleText": "服务角色申请"
           }
-        },
-        {
-          "path": "share",
-          "style": {
-            "navigationBarTitleText": "分享推广码"
-          }
         }
       ]
     },

+ 287 - 3
cfc-frontend/pages/profile-main/profile.vue

@@ -35,7 +35,6 @@
               <text class="nickname">{{ nickname || '未设置昵称' }}</text>
               <text v-if="roleLabel" class="role-tag">{{ roleLabel }}</text>
             </view>
-            <text v-if="myReferrer && myReferrer.nickname" class="referrer-line">推荐人:{{ myReferrer.nickname }}</text>
             <view class="user-sub" v-if="memberLevel">
               <view class="level-badge" :class="'level-' + memberLevel">{{ memberLevelText }}</view>
               <text v-if="isLifetime" class="days-left">永久有效</text>
@@ -58,7 +57,12 @@
         </view>
       </view>
 
-      <!-- 家庭成员速览 -->
+      <!-- 我的推广码浮动按钮 -->
+      <view class="share-qr-float" @click="showShareQr">
+        <text class="share-qr-icon">📱</text>
+      </view>
+
+      <!-- 家族成员速览 -->
       <view class="family-strip" v-if="familyMembers.length > 0">
         <view class="family-strip-header">
           <text class="family-strip-title">我的家庭</text>
@@ -261,6 +265,41 @@
       </view>
 
       </template>
+    <view class="share-qr-modal" v-if="showShareQrModal" @click="closeShareQrModal">
+      <view class="modal-mask" @click.stop />
+      <view class="modal-qr" @click.stop>
+        <view class="modal-qr-header">
+          <text class="modal-qr-title">我的分享海报</text>
+          <text class="modal-qr-close" @click="closeShareQrModal">✕</text>
+        </view>
+        <view class="modal-qr-content">
+          <view v-if="qrLoading" class="qrcode-loading">
+            <text class="loading-spinner">⏳</text>
+            <text class="loading-text">生成中...</text>
+          </view>
+          <view v-else="!qrBase64" class="qrcode-empty">
+            <text>暂无二维码</text>
+          </view>
+          <view v-else class="qrcode-image">
+            <image class="qrcode-img" :src="qrBase64" mode="aspectFit"></image>
+          </view>
+          <view class="qrcode-info">
+            <text class="qrcode-text">邀请码: {{ referralCode }}</text>
+            <text class="qrcode-desc">扫码注册,好友成为您的推荐人</text>
+          </view>
+        </view>
+        <view class="modal-qr-actions">
+          <view class="action-btn copy-btn" @click="copyReferralCode">
+            <text class="copy-icon">📋</text>
+            <text class="copy-text">复制邀请码</text>
+          </view>
+          <view class="action-btn share-btn" @click="shareToWechat">
+            <text class="share-icon">📤</text>
+            <text class="share-text">分享给朋友</text>
+          </view>
+        </view>
+      </view>
+    </view>
     <FloatingAvatar />
   </view>
 </template>
@@ -273,7 +312,8 @@ import {
   verifyPassword, getPointsBalance, getUserInfo,
   cartCount, getOrderCounts, getReferralSummary,
   getProfileStats, getMyReferrer, getTeamConsumption,
-  getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount
+  getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount,
+  getReferralCode, extractReferralCode, getShareQrCode
 } from '../../utils/api.js'
 
 export default {
@@ -311,6 +351,12 @@ export default {
       teacherStatus: 'pending',
       profileProgress: 0,
       myReferrer: null,
+      // 分享推广码
+      showShareQrModal: false,
+      referralCode: '',
+      qrBase64: '',
+      qrLoading: false,
+      referralCodeLoaded: false,
     }
   },
   computed: {
@@ -505,6 +551,81 @@ export default {
         console.error('获取推荐人失败', e)
       }
     },
+    // ===== 分享推广码 =====
+    showShareQr() {
+      this.showShareQrModal = true
+      if (!this.referralCodeLoaded) {
+        this.loadReferralCode()
+      } else if (this.referralCode && !this.qrBase64) {
+        this.generateQrcode()
+      }
+    },
+    closeShareQrModal() {
+      this.showShareQrModal = false
+    },
+    async loadReferralCode() {
+      if (this.referralCodeLoaded) return
+      var self = this
+      try {
+        var res = await getReferralCode()
+        var code = extractReferralCode(res)
+        self.referralCode = code || ''
+        self.referralCodeLoaded = true
+        if (code) {
+          self.generateQrcode()
+        }
+      } catch (e) {
+        console.error('获取推广码失败', e)
+        self.referralCodeLoaded = true
+      }
+    },
+    generateQrcode() {
+      if (!this.referralCode) return
+      this.qrLoading = true
+      var self = this
+      var scene = 'ref=' + this.referralCode
+      // 首页路径,扫码后进入首页并绑定推荐人
+      getShareQrCode('/pages/index-home/index', scene)
+        .then(function(res) {
+          if (res && res.code === 200 && res.data) {
+            self.qrBase64 = res.data.qrCodeBase64
+          }
+        })
+        .catch(function(e) {
+          console.error('生成二维码失败', e)
+          uni.showToast({ title: '生成失败,请重试', icon: 'none' })
+        })
+        .finally(function() {
+          self.qrLoading = false
+        })
+    },
+    copyReferralCode() {
+      if (!this.referralCode) return
+      uni.setClipboardData({
+        data: this.referralCode,
+        success: function() {
+          uni.showToast({ title: '邀请码已复制', icon: 'success' })
+        }
+      })
+    },
+    shareToWechat() {
+      if (!this.referralCode) return
+      var path = '/pages/index-home/index?refCode=' + this.referralCode
+      uni.share({
+        provider: 'weixin',
+        scene: { type: 0 },
+        type: 5,
+        imageUrl: this.qrBase64,
+        path: path,
+        title: '浠艾福 — 邀请你加入',
+        success: function() {
+          uni.showToast({ title: '分享成功', icon: 'success' })
+        },
+        fail: function() {
+          uni.showToast({ title: '分享失败', icon: 'none' })
+        }
+      })
+    },
     goToServiceCenter() {
       uni.navigateTo({ url: '/pages/service-center/index' })
     },
@@ -1170,4 +1291,167 @@ export default {
   color: #FF4D4F;
   font-weight: 500;
 }
+
+/* 分享推广码浮动按钮 */
+.share-qr-float {
+  position: fixed;
+  top: 40rpx;
+  right: 30rpx;
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  background: #fff;
+  box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.15);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 99;
+}
+.share-qr-icon {
+  font-size: 40rpx;
+  line-height: 1;
+}
+
+/* 分享海报弹窗 */
+.share-qr-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.modal-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+}
+.modal-qr {
+  position: relative;
+  width: 70%;
+  max-width: 600rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.modal-qr-header {
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.modal-qr-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.modal-qr-close {
+  font-size: 32rpx;
+  color: #999;
+  padding: 10rpx;
+}
+.modal-qr-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+}
+.qrcode-loading,
+.qrcode-empty {
+  width: 340rpx;
+  height: 340rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: #FAFAFA;
+  border-radius: 16rpx;
+  border: 2rpx dashed #E5E7EB;
+}
+.loading-spinner {
+  font-size: 48rpx;
+  margin-bottom: 16rpx;
+}
+.loading-text {
+  font-size: 24rpx;
+  color: #94A3B8;
+}
+.qrcode-empty text {
+  font-size: 24rpx;
+  color: #94A3B8;
+}
+.qrcode-image {
+  width: 340rpx;
+  height: 340rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #fff;
+  border-radius: 16rpx;
+  border: 1rpx solid #E5E7EB;
+  overflow: hidden;
+}
+.qrcode-img {
+  width: 330rpx;
+  height: 330rpx;
+}
+.qrcode-info {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 20rpx;
+  gap: 8rpx;
+}
+.qrcode-text {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.qrcode-desc {
+  font-size: 22rpx;
+  color: #94A3B8;
+}
+.modal-qr-actions {
+  width: 100%;
+  display: flex;
+  gap: 20rpx;
+  margin-top: 24rpx;
+}
+.action-btn {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8rpx;
+  padding: 24rpx;
+  border-radius: 16rpx;
+  font-size: 24rpx;
+  font-weight: bold;
+}
+.copy-btn {
+  background: #FFF7ED;
+  color: #F97316;
+  border: 1rpx solid #FED7AA;
+}
+.share-btn {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+}
+.copy-icon, .share-icon {
+  font-size: 40rpx;
+}
+.copy-text, .share-text {
+  font-size: 24rpx;
+  font-weight: bold;
+}
 </style>

+ 0 - 56
cfc-frontend/pages/profile/components/ProfileMenu.vue

@@ -74,14 +74,6 @@
           <text>👁 对家庭成员可见</text>
           <switch :checked="showToFamily === 1" @change="onShowToFamilyChange" color="#5B9BD5" />
         </view>
-        <view class="menu-item" @click="goToShare">
-          <text>📤 分享推广码</text>
-          <text class="arrow">›</text>
-        </view>
-        <view class="menu-item" @click="showInviteActionSheet">
-          <text>📨 邀请加入</text>
-          <text class="arrow">›</text>
-        </view>
         <view class="menu-item" @click="logout">
           <text>🚪 退出登录</text>
           <text class="arrow">›</text>
@@ -219,54 +211,6 @@ var res = await getReferralCode()
     goToAfterSales() {
       uni.navigateTo({ url: '/pages/shop/after-sales/after-sales' })
     },
-    
-    goToShare() {
-      uni.navigateTo({ url: '/pages/profile/share' })
-    },
-    showInviteActionSheet() {
-      var self = this
-      var ui = uni.getStorageSync('userInfo') || {}
-      if (!ui.familyId) {
-        ensureFamily().then(function(result) {
-          if (result && result.created) {
-            self.showInviteActionSheet()
-          }
-        })
-        return
-      }
-      const userInfo = ui
-      const items = ['👨‍👩‍👧‍👦 邀请家人', '👨‍🏫 邀请成长规划师']
-      const types = ['family', 'guide']
-      uni.showActionSheet({
-        itemList: items,
-        success: (res) => {
-          self.inviteGenerate(types[res.tapIndex], userInfo)
-        }
-      })
-    },
-    inviteGenerate(type, userInfo) {
-      const inviteTypes = {
-        family: {
-          title: (userInfo && userInfo.nickname ? userInfo.nickname : '家人') + ' 邀请你加入家庭',
-          path: '/pages/login/login?invite_type=family'
-        },
-        guide: {
-          title: (userInfo && userInfo.nickname ? userInfo.nickname : '家长') + ' 邀请你成为家庭成长规划师',
-          path: '/pages/login/login?invite_type=guide'
-        }
-      }
-      const config = inviteTypes[type] || inviteTypes.family
-      var path = config.path
-      if (this.referralCode) {
-        path = path + '&refCode=' + this.referralCode
-      }
-      this.$emit('invite-generate', {
-        title: config.title,
-        path: path,
-        imageUrl: '/static/invite-card.png'
-      })
-      uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })
-    },
     async savePassword() {
       if (this.newPassword.length !== 6) {
         uni.showToast({ title: '请输入6位密码', icon: 'none' })

+ 0 - 381
cfc-frontend/pages/profile/share.vue

@@ -1,381 +0,0 @@
-<template>
-  <view class="container">
-    <!-- 未登录 -->
-    <view v-if="!isLoggedIn" class="empty-state">
-      <text class="empty-icon">👤</text>
-      <text class="empty-title">请先登录</text>
-      <text class="empty-desc">登录后即可生成您的专属推广码</text>
-      <button class="btn-login" @click="goLogin">去登录</button>
-    </view>
-
-    <!-- 已登录 -->
-    <view v-else class="content">
-      <!-- 头部 -->
-      <view class="header">
-        <text class="title">分享推广码</text>
-        <text class="subtitle">将二维码分享给好友,扫码注册即成为您的下线</text>
-      </view>
-
-      <!-- 我的推广码 -->
-      <view class="code-card">
-        <view class="code-label">
-          <text class="label-text">我的推广码</text>
-          <view class="copy-btn" @click="copyCode">
-            <text class="copy-icon">📋</text>
-            <text class="copy-text">{{ copied ? '已复制' : '复制' }}</text>
-          </view>
-        </view>
-        <text class="code-value">{{ referralCode || '加载中...' }}</text>
-      </view>
-
-      <!-- 二维码区域 -->
-      <view class="qrcode-section" v-if="qrLoading || qrBase64">
-        <view class="qrcode-label">
-          <text class="label-text">扫码进入首页</text>
-        </view>
-        <view class="qrcode-wrap">
-          <image
-            v-if="qrBase64"
-            class="qrcode-img"
-            :src="qrBase64"
-            mode="aspectFit"
-          />
-          <view v-else class="qrcode-loading">
-            <text class="loading-spinner">⏳</text>
-            <text class="loading-text">生成中...</text>
-          </view>
-        </view>
-      </view>
-
-      <!-- 操作按钮 -->
-      <view class="actions" v-if="qrBase64">
-        <view class="action-btn save-btn" @click="saveQrcode">
-          <text class="action-icon">💾</text>
-          <text class="action-text">保存图片</text>
-        </view>
-        <view class="action-btn share-btn" @click="shareToWeixin">
-          <text class="action-icon">📤</text>
-          <text class="action-text">分享给朋友</text>
-        </view>
-      </view>
-
-      <!-- 说明 -->
-      <view class="tips">
-        <view class="tip-item">
-          <text class="tip-icon">💡</text>
-          <text class="tip-text">好友扫码后自动绑定推荐关系</text>
-        </view>
-        <view class="tip-item">
-          <text class="tip-icon">💡</text>
-          <text class="tip-text">好友注册后您将获得积分奖励</text>
-        </view>
-        <view class="tip-item">
-          <text class="tip-icon">💡</text>
-          <text class="tip-text">推广码可长期使用,无过期时间</text>
-        </view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getReferralCode, extractReferralCode, getShareQrCode } from '../../utils/api.js'
-
-export default {
-  data() {
-    return {
-      referralCode: '',
-      qrBase64: '',
-      qrLoading: false,
-      copied: false
-    }
-  },
-  computed: {
-    isLoggedIn() {
-      return !!uni.getStorageSync('token')
-    }
-  },
-  onLoad() {
-    this.loadReferralCode()
-    this._onLoadFired = true
-  },
-  onShow() {
-    if (this._onLoadFired) {
-      this._onLoadFired = false
-    } else if (this.isLoggedIn && !this.referralCode) {
-      this.loadReferralCode()
-    }
-  },
-  methods: {
-    async loadReferralCode() {
-      if (!this.isLoggedIn) return
-      try {
-        var res = await getReferralCode()
-        var code = extractReferralCode(res)
-        this.referralCode = code || ''
-        if (this.referralCode) {
-          this.generateQrcode()
-        }
-      } catch (e) {
-        console.error('获取推广码失败', e)
-      }
-    },
-    generateQrcode() {
-      if (!this.referralCode) return
-      this.qrLoading = true
-      var scene = 'ref=' + this.referralCode
-      // 首页路径,扫码后进入首页并绑定推荐人
-      getShareQrCode('/pages/index-home/index', scene)
-        .then(function(res) {
-          if (res && res.code === 200 && res.data) {
-            this.qrBase64 = res.data.qrCodeBase64
-          }
-        }.bind(this))
-        .catch(function(e) {
-          console.error('生成二维码失败', e)
-          uni.showToast({ title: '生成失败,请重试', icon: 'none' })
-        })
-        .finally(function() {
-          this.qrLoading = false
-        }.bind(this))
-    },
-    copyCode() {
-      if (!this.referralCode) return
-      this.copied = true
-      uni.setClipboardData({
-        data: this.referralCode,
-        success: function() {
-          uni.showToast({ title: '推广码已复制', icon: 'success' })
-          setTimeout(function() {
-            this.copied = false
-          }.bind(this), 2000)
-        }.bind(this)
-      })
-    },
-    saveQrcode() {
-      if (!this.qrBase64) return
-      // 将 base64 转为临时文件再保存
-      var self = this
-      var rawBase64 = this.qrBase64.indexOf('base64,') >= 0
-        ? this.qrBase64.split('base64,')[1]
-        : this.qrBase64
-      var fsm = uni.getFileSystemManager()
-      var tempPath = wx.env.USER_DATA_PATH + '/share_qrcode_' + Date.now() + '.png'
-      fsm.writeFileSync(tempPath, rawBase64, 'base64')
-      uni.saveImageToPhotosAlbum({
-        filePath: tempPath,
-        success: function() {
-          uni.showToast({ title: '保存成功', icon: 'success' })
-        },
-        fail: function() {
-          uni.showToast({ title: '保存失败', icon: 'none' })
-        }
-      })
-    },
-    shareToWeixin() {
-      if (!this.referralCode) return
-      // 通过 uni.share 分享小程序卡片
-      var path = '/pages/index-home/index?refCode=' + this.referralCode
-      uni.share({
-        provider: 'weixin',
-        scene: { type: 0 },
-        type: 5,
-        imageUrl: this.qrBase64,
-        path: path,
-        title: '浠艾福 — 邀请你加入',
-        success: function() {
-          uni.showToast({ title: '分享成功', icon: 'success' })
-        },
-        fail: function() {
-          uni.showToast({ title: '分享失败', icon: 'none' })
-        }
-      })
-    },
-    goLogin() {
-      uni.navigateTo({ url: '/pages/login/login' })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  min-height: 100vh;
-  background: #F8F4EF;
-  padding-bottom: 60rpx;
-}
-
-/* 未登录 */
-.empty-state {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  min-height: 70vh;
-  padding: 60rpx;
-}
-.empty-icon { font-size: 100rpx; margin-bottom: 24rpx; }
-.empty-title { font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 12rpx; }
-.empty-desc { font-size: 26rpx; color: #999; margin-bottom: 40rpx; }
-.btn-login {
-  width: 60%;
-  height: 80rpx;
-  line-height: 80rpx;
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  color: #fff;
-  font-size: 30rpx;
-  font-weight: bold;
-  border-radius: 40rpx;
-  text-align: center;
-  border: none;
-}
-.btn-login::after { border: none; }
-
-/* 内容区 */
-.content { padding: 0 30rpx; }
-
-.header {
-  text-align: center;
-  padding: 60rpx 0 40rpx;
-}
-.title {
-  font-size: 44rpx;
-  font-weight: bold;
-  color: #1E293B;
-  display: block;
-  margin-bottom: 12rpx;
-}
-.subtitle {
-  font-size: 26rpx;
-  color: #94A3B8;
-  display: block;
-}
-
-/* 推广码卡片 */
-.code-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 32rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.code-label {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-.label-text {
-  font-size: 26rpx;
-  color: #94A3B8;
-}
-.copy-btn {
-  display: flex;
-  align-items: center;
-  gap: 8rpx;
-  padding: 8rpx 20rpx;
-  background: #FFF7ED;
-  border-radius: 20rpx;
-  border: 1rpx solid #FED7AA;
-}
-.copy-icon { font-size: 24rpx; }
-.copy-text { font-size: 24rpx; color: #F97316; }
-.code-value {
-  font-size: 48rpx;
-  font-weight: bold;
-  color: #F97316;
-  font-family: monospace;
-  text-align: center;
-  display: block;
-  letter-spacing: 8rpx;
-}
-
-/* 二维码区域 */
-.qrcode-section {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 32rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-.qrcode-label {
-  margin-bottom: 24rpx;
-}
-.qrcode-wrap {
-  width: 360rpx;
-  height: 360rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #FAFAFA;
-  border-radius: 16rpx;
-  border: 2rpx dashed #E5E7EB;
-}
-.qrcode-img {
-  width: 340rpx;
-  height: 340rpx;
-  border-radius: 12rpx;
-}
-.qrcode-loading {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 16rpx;
-}
-.loading-spinner { font-size: 48rpx; }
-.loading-text { font-size: 24rpx; color: #94A3B8; }
-
-/* 操作按钮 */
-.actions {
-  display: flex;
-  gap: 20rpx;
-  margin-bottom: 30rpx;
-}
-.action-btn {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 12rpx;
-  padding: 32rpx;
-  border-radius: 20rpx;
-}
-.save-btn {
-  background: #fff;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.share-btn {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-}
-.action-icon { font-size: 48rpx; }
-.action-text {
-  font-size: 26rpx;
-  font-weight: bold;
-}
-.save-btn .action-text { color: #333; }
-.share-btn .action-text { color: #fff; }
-
-/* 说明 */
-.tips {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 32rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.tip-item {
-  display: flex;
-  align-items: flex-start;
-  gap: 12rpx;
-  margin-bottom: 16rpx;
-}
-.tip-item:last-child { margin-bottom: 0; }
-.tip-icon { font-size: 28rpx; flex-shrink: 0; }
-.tip-text {
-  font-size: 24rpx;
-  color: #64748B;
-  line-height: 1.6;
-}
-</style>