Browse Source

chore: 恢复 pull --rebase 前被 stash drop 丢弃的 WIP(10 文件)

从悬空 stash 7c1cca889 (tag wip-recover-20261007) 恢复:promotion 分享(SharePoster/index/invite)、health-main、family/add-member、guide/hierarchy、cfc-web ProductEdit 会员可见级别、package 依赖;删除无引用的 guide/invite/share.vue
liaoxg 4 days ago
parent
commit
c8c65ebbaf

+ 5 - 4
cfc-frontend/pages/family/add-member.vue

@@ -240,10 +240,11 @@ if (this.form.phone && !this.isEdit) {
          }
        })
      })
-     if (phoneConfirmed) {
-       uni.navigateTo({ url: '/pages/guide/invite/share' })
-       return
-     }
+      if (phoneConfirmed) {
+        // 原「邀请分享」页已删除,家庭邀请统一走家庭成员页的分享卡片流程
+        uni.navigateTo({ url: '/pages/profile-extra/family-members' })
+        return
+      }
    }
  } catch (e) {
    console.log('手机号存在性检查失败,忽略', e)

+ 0 - 37
cfc-frontend/pages/guide/hierarchy/index.vue

@@ -67,15 +67,6 @@
         </view>
       </view>
     </view>
-
-    <!-- 邀请码分享 -->
-    <view class="section">
-      <view class="section-title">邀请下级成长规划师</view>
-      <button class="btn-share" @click="goToShare">
-        <text class="btn-icon">📤</text>
-        <text class="btn-text">分享邀请码</text>
-      </button>
-    </view>
   </view>
 </template>
 
@@ -142,12 +133,6 @@ export default {
       uni.navigateTo({
         url: `/pages/assessment/results?guideId=${guide.id}`
       })
-    },
-
-    goToShare() {
-      uni.navigateTo({
-        url: '/pages/guide/invite/share'
-      })
     }
   }
 }
@@ -276,26 +261,4 @@ export default {
   border-radius: 10rpx;
   padding: 20rpx;
 }
-
-.btn-share {
-  width: 100%;
-  height: 80rpx;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: #fff;
-  border-radius: 40rpx;
-  font-size: 28rpx;
-  font-weight: bold;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 10rpx;
-}
-
-.btn-icon {
-  font-size: 32rpx;
-}
-
-.btn-text {
-  font-size: 28rpx;
-}
 </style>

+ 0 - 581
cfc-frontend/pages/guide/invite/share.vue

