Browse Source

fix(frontend): 修复我的页面主包引用分包组件导致内容不显示

主包页面 pages/profile-main/profile 无法引用分包 pages/profile/components 中的组件,
导致 ProfileHeader/Badges/Growth/Menu 加载失败,页面仅显示内联个人信息卡片。

将 4 个 Profile 组件复制到全局 components/ 目录,并更新两处 import 路径。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 1 month ago
parent
commit
aceec52042

+ 173 - 0
cfc-frontend/components/ProfileBadges.vue

@@ -0,0 +1,173 @@
+<template>
+  <view class="section">
+    <view class="section-header">
+      <text class="section-title">🏅 成就徽章</text>
+      <text class="section-more" @click="goAllBadges">全部 ›</text>
+    </view>
+    <view class="badge-grid">
+      <view class="badge-item" v-for="badge in badges" :key="badge.id">
+        <view :class="['badge-icon-wrap', badge.unlocked ? 'unlocked' : 'locked']">
+          <text class="badge-icon">{{ badge.icon }}</text>
+        </view>
+        <text class="badge-name">{{ badge.name }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getUserBadges } from '../utils/api.js'
+
+export default {
+  name: 'ProfileBadges',
+  data() {
+    return {
+      badgesLoaded: false,
+      badges: [
+        { id: 1, icon: '🏆', name: '连续7天', unlocked: false },
+        { id: 2, icon: '⭐', name: '积分达人', unlocked: false },
+        { id: 3, icon: '🔥', name: '执行之星', unlocked: false },
+        { id: 4, icon: '🌟', name: '学习先锋', unlocked: false }
+      ]
+    }
+  },
+  onShow() {
+    if (!uni.getStorageSync('token')) return
+    this.loadBadges()
+  },
+  methods: {
+    getDefaultBadges() {
+      return [
+        { id: 1, icon: '\u{1F3C6}', name: '连续7天', unlocked: false },
+        { id: 2, icon: '\u{2B50}', name: '积分达人', unlocked: false },
+        { id: 3, icon: '\u{1F525}', name: '执行之星', unlocked: false },
+        { id: 4, icon: '\u{1F31F}', name: '学习先锋', unlocked: false },
+        { id: 5, icon: '\u{1F4AA}', name: '体能达人', unlocked: false },
+        { id: 6, icon: '\u{1F9E0}', name: '心智成长', unlocked: false },
+        { id: 7, icon: '👨‍👩‍👧‍👦', name: '家庭之星', unlocked: false },
+        { id: 8, icon: '\u{1F4DA}', name: '阅读达人', unlocked: false },
+        { id: 9, icon: '\u{1F3AF}', name: '全能冠军', unlocked: false }
+      ]
+    },
+    loadBadges() {
+      let memberId = uni.getStorageSync('currentChildId') || null
+      if (!memberId) {
+        // Try to get first child id
+        const children = uni.getStorageSync('childrenList')
+        if (children && children.length > 0) {
+          memberId = children[0].id
+        }
+      }
+
+      const defaultBadges = this.getDefaultBadges()
+
+      if (memberId) {
+        getUserBadges(memberId).then((res) => {
+          if (res.code === 200 && res.data && res.data.length > 0) {
+            const list = res.data.map((item, i) => ({
+              id: item.badge && item.badge.id || i + 1,
+              icon: item.badge && item.badge.icon || '⭐',
+              name: item.badge && item.badge.name || '未知勋章',
+              unlocked: true
+            }))
+            while (list.length < 9) {
+              const idx = list.length
+              list.push({
+                id: defaultBadges[idx].id,
+                icon: defaultBadges[idx].icon,
+                name: defaultBadges[idx].name,
+                unlocked: false
+              })
+            }
+            this.badges = list.slice(0, 9)
+          } else {
+            this.loadBadgesLocal(defaultBadges)
+          }
+        }).catch(() => {
+          this.loadBadgesLocal(defaultBadges)
+        })
+      } else {
+        this.loadBadgesLocal(defaultBadges)
+      }
+    },
+    loadBadgesLocal(defaultBadges) {
+      const streakDays = parseInt(uni.getStorageSync('streakDays') || 0)
+      const totalPoints = parseInt(uni.getStorageSync('totalPoints') || 0)
+      const completedTasks = parseInt(uni.getStorageSync('completedTasks') || 0)
+      const badges = JSON.parse(JSON.stringify(defaultBadges))
+      if (streakDays >= 7) badges[0].unlocked = true
+      if (totalPoints >= 1000) badges[1].unlocked = true
+      if (completedTasks >= 50) badges[2].unlocked = true
+      if (totalPoints >= 500) badges[3].unlocked = true
+      if (streakDays >= 30) badges[4].unlocked = true
+      if (completedTasks >= 100) badges[8].unlocked = true
+      this.badges = badges
+      this.badgesLoaded = true
+    },
+    goAllBadges() {
+      uni.showToast({ title: '即将上线', icon: 'none' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.section {
+  margin: 20rpx 30rpx;
+}
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.section-more {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.badge-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.badge-item {
+  width: 20%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.badge-icon-wrap {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 8rpx;
+  font-size: 40rpx;
+}
+.badge-icon-wrap.unlocked {
+  background: linear-gradient(135deg, #fdfbfb, #ebedee);
+  box-shadow: 0 4rpx 16rpx rgba(253,160,91,0.3);
+}
+.badge-icon-wrap.locked {
+  background: #f0f0f0;
+  opacity: 0.5;
+}
+.badge-name {
+  font-size: 20rpx;
+  color: #999;
+  text-align: center;
+}
+</style>

+ 134 - 0
cfc-frontend/components/ProfileGrowth.vue

@@ -0,0 +1,134 @@
+<template>
+  <view class="section">
+    <view class="section-header">
+      <text class="section-title">📈 成长报告</text>
+      <text class="section-more" @click="goGrowthReport">查看 ›</text>
+    </view>
+    <view class="report-card" @click="goGrowthReport">
+      <view class="report-stat-item">
+        <text class="report-stat">{{ growthReport.monthActiveDays || 0 }}天</text>
+        <text class="report-label">本月活跃</text>
+      </view>
+      <view class="report-divider"></view>
+      <view class="report-stat-item">
+        <text class="report-stat">{{ growthReport.totalTasks || 0 }}</text>
+        <text class="report-label">累计任务</text>
+      </view>
+      <view class="report-divider"></view>
+      <view class="report-stat-item">
+        <text class="report-stat">{{ growthReport.completionRate || 0 }}%</text>
+        <text class="report-label">完成率</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getGrowthReport } from '../utils/api.js'
+
+export default {
+  name: 'ProfileGrowth',
+  data() {
+    return {
+      growthReport: {
+        monthActiveDays: 0,
+        totalTasks: 0,
+        completionRate: 0
+      }
+    }
+  },
+  onShow() {
+    if (!uni.getStorageSync('token')) return
+    this.loadGrowthReport()
+  },
+  methods: {
+    loadGrowthReport() {
+      let memberId = uni.getStorageSync('currentChildId') || null
+      if (!memberId) {
+        const children = uni.getStorageSync('childrenList')
+        if (children && children.length > 0) {
+          memberId = children[0].id
+        }
+      }
+      if (!memberId) {
+        this.loadGrowthReportLocal()
+        return
+      }
+      getGrowthReport(memberId).then((res) => {
+        if (res.code === 200 && res.data) {
+          const d = res.data
+          this.growthReport.monthActiveDays = d.monthActiveDays || d.activeDays || 0
+          this.growthReport.totalTasks = (d.taskStats && d.taskStats.total) || d.totalTasks || 0
+          this.growthReport.completionRate = (d.taskStats && d.taskStats.completionRate) || d.completionRate || 0
+        } else {
+          this.loadGrowthReportLocal()
+        }
+      }).catch(() => {
+        this.loadGrowthReportLocal()
+      })
+    },
+    loadGrowthReportLocal() {
+      const streakDays = parseInt(uni.getStorageSync('streakDays') || 0)
+      const completedTasks = parseInt(uni.getStorageSync('completedTasks') || 0)
+      this.growthReport.monthActiveDays = streakDays > 0 ? Math.min(streakDays, 30) : 0
+      this.growthReport.totalTasks = completedTasks || 0
+      this.growthReport.completionRate = 85
+    },
+    goGrowthReport() {
+      const memberId = uni.getStorageSync('currentChildId') || ''
+      uni.navigateTo({ url: '/pages/growth/report?memberId=' + memberId })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.section {
+  margin: 20rpx 30rpx;
+}
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.section-more {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.report-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  display: flex;
+  align-items: center;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.report-stat-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.report-stat {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #667eea;
+}
+.report-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.report-divider {
+  width: 1rpx;
+  height: 60rpx;
+  background: #f0f0f0;
+}
+</style>

+ 191 - 0
cfc-frontend/components/ProfileHeader.vue

@@ -0,0 +1,191 @@
+<template>
+  <view>
+    <!-- 退出切换按钮(仅家长切换到孩子状态时显示) -->
+    <view class="exit-switch" v-if="isSwitchedChild" @click="onExitSwitch">
+      <text>🔄 退出切换</text>
+    </view>
+
+    <view class="user-card">
+      <view class="avatar" @click="$emit('avatar-click')">👤</view>
+      <view class="user-info">
+        <view class="nickname">{{ nickname || '未设置昵称' }}</view>
+        <view class="role">{{ role === 'parent' ? '家长模式' : (role === 'child' ? '孩子模式' : '成长规划师模式') }}</view>
+        <view class="system-points" v-if="role === 'parent' && children.length > 0">🏅 系统积分: {{ children[0].systemPoints || 0 }}</view>
+      </view>
+      <!-- 切换按钮仅在非切换状态下显示 -->
+      <button class="btn-switch" v-if="!isSwitchedChild" @click="onSwitchMode">切换</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getChildren, verifyPassword } from '../utils/api.js'
+
+export default {
+  name: 'ProfileHeader',
+  data() {
+    return {
+      nickname: '',
+      role: 'parent',
+      children: [],
+      isSwitchedChild: false
+    }
+  },
+  computed: {
+    isLoggedIn() {
+      return !!uni.getStorageSync('token')
+    }
+  },
+  onShow() {
+    if (!uni.getStorageSync('token')) return
+    const currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
+    const userInfo = uni.getStorageSync('userInfo')
+    this.nickname = this.$store.state.nickname || (userInfo && userInfo.nickname) || ''
+    this.role = currentRole
+    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
+    this.loadChildren()
+  },
+  methods: {
+    async loadChildren() {
+      try {
+        const res = await getChildren()
+        this.children = res.data || []
+      } catch (e) {
+        console.error('获取孩子列表失败', e)
+      }
+    },
+    onSwitchMode() {
+      if (this.role === 'child') {
+        uni.showModal({
+          title: '验证密码',
+          content: '请输入家长密码',
+          editable: true,
+          success: (res) => {
+            if (res.confirm && res.content) {
+              verifyPassword(res.content).then((result) => {
+                if (result.code === 200) {
+                  this.doSwitchMode()
+                } else {
+                  uni.showToast({ title: '密码错误', icon: 'none' })
+                }
+              }).catch(() => {
+                uni.showToast({ title: '验证失败', icon: 'none' })
+              })
+            }
+          }
+        })
+      } else {
+        this.doSwitchMode()
+      }
+    },
+    doSwitchMode() {
+      if (this.children.length === 0) {
+        this.loadChildren()
+        if (this.children.length === 0) {
+          uni.showModal({
+            title: '提示',
+            content: '您还没有添加孩子,请先在家庭成员中添加',
+            success: (res) => {
+              if (res.confirm) {
+                uni.navigateTo({ url: '/pages/profile-extra/family-members' })
+              }
+            }
+          })
+          return
+        }
+      }
+      if (this.children.length === 1) {
+        this.$store.commit('switchToChild', this.children[0].id)
+        this.role = 'child'
+        this.isSwitchedChild = true
+        uni.showToast({ title: '已切换为孩子模式', icon: 'success' })
+        return
+      }
+      uni.showActionSheet({
+        itemList: this.children.map(c => c.nickname),
+        success: (res) => {
+          const selectedChild = this.children[res.tapIndex]
+          this.$store.commit('switchToChild', selectedChild.id)
+          this.role = 'child'
+          this.isSwitchedChild = true
+          uni.showToast({ title: `已切换为${selectedChild.nickname}模式`, icon: 'success' })
+        }
+      })
+    },
+    onExitSwitch() {
+      if (!this.isSwitchedChild) return
+      uni.showModal({
+        title: '退出切换',
+        content: '请输入家长密码以确认退出',
+        editable: true,
+        success: (res) => {
+          if (res.confirm && res.content) {
+            verifyPassword(res.content).then((result) => {
+              if (result.code === 200) {
+                this.$store.commit('switchBackToParent')
+                this.isSwitchedChild = false
+                this.role = 'parent'
+                uni.showToast({ title: '已退出切换', icon: 'success' })
+                uni.reLaunch({ url: '/pages/index-home/index' })
+              } else {
+                uni.showToast({ title: '密码错误', icon: 'none' })
+              }
+            }).catch(() => {
+              uni.showToast({ title: '验证失败', icon: 'none' })
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.exit-switch {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 0 30rpx 16rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  padding: 16rpx;
+  border-radius: 12rpx;
+  font-size: 26rpx;
+}
+
+.user-card {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  margin: 0 30rpx 20rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.avatar {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 48rpx;
+  margin-right: 24rpx;
+  flex-shrink: 0;
+}
+.user-info { flex: 1; }
+.nickname { font-size: 34rpx; font-weight: bold; color: #333; margin-bottom: 8rpx; }
+.role { font-size: 24rpx; color: #999; margin-bottom: 6rpx; }
+.system-points { font-size: 24rpx; color: #F97316; margin-top: 4rpx; }
+
+.btn-switch {
+  background: #667eea;
+  color: #fff;
+  font-size: 24rpx;
+  padding: 10rpx 30rpx;
+  border-radius: 30rpx;
+  line-height: 1.8;
+}
+</style>

+ 375 - 0
cfc-frontend/components/ProfileMenu.vue

@@ -0,0 +1,375 @@
+<template>
+  <view>
+    <view class="menu-list">
+      <!-- ===== 个人 ===== -->
+      <view class="menu-group">
+        <view class="menu-group-title">── 个人 ──</view>
+        <view class="menu-item" v-if="role === 'parent'" @click="goToFamilyMembers">
+          <text>👨‍👩‍👧‍👦 家庭成员</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" v-if="role === 'parent'" @click="goToAppointments">
+          <text>📅 测评预约</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" @click="goToDailyTasks">
+          <text>📋 每日任务</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" v-if="role === 'parent'" @click="goToOnboarding">
+          <text>🎯 新手任务</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" @click="goToReportManagement">
+          <text>📋 报告管理</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" v-if="role === 'parent'" @click="goToServiceRoleApply">
+          <text>📋 服务角色申请</text>
+          <text class="arrow">›</text>
+        </view>
+      </view>
+
+      <!-- ===== 服务 ===== -->
+      <view class="menu-group" v-if="showServices">
+        <view class="menu-group-title">── 服务 ──</view>
+        <view class="menu-item" @click="goToMembership">
+          <text>👑 会员中心</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" v-if="isVendor" @click="goToVendorCenter">
+          <text>🏪 服务商中心</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" @click="goToOrders">
+          <text>🛒 订单管理</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" @click="goToConsigneeList">
+          <text>📍 收货人管理</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" @click="goToAfterSales">
+          <text>🔄 售后记录</text>
+          <text class="arrow">›</text>
+        </view>
+      </view>
+
+      <!-- ===== 设置 ===== -->
+      <view class="menu-group">
+        <view class="menu-group-title">── 设置 ──</view>
+        <view class="menu-item" v-if="role === 'parent'" @click="showPasswordModal = true">
+          <text>🔐 {{ hasPassword ? '重置密码' : '设置密码' }}</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item menu-item-switch" v-if="role === 'parent'">
+          <text>👁 对家庭成员可见</text>
+          <switch :checked="showToFamily === 1" @change="onShowToFamilyChange" color="#5B9BD5" />
+        </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>
+        </view>
+      </view>
+    </view>
+
+    <!-- 设置/重置密码弹窗 -->
+    <view class="modal-mask" v-if="showPasswordModal" @click="showPasswordModal = false">
+      <view class="modal" @click.stop>
+        <view class="modal-title">{{ hasPassword ? '重置密码' : '设置密码' }}</view>
+        <view class="form-item" v-if="hasPassword">
+          <input v-model="oldPassword" type="number" password placeholder="输入旧密码" maxlength="6" />
+        </view>
+        <view class="form-item">
+          <input v-model="newPassword" type="number" password placeholder="输入新密码" maxlength="6" />
+        </view>
+        <view class="form-item">
+          <input v-model="confirmPassword" type="number" password placeholder="确认新密码" maxlength="6" />
+        </view>
+        <view class="modal-btns">
+          <button @click="showPasswordModal = false">取消</button>
+          <button class="btn-primary" @click="savePassword">确定</button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { setPassword, verifyPassword, updateMemberVisibility, getVisibleFamilyMembers, vendorStatus, ensureFamily } from '../utils/api.js'
+
+export default {
+  name: 'ProfileMenu',
+  props: {
+    role: {
+      type: String,
+      default: 'parent'
+    },
+    showServices: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      hasPassword: false,
+      showPasswordModal: false,
+      oldPassword: '',
+      newPassword: '',
+      confirmPassword: '',
+      showToFamily: 1,
+      isVendor: false
+    }
+  },
+  onShow() {
+    if (!uni.getStorageSync('token')) return
+    this.hasPassword = !!uni.getStorageSync('passwordSet')
+    this.loadShowToFamily()
+    this.checkVendorStatus()
+  },
+  methods: {
+    async loadShowToFamily() {
+      try {
+        const uid = this.$store.state.userId || uni.getStorageSync('userId')
+        const res = await getVisibleFamilyMembers()
+        const members = res.data || []
+        const self = members.find(m => m.id == uid)
+        if (self) {
+          this.showToFamily = self.showToFamily !== 0 ? 1 : 0
+        }
+      } catch (e) {}
+    },
+    goToOnboarding() {
+      uni.navigateTo({ url: '/pages/profile-extra/onboarding' })
+    },
+    goToReportManagement() {
+      uni.navigateTo({ url: '/pages/profile/report-management' })
+    },
+    goToServiceRoleApply() {
+      uni.navigateTo({ url: '/pages/profile/service-role-apply' })
+    },
+    goToFamilyMembers() {
+      uni.navigateTo({ url: '/pages/profile-extra/family-members' })
+    },
+    goToAppointments() {
+      uni.navigateTo({ url: '/pages/parent/appointments' })
+    },
+    goToDailyTasks() {
+      uni.navigateTo({ url: '/pages/tasks/daily-tasks' })
+    },
+    async onShowToFamilyChange(e) {
+      const val = e.detail.value ? 1 : 0
+      const uid = this.$store.state.userId || uni.getStorageSync('userId')
+      try {
+        await updateMemberVisibility(uid, 'parent', val)
+        this.showToFamily = val
+        uni.showToast({ title: val ? '已对家庭成员可见' : '已对家庭成员隐藏', icon: 'none' })
+      } catch (e) {
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      }
+    },
+    goToMembership() {
+      uni.navigateTo({ url: '/pages/membership/index' })
+    },
+    goToVendorCenter() {
+      uni.navigateTo({ url: '/pages/vendor/center' })
+    },
+    goToOrders() {
+      uni.navigateTo({ url: '/pages/shop/order-list/order-list' })
+    },
+    goToConsigneeList() {
+      uni.navigateTo({ url: '/pages/shop/consignee-list/consignee-list' })
+    },
+    goToAfterSales() {
+      uni.navigateTo({ url: '/pages/shop/after-sales/after-sales' })
+    },
+    
+    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
+      this.$emit('invite-generate', {
+        title: config.title,
+        path: config.path + '&invite_code=',
+        imageUrl: '/static/invite-card.png'
+      })
+      uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })
+    },
+    async savePassword() {
+      if (this.newPassword.length !== 6) {
+        uni.showToast({ title: '请输入6位密码', icon: 'none' })
+        return
+      }
+      if (this.newPassword !== this.confirmPassword) {
+        uni.showToast({ title: '两次密码不一致', icon: 'none' })
+        return
+      }
+      if (this.hasPassword && this.oldPassword.length !== 6) {
+        uni.showToast({ title: '请输入旧密码', icon: 'none' })
+        return
+      }
+      try {
+        if (this.hasPassword) {
+          const verifyRes = await verifyPassword(this.oldPassword)
+          if (verifyRes.code !== 200) {
+            uni.showToast({ title: '旧密码错误', icon: 'none' })
+            return
+          }
+        }
+        await setPassword(this.newPassword)
+        uni.setStorageSync('passwordSet', true)
+        this.hasPassword = true
+        uni.showToast({ title: '密码设置成功', icon: 'success' })
+        this.showPasswordModal = false
+        this.oldPassword = ''
+        this.newPassword = ''
+        this.confirmPassword = ''
+      } catch (e) {
+        console.error('设置密码失败', e)
+      }
+    },
+    logout() {
+      uni.showModal({
+        title: '确认退出',
+        content: '确定要退出登录吗?',
+        success: (res) => {
+          if (res.confirm) {
+            this.$store.commit('logout')
+            uni.reLaunch({ url: '/pages/index-home/index' })
+          }
+        }
+      })
+    },
+    async checkVendorStatus() {
+      try {
+        const res = await vendorStatus()
+        this.isVendor = res.data && res.data.vendorStatus === 'approved'
+      } catch (e) {
+        this.isVendor = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.menu-list {
+  margin: 30rpx;
+}
+.menu-group {
+  background: #fff;
+  border-radius: 20rpx;
+  margin-bottom: 20rpx;
+  overflow: hidden;
+}
+.menu-group-title {
+  padding: 20rpx 30rpx 10rpx;
+  font-size: 22rpx;
+  color: #999;
+  text-align: center;
+}
+.menu-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28rpx 30rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+  font-size: 28rpx;
+  color: #333;
+}
+.menu-item:last-child {
+  border-bottom: none;
+}
+.arrow {
+  color: #ccc;
+  font-size: 28rpx;
+}
+.menu-item-switch {
+  justify-content: space-between;
+}
+.menu-item-switch text {
+  flex: 1;
+}
+
+.modal-mask {
+  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 {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  width: 80%;
+}
+.modal-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+.form-item {
+  margin-bottom: 20rpx;
+}
+.form-item input {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+}
+.modal-btns {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 30rpx;
+}
+.modal-btns button {
+  flex: 1;
+  font-size: 28rpx;
+  padding: 20rpx;
+  border-radius: 12rpx;
+}
+.modal-btns .btn-primary {
+  background: #667eea;
+  color: #fff;
+}
+</style>

+ 4 - 4
cfc-frontend/pages/profile-main/profile.vue

@@ -52,10 +52,10 @@
 
 <script>
 import TabTransition from '../../components/tab-transition.vue'
-import ProfileHeader from '../profile/components/ProfileHeader.vue'
-import ProfileBadges from '../profile/components/ProfileBadges.vue'
-import ProfileGrowth from '../profile/components/ProfileGrowth.vue'
-import ProfileMenu from '../profile/components/ProfileMenu.vue'
+import ProfileHeader from '../../components/ProfileHeader.vue'
+import ProfileBadges from '../../components/ProfileBadges.vue'
+import ProfileGrowth from '../../components/ProfileGrowth.vue'
+import ProfileMenu from '../../components/ProfileMenu.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import HealthBadge from '../../components/HealthBadge.vue'
 import HealthTimeline from '../../components/HealthTimeline.vue'

+ 2 - 2
cfc-frontend/pages/profile/index.vue

@@ -21,8 +21,8 @@
 
 <script>
 import PageBanner from '../../components/PageBanner.vue'
-import ProfileHeader from './components/ProfileHeader.vue'
-import ProfileMenu from './components/ProfileMenu.vue'
+import ProfileHeader from '../../components/ProfileHeader.vue'
+import ProfileMenu from '../../components/ProfileMenu.vue'
 
 export default {
   components: {