2026-06-19-profile-page-redesign.md 19 KB

富沛(我的)页面改造 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 改造"我的"Tab页为品牌统一的富沛页面,补全品牌头/能量展示/菜单分组/勋章墙/成长报告/邀请合并等设计规范要求

Architecture: 后端勋章API和统计API已存在,前端改造 profile/profile.vue 一个主文件(938行)+ mind/index.vue 一个文件 + utils/api.js 新增两个方法

Tech Stack: uni-app Vue 2, 微信小程序

Design Spec Ref: docs/superpowers/specs/2026-06-19-profile-page-redesign.md

All existing issues in backend Java files are pre-existing and unrelated to these changes.


注意事项(Agent执行时阅读,不写入文件)

  • 搜索范围限制: glob 仅在 cfc-frontend/components/cfc-frontend/pages/cfc-frontend/utils/ 下搜索。禁止搜索 node_modules/unpackage/dist/
  • NEVER 在模板中使用可选链 ?.(微信小程序不支持)
  • NEVER git commitgit push,只修改文件
  • 计算属性使用 function 关键字(Vue 2 Options API)
  • v-forv-if 不要在同一元素上(用 <template> 包裹)
  • DO NOT 修改任何后端文件 — 后端API已存在
  • DO NOT 修改任何其他前端文件 — 只改 profile/profile.vuemind/index.vueutils/api.js
  • DO NOT 搜索或引用任何 node_modules

Task 1: api.js 新增 getUserBadges 和 getGrowthReport 方法

Files:

  • Modify: cfc-frontend/utils/api.js(在文件末尾追加)

  • [ ] Step 1: 在 api.js 末尾新增方法

    // ===================== 勋章系统 =====================
    
    export const getUserBadges = (childId) => {
    return request('/api/badges/child/list', 'POST', { childId })
    }
    
    // ===================== 成长报告 =====================
    
    export const getGrowthReport = (childId) => {
    return request('/api/stats/child/overview', 'POST', { childId })
    }
    

Task 2: profile.vue 改造 — 数据层

Files:

  • Modify: cfc-frontend/pages/profile/profile.vue

  • [ ] Step 1: data() 新增字段

data()growthReport 对象之后添加:

wealthEnergy: 0,
energyTrend: 0,
badgesLoaded: false,
  • Step 2: 更新 loadEnergyData() 方法

替换整个 loadEnergyData 方法(当前第509-523行)为提取 wealth 维度:

loadEnergyData: function() {
  var self = this
  var childId = uni.getStorageSync('currentChildId') || null
  if (!childId && self.children.length > 0) {
    childId = self.children[0].id
  }
  if (!childId) return
  try {
    getEnergyOverview(childId).then(function(res) {
      if (res && res.data) {
        self.totalEnergy = res.data.totalEnergy || 0
        var dims = res.data.dimensions
        if (dims && dims.length > 0) {
          for (var i = 0; i < dims.length; i++) {
            if (dims[i].code === 'wealth') {
              self.wealthEnergy = dims[i].score || 0
              break
            }
          }
        }
      }
    })
  } catch (e) { console.log('获取能量概览失败', e) }
}

注意:需要在文件顶部 import 中添加 getEnergyOverview 引用(确认已有)。

  • Step 3: 替换 loadBadges() 方法

替换当前 loadBadges 方法(第525-536行):

