2026-06-11-5-tab-pages.md 20 KB

5-Tab Pages 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.

Goal: Complete all 5 bottom-tab pages (首页/parent-index, 首页/child-index, 首页/teacher-index, 行远, 富沛) to match the detailed design in docs/需求分析/底签页面详细设计.md

Architecture: Each tab page is an independent Vue component. The 首页 uses role-based conditional rendering (parent-index / child-index / teacher-index). Shared components (EnergyWeekCard, TaskItem, BadgeGrid) are extracted to components/.

Tech Stack: uni-app Vue 2 (Options API), SCSS, flexbox layout

Design Tokens: --color-primary: #F97316 (warm orange), --color-accent: #5B9BD5 (blue), --bg-header: #1A2A4A → #1E3A5F (deep blue gradient)


Task 1: parent-index — Add 待处理事项 action-cards and 成长建议 section

Files:

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

  • [ ] Step 1: Add "待处理事项" section with action cards

Insert after the milestone-section div and before review-section:

<!-- ===== 待处理事项 ===== -->
<view class="pending-section animate-fade-in animate-stagger-8">
  <view class="section-title">📋 待处理事项</view>
  <view class="pending-grid">
    <view class="pending-card" @click="goToReview">
      <view class="pending-icon pending-icon--blue">
        <text class="pending-emoji">📝</text>
      </view>
      <view class="pending-info">
        <text class="pending-count accent-color">{{ pendingReviewTasks.length }}</text>
        <text class="pending-label">待审核任务</text>
      </view>
    </view>
    <view class="pending-card" @click="goToWishApprove">
      <view class="pending-icon pending-icon--pink">
        <text class="pending-emoji">💝</text>
      </view>
      <view class="pending-info">
        <text class="pending-count pink-color">{{ pendingReviewWishes.length }}</text>
        <text class="pending-label">待审批心愿</text>
      </view>
    </view>
    <view class="pending-card" @click="goToAssessmentReport">
      <view class="pending-icon pending-icon--green">
        <text class="pending-emoji">📊</text>
      </view>
      <view class="pending-info">
        <text class="pending-count teal-color">{{ hasNewReport ? '有新' : '暂无' }}</text>
        <text class="pending-label">最新测评报告</text>
      </view>
    </view>
    <view class="pending-card" @click="goToActivities">
      <view class="pending-icon pending-icon--orange">
        <text class="pending-emoji">🎉</text>
      </view>
      <view class="pending-info">
        <text class="pending-count orange-color">{{ hotActivityCount }}</text>
        <text class="pending-label">活动推荐</text>
      </view>
    </view>
  </view>
</view>
  • Step 2: Add "成长建议" section

Insert after pending-section:

<!-- ===== 成长建议 ===== -->
<view class="advice-section animate-fade-in animate-stagger-9" v-if="growthAdvice.length > 0">
  <view class="section-title">💡 成长建议</view>
  <PlayfulCard variant="flat" class="advice-card" v-for="advice in growthAdvice" :key="advice.id" padding="0">
    <view class="advice-content" @click="goToAdviceDetail(advice)">
      <view class="advice-icon-wrap">
        <text class="advice-icon">💡</text>
      </view>
      <view class="advice-text-wrap">
        <text class="advice-title">{{ advice.title }}</text>
        <text class="advice-desc">{{ advice.summary }}</text>
      </view>
      <text class="advice-arrow">›</text>
    </view>
  </PlayfulCard>
</view>
  • Step 3: Add data properties and methods

In the data() function, add:

pendingReviewWishes: [],
hotActivityCount: 0,
hasNewReport: false,
growthAdvice: []

In methods, add:

goToWishApprove() {
  uni.navigateTo({ url: '/pages/wishes/approve' })
},
goToAssessmentReport() {
  uni.navigateTo({ url: '/pages/assessment/report' })
},
goToActivities() {
  uni.switchTab({ url: '/pages/discover/index' })
},
goToAdviceDetail(advice) {
  if (advice.link) uni.navigateTo({ url: advice.link })
}
  • [ ] Step 4: Add CSS styles for pending and advice sections

    .pending-section { margin-bottom: 48rpx; }
    .pending-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
    .pending-card {
    width: calc(50% - 8rpx);
    background: #fff;
    border-radius: 20rpx;
    padding: 24rpx;
    box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
    display: flex;
    align-items: center;
    }
    .pending-icon {
    width: 72rpx; height: 72rpx;
    border-radius: 16rpx;
    display: flex; align-items: center; justify-content: center;
    margin-right: 16rpx; flex-shrink: 0;
    }
    .pending-icon--blue { background: rgba(91,155,213,0.12); }
    .pending-icon--pink { background: rgba(236,72,153,0.12); }
    .pending-icon--green { background: rgba(16,185,129,0.12); }
    .pending-icon--orange { background: rgba(249,115,22,0.12); }
    .pending-emoji { font-size: 32rpx; }
    .pending-info { flex: 1; min-width: 0; }
    .pending-count { font-size: 36rpx; font-weight: 800; display: block; }
    .pending-label { font-size: 22rpx; color: #666; margin-top: 4rpx; display: block; }
    .pink-color { color: #EC4899; }
    .orange-color { color: #F97316; }
    .advice-section { margin-bottom: 48rpx; }
    .advice-card { margin-bottom: 12rpx; }
    .advice-content {
    display: flex; align-items: center; padding: 24rpx;
    }
    .advice-icon-wrap {
    width: 56rpx; height: 56rpx;
    border-radius: 14rpx;
    background: rgba(91,155,213,0.1);
    display: flex; align-items: center; justify-content: center;
    margin-right: 16rpx; flex-shrink: 0;
    }
    .advice-icon { font-size: 28rpx; }
    .advice-text-wrap { flex: 1; min-width: 0; }
    .advice-title { font-size: 28rpx; font-weight: 600; color: #333; display: block; }
    .advice-desc { font-size: 24rpx; color: #999; margin-top: 4rpx; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .advice-arrow { font-size: 36rpx; color: #ccc; margin-left: 12rpx; }
    

Task 2: child-index — Add 最近能量变动 section

Files:

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

  • [ ] Step 1: Add energy logs section after achievements

Insert after achievement-section div and before bottom-spacer:

<!-- ===== 最近能量变动 ===== -->
<view class="energy-log-section animate-slide-up animate-stagger-6" v-if="energyLogs.length > 0">
  <view class="section-header">
    <text class="section-title">💧 最近能量变动</text>
    <text class="section-link" @click="goToEnergyDetail">查看详情 →</text>
  </view>
  <view class="energy-log-list">
    <view class="energy-log-item" v-for="log in energyLogs" :key="log.id">
      <view class="log-icon-wrap" :class="'log-icon--' + (log.amount >= 0 ? 'up' : 'down')">
        <text class="log-icon">{{ log.amount >= 0 ? '⬆' : '⬇' }}</text>
      </view>
      <view class="log-info">
        <text class="log-title">{{ log.reason || log.description }}</text>
        <text class="log-time">{{ log.createdAt || log.time }}</text>
      </view>
      <text class="log-amount" :class="log.amount >= 0 ? 'log-amount--up' : 'log-amount--down'">
        {{ log.amount >= 0 ? '+' : '' }}{{ log.amount }}
      </text>
    </view>
  </view>
</view>
  • Step 2: Add data property and data loading

In data(), add:

energyLogs: []

In loadData(), after energy overview load, add:

try {
  const logsRes = await getEnergyLogs({ childId: energyChildId, size: 5 })
  if (logsRes && logsRes.data) {
    this.energyLogs = logsRes.data.records || logsRes.data
  }
} catch (e) {
  console.log('获取能量日志失败', e)
  this.energyLogs = []
}

In the import section, add getEnergyLogs:

import { ..., getEnergyLogs } from '../../utils/api.js'
  • Step 3: Add method and CSS

Add method:

goToEnergyDetail() {
  if (this.childId) {
    uni.navigateTo({ url: '/pages/energy/detail?childId=' + this.childId + '&code=all' })
  }
}

Add CSS:

.energy-log-section { margin-bottom: 28rpx; }
.energy-log-list { background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
.energy-log-item {
  display: flex; align-items: center;
  padding: 20rpx 24rpx;
  border-bottom: 1rpx solid #f0f0f0;
}
.energy-log-item:last-child { border-bottom: none; }
.log-icon-wrap {
  width: 56rpx; height: 56rpx;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  margin-right: 16rpx; flex-shrink: 0;
}
.log-icon--up { background: rgba(16,185,129,0.12); }
.log-icon--down { background: rgba(239,68,68,0.12); }
.log-icon { font-size: 24rpx; }
.log-info { flex: 1; min-width: 0; }
.log-title { font-size: 26rpx; color: #333; display: block; }
.log-time { font-size: 20rpx; color: #999; margin-top: 4rpx; display: block; }
.log-amount { font-size: 28rpx; font-weight: 700; margin-left: 12rpx; }
.log-amount--up { color: #10B981; }
.log-amount--down { color: #EF4444; }

Task 3: teacher-index — Redesign per design spec

Files:

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

  • [ ] Step 1: Replace header with 规划师工作台 brand header

Replace the current header section:

<!-- ===== 规划师工作台 ===== -->
<view class="teacher-header" v-if="teacherInfo">
  <view class="header-content">
    <view class="header-top">
      <view class="header-avatar-area">
        <view class="avatar avatar-placeholder">{{ avatarChar }}</view>
        <view class="header-info">
          <text class="header-name">{{ teacherInfo.nickname || '成长规划师' }}</text>
          <text class="header-level">{{ teacherInfo.level || '规划师' }}</text>
        </view>
      </view>
      <view class="switch-btn" @click="switchToParent">
        <text class="switch-text">切换家长端</text>
      </view>
    </view>
    <view class="header-stats">
      <view class="header-stat-item">
        <text class="header-stat-value">{{ stats.serviceFamilyCount }}</text>
        <text class="header-stat-label">绑定家庭</text>
      </view>
      <view class="header-stat-divider"></view>
      <view class="header-stat-item">
        <text class="header-stat-value">{{ stats.pendingTaskCount }}</text>
        <text class="header-stat-label">处理中任务</text>
      </view>
      <view class="header-stat-divider"></view>
      <view class="header-stat-item">
        <text class="header-stat-value">{{ todayAppointments.length }}</text>
        <text class="header-stat-label">今日日程</text>
      </view>
    </view>
  </view>
</view>
  • [ ] Step 2: Add 今日日程 section

    <!-- ===== 今日日程 ===== -->
    <view class="schedule-section" v-if="todayAppointments.length > 0">
    <view class="section-title">📅 今日日程</view>
    <view class="schedule-card" v-for="apt in todayAppointments" :key="apt.id">
    <view class="schedule-time">{{ apt.appointmentTime || apt.time }}</view>
    <view class="schedule-content">
      <text class="schedule-title">{{ apt.title }}</text>
      <text class="schedule-desc">{{ apt.description }}</text>
    </view>
    </view>
    </view>
    
  • [ ] Step 3: Add 绑定家庭 section (replacing current stats grid)

    <!-- ===== 绑定家庭 ===== -->
    <view class="family-section">
    <view class="section-title">👨‍👩‍👧‍👦 绑定家庭</view>
    <scroll-view class="family-scroll" scroll-x enhanced :show-scrollbar="false" v-if="boundFamilies.length > 0">
    <view class="family-card" v-for="family in boundFamilies" :key="family.id" @click="goToFamilyDetail(family)">
      <text class="family-name">{{ family.familyName || family.name }}</text>
      <text class="family-child-count">{{ family.childCount || 0 }}个孩子</text>
      <text class="family-latest-task" v-if="family.latestTask">最近: {{ family.latestTask }}</text>
    </view>
    </scroll-view>
    <BaseEmpty v-else icon="👨‍👩‍👧‍👦" title="暂无绑定家庭" description="邀请家长或等待家长邀请" :compact="true" />
    </view>
    
  • [ ] Step 4: Add 快捷操作 grid

    <!-- ===== 快捷操作 ===== -->
    <view class="quick-section">
    <view class="section-title">⚡ 快捷操作</view>
    <view class="quick-grid-2x2">
    <view class="quick-item" @click="goToAssignTask">
      <view class="quick-item-icon quick-icon--assign"><text class="quick-item-emoji">📝</text></view>
      <text class="quick-item-label">下发任务</text>
    </view>
    <view class="quick-item" @click="goToInputAssessment">
      <view class="quick-item-icon quick-icon--assessment"><text class="quick-item-emoji">📊</text></view>
      <text class="quick-item-label">录入测评</text>
    </view>
    <view class="quick-item" @click="goToContactParent">
      <view class="quick-item-icon quick-icon--contact"><text class="quick-item-emoji">💬</text></view>
      <text class="quick-item-label">联系家长</text>
    </view>
    <view class="quick-item" @click="goToIncome">
      <view class="quick-item-icon quick-icon--income"><text class="quick-item-emoji">💰</text></view>
      <text class="quick-item-label">查看收入</text>
    </view>
    </view>
    </view>
    
  • [ ] Step 5: Add data, methods, and CSS

Data additions:

todayAppointments: [],
boundFamilies: []

Methods:

goToFamilyDetail(family) {
  uni.navigateTo({ url: '/pages/guide/family-detail?familyId=' + family.id })
},
goToAssignTask() {
  uni.navigateTo({ url: '/pages/guide/assign-task' })
},
goToInputAssessment() {
  uni.navigateTo({ url: '/pages/assessment/input-result' })
},
goToContactParent() {
  uni.navigateTo({ url: '/pages/guide/messages' })
},
goToIncome() {
  uni.navigateTo({ url: '/pages/guide/income' })
}

Task 4: Create Shared Components

Files:

  • Create: cfc-frontend/components/EnergyWeekCard.vue
  • Create: cfc-frontend/components/TaskItem.vue
  • Create: cfc-frontend/components/BadgeGrid.vue

  • [ ] Step 1: Create EnergyWeekCard.vue

    <template>
    <view class="energy-week-card">
    <view class="ewc-header">
      <text class="ewc-title">📈 {{ title }}</text>
    </view>
    <view class="ewc-body">
      <view class="ewc-stat-row">
        <text class="ewc-stat-label">本周获取</text>
        <text class="ewc-stat-value accent">+{{ weekGain }}</text>
        <text class="ewc-stat-label" style="margin-left: 24rpx;">上周</text>
        <text class="ewc-stat-value" :class="gainTrend >= 0 ? 'up' : 'down'">{{ lastWeekGain }}</text>
        <text class="ewc-trend" :class="gainTrend >= 0 ? 'up' : 'down'">
          {{ gainTrend >= 0 ? '↑' : '↓' }}{{ Math.abs(gainTrend) }}%
        </text>
      </view>
      <view class="ewc-mini-chart">
        <view class="ewc-bar" v-for="(val, i) in weekData" :key="i"
          :style="{ height: (val / maxVal * 80) + 'rpx' }"
          :class="{ 'ewc-bar--today': i === weekData.length - 1 }">
        </view>
      </view>
      <text class="ewc-tip">{{ tip }}</text>
    </view>
    </view>
    </template>
    <script>
    export default {
    props: {
    title: { type: String, default: '能量周报' },
    weekGain: { type: Number, default: 0 },
    lastWeekGain: { type: Number, default: 0 },
    gainTrend: { type: Number, default: 0 },
    weekData: { type: Array, default: () => [10,20,30,40,50,60,70] },
    tip: { type: String, default: '' }
    },
    computed: {
    maxVal() {
      return Math.max(...this.weekData, 1)
    }
    }
    }
    </script>
    <style scoped>
    .energy-week-card { background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
    .ewc-header { margin-bottom: 16rpx; }
    .ewc-title { font-size: 28rpx; font-weight: 700; color: #333; }
    .ewc-body { }
    .ewc-stat-row { display: flex; align-items: baseline; flex-wrap: wrap; margin-bottom: 16rpx; }
    .ewc-stat-label { font-size: 22rpx; color: #999; }
    .ewc-stat-value { font-size: 28rpx; font-weight: 700; }
    .ewc-stat-value.accent { color: #F97316; }
    .ewc-stat-value.up { color: #10B981; }
    .ewc-stat-value.down { color: #EF4444; }
    .ewc-trend { font-size: 22rpx; margin-left: 8rpx; }
    .ewc-trend.up { color: #10B981; }
    .ewc-trend.down { color: #EF4444; }
    .ewc-mini-chart { display: flex; align-items: flex-end; gap: 8rpx; height: 90rpx; margin-bottom: 12rpx; }
    .ewc-bar { flex: 1; background: #E2E8F0; border-radius: 4rpx 4rpx 0 0; min-height: 8rpx; }
    .ewc-bar--today { background: #F97316; }
    .ewc-tip { font-size: 22rpx; color: #F97316; display: block; }
    </style>
    
  • [ ] Step 2: Create TaskItem.vue

    <template>
    <view class="task-item" @click="$emit('click', task)">
    <view class="task-item-left">
      <text class="task-item-name">{{ task.title }}</text>
      <text class="task-item-deadline" v-if="task.deadline">⏰ {{ task.deadline }}</text>
    </view>
    <view class="task-item-right">
      <text class="task-item-points" v-if="task.points">+{{ task.points }}⭐</text>
      <text class="task-item-action" @click.stop="$emit('action', task)">去完成</text>
    </view>
    </view>
    </template>
    <script>
    export default {
    props: { task: { type: Object, required: true } }
    }
    </script>
    <style scoped>
    .task-item { display: flex; justify-content: space-between; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; }
    .task-item:last-child { border-bottom: none; }
    .task-item-left { flex: 1; min-width: 0; }
    .task-item-name { font-size: 26rpx; color: #333; display: block; }
    .task-item-deadline { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
    .task-item-right { display: flex; align-items: center; gap: 16rpx; margin-left: 16rpx; flex-shrink: 0; }
    .task-item-points { font-size: 24rpx; font-weight: 600; color: #F97316; }
    .task-item-action { font-size: 24rpx; color: #5B9BD5; padding: 8rpx 16rpx; border: 2rpx solid #5B9BD5; border-radius: 20rpx; }
    </style>
    
  • [ ] Step 3: Create BadgeGrid.vue

    <template>
    <view class="badge-grid">
    <view class="badge-grid-inner">
      <view class="badge-cell" v-for="badge in badges" :key="badge.id"
        :class="{ 'badge-cell--locked': !badge.unlocked }">
        <view class="badge-icon-wrap" :class="{ 'badge-icon-wrap--locked': !badge.unlocked }">
          <text class="badge-icon">{{ badge.unlocked ? badge.icon : '🔒' }}</text>
        </view>
        <text class="badge-name">{{ badge.name }}</text>
      </view>
    </view>
    </view>
    </template>
    <script>
    export default {
    props: { badges: { type: Array, default: () => [] } }
    }
    </script>
    <style scoped>
    .badge-grid { background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
    .badge-grid-inner { display: flex; flex-wrap: wrap; gap: 16rpx; }
    .badge-cell { width: calc(33.33% - 11rpx); display: flex; flex-direction: column; align-items: center; padding: 12rpx 0; }
    .badge-cell--locked { opacity: 0.5; }
    .badge-icon-wrap { width: 80rpx; height: 80rpx; border-radius: 50%; background: linear-gradient(135deg, #FFD700, #FFA500); display: flex; align-items: center; justify-content: center; margin-bottom: 8rpx; }
    .badge-icon-wrap--locked { background: #E2E8F0; }
    .badge-icon { font-size: 36rpx; }
    .badge-name { font-size: 22rpx; color: #333; text-align: center; }
    </style>
    

Task 5: Verify all 5 tab pages against design spec

Files:

  • Review: cfc-frontend/pages/index/index.vue — check role routing
  • Review: cfc-frontend/pages/index/parent-index.vue — verify all sections
  • Review: cfc-frontend/pages/index/child-index.vue — verify all sections
  • Review: cfc-frontend/pages/teacher/index.vue — verify all sections
  • Review: cfc-frontend/pages/body/index.vue — verify against design
  • Review: cfc-frontend/pages/mind/index.vue — verify against design
  • Review: cfc-frontend/pages/action/index.vue — verify against design
  • Review: cfc-frontend/pages/profile/profile.vue — verify against design

  • [ ] Step 1: Check that index.vue properly routes to all 3 role components

  • [ ] Step 2: Verify parent-index has: philosophy banner, children row, health overview, pending items, growth advice

  • [ ] Step 3: Verify child-index has: energy sandbox, streak badge, points card, today tasks, quick actions, energy logs

  • [ ] Step 4: Verify teacher-index has: workbench header, today schedule, bound families, quick actions

  • [ ] Step 5: Check LSP diagnostics clean on all changed files