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)
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>
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>
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; }
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>
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'
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; }
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' })
}
Files:
cfc-frontend/components/EnergyWeekCard.vuecfc-frontend/components/TaskItem.vueCreate: 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>
Files:
cfc-frontend/pages/index/index.vue — check role routingcfc-frontend/pages/index/parent-index.vue — verify all sectionscfc-frontend/pages/index/child-index.vue — verify all sectionscfc-frontend/pages/teacher/index.vue — verify all sectionscfc-frontend/pages/body/index.vue — verify against designcfc-frontend/pages/mind/index.vue — verify against designcfc-frontend/pages/action/index.vue — verify against designReview: 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