loadBadges: function() {
  var self = this
  var childId = uni.getStorageSync('currentChildId') || null
  if (!childId && self.children.length > 0) {
    childId = self.children[0].id
  }

  // 默认9个勋章(API失败时的fallback)
  var defaultBadges = [
    { 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: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}', name: '家庭之星', unlocked: false },
    { id: 8, icon: '\u{1F4DA}', name: '阅读达人', unlocked: false },
    { id: 9, icon: '\u{1F3AF}', name: '全能冠军', unlocked: false }
  ]

  // 先尝试API
  if (childId) {
    try {
      getUserBadges(childId).then(function(res) {
        if (res.code === 200 && res.data && res.data.length > 0) {
          // API返回的数据结构: [{ childBadge: {...}, badge: { id, name, icon, ... } }]
          var list = []
          for (var i = 0; i < res.data.length; i++) {
            var item = res.data[i]
            var badge = item.badge || {}
            list.push({
              id: badge.id || i + 1,
              icon: badge.icon || '\u{2B50}',
              name: badge.name || '未知勋章',
              unlocked: true
            })
          }
          // 补全到9个
          while (list.length < 9) {
            var idx = list.length
            list.push({
              id: defaultBadges[idx].id,
              icon: defaultBadges[idx].icon,
              name: defaultBadges[idx].name,
              unlocked: false
            })
          }
          self.badges = list.slice(0, 9)
          self.badgesLoaded = true
          return
        }
        // API返回无数据,fallback到本地逻辑
        self.loadBadgesLocal(defaultBadges)
      }).catch(function() {
        self.loadBadgesLocal(defaultBadges)
      })
    } catch (e) {
      self.loadBadgesLocal(defaultBadges)
    }
  } else {
    self.loadBadgesLocal(defaultBadges)
  }
},
loadBadgesLocal: function(defaultBadges) {
  var streakDays = parseInt(uni.getStorageSync('streakDays') || 0)
  var totalPoints = parseInt(uni.getStorageSync('totalPoints') || 0)
  var completedTasks = parseInt(uni.getStorageSync('completedTasks') || 0)
  // 更新解锁状态
  var 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
}
  • Step 4: 新增 loadGrowthReport() 方法

methodsloadBadges 之后、loadActionData 之前添加:

loadGrowthReport: function() {
  var self = this
  var childId = uni.getStorageSync('currentChildId') || null
  if (!childId && self.children.length > 0) {
    childId = self.children[0].id
  }
  if (!childId) {
    // 无childId时使用本地mock
    self.loadGrowthReportLocal()
    return
  }
  try {
    getGrowthReport(childId).then(function(res) {
      if (res.code === 200 && res.data) {
        var data = res.data
        self.growthReport.monthActiveDays = data.monthActiveDays || data.activeDays || 0
        self.growthReport.totalTasks = (data.taskStats && data.taskStats.total) || data.totalTasks || 0
        self.growthReport.completionRate = (data.taskStats && data.taskStats.completionRate) || data.completionRate || 0
      } else {
        self.loadGrowthReportLocal()
      }
    }).catch(function() {
      self.loadGrowthReportLocal()
    })
  } catch (e) {
    self.loadGrowthReportLocal()
  }
},
loadGrowthReportLocal: function() {
  var streakDays = parseInt(uni.getStorageSync('streakDays') || 0)
  var 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
}
  • Step 5: 在 onShow 中调用 loadGrowthReport()

onShow 方法的 this.loadBadges() 之后添加:

this.loadGrowthReport()
  • Step 6: 更新 import

api.js 的 import 行中,确认已有 getEnergyOverview;添加 getUserBadgesgetGrowthReport

import { setPassword, verifyPassword, getChildren, vendorStatus, getEnergyOverview, getUserActionStats, getReferralCode, getReferralSummary, getCommissionSummary, updateMemberVisibility, getVisibleFamilyMembers, getUserBadges, getGrowthReport } from '../../utils/api.js'

Task 3: profile.vue 改造 — 品牌头 + 能量展示

  • Step 1: 更新 PageBanner tagline

修改模板第14-16行:

<PageBanner theme="wealth"
  tagline="丰盈内心,富足生活,厚德载物"
  quote="" />
  • Step 2: 在 PageBanner 之后、exit-switch 之前添加能量值条

在第18行(exit-switch之前)插入:

<!-- 富沛能量值 -->
<view class="wealth-energy-header" v-if="isLoggedIn && wealthEnergy > 0">
  <text class="energy-header-icon">💧</text>
  <text class="energy-header-value">{{ wealthEnergy }}</text>
  <text class="energy-header-label">富沛能量</text>
  <text class="energy-header-trend" v-if="energyTrend > 0">↑较上周+{{ energyTrend }}</text>
</view>
  • Step 3: 添加能量值条样式

<style scoped>.user-card 之前添加:

/* ===== 富沛能量值条 ===== */
.wealth-energy-header {
  background: linear-gradient(135deg, #42A5F5, #64B5F6);
  padding: 16rpx 30rpx;
  display: flex;
  flex-direction: row;
  align-items: center;
}
.energy-header-icon {
  font-size: 36rpx;
  margin-right: 12rpx;
}
.energy-header-value {
  font-size: 40rpx;
  font-weight: bold;
  color: #fff;
  margin-right: 12rpx;
}
.energy-header-label {
  font-size: 26rpx;
  color: #D6EAF8;
  margin-right: auto;
}
.energy-header-trend {
  font-size: 24rpx;
  color: #4ADE80;
  font-weight: 500;
}

Task 4: profile.vue 改造 — 用户卡片扩展

  • Step 1: 在用户卡片的系统积分行之后添加能量行

system-points 行(第28行)之后插入:

<view class="wealth-energy-row" v-if="wealthEnergy > 0">
  <text class="wealth-energy-icon">💧</text>
  <text class="wealth-energy-value">富沛能量: {{ wealthEnergy }}</text>
  <text class="wealth-energy-trend" v-if="energyTrend > 0"> ↑较上周+{{ energyTrend }}</text>
</view>
  • Step 2: 添加样式

.system-points 样式之后添加:

.wealth-energy-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-top: 8rpx;
}
.wealth-energy-icon {
  font-size: 24rpx;
  margin-right: 8rpx;
}
.wealth-energy-value {
  font-size: 24rpx;
  color: #D6EAF8;
  font-weight: 500;
}
.wealth-energy-trend {
  font-size: 22rpx;
  color: #4ADE80;
  margin-left: 8rpx;
}

Task 5: profile.vue 改造 — 功能菜单分组 + 邀请合并

  • Step 1: 替换整个 menu-list 区块

将当前的 menu-list(第131-183行)替换为分组结构:

<view class="menu-list">
  <!-- ===== 个人 ===== -->
  <view class="menu-group">
    <view class="menu-group-title">── 个人 ──</view>
    <view class="menu-item" @click="goToEditInfo">
      <text>📝 修改个人资料</text>
      <text class="arrow">›</text>
    </view>
    <view class="menu-item" v-if="role === 'parent'" @click="goToChildren">
      <text>👶 我的孩子</text>
      <text class="arrow">›</text>
    </view>
    <view class="menu-item" v-if="role === 'parent'" @click="goToFamilyMembers">
      <text>👨‍👩‍👧‍👦 家庭成员</text>
      <text class="arrow">›</text>
    </view>
  </view>

  <!-- ===== 财富 ===== -->
  <view class="menu-group">
    <view class="menu-group-title">── 财富 ──</view>
    <view class="menu-item" @click="goToPromotion">
      <text>📢 推广中心</text>
      <text class="arrow">›</text>
    </view>
    <view class="menu-item" @click="goToPointsLogs">
      <text>📊 积分记录</text>
      <text class="arrow">›</text>
    </view>
  </view>

  <!-- ===== 服务 ===== -->
  <view class="menu-group">
    <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>

  <!-- ===== 设置 ===== -->
  <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>

移除现有的 inviteFamily/inviteTeacher 两个 button 元素(当前第161-168行)。

  • Step 2: 新增 goToOrders 方法

methodsgoToVendorCenter 之后添加:

goToOrders: function() {
  uni.showToast({ title: '即将上线', icon: 'none' })
}
  • Step 3: 新增 showInviteActionSheet 方法

methodslogout 方法之前添加:

showInviteActionSheet: function() {
  var self = this
  var familyId = uni.getStorageSync('familyId')
  var userInfo = uni.getStorageSync('userInfo')
  if (!familyId) {
    uni.showToast({ title: '您暂未加入家庭', icon: 'none' })
    return
  }

  var items = ['👨‍👩‍👧‍👦 邀请家人', '👨‍🏫 邀请成长规划师']
  var types = ['family', 'guide']

  uni.showActionSheet({
    itemList: items,
    success: function(res) {
      var type = types[res.tapIndex]
      self.inviteGenerate(type, familyId, userInfo)
    },
    fail: function() {}
  })
},
inviteGenerate: function(type, familyId, userInfo) {
  var self = this
  try {
    var 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' }
    }
    var config = inviteTypes[type] || inviteTypes.family
    self.shareData = {
      title: config.title,
      path: config.path + '&invite_code=',
      imageUrl: '/static/invite-card.png'
    }
    uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })
  } catch (e) {
    uni.showToast({ title: e.message || '邀请失败', icon: 'none' })
  }
}
  • Step 4: 删除旧的 inviteFamily 和 inviteTeacher 方法