@@ -1,581 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <text class="title">邀请分享</text>
-      <text class="subtitle">邀请家长或新成长规划师加入</text>
-    </view>
-
-    <!-- 邀请码列表 -->
-    <view class="invite-codes-section">
-      <view class="section-title">我的邀请码</view>
-      <view class="invite-codes-list">
-        <view 
-          class="invite-code-item" 
-          v-for="code in inviteCodes" 
-          :key="code.id"
-        >
-          <view class="code-info">
-            <text class="code-text">{{ code.code }}</text>
-            <text class="code-status" :class="'status-' + code.status">
-              {{ getStatusText(code.status) }}
-            </text>
-          </view>
-          <view class="code-stats">
-            <text class="stat">已使用: {{ code.useCount }}/{{ code.maxUseCount }}</text>
-            <text class="stat" v-if="code.expireAt">过期: {{ formatDate(code.expireAt) }}</text>
-          </view>
-        </view>
-      </view>
-      <button class="btn-generate" @click="generateInviteCode">生成新邀请码</button>
-    </view>
-
-    <!-- 分享方式 -->
-    <view class="share-section">
-      <view class="section-title">分享方式</view>
-      
-      <!-- 复制链接 -->
-      <view class="share-item" @click="copyInviteLink">
-        <view class="share-icon">🔗</view>
-        <view class="share-content">
-          <text class="share-title">复制邀请链接</text>
-          <text class="share-desc">分享链接给家长或新成长规划师</text>
-        </view>
-        <text class="share-arrow">›</text>
-      </view>
-
-      <!-- 生成二维码 -->
-      <view class="share-item" @click="showQrcode">
-        <view class="share-icon">📱</view>
-        <view class="share-content">
-          <text class="share-title">生成二维码</text>
-          <text class="share-desc">扫码即可加入</text>
-        </view>
-        <text class="share-arrow">›</text>
-      </view>
-
-      <!-- 转发给朋友 -->
-      <view class="share-item" @click="shareToFriend">
-        <view class="share-icon">📤</view>
-        <view class="share-content">
-          <text class="share-title">转发给朋友</text>
-          <text class="share-desc">直接分享到微信</text>
-        </view>
-        <text class="share-arrow">›</text>
-      </view>
-    </view>
-
-    <!-- 使用说明 -->
-    <view class="tips-section">
-      <view class="section-title">使用说明</view>
-      <view class="tips-list">
-        <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 class="item">
-          <text class="tip-icon">💡</text>
-          <text class="tip-text">邀请码可以设置使用次数和过期时间</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- 二维码弹窗 -->
-    <view class="qrcode-modal" v-if="showQrcodeModal" @click="showQrcodeModal = false">
-      <view class="modal-content" @click.stop>
-        <view class="modal-header">
-          <text class="modal-title">邀请二维码</text>
-          <text class="modal-close" @click="showQrcodeModal = false">✕</text>
-        </view>
-        <view class="qrcode-container">
-          <canvas canvas-id="qrcodeCanvas" class="qrcode-canvas"></canvas>
-        </view>
-        <view class="modal-actions">
-          <button class="btn-save" @click="saveQrcode">保存图片</button>
-          <button class="btn-close" @click="showQrcodeModal = false">关闭</button>
-        </view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getInviteCodes, generateInviteCode, generateQrCodeFromText } from '../../../utils/api.js'
-import { parseDate } from '../../../utils/format.js'
-
-export default {
-  data() {
-    return {
-      inviteCodes: [],
-      currentInviteCode: null,
-      showQrcodeModal: false
-    }
-  },
-  onLoad() {
-    this.loadInviteCodes()
-    this._onLoadFired = true
-  },
-  onShow() {
-    if (this._onLoadFired) {
-      this._onLoadFired = false
-    } else {
-      this.loadInviteCodes()
-    }
-  },
-  methods: {
-    async loadInviteCodes() {
-      try {
-        const res = await getInviteCodes()
-        if (res.code === 200) {
-          this.inviteCodes = res.data || []
-          // 如果有可用的邀请码,使用第一个
-          const activeCode = this.inviteCodes.find(code => code.status === 'active')
-          if (activeCode) {
-            this.currentInviteCode = activeCode
-          }
-        }
-      } catch (e) {
-        console.error('获取邀请码失败', e)
-      }
-    },
-
-    async generateInviteCode() {
-      try {
-        uni.showLoading({ title: '生成中...' })
-        const res = await generateInviteCode()
-        uni.hideLoading()
-
-        if (res.code === 200) {
-          this.currentInviteCode = res.data
-          uni.showToast({
-            title: '生成成功',
-            icon: 'success'
-          })
-          this.loadInviteCodes()
-        } else {
-          uni.showToast({
-            title: res.message || '生成失败',
-            icon: 'none'
-          })
-        }
-      } catch (e) {
-        uni.hideLoading()
-        uni.showToast({
-          title: '生成失败',
-          icon: 'none'
-        })
-      }
-    },
-
-    copyInviteLink() {
-      if (!this.currentInviteCode) {
-        uni.showToast({
-          title: '请先生成邀请码',
-          icon: 'none'
-        })
-        return
-      }
-
-      const inviteUrl = `pages/guide/register/index?inviteCode=${this.currentInviteCode.code}`
-      
-      uni.setClipboardData({
-        data: inviteUrl,
-        success: () => {
-          uni.showToast({
-            title: '链接已复制',
-            icon: 'success'
-          })
-        }
-      })
-    },
-
-    showQrcode() {
-      if (!this.currentInviteCode) {
-        uni.showToast({
-          title: '请先生成邀请码',
-          icon: 'none'
-        })
-        return
-      }
-
-      this.showQrcodeModal = true
-      this.$nextTick(() => {
-        this.drawQrcode()
-      })
-    },
-
-    async drawQrcode() {
-      if (!this.currentInviteCode) return
-      const code = this.currentInviteCode.code
-      const pagePath = '/pages/guide/register/index?inviteCode=' + code
-
-      // 通过后端生成真实二维码
-      let qrBase64 = ''
-      try {
-        const res = await generateQrCodeFromText(pagePath)
-        if (res.code === 200 && res.data) {
-          qrBase64 = res.data.qrCodeBase64
-        }
-      } catch (e) {
-        console.error('生成二维码失败', e)
-      }
-
-      const ctx = uni.createCanvasContext('qrcodeCanvas')
-      const canvasWidth = 300
-      const canvasHeight = 300
-
-      ctx.setFillStyle('#FFFFFF')
-      ctx.fillRect(0, 0, canvasWidth, canvasHeight)
-
-      if (qrBase64) {
-        // 将 base64 写入临时文件,再绘制到 Canvas
-        const rawBase64 = qrBase64.indexOf('base64,') >= 0 ? qrBase64.split('base64,')[1] : qrBase64
-        const fsm = uni.getFileSystemManager()
-        const tempPath = wx.env.USER_DATA_PATH + '/qr_invite_' + Date.now() + '.png'
-        try {
-          fsm.writeFileSync(tempPath, rawBase64, 'base64')
-          ctx.drawImage(tempPath, 40, 30, 220, 220)
-        } catch (e) {
-          this._drawQrFallback(ctx, canvasWidth, canvasHeight, code)
-        }
-      } else {
-        this._drawQrFallback(ctx, canvasWidth, canvasHeight, code)
-      }
-
-      ctx.draw()
-    },
-
-    _drawQrFallback(ctx, w, h, code) {
-      ctx.setFillStyle('#CCCCCC')
-      ctx.fillRect(40, 30, 220, 220)
-      ctx.setFillStyle('#999999')
-      ctx.font = '16px sans-serif'
-      ctx.textAlign = 'center'
-      ctx.fillText('邀请码: ' + code, w / 2, 150)
-    },
-
-    saveQrcode() {
-      uni.canvasToTempFilePath({
-        canvasId: 'qrcodeCanvas',
-        success: (res) => {
-          uni.saveImageToPhotosAlbum({
-            filePath: res.tempFilePath,
-            success: () => {
-              uni.showToast({
-                title: '保存成功',
-                icon: 'success'
-              })
-            },
-            fail: () => {
-              uni.showToast({
-                title: '保存失败',
-                icon: 'none'
-              })
-            }
-          })
-        }
-      })
-    },
-
-    shareToFriend() {
-      if (!this.currentInviteCode) {
-        uni.showToast({
-          title: '请先生成邀请码',
-          icon: 'none'
-        })
-        return
-      }
-
-      const inviteUrl = `pages/guide/register/index?inviteCode=${this.currentInviteCode.code}`
-      
-      uni.share({
-        provider: 'weixin',
-        scene: {
-          type: 0,
-          title: '成为成长规划师',
-          path: inviteUrl
-        },
-        success: () => {
-          uni.showToast({
-            title: '分享成功',
-            icon: 'success'
-          })
-        },
-        fail: () => {
-          uni.showToast({
-            title: '分享失败',
-            icon: 'none'
-          })
-        }
-      })
-    },
-
-    getStatusText(status) {
-      const statusMap = {
-        'active': '可用',
-        'used': '已用完',
-        'expired': '已过期'
-      }
-      return statusMap[status] || status
-    },
-
-    formatDate(dateStr) {
-      if (!dateStr) return ''
-      const date = parseDate(dateStr)
-      if (!date) return ''
-      return `${date.getMonth() + 1}月${date.getDate()}日`
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  padding: 30rpx;
-  background: #F8F8F8;
-  min-height: 100vh;
-}
-
-.header {
-  text-align: center;
-  padding: 60rpx 0 40rpx;
-}
-
-.title {
-  font-size: 48rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-  margin-bottom: 20rpx;
-}
-
-.subtitle {
-  font-size: 28rpx;
-  color: #666;
-}
-
-.section-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-  margin-bottom: 20rpx;
-}
-
-.invite-codes-section,
-.share-section,
-.tips-section {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-}
-
-.invite-codes-list {
-  margin-bottom: 20rpx;
-}
-
-.invite-code-item {
-  background: #F8F8F8;
-  border-radius: 10rpx;
-  padding: 20rpx;
-  margin-bottom: 20rpx;
-}
-
-.code-info {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10rpx;
-}
-
-.code-text {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-  font-family: monospace;
-}
-
-.code-status {
-  font-size: 24rpx;
-  padding: 4rpx 12rpx;
-  border-radius: 20rpx;
-}
-
-.status-active {
-  background: #E8F5E9;
-  color: #4CAF50;
-}
-
-.status-used {
-  background: #FFF3E0;
-  color: #FF9800;
-}
-
-.status-expired {
-  background: #FFEBEE;
-  color: #F44336;
-}
-
-.code-stats {
-  display: flex;
-  justify-content: space-between;
-  font-size: 24rpx;
-  color: #999;
-}
-
-.stat {
-  margin-right: 20rpx;
-}
-
-.btn-generate {
-  width: 100%;
-  height: 80rpx;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: #fff;
-  border-radius: 40rpx;
-  font-size: 28rpx;
-  font-weight: bold;
-}
-
-.share-item {
-  display: flex;
-  align-items: center;
-  padding: 30rpx 0;
-  border-bottom: 1rpx solid #F0F0F0;
-}
-
-.share-item:last-child {
-  border-bottom: none;
-}
-
-.share-icon {
-  font-size: 48rpx;
-  margin-right: 20rpx;
-}
-
-.share-content {
-  flex: 1;
-}
-
-.share-title {
-  font-size: 30rpx;
-  color: #333;
-  display: block;
-  margin-bottom: 8rpx;
-}
-
-.share-desc {
-  font-size: 24rpx;
-  color: #999;
-}
-
-.share-arrow {
-  font-size: 40rpx;
-  color: #CCC;
-}
-
-.tips-list {
-  margin-top: 20rpx;
-}
-
-.tip-item {
-  display: flex;
-  align-items: flex-start;
-  margin-bottom: 20rpx;
-}
-
-.tip-icon {
-  font-size: 32rpx;
-  margin-right: 12rpx;
-}
-
-.tip-text {
-  font-size: 26rpx;
-  color: #666;
-  line-height: 1.5;
-}
-
-.qrcode-modal {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 999;
-}
-
-.modal-content {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 40rpx;
-  width: 80%;
-  max-width: 400rpx;
-}
-
-.modal-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 30rpx;
-}
-
-.modal-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-}
-
-.modal-close {
-  font-size: 40rpx;
-  color: #999;
-  padding: 10rpx;
-}
-
-.qrcode-container {
-  display: flex;
-  justify-content: center;
-  margin-bottom: 30rpx;
-}
-
-.qrcode-canvas {
-  width: 300rpx;
-  height: 300rpx;
-  border: 1rpx solid #E0E0E0;
-  border-radius: 10rpx;
-}
-
-.modal-actions {
-  display: flex;
-  gap: 20rpx;
-}
-
-.btn-save,
-.btn-close {
-  flex: 1;
-  height: 80rpx;
-  border-radius: 40rpx;
-  font-size: 28rpx;
-  font-weight: bold;
-}
-
-.btn-save {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: #fff;
-}
-
-.btn-close {
-  background: #F0F0F0;
-  color: #333;
-}
-</style>

