Browse Source

feat: 登录页改造为底部滑出浮层(login-sheet)

- 新增 components/login-sheet.vue:底部登录浮层(height: 92vh、scroll-view flex:1、opacity+translateY 动画、z-index: 9999、tapLock 防重复、open/close 方法)
- 新增 mixins/login-sheet.js:全局 mixin(openLoginSheet 沿 $parent 链查找宿主 ref,已登录守卫、未挂浮层回退整页登录)
- main.js:全局注册 login-sheet 组件 + 注入 loginSheetMixin
- 9 个页面接入 login-sheet 组件
- 修复:面板高度足够显示协议行;四步闭环与五维标签强制单行显示(flex-wrap: nowrap + margin 替代 gap)
- DimensionVisitorCard.vue:登录引导卡片,支持通过 openLoginSheet 唤起浮层
liaoxg 1 week ago
parent
commit
cc41273a9d

+ 201 - 0
cfc-frontend/components/DimensionVisitorCard.vue

@@ -0,0 +1,201 @@
+<template>
+  <view class="dvc">
+    <!-- 价值预览卡:维度浅底 + 维度色 CTA -->
+    <view class="dvc-card" :style="{ background: softColor }">
+      <view class="dvc-head">
+        <text class="dvc-title">{{ cardTitle }}</text>
+        <text class="dvc-sub">登录后即可解锁你的专属内容</text>
+      </view>
+
+      <view class="dvc-benefits">
+        <view class="dvc-benefit" v-for="(b, i) in benefits" :key="i">
+          <text class="dvc-dot" :style="{ background: mainColor }"></text>
+          <text class="dvc-benefit-text">{{ b }}</text>
+        </view>
+      </view>
+
+      <view class="dvc-actions">
+        <button class="dvc-btn-primary" :style="{ background: mainColor }" @click="onLogin">{{ ctaText }}</button>
+        <button
+          v-if="secondaryText"
+          class="dvc-btn-secondary"
+          :style="{ color: mainColor, borderColor: mainColor }"
+          @click="onSecondary">{{ secondaryText }}</button>
+      </view>
+    </view>
+
+    <!-- 维度专属预览占位(登录后展示真实数据) -->
+    <view class="dvc-preview" v-if="previewText">
+      <text class="dvc-preview-icon">{{ previewIcon }}</text>
+      <text class="dvc-preview-text">{{ previewText }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+/**
+ * 维度游客价值卡 —— 五维主页面登录前统一复用
+ *
+ * 维度色严格对齐 uni.scss 五维令牌(v4.0):
+ *   身 body   #FF8C42   心 mind   #FF6B9D
+ *   智 wisdom #6366F1   行 action #10B981   富 wealth #F59E0B
+ * 此处用显式 hex(与令牌值一致)而非 var(--x),确保 mp-weixin 运行时稳定渲染。
+ *
+ * 登录行为复用全局 mixin 的 openLoginSheet()(main.js 已全局注册)。
+ */
+var DIM_TOKENS = {
+  body:   { main: '#FF8C42', soft: 'rgba(255, 140, 66, 0.15)',  icon: '🌿' },
+  mind:   { main: '#FF6B9D', soft: 'rgba(255, 107, 157, 0.15)', icon: '🔥' },
+  wisdom: { main: '#6366F1', soft: 'rgba(99, 102, 241, 0.15)',  icon: '⚡' },
+  action: { main: '#10B981', soft: 'rgba(16, 185, 129, 0.15)',  icon: '🤝' },
+  wealth: { main: '#F59E0B', soft: 'rgba(245, 158, 11, 0.15)',  icon: '💧' }
+}
+
+export default {
+  props: {
+    /** 维度编码:body | mind | wisdom | action | wealth */
+    dimCode: { type: String, default: 'body' },
+    /** 卡片标题 */
+    cardTitle: { type: String, default: '登录后查看专属内容' },
+    /** 价值收益列表(2–3 条) */
+    benefits: {
+      type: Array,
+      default: function() { return [] }
+    },
+    /** 主登录按钮文案 */
+    ctaText: { type: String, default: '立即登录' },
+    /** 次级动作文案(可选) */
+    secondaryText: { type: String, default: '' },
+    /** 次级动作跳转地址(可选);留空则 emit('secondary') */
+    secondaryUrl: { type: String, default: '' },
+    /** 维度专属预览占位文案(可选);留空则不渲染预览块 */
+    previewText: { type: String, default: '' }
+  },
+  computed: {
+    token: function() {
+      return DIM_TOKENS[this.dimCode] || DIM_TOKENS.body
+    },
+    mainColor: function() { return this.token.main },
+    softColor: function() { return this.token.soft },
+    previewIcon: function() { return this.token.icon }
+  },
+  methods: {
+    onLogin: function() {
+      // 全局 mixin 提供 openLoginSheet;极端情况下回退整页登录
+      if (typeof this.openLoginSheet === 'function') {
+        this.openLoginSheet()
+      } else {
+        uni.navigateTo({ url: '/pages/login/login' })
+      }
+    },
+    onSecondary: function() {
+      if (this.secondaryUrl) {
+        uni.navigateTo({ url: this.secondaryUrl })
+      } else {
+        this.$emit('secondary')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dvc {
+  margin: 12rpx 30rpx 0;
+}
+
+.dvc-card {
+  border-radius: 24rpx;
+  padding: 32rpx 28rpx;
+}
+
+.dvc-head {
+  margin-bottom: 22rpx;
+}
+.dvc-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1f2937;
+  display: block;
+  line-height: 1.4;
+}
+.dvc-sub {
+  font-size: 22rpx;
+  color: #999999;
+  margin-top: 6rpx;
+  display: block;
+}
+
+.dvc-benefits {
+  margin-bottom: 26rpx;
+}
+.dvc-benefit {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.dvc-dot {
+  width: 12rpx;
+  height: 12rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+.dvc-benefit-text {
+  font-size: 26rpx;
+  color: #333333;
+  line-height: 1.5;
+}
+
+.dvc-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 20rpx;
+}
+.dvc-btn-primary {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  color: #ffffff;
+  font-size: 28rpx;
+  font-weight: 600;
+  border: none;
+  padding: 0;
+}
+.dvc-btn-primary:active { opacity: 0.85; }
+.dvc-btn-secondary {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  background: #ffffff;
+  font-size: 28rpx;
+  font-weight: 500;
+  border: 2rpx solid;
+  padding: 0;
+}
+.dvc-btn-secondary:active { opacity: 0.7; }
+
+.dvc-preview {
+  margin-top: 24rpx;
+  background: #ffffff;
+  border: 1rpx dashed #e5e7eb;
+  border-radius: 20rpx;
+  padding: 40rpx 28rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.dvc-preview-icon {
+  font-size: 44rpx;
+  margin-bottom: 14rpx;
+}
+.dvc-preview-text {
+  font-size: 24rpx;
+  color: #999999;
+  text-align: center;
+  line-height: 1.6;
+}
+</style>

+ 455 - 0
cfc-frontend/components/login-sheet.vue

@@ -0,0 +1,455 @@
+<template>
+  <view class="login-sheet-overlay" :class="{ 'login-sheet-overlay--visible': visible }" @click.self="handleOverlayClick">
+    <view class="login-sheet-panel" :class="{ 'login-sheet-panel--show': panelShow }">
+      <view class="login-sheet-header">
+        <view class="login-sheet-title">欢迎回来</view>
+      </view>
+      <scroll-view scroll-y class="login-sheet-scroll" :show-scrollbar="false">
+        <view class="login-sheet-content">
+          <!-- 品牌区 -->
+          <view class="brand-section">
+            <image class="logo" src="/static/logo.png" mode="aspectFit" @error="handleImageError"></image>
+            <text class="app-name">浠艾福</text>
+            <text class="identity">您的专属家庭主动健康管理师</text>
+            <text class="slogan">家里每个人的状态,你真的看见了?</text>
+          </view>
+
+          <!-- 理念卡 -->
+          <view class="concept-card">
+            <view class="concept-desc">
+              不等问题发生,先一步<text class="concept-highlight">识别风险 · 主动干预 · 持续评估</text>,把全家健康管理融入每一天。
+            </view>
+            <view class="loop-row">
+              <view class="loop-pill loop-pill-indigo"><text>🔍</text><text class="loop-pill-text">提前识别</text></view>
+              <text class="loop-arrow">→</text>
+              <view class="loop-pill loop-pill-green"><text>🛡️</text><text class="loop-pill-text">风险管控</text></view>
+              <text class="loop-arrow">→</text>
+              <view class="loop-pill loop-pill-amber"><text>⚡</text><text class="loop-pill-text">主动干预</text></view>
+              <text class="loop-arrow">→</text>
+              <view class="loop-pill loop-pill-red"><text>📊</text><text class="loop-pill-text">效果评估</text></view>
+            </view>
+          </view>
+
+          <!-- 五维标签 -->
+          <view class="dims-row">
+            <view class="dim-tag dim-tag-body"><text>🌏</text><text>身·健康</text></view>
+            <view class="dim-tag dim-tag-wisdom"><text>⚡</text><text>智·赋能</text></view>
+            <view class="dim-tag dim-tag-wealth"><text>💧</text><text>富·创富</text></view>
+            <view class="dim-tag dim-tag-action"><text>🌿</text><text>行·和谐</text></view>
+            <view class="dim-tag dim-tag-mind"><text>🔥</text><text>心·大爱</text></view>
+          </view>
+
+          <!-- 登录表单 -->
+          <view class="login-form">
+            <view class="phone-section">
+              <button
+                class="phone-btn"
+                open-type="getPhoneNumber"
+                @getphonenumber="getPhoneNumber"
+                @tap="onPhoneBtnTap"
+                :loading="loading"
+                :disabled="!agreed || loading || tapLock"
+              >
+                <text class="phone-btn-icon">📱</text>
+                <text>手机号快捷登录</text>
+              </button>
+            </view>
+
+            <!-- 暂不登录 -->
+            <view class="skip-section">
+              <text class="skip-btn" @click="handleSkipLogin">暂不登录,先逛逛</text>
+            </view>
+
+            <view class="agreement">
+              <checkbox-group @change="onAgreementChange">
+                <label>
+                  <checkbox value="agree" :checked="agreed" />
+                  <text class="agreement-text">我已阅读并同意</text>
+                </label>
+              </checkbox-group>
+              <text class="link" @click="showAgreement">《用户协议》</text>
+              <text class="agreement-text">和</text>
+              <text class="link" @click="showPrivacy">《隐私政策》</text>
+            </view>
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { wechatPhoneLogin } from '../utils/api.js'
+
+export default {
+  name: 'login-sheet',
+  data() {
+    return {
+      visible: false,
+      panelShow: false,
+      loading: false,
+      // tapLock:在「点击」瞬间上锁,用于关闭授权弹窗弹出前的连点窗口(见 onPhoneBtnTap)
+      tapLock: false,
+      agreed: false,
+      nickname: '',
+      avatar: '',
+      redirectUrl: '',
+      _tapLockTimer: null
+    }
+  },
+  beforeDestroy() {
+    this.clearTimers()
+  },
+  methods: {
+    clearTimers() {
+      if (this._tapLockTimer) {
+        clearTimeout(this._tapLockTimer)
+        this._tapLockTimer = null
+      }
+    },
+    // 由宿主页面通过 mixin openLoginSheet() 调用,或 pages/login/login.vue 兜底壳调用
+    open(options) {
+      options = options || {}
+      // 处理邀请码
+      if (options.inviteCode) {
+        uni.setStorageSync('inviteCode', options.inviteCode)
+        uni.setStorageSync('inviteType', options.inviteType || 'family')
+      }
+      // 处理推荐码(refCode)
+      if (options.refCode) {
+        uni.setStorageSync('refCode', options.refCode)
+      }
+      // 存储登录后跳转地址
+      if (options.redirect) {
+        this.redirectUrl = decodeURIComponent(options.redirect)
+      }
+      this.visible = true
+      this.$nextTick(() => {
+        setTimeout(() => {
+          this.panelShow = true
+        }, 10)
+      })
+    },
+    close() {
+      this.panelShow = false
+      setTimeout(() => {
+        this.visible = false
+      }, 250)
+    },
+    handleOverlayClick() {
+      this.close()
+    },
+    handleImageError() {
+      console.log('Logo 加载失败,使用默认显示')
+    },
+    onAgreementChange(e) {
+      this.agreed = e.detail.value.length > 0
+    },
+    showAgreement() {
+      uni.navigateTo({
+        url: '/pages/policy/agreement'
+      })
+    },
+    showPrivacy() {
+      uni.navigateTo({
+        url: '/pages/policy/privacy'
+      })
+    },
+    handleSkipLogin() {
+      // 不登录直接关闭浮层,停留当前页
+      this.close()
+    },
+    // 点击瞬间上锁,关闭「授权弹窗弹出前」的连点窗口
+    onPhoneBtnTap() {
+      if (this.loading || this.tapLock) return
+      this.tapLock = true
+      if (this._tapLockTimer) clearTimeout(this._tapLockTimer)
+      this._tapLockTimer = setTimeout(() => {
+        this.tapLock = false
+        this._tapLockTimer = null
+      }, 3000)
+    },
+    getPhoneNumber(e) {
+      // 授权流程已结束,释放 tap 锁与兜底定时器,按钮恢复可点
+      if (this._tapLockTimer) {
+        clearTimeout(this._tapLockTimer)
+        this._tapLockTimer = null
+      }
+      this.tapLock = false
+
+      if (!this.agreed) {
+        uni.showToast({ title: '请先同意用户协议', icon: 'none' })
+        return
+      }
+      if (!e.detail || !e.detail.code) {
+        uni.showToast({ title: '授权失败,请重试', icon: 'none' })
+        return
+      }
+      var self = this
+      self.loading = true
+      // 点击授权时实时获取 login code(微信 code 一次性有效,不能复用旧 code)
+      uni.login({
+        provider: 'weixin',
+        success: function(loginRes) {
+          if (!loginRes.code) {
+            uni.showToast({ title: '网络异常,请重试', icon: 'none' })
+            self.loading = false
+            return
+          }
+          wechatPhoneLogin({ code: loginRes.code, phoneCode: e.detail.code }).then(function(res) {
+            self.completeLogin(res.data)
+          }).catch(function(err) {
+            uni.showToast({ title: err.message || '登录失败', icon: 'none' })
+            self.loading = false
+          })
+        },
+        fail: function() {
+          uni.showToast({ title: '网络异常,请重试', icon: 'none' })
+          self.loading = false
+        }
+      })
+    },
+    completeLogin(data) {
+      if (!data) return
+      // 写入登录态
+      if (data.token) uni.setStorageSync('token', data.token)
+      if (data.userId) uni.setStorageSync('userId', data.userId)
+      if (data.openid) uni.setStorageSync('openid', data.openid)
+      if (data.nickname) {
+        this.nickname = data.nickname
+        uni.setStorageSync('nickname', data.nickname)
+      }
+      if (data.avatar) {
+        this.avatar = data.avatar
+        uni.setStorageSync('avatar', data.avatar)
+      }
+      // 通知宿主页面(页面通过 @success="onSheetLoginSuccess" 刷新数据)
+      this.$emit('success', data)
+      // 关闭浮层,停留当前页
+      this.close()
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* ===== 浮层遮罩 ===== */
+.login-sheet-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 9999;
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.25s ease;
+}
+.login-sheet-overlay--visible {
+  opacity: 1;
+  visibility: visible;
+}
+
+/* ===== 底部面板 ===== */
+.login-sheet-panel {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 92vh;
+  max-height: 92vh;
+  background: linear-gradient(180deg, #fffaf3 0%, #fff3e4 100%);
+  border-radius: 32rpx 32rpx 0 0;
+  overflow: hidden;
+  transform: translateY(100%);
+  transition: transform 0.3s ease;
+  display: flex;
+  flex-direction: column;
+}
+.login-sheet-panel--show {
+  transform: translateY(0);
+}
+
+.login-sheet-header {
+  padding: 24rpx 32rpx 8rpx;
+  display: flex;
+  justify-content: center;
+}
+.login-sheet-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+/* ===== 滚动区域 ===== */
+.login-sheet-scroll {
+  flex: 1;
+  min-height: 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.login-sheet-content {
+  padding: 24rpx 48rpx 60rpx;
+  box-sizing: border-box;
+}
+
+/* ===== 品牌区 ===== */
+.brand-section {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24rpx 0 8rpx;
+}
+.logo {
+  width: 128rpx;
+  height: 128rpx;
+  border-radius: 32rpx;
+}
+.app-name {
+  font-size: 44rpx;
+  font-weight: bold;
+  color: #333;
+  margin-top: 16rpx;
+}
+.identity {
+  font-size: 26rpx;
+  color: #f97316;
+  margin-top: 8rpx;
+}
+.slogan {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 12rpx;
+  text-align: center;
+}
+
+/* ===== 理念卡 ===== */
+.concept-card {
+  background: rgba(249, 115, 22, 0.08);
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-top: 28rpx;
+}
+.concept-desc {
+  font-size: 26rpx;
+  color: #555;
+  line-height: 1.6;
+}
+.concept-highlight {
+  color: #f97316;
+  font-weight: 600;
+}
+.loop-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 16rpx;
+  flex-wrap: nowrap;  /* 禁止换行,确保四步/五维在一行 */
+  margin-bottom: 16rpx;
+}
+.loop-pill {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  border-radius: 999rpx;
+  padding: 6rpx 14rpx;
+  font-size: 22rpx;
+}
+.loop-pill-indigo { background: rgba(99, 102, 241, 0.12); color: #6366f1; }
+.loop-pill-green { background: rgba(16, 185, 129, 0.12); color: #10b981; }
+.loop-pill-amber { background: rgba(245, 158, 11, 0.12); color: #f59e0b; }
+.loop-pill-red { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
+.loop-pill-text { margin-left: 4rpx; }
+.loop-arrow {
+  font-size: 24rpx;
+  color: #bbb;
+  padding: 0 4rpx;
+}
+
+/* ===== 五维标签 ===== */
+.dims-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  flex-wrap: nowrap;  /* 禁止换行,保持五维维度在一行 */
+  margin-top: 24rpx;
+  padding: 0 32rpx;  /* 左右留白,避免贴边 */
+  box-sizing: border-box;
+}
+.dim-tag {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  border-radius: 999rpx;
+  padding: 8rpx 14rpx;
+  font-size: 22rpx;
+  color: #fff;
+  margin: 0 8rpx;  /* 使用 margin 替代 gap,微信小程序不支持 gap */
+  white-space: nowrap;
+}
+.dim-tag-body { background: #10b981; }
+.dim-tag-wisdom { background: #6366f1; }
+.dim-tag-wealth { background: #f59e0b; }
+.dim-tag-action { background: #14b8a6; }
+.dim-tag-mind { background: #ef4444; }
+
+/* ===== 登录表单 ===== */
+.login-form {
+  margin-top: 32rpx;
+}
+.phone-btn {
+  width: 100%;
+  height: 96rpx;
+  border-radius: 48rpx;
+  background: linear-gradient(135deg, #f97316, #fb923c);
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 600;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  border: none;
+}
+.phone-btn::after {
+  border: none;
+}
+.phone-btn[disabled] {
+  background: #e0d8cc !important;
+  color: #aaa !important;
+}
+.phone-btn-icon {
+  margin-right: 8rpx;
+}
+
+/* ===== 暂不登录 ===== */
+.skip-section {
+  display: flex;
+  justify-content: center;
+  margin-top: 24rpx;
+}
+.skip-btn {
+  font-size: 26rpx;
+  color: #999;
+  text-decoration: underline;
+}
+
+/* ===== 协议 ===== */
+.agreement {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  flex-wrap: wrap;
+  margin-top: 24rpx;
+  font-size: 24rpx;
+  color: #999;
+}
+.link {
+  color: #f97316;
+}
+</style>

+ 5 - 0
cfc-frontend/main.js

@@ -3,12 +3,17 @@ import Vue from 'vue'
 import store from './store'
 import store from './store'
 import FamilyEmptyState from './components/FamilyEmptyState/FamilyEmptyState.vue'
 import FamilyEmptyState from './components/FamilyEmptyState/FamilyEmptyState.vue'
 import CustomTabBar from './custom-tab-bar/index.vue'
 import CustomTabBar from './custom-tab-bar/index.vue'
+import LoginSheet from './components/login-sheet.vue'
+import loginSheetMixin from './mixins/login-sheet.js'
 import { formatPriceWithSymbol, formatPrice, formatRate, formatScore } from '@/utils/format'
 import { formatPriceWithSymbol, formatPrice, formatRate, formatScore } from '@/utils/format'
 
 
 Vue.config.productionTip = false
 Vue.config.productionTip = false
 
 
 Vue.component('FamilyEmptyState', FamilyEmptyState)
 Vue.component('FamilyEmptyState', FamilyEmptyState)
 Vue.component('custom-tab-bar', CustomTabBar)
 Vue.component('custom-tab-bar', CustomTabBar)
+// 全局登录浮层:注册组件 + 注入 mixin(页面任意位置 this.openLoginSheet() 即从底部浮出)
+Vue.component('login-sheet', LoginSheet)
+Vue.mixin(loginSheetMixin)
 
 
 Vue.prototype.$store = store
 Vue.prototype.$store = store
 Vue.prototype.formatPriceWithSymbol = formatPriceWithSymbol
 Vue.prototype.formatPriceWithSymbol = formatPriceWithSymbol

+ 98 - 0
cfc-frontend/mixins/login-sheet.js

@@ -0,0 +1,98 @@
+/**
+ * 全局登录浮层 mixin(注册于 main.js:Vue.mixin(loginSheetMixin))
+ * 页面内任意位置调用 this.openLoginSheet(options) 即从底部浮出登录浮层
+ *   - options:{ inviteCode, inviteType, refCode, redirect, onSuccess }
+ *     onSuccess 为登录成功回调(JS 直接传函数,不经序列化),在 storage 写入、浮层关闭、
+ *     pendingFamilyInviteCode 跳转判断之前执行,适合「登录后续接购买/报名」类流程
+ *   - 组件内(LoginGuideCard)调用时,本 mixin 会沿 $parent 链向上找到宿主页面浮层;
+ *     宿主页面未挂浮层时回退到整页登录(历史行为,向后兼容)
+ *
+ * 已登录守卫:
+ *   - 若 uni.getStorageSync('token') 存在,不弹浮层,优先调用页面 onLoginSuccess(),
+ *     否则 loadData(),都无则静默返回(防止已登录用户误触发登录浮层)
+ */
+function findLoginSheet(vm) {
+  var cur = vm
+  var depth = 0
+  while (cur && depth < 20) {
+    if (cur.$refs && cur.$refs.loginSheet) {
+      return cur.$refs.loginSheet
+    }
+    cur = cur.$parent
+    depth++
+  }
+  return null
+}
+
+export default {
+  methods: {
+    openLoginSheet: function(options) {
+      options = options || {}
+      // 已登录守卫
+      var token = uni.getStorageSync('token')
+      if (token) {
+        if (typeof this.onLoginSuccess === 'function') {
+          try {
+            this.onLoginSuccess()
+          } catch (e) {
+            console.warn('onLoginSuccess 调用失败', e)
+          }
+          return
+        }
+        if (typeof this.loadData === 'function') {
+          try {
+            this.loadData()
+          } catch (e) {
+            console.warn('loadData 调用失败', e)
+          }
+          return
+        }
+        return
+      }
+
+      var sheet = findLoginSheet(this)
+      if (sheet && typeof sheet.open === 'function') {
+        sheet.open(options)
+      } else {
+        // 宿主页面未挂浮层时回退到整页登录(历史行为,向后兼容)
+        var url = '/pages/login/login'
+        var qs = []
+        if (options.inviteCode) {
+          qs.push('inviteCode=' + encodeURIComponent(options.inviteCode))
+          qs.push('inviteType=' + encodeURIComponent(options.inviteType || 'family'))
+        }
+        if (options.refCode) {
+          qs.push('refCode=' + encodeURIComponent(options.refCode))
+        }
+        if (options.redirect) {
+          qs.push('redirect=' + encodeURIComponent(options.redirect))
+        }
+        if (qs.length) url += '?' + qs.join('&')
+        uni.navigateTo({ url: url })
+      }
+    },
+
+    closeLoginSheet: function() {
+      var sheet = findLoginSheet(this)
+      if (sheet && typeof sheet.close === 'function') {
+        sheet.close()
+      }
+    },
+
+    onSheetLoginSuccess: function(data) {
+      if (typeof this.onLoginSuccess === 'function') {
+        try {
+          this.onLoginSuccess(data)
+        } catch (e) {
+          console.warn('onSheetLoginSuccess 调用失败', e)
+        }
+      } else if (typeof this.loadData === 'function') {
+        try {
+          this.loadData()
+        } catch (e) {
+          console.warn('loadData 调用失败', e)
+        }
+      }
+    }
+  }
+}

+ 5 - 3
cfc-frontend/pages/action-detail/index.vue

@@ -30,7 +30,7 @@
     </view>
     </view>
 
 
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
-<DimensionIntroCard
+<DimensionVisitorCard
   v-if="!isLoggedIn"
   v-if="!isLoggedIn"
   dimCode="action"
   dimCode="action"
   card-title="行维度 · 关系滋养"
   card-title="行维度 · 关系滋养"
@@ -41,7 +41,8 @@
   ]"
   ]"
   cta-text="登录绘制关系网络"
   cta-text="登录绘制关系网络"
   secondary-text="了解关系维度"
   secondary-text="了解关系维度"
-  secondary-url="/pages/action-detail/dimension-intro" />
+  secondary-url="/pages/action-detail/dimension-intro"
+  preview-text="登录后绘制你的家庭关系网络图" />
 
 
     <!-- 登录后:五维能量条 -->
     <!-- 登录后:五维能量条 -->
     <FamilyEnergyBar
     <FamilyEnergyBar
@@ -266,13 +267,14 @@ import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
+import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import DimensionSubDims from './components/DimensionSubDims.vue'
 import DimensionSubDims from './components/DimensionSubDims.vue'
 import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getOctopusTentacles, getPearlResources, deletePearlResource, getAbilityMap, getRadarData, getPearlReminders, getOctopusEffects, getOctopusAnalysisSummary, getOctopusExpansionSuggest, getOctopusEffectKeyPersons } from '../../utils/api.js'
 import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getOctopusTentacles, getPearlResources, deletePearlResource, getAbilityMap, getRadarData, getPearlReminders, getOctopusEffects, getOctopusAnalysisSummary, getOctopusExpansionSuggest, getOctopusEffectKeyPersons } from '../../utils/api.js'
 
 
 export default {
 export default {
-  components: { TabTransition, PageBanner, FamilyEnergyBar, FamilyRelationGraph, PearlDiagram, AbilityDiagram, OctopusDiagram, FamilyMemberStrip, DimensionTasks, DimensionActivities, DimensionProducts, DimensionIntroCard, FloatingAvatar, RadarChart, DimensionSubDims },
+  components: { TabTransition, PageBanner, FamilyEnergyBar, FamilyRelationGraph, PearlDiagram, AbilityDiagram, OctopusDiagram, FamilyMemberStrip, DimensionTasks, DimensionActivities, DimensionProducts, DimensionIntroCard, DimensionVisitorCard, FloatingAvatar, RadarChart, DimensionSubDims },
   data() {
   data() {
     return {
     return {
       isLoggedIn: false,
       isLoggedIn: false,

+ 5 - 3
cfc-frontend/pages/body-detail/index.vue

@@ -213,7 +213,7 @@
     </view>
     </view>
 
 
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
-<DimensionIntroCard
+<DimensionVisitorCard
   v-if="!isLoggedIn"
   v-if="!isLoggedIn"
   dimCode="body"
   dimCode="body"
   card-title="身体维度 · 全家健康管理"
   card-title="身体维度 · 全家健康管理"
@@ -224,7 +224,8 @@
   ]"
   ]"
   cta-text="立即登录查看健康概览"
   cta-text="立即登录查看健康概览"
   secondary-text="查看健康报告"
   secondary-text="查看健康报告"
-  secondary-url="/pages/health/report-list" />
+  secondary-url="/pages/health/report-list"
+  preview-text="登录后展示你的健康概览 · 趋势曲线" />
 
 
     <view class="bottom-spacer"></view>
     <view class="bottom-spacer"></view>
     <FloatingAvatar />
     <FloatingAvatar />
@@ -238,6 +239,7 @@ import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
+import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 
 
 import {
 import {
@@ -255,7 +257,7 @@ import {
 } from '../../utils/api.js'
 } from '../../utils/api.js'
 
 
 export default {
 export default {
-  components: { TabTransition, PageBanner, FamilyEnergyBar, RadarChart, FloatingAvatar, FamilyMemberStrip, DimensionIntroCard },
+  components: { TabTransition, PageBanner, FamilyEnergyBar, RadarChart, FloatingAvatar, FamilyMemberStrip, DimensionIntroCard, DimensionVisitorCard },
   data() {
   data() {
     return {
     return {
       isLoggedIn: false,
       isLoggedIn: false,

+ 5 - 3
cfc-frontend/pages/mind-detail/index.vue

@@ -40,7 +40,7 @@
     </view>
     </view>
 
 
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
-<DimensionIntroCard
+<DimensionVisitorCard
   v-if="!isLoggedIn"
   v-if="!isLoggedIn"
   dimCode="mind"
   dimCode="mind"
   card-title="心维度 · 心理成长"
   card-title="心维度 · 心理成长"
@@ -51,7 +51,8 @@
   ]"
   ]"
   cta-text="登录查看心理画像"
   cta-text="登录查看心理画像"
   secondary-text="了解心理维度"
   secondary-text="了解心理维度"
-  secondary-url="/pages/mind-detail/dimension-intro" />
+  secondary-url="/pages/mind-detail/dimension-intro"
+  preview-text="登录后展示你的心理画像与情绪关联" />
 
 
         <!-- 登录后:五维能量条 -->
         <!-- 登录后:五维能量条 -->
         <FamilyEnergyBar
         <FamilyEnergyBar
@@ -233,6 +234,7 @@
 import TabTransition from '../../components/tab-transition.vue'
 import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
+import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import PsychCrisisBanner from '@/pages/mind-detail/components/PsychCrisisBanner.vue'
 import PsychCrisisBanner from '@/pages/mind-detail/components/PsychCrisisBanner.vue'
@@ -245,7 +247,7 @@ import config from '../../config.js'
 import { parseDate } from '../../utils/format.js'
 import { parseDate } from '../../utils/format.js'
 
 
 export default {
 export default {
-components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisisBanner, FloatingAvatar, FamilyTianpanCard, FamilyMemberStrip, DimensionIntroCard },
+components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisisBanner, FloatingAvatar, FamilyTianpanCard, FamilyMemberStrip, DimensionIntroCard, DimensionVisitorCard },
   data() {
   data() {
     return {
     return {
       isLoggedIn: false,
       isLoggedIn: false,

+ 2 - 2
cfc-frontend/pages/vendor/team-system/team-system.vue

@@ -220,7 +220,7 @@ export default {
     // 获取团队成员
     // 获取团队成员
     async fetchTeamMembers() {
     async fetchTeamMembers() {
       try {
       try {
-        const res = await getVendorTeamMembers(this.systemInfo?.id)
+        const res = await getVendorTeamMembers(this.systemInfo && this.systemInfo.id)
         return res.data || []
         return res.data || []
       } catch (e) {
       } catch (e) {
         return []
         return []
@@ -231,7 +231,7 @@ export default {
     async fetchTeamCustomers() {
     async fetchTeamCustomers() {
       try {
       try {
         const res = await getVendorTeamCustomers({
         const res = await getVendorTeamCustomers({
-          systemId: this.systemInfo?.id,
+          systemId: this.systemInfo && this.systemInfo.id,
           parentId: null,
           parentId: null,
           page: 1,
           page: 1,
           size: 50
           size: 50

+ 3 - 2
cfc-frontend/pages/wealth/index.vue

@@ -22,7 +22,7 @@
       </view>
       </view>
 
 
       <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
       <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
-      <DimensionIntroCard
+      <DimensionVisitorCard
         dimCode="wealth"
         dimCode="wealth"
         card-title="富维度 · 利他创富"
         card-title="富维度 · 利他创富"
         :benefits="[
         :benefits="[
@@ -268,6 +268,7 @@ import BodyWealthAlert from '@/pages/wealth/components/BodyWealthAlert.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
+import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import DimensionSubDims from './components/DimensionSubDims.vue'
 import DimensionSubDims from './components/DimensionSubDims.vue'
 import {
 import {
   getChildren,
   getChildren,
@@ -288,7 +289,7 @@ import nav from '../../utils/nav.js'
 import config from '../../config.js'
 import config from '../../config.js'
 
 
 export default {
 export default {
-  components: { TabTransition, WealthEnergyRing, WealthSubScores, BodyWealthAlert, FamilyMemberStrip, FamilyEnergyBar, DimensionIntroCard, DimensionSubDims },
+  components: { TabTransition, WealthEnergyRing, WealthSubScores, BodyWealthAlert, FamilyMemberStrip, FamilyEnergyBar, DimensionIntroCard, DimensionVisitorCard, DimensionSubDims },
   data() {
   data() {
     return {
     return {
       showTabTransition: true,
       showTabTransition: true,

+ 5 - 3
cfc-frontend/pages/wisdom-detail/index.vue

@@ -47,7 +47,7 @@
     </view>
     </view>
 
 
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
     <!-- 游客价值卡(统一组件:维度色 CTA + 价值预览) -->
-    <DimensionIntroCard
+    <DimensionVisitorCard
       v-if="!isLoggedIn"
       v-if="!isLoggedIn"
       dimCode="wisdom"
       dimCode="wisdom"
       card-title="智维度 · 认知赋能"
       card-title="智维度 · 认知赋能"
@@ -58,7 +58,8 @@
       ]"
       ]"
       cta-text="登录做认知测评"
       cta-text="登录做认知测评"
       secondary-text="了解认知维度"
       secondary-text="了解认知维度"
-      secondary-url="/pages/wisdom-detail/dimension-intro" />
+      secondary-url="/pages/wisdom-detail/dimension-intro"
+      preview-text="登录后展示你的认知能力雷达图" />
 
 
     <!-- 4. 五维能量条(登录后) -->
     <!-- 4. 五维能量条(登录后) -->
     <FamilyEnergyBar
     <FamilyEnergyBar
@@ -240,11 +241,12 @@ import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
+import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import { getVisibleSections, getChildren, getFamilyEnergySandbox, getEnergySandbox, getAssessmentLatestResult, getDanReportByChild, getGutCognitionIndicators, getLatestIndicators, getRadarData } from '../../utils/api.js'
 import { getVisibleSections, getChildren, getFamilyEnergySandbox, getEnergySandbox, getAssessmentLatestResult, getDanReportByChild, getGutCognitionIndicators, getLatestIndicators, getRadarData } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import { parseDate } from '../../utils/format.js'
 
 
 export default {
 export default {
-  components: { TabTransition, RadarChart, FamilyEnergyBar, FloatingAvatar, FamilyMemberStrip, DimensionIntroCard },
+  components: { TabTransition, RadarChart, FamilyEnergyBar, FloatingAvatar, FamilyMemberStrip, DimensionIntroCard, DimensionVisitorCard },
   data() {
   data() {
     return {
     return {
       isLoggedIn: false,
       isLoggedIn: false,