删除当前第454-498行的 inviteFamilyinviteFamilyGenerateinviteTeacherinviteTeacherGenerate 四个方法。

  • Step 5: 添加菜单分组样式

.menu-list 样式之前添加:

/* ===== 功能菜单分组 ===== */
.menu-group {
  margin-top: 20rpx;
}
.menu-group-title {
  font-size: 24rpx;
  color: #ccc;
  text-align: center;
  padding: 12rpx 0;
  letter-spacing: 4rpx;
}

Task 6: profile.vue 改造 — 勋章墙扩展

  • Step 1: 更新勋章墙模板

替换当前勋章网格(第97-105行)为动态3×3网格:

<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>

(当前模板结构已基本一致,只需确认v-for写法 — 当前已是 v-for="badge in badges",保持不变)

  • Step 2: 更新勋章网格样式为3列

修改 .badge-grid 样式(第714-720行):

.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);
  justify-content: flex-start;
}
.badge-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 33.33%;
  margin-bottom: 20rpx;
}

Task 7: profile.vue 改造 — 成长报告跳转

  • Step 1: 更新 goGrowthReport 方法

替换当前方法(第583-585行):

goGrowthReport: function() {
  var childId = uni.getStorageSync('currentChildId') || ''
  uni.navigateTo({ url: '/pages/growth/report?childId=' + childId })
}

Task 8: mind/index.vue 心理Tab新增心愿单入口

Files:

  • Modify: cfc-frontend/pages/mind/index.vue

  • [ ] Step 1: 在心理Tab内容底部添加心愿单区块

在心理Tab内容内、<!-- ===== 推荐阅读 ===== --> 区块之后、</template>(心理Tab闭合)之前插入:

<!-- 心愿单入口(心理Tab) -->
<view class="section wishlist-section" v-if="isLoggedIn">
  <view class="wishlist-card" @click="goWishlist">
    <view class="wishlist-left">
      <text class="wishlist-icon">💝</text>
    </view>
    <view class="wishlist-body">
      <text class="wishlist-title">心愿单</text>
      <text class="wishlist-desc">孩子可在此创建、兑换心愿</text>
    </view>
    <text class="wishlist-arrow">›</text>
  </view>
</view>
  • Step 2: 新增 goWishlist 方法

在 methods 中 goAssessment 方法之前添加:

goWishlist: function() {
  uni.navigateTo({ url: '/pages/rewards/rewards' })
}
  • Step 3: 添加心愿单样式

<style scoped> 末尾添加:

/* ===== 心愿单入口 ===== */
.wishlist-card {
  background: #fff;
  border-radius: 20rpx;
  padding: 28rpx 24rpx;
  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  display: flex;
  flex-direction: row;
  align-items: center;
}
.wishlist-left {
  width: 72rpx;
  height: 72rpx;
  background: linear-gradient(135deg, #FF6B35, #FFD700);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 20rpx;
  flex-shrink: 0;
}
.wishlist-icon {
  font-size: 36rpx;
}
.wishlist-body {
  flex: 1;
}
.wishlist-title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  display: block;
  margin-bottom: 4rpx;
}
.wishlist-desc {
  font-size: 22rpx;
  color: #999;
}
.wishlist-arrow {
  font-size: 36rpx;
  color: #ccc;
  margin-left: 12rpx;
}

Task 9: 编译验证

  • Step 1: LSP诊断检查

使用 lsp_diagnostics 检查 cfc-frontend/pages/profile/profile.vuecfc-frontend/pages/mind/index.vuecfc-frontend/utils/api.js

  • Step 2: 手动验证清单
  • 品牌头 slogan 更新
  • 品牌头下方显示富沛能量值
  • 用户卡片显示富沛能量行
  • 功能菜单4组分组标题
  • 「邀请加入」ActionSheet弹窗选择类型
  • 勋章墙3×3网格展示(优先调API)
  • 成长报告显示真实数据
  • 心智页心理Tab显示心愿单入口