+ 2 - 2
cfc-frontend/pages/health-main/index.vue

@@ -543,8 +543,8 @@ export default {
       try {
         var api = require('../../utils/api.js')
         var memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''
-        // 健康报告:不传 memberId,由后端返回当前家庭的全部报告(size=3 仅返回最近 3 条)
-        api.getReportList(null, 3).then(function(res) {
+        // 健康报告:传入当前用户memberId,返回当前用户的报告(size=3 仅返回最近 3 条)
+        api.getReportList(memberId, 3).then(function(res) {
           if (res.code === 200 && res.data) {
             var list = Array.isArray(res.data) ? res.data : (res.data.list || [])
             self.reports = list

+ 70 - 57
cfc-frontend/pages/promotion/components/SharePoster.vue

@@ -17,7 +17,6 @@ export default {
     show: { type: Boolean, default: false },
     userAvatar: { type: String, default: '' },
     nickname: { type: String, default: '' },
-    referralCode: { type: String, default: '' },
     familyName: { type: String, default: '' },
     qrCodeBase64: { type: String, default: '' }
   },
@@ -37,10 +36,32 @@ export default {
     //  ENTRY POINT
     // ============================================================
     drawPoster() {
+      var self = this
+      uni.createSelectorQuery().in(this)
+        .select('#sharePosterCanvas')
+        .boundingClientRect(function(rect) {
+          var scaleX = 1
+          var scaleY = 1
+          if (rect && rect.width && rect.height) {
+            scaleX = rect.width / 500
+            scaleY = rect.height / 750
+          }
+          self._renderPoster(scaleX, scaleY)
+        })
+        .exec()
+    },
+
+    _renderPoster(scaleX, scaleY) {
       var ctx = uni.createCanvasContext('sharePosterCanvas', this)
       var w = 500
       var h = 750
 
+      // 画布 CSS 用 rpx,实际像素随设备变化(375px 屏约 310x465),而绘制按 500x750 设计坐标;
+      // 不缩放坐标系时右侧/底部内容(含二维码)会被裁剪出画布,导致海报显示不全。
+      if (scaleX !== 1 || scaleY !== 1) {
+        ctx.scale(scaleX, scaleY)
+      }
+
       // 1. Background gradient — warm orange
       var gradient = ctx.createLinearGradient(0, 0, w * 0.3, h)
       gradient.addColorStop(0, '#F97316')
@@ -54,15 +75,15 @@ export default {
 
       // 3. White/cream content card
       var cardX = 28
-      var cardY = 112
+      var cardY = 108
       var cardW = 444
-      var cardH = 528
+      var cardH = 496
       this._drawContentCard(ctx, cardX, cardY, cardW, cardH)
 
       // 4. Avatar decorative frame (drawn before async avatar download)
       var avatarX = w / 2
-      var avatarY = cardY + 62
-      var avatarR = 48
+      var avatarY = cardY + 72
+      var avatarR = 52
       this._drawAvatarFrame(ctx, avatarX, avatarY, avatarR)
 
       // 5. Download avatar (async) then draw remaining content
@@ -254,71 +275,61 @@ export default {
     },
 
     // ============================================================
-    //  CONTENT AFTER AVATAR — nickname, referral code, QR code, CTA
+    //  NICKNAME TRUNCATION — 昵称过长时截断,避免标题溢出海报
+    // ============================================================
+    _shortName(name, max) {
+      var s = (name || '').trim()
+      if (!s) return ''
+      if (s.length <= max) return s
+      return s.substring(0, max) + '…'
+    },
+
+    // ============================================================
+    //  HEADLINE + INTRO + QR — 邀请标题 + 简单介绍 + 二维码(不展示邀请码)
     // ============================================================
     _drawContentAfterAvatar(ctx, w, h, _cardX, _cardW, _avatarX, avatarY) {
       var self = this
       var base64Data, commaIdx, fs, filePath
+      var centerX = w / 2
 
-      // --- Nickname ---
-      var nameY = avatarY + 70
+      // --- 邀请标题:XXX邀请你关注家庭健康 ---
+      var name = this._shortName(this.nickname, 8)
+      var headlineY = avatarY + 78
       ctx.setFillStyle('#1C1917')
-      ctx.setFontSize(22)
-      ctx.setTextAlign('center')
-      ctx.setTextBaseline('top')
-      ctx.fillText(this.nickname || '好友', w / 2, nameY)
-
-      // --- "我的邀请码" label ---
-      var labelY = nameY + 42
-      ctx.setFontSize(15)
-      ctx.setFillStyle('#A8A29E')
-      ctx.fillText('我的邀请码', w / 2, labelY)
-
-      // --- Referral code (prominent, warm orange) ---
-      var codeY = labelY + 38
-      var codeText = this.referralCode || '------'
-      ctx.setFontSize(44)
-      ctx.setFillStyle('#F97316')
+      ctx.setFontSize(21)
       ctx.setTextAlign('center')
       ctx.setTextBaseline('top')
+      ctx.fillText(name ? name + '邀请你关注家庭健康' : '邀请你关注家庭健康', centerX, headlineY)
 
-      // Gentle underline accent
-      var codeWidth = 0
-      if (ctx.measureText) {
-        try { codeWidth = ctx.measureText(codeText).width } catch (e) { codeWidth = codeText.length * 26 }
-      } else {
-        codeWidth = codeText.length * 26
-      }
-      var underlineY = codeY + 54
-      ctx.setStrokeStyle('rgba(249,115,22,0.12)')
-      ctx.setLineWidth(2)
+      // --- 标题下的短分隔线 ---
+      var dividerY = headlineY + 36
+      ctx.setStrokeStyle('rgba(249,115,22,0.35)')
+      ctx.setLineWidth(1.5)
       ctx.beginPath()
-      ctx.moveTo(w / 2 - codeWidth / 2 - 8, underlineY)
-      ctx.lineTo(w / 2 + codeWidth / 2 + 8, underlineY)
+      ctx.moveTo(centerX - 22, dividerY)
+      ctx.lineTo(centerX + 22, dividerY)
       ctx.stroke()
 
-      ctx.fillText(codeText, w / 2, codeY)
+      // --- 简单介绍:品牌 / 五维 / 定位 ---
+      var brandY = dividerY + 14
+      ctx.setFillStyle('#F97316')
+      ctx.setFontSize(14)
+      ctx.fillText(this.familyName ? this.familyName + ' · 家庭健康俱乐部' : '浠艾福 · 家庭健康俱乐部', centerX, brandY)
 
-      // --- 邀请文案(二维码上方)---
-      var hintY = codeY + 80
-      ctx.setFontSize(12)
-      ctx.setFillStyle('#D6D3D1')
-      ctx.setTextAlign('center')
-      ctx.setTextBaseline('top')
-      var inviteText = ''
-      if (this.nickname && this.familyName) {
-        inviteText = this.nickname + '邀请你来' + this.familyName
-      } else if (this.nickname) {
-        inviteText = this.nickname + '邀请你加入浠艾福'
-      } else {
-        inviteText = '长按识别二维码 · 一起加入浠艾福'
-      }
-      ctx.fillText(inviteText, w / 2, hintY)
+      var dimY = brandY + 24
+      ctx.setFillStyle('#78716C')
+      ctx.setFontSize(13)
+      ctx.fillText('身体 · 心理 · 智慧 · 行动 · 财富', centerX, dimY)
+
+      var descY = dimY + 22
+      ctx.setFillStyle('#A8A29E')
+      ctx.setFontSize(13)
+      ctx.fillText('一家专注全家健康的成长平台', centerX, descY)
 
       // --- QR code ---
       var qrSize = 180
       var qrX = (w - qrSize) / 2
-      var qrY = hintY + 26
+      var qrY = descY + 24
 
       if (this.qrCodeBase64) {
         base64Data = this.qrCodeBase64
@@ -414,7 +425,7 @@ export default {
       ctx.setFontSize(17)
       ctx.setTextAlign('center')
       ctx.setTextBaseline('top')
-      ctx.fillText('扫码邀请好友加入', centerX, bottomY)
+      ctx.fillText('长按识别二维码,了解浠艾福', centerX, bottomY)
 
       // 5 dimension dots in the bottom area (outside card)
       var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
@@ -449,6 +460,8 @@ export default {
       var self = this
       uni.canvasToTempFilePath({
         canvasId: 'sharePosterCanvas',
+        destWidth: 500,
+        destHeight: 750,
         success: function(res) {
           uni.saveImageToPhotosAlbum({
             filePath: res.tempFilePath,
@@ -502,14 +515,14 @@ export default {
   align-items: center;
 }
 .poster-canvas {
-  width: 500px;
-  height: 750px;
+  width: 620rpx;
+  height: 930rpx;
   border-radius: 16rpx;
 }
 .poster-actions {
   display: flex;
   flex-direction: column;
-  width: 500px;
+  width: 620rpx;
   margin-top: 30rpx;
 }
 .poster-btn {

+ 1 - 1
cfc-frontend/pages/promotion/index.vue

@@ -201,7 +201,7 @@
       </view>
     </view>
 
-    <SharePoster :show="showPoster" :referralCode="shareReferralCode" :qrCodeBase64="inviteQrCode" :nickname="shareNickname" :userAvatar="shareAvatar" :familyName="familyName" @close="showPoster = false" />
+    <SharePoster :show="showPoster" :qrCodeBase64="inviteQrCode" :nickname="shareNickname" :userAvatar="shareAvatar" :familyName="familyName" @close="showPoster = false" />
   </view>
 </template>
 

+ 1 - 1
cfc-frontend/pages/promotion/invite.vue

@@ -83,7 +83,7 @@
       </view>
     </view>
 
-    <SharePoster :show="showPoster" :referralCode="referralCode" :qrCodeBase64="qrCodeBase64" :nickname="userNickname" :userAvatar="userAvatar" @close="showPoster = false" />
+    <SharePoster :show="showPoster" :qrCodeBase64="qrCodeBase64" :nickname="userNickname" :userAvatar="userAvatar" @close="showPoster = false" />
 
   </view>
 </template>

+ 40 - 20
cfc-web/src/views/admin/ProductEdit.vue

@@ -4,7 +4,7 @@
       <div slot="header" class="admin-page-header">
         <span class="admin-page-title">{{ isEdit ? '编辑商品' : '新增商品' }}</span>
         <div class="admin-page-actions">
-          <el-button size="mini" @click="$router.back()">返回</el-button>
+          <el-button size="small" @click="$router.back()">返回</el-button>
         </div>
       </div>
 
@@ -260,15 +260,15 @@
         </el-form-item>
 
         <el-form-item label="最低可见级别">
-          <el-select v-model="form.minVisibleLevel" placeholder="所有用户(含未登录)" clearable style="width: 220px;">
+          <el-select v-model="form.minVisibleLevel" placeholder="全部可见" clearable style="width: 200px;">
             <el-option
-              v-for="opt in visibleLevelOptions"
-              :key="opt.value"
-              :label="opt.label"
-              :value="opt.value"
+              v-for="lv in membershipLevels"
+              :key="lv.levelCode"
+              :label="lv.levelName + ' (' + lv.levelCode + ')'"
+              :value="lv.levelCode"
             />
           </el-select>
-          <span style="margin-left: 10px; font-size: 12px; color: #999;">所有用户(含未登录)=游客及全部会员可见;选择级别后仅该级别及更高级别会员可见</span>
+          <span style="margin-left: 10px; font-size: 12px; color: #999;">留空=全部会员可见;选择后仅该级别及更高级别会员可见</span>
         </el-form-item>
 
         <el-form-item label="商品维度">
@@ -340,12 +340,12 @@
           </div>
         </el-form-item>
 
-        <el-form-item>
+        <div class="edit-form-footer">
           <el-button type="primary" :loading="saving" @click="handleSave()">
 {{ isEdit ? '保存修改' : '创建商品' }}
           </el-button>
           <el-button @click="$router.back()">取消</el-button>
-        </el-form-item>
+        </div>
       </el-form>
     </el-card>
   </div>
@@ -412,7 +412,6 @@ export default {
       giftRuleEnabled: false,
       giftRuleLevel: '',
       membershipLevels: [],
-      allMembershipLevels: [],
       // 可选赠品配置:{ [levelCode]: [{giftType,giftProductId,cfValue,sortOrder}] }
       giftConfigsByLevel: {},
       onShelfProducts: [],
@@ -576,13 +575,6 @@ export default {
       const roles = rolesStr ? JSON.parse(rolesStr) : []
       return roles.includes('supplier') || roles.includes('supplier_admin')
     },
-  visibleLevelOptions() {
-      const options = [{ label: '所有用户(含未登录)', value: '' }]
-      ;(this.allMembershipLevels || []).forEach(lv => {
-        options.push({ label: lv.levelName + ' (' + lv.levelCode + ')', value: lv.levelCode })
-      })
-      return options
-    },
   uploadActionUrl() {
     const baseURL = process.env.VUE_APP_BASE_API || ''
     return baseURL + '/api/admin/articles/upload/image'
@@ -773,7 +765,6 @@ async loadGiftRule(productId) {
       try {
         const lvRes = await listMembershipLevels()
         if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
-          this.allMembershipLevels = lvRes.data
           this.membershipLevels = lvRes.data.filter(l => l.levelCode !== 'FREE')
         }
         const ruleRes = await listProductGiftRules(productId)
@@ -811,7 +802,6 @@ async loadGiftRule(productId) {
         try {
           const lvRes = await listMembershipLevels()
           if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
-            this.allMembershipLevels = lvRes.data
             this.membershipLevels = lvRes.data.filter(l => l.levelCode !== 'FREE')
           }
         } catch (e) {
@@ -1056,7 +1046,6 @@ async loadGiftRule(productId) {
         try {
           const productData = {
             ...this.form,
-            minVisibleLevel: this.form.minVisibleLevel || null,
             price: (this.form.price != null) ? Math.round(this.form.price * 100) : 0,
             memberPrice: (this.form.memberPrice != null && this.form.memberPrice > 0) ? Math.round(this.form.memberPrice * 100) : 0,
             images: JSON.stringify(this.imageList)
@@ -1366,4 +1355,35 @@ async loadGiftRule(productId) {
   font-size: 12px;
   color: #999;
 }
+
+/* 企业级视觉对齐:与商品列表页(ProductManage)统一设计语言 */
+.product-edit /deep/ .el-card {
+  border-radius: 8px;
+  border: 1px solid #f0f0f0;
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
+}
+.product-edit /deep/ .el-card__header {
+  padding: 20px 24px;
+}
+.product-edit /deep/ .el-card__body {
+  padding: 24px;
+}
+.product-edit /deep/ .admin-page-title {
+  font-size: 20px;
+  font-weight: 600;
+  line-height: 1.4;
+  color: rgba(0, 0, 0, 0.88);
+}
+.product-edit /deep/ .el-form-item__label {
+  font-weight: 500;
+  color: rgba(0, 0, 0, 0.65);
+}
+.product-edit /deep/ .el-divider__text {
+  font-size: 14px;
+  font-weight: 500;
+  color: rgba(0, 0, 0, 0.88);
+}
+.product-edit /deep/ .el-divider--horizontal {
+  margin: 24px 0;
+}
 </style>

+ 13 - 0
package-lock.json

@@ -11,6 +11,7 @@
       "dependencies": {
         "dom-to-svg": "^0.12.2",
         "esbuild": "^0.28.1",
+        "playwright-core": "^1.63.0",
         "puppeteer": "^25.5.0"
       }
     },
@@ -682,6 +683,18 @@
       "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
       "license": "ISC"
     },
+    "node_modules/playwright-core": {
+      "version": "1.63.0",
+      "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.63.0.tgz",
+      "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
+      "license": "Apache-2.0",
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.5.25",
       "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.25.tgz",

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
   "dependencies": {
     "dom-to-svg": "^0.12.2",
     "esbuild": "^0.28.1",
+    "playwright-core": "^1.63.0",
     "puppeteer": "^25.5.0"
   }
 }