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: 家庭成员详情页根据 dimensionCode 参数(body/mind/wisdom/action)展示维度专属内容
Architecture: 在现有 member-detail.vue 的 FamilyRelationGraph 下方插入动态组件区域,通过 <component :is="dimensionSection" /> 根据 dimensionCode 切换 4 个独立子组件。共用部分(导航、成员卡、能量条、关系图谱)不变。
Tech Stack: uni-app Vue 2 (Options API), 微信小程序
Files:
Modify: cfc-frontend/pages/member-detail/member-detail.vue
[ ] Step 1: 在 template 中 FamilyRelationGraph 之后插入动态组件区
在 member-detail.vue 的 template 中,FamilyRelationGraph 的 </view> 闭合标签之后、<!-- 底部占位 --> 之前,添加:
<!-- 维度专属内容 -->
<view class="dimension-section">
<component :is="dimensionSection" />
</view>
在现有 import 块末尾添加:
import BodySection from '../../components/member-detail-sections/body-section.vue'
import MindSection from '../../components/member-detail-sections/mind-section.vue'
import WisdomSection from '../../components/member-detail-sections/wisdom-section.vue'
import ActionSection from '../../components/member-detail-sections/action-section.vue'
在 components: { ... } 中添加这四个组件。
[ ] Step 3: 添加 computed 属性映射 dimensionCode → 组件
dimensionSection: function() {
var map = {
body: BodySection,
mind: MindSection,
wisdom: WisdomSection,
action: ActionSection
}
return map[this.dimensionCode] || null
},
[ ] Step 4: 添加样式
.dimension-section {
margin: 20rpx 30rpx;
}
[ ] Step 5: 确认 member-detail.vue 的 props 传递正确
在 <component :is="dimensionSection"> 上添加 props 绑定:
<component :is="dimensionSection"
:memberId="targetMemberId"
:childId="childId"
:memberType="memberType"
:memberInfo="memberInfo"
:sandboxData="sandboxData"
:energyMap="energyMapForGraph" />
Files:
cfc-frontend/components/member-detail-sections/body-section.vueReference: cfc-frontend/pages/body/index.vue 的 loadHealthReport, loadDimensionData, getCheckinList
[ ] Step 1: 创建 body-section.vue 基本结构(template + script + style)
<template>
<view class="body-section">
<!-- 最新健康报告 -->
<view class="section-card" v-if="healthReport">
<view class="section-header">
<text class="section-title">📋 最新健康报告</text>
</view>
<view class="report-card" @click="goReportDetail">
<text class="report-score">{{ healthReport.overallScore || '--' }}</text>
<text class="report-label">综合健康分</text>
</view>
<view class="report-meta">
<text class="report-date">报告日期:{{ formatDate(healthReport.reportDate) }}</text>
</view>
</view>
<view class="section-card" v-else-if="!loading">
<view class="empty-state">
<text class="empty-text">暂无健康报告</text>
<text class="empty-sub">规划师录入后将自动显示</text>
</view>
</view>
<!-- 七维健康图 -->
<view class="section-card" v-if="dimensionOverview">
<view class="section-header">
<text class="section-title">📊 七维健康图</text>
</view>
<RadarChart
:dimensions="dimensionOverview.dimensions.map(function(d) { return { label: d.label, key: d.dimension } })"
:scores="dimensionOverview.dimensions.map(function(d) { return d.score })"
fillColor="#FF8C42"
gridColor="#FED7AA"
labelColor="#9A3412"
:width="580"
:height="400" />
</view>
<!-- 最近打卡 -->
<view class="section-card" v-if="checkinList.length > 0">
<view class="section-header">
<text class="section-title">🏃 最近打卡</text>
</view>
<view class="checkin-item" v-for="item in checkinList" :key="item.id">
<text class="checkin-type">{{ item.typeName || item.type }}</text>
<text class="checkin-time">{{ formatDate(item.createTime) }}</text>
</view>
</view>
<!-- 仅孩子成员提示 -->
<view class="section-card" v-if="!childId && !loading">
<view class="empty-state">
<text class="empty-text">该成员暂无健康数据</text>
</view>
</view>
<!-- 加载中 -->
<view class="loading" v-if="loading">加载中...</view>
</view>
</template>
[ ] Step 2: 添加 script 逻辑
<script>
import RadarChart from '../RadarChart.vue'
import { getHealthAnalysis, getDimensionOverview, getCheckinList } from '../../utils/api.js'
export default {
components: { RadarChart },
props: {
memberId: [Number, String],
childId: [Number, String],
memberType: String,
memberInfo: Object,
sandboxData: Object,
energyMap: Object
},
data: function() {
return {
loading: true,
healthReport: null,
dimensionOverview: null,
checkinList: []
}
},
mounted: function() {
if (this.childId) {
this.loadData()
} else {
this.loading = false
}
},
methods: {
loadData: function() {
var self = this
// 并行加载
getHealthAnalysis(this.childId).then(function(res) {
if (res.code === 200 && res.data) {
self.healthReport = res.data
}
}).catch(function(e) { console.log('加载健康分析失败', e) })
getDimensionOverview({ childId: this.childId }).then(function(res) {
if (res.code === 200 && res.data) {
self.dimensionOverview = res.data
}
}).catch(function(e) { console.log('加载维度概览失败', e) })
getCheckinList({ childId: this.childId, page: 1, size: 3 }).then(function(res) {
if (res.code === 200 && res.data) {
self.checkinList = Array.isArray(res.data) ? res.data : (res.data.records || [])
}
}).catch(function(e) { console.log('加载打卡记录失败', e) })
this.loading = false
},
formatDate: function(dateStr) {
if (!dateStr) return ''
var d = new Date(dateStr)
return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
},
goReportDetail: function() {
uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + this.childId })
}
}
}
</script>
[ ] Step 3: 添加样式
<style scoped>
.body-section {}
.section-card {
background: #fff;
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
}
.section-header {
margin-bottom: 16rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.report-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0;
}
.report-score {
font-size: 72rpx;
font-weight: bold;
color: #FF8C42;
}
.report-label {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.report-meta {
padding-top: 12rpx;
border-top: 1rpx solid #f0f0f0;
}
.report-date {
font-size: 24rpx;
color: #999;
}
.checkin-item {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.checkin-type {
font-size: 28rpx;
color: #333;
}
.checkin-time {
font-size: 24rpx;
color: #999;
}
.empty-state {
padding: 30rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
.empty-sub {
font-size: 24rpx;
color: #ccc;
margin-top: 8rpx;
}
.loading {
text-align: center;
padding: 40rpx;
color: #999;
}
</style>
Files:
cfc-frontend/components/member-detail-sections/mind-section.vueReference: cfc-frontend/pages/mind/index.vue 的 loadContacts, loadHealthAlerts, loadMilestones
[ ] Step 1: 创建 mind-section.vue 模板
<template>
<view class="mind-section">
<!-- 亲密关系评分 -->
<view class="section-card" v-if="relationshipScores.length > 0">
<view class="section-header">
<text class="section-title">💞 亲密关系评分</text>
</view>
<view class="relation-item" v-for="score in relationshipScores" :key="score.memberId">
<text class="relation-name">{{ score.nickname || '成员' }}</text>
<view class="relation-bar-wrap">
<view class="relation-bar" :style="{ width: score.trust + '%', background: getScoreColor(score.trust) }"></view>
</view>
<text class="relation-score">{{ score.trust }}</text>
</view>
</view>
<!-- 联系人 -->
<view class="section-card" v-if="contactList.length > 0">
<view class="section-header">
<text class="section-title">📞 联系人</text>
</view>
<view class="contact-item" v-for="item in contactList" :key="item.id">
<text class="contact-name">{{ item.name }}</text>
<text class="contact-relation">{{ item.relation || '' }}</text>
</view>
</view>
<!-- 健康告警 -->
<view class="section-card" v-if="healthAlerts.length > 0">
<view class="section-header">
<text class="section-title">⚠️ 健康告警</text>
</view>
<view class="alert-item" v-for="alert in healthAlerts" :key="alert.id">
<text class="alert-title">{{ alert.title || alert.content }}</text>
</view>
</view>
<!-- 里程碑 -->
<view class="section-card" v-if="milestones.length > 0">
<view class="section-header">
<text class="section-title">🎯 里程碑</text>
</view>
<view class="milestone-item" v-for="ms in milestones" :key="ms.id">
<text class="milestone-title">{{ ms.title || ms.name }}</text>
<text class="milestone-date">{{ formatDate(ms.dueDate) }}</text>
</view>
</view>
<!-- 加载中 -->
<view class="loading" v-if="loading">加载中...</view>
</view>
</template>
[ ] Step 2: 添加 mind-section.vue script
<script>
import { getContactList, getHealthAlerts, getMilestones, getFamilySummary } from '../../utils/api.js'
export default {
props: {
memberId: [Number, String],
childId: [Number, String],
memberType: String,
memberInfo: Object,
sandboxData: Object,
energyMap: Object
},
data: function() {
return {
loading: true,
contactList: [],
healthAlerts: [],
milestones: [],
relationshipScores: []
}
},
mounted: function() {
this.loadData()
},
methods: {
loadData: function() {
var self = this
var familyId = uni.getStorageSync('familyId')
if (!familyId) {
var userInfo = uni.getStorageSync('userInfo')
if (userInfo && userInfo.familyId) familyId = userInfo.familyId
}
// 从 energyMap 构建关系评分
if (this.energyMap && this.sandboxData && this.sandboxData.members) {
var scores = []
var selfId = this.memberId
for (var i = 0; i < this.sandboxData.members.length; i++) {
var m = this.sandboxData.members[i]
var mid = m.memberId || m.id
if (mid != selfId) {
var e = this.energyMap[mid]
var trust = (e && e.bodyScore) ? Math.round((e.bodyScore + (e.mindScore || 50)) / 2) : 70
scores.push({ memberId: mid, nickname: m.name || m.nickname || '成员', trust: trust })
}
}
self.relationshipScores = scores
}
if (familyId) {
getContactList({ memberId: this.memberId }).then(function(res) {
if (res.code === 200) {
self.contactList = Array.isArray(res.data) ? res.data : []
}
}).catch(function() {})
getHealthAlerts(familyId).then(function(res) {
if (res.code === 200) {
self.healthAlerts = Array.isArray(res.data) ? res.data.slice(0, 5) : []
}
}).catch(function() {})
getMilestones(familyId).then(function(res) {
if (res.code === 200) {
self.milestones = Array.isArray(res.data) ? res.data.slice(0, 5) : []
}
}).catch(function() {})
}
this.loading = false
},
getScoreColor: function(score) {
if (score >= 80) return '#10B981'
if (score >= 60) return '#F59E0B'
return '#EF4444'
},
formatDate: function(dateStr) {
if (!dateStr) return ''
var d = new Date(dateStr)
return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
}
}
}
</script>
[ ] Step 3: 添加样式(与 body-section 风格一致)
<style scoped>
.mind-section {}
.section-card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06); }
.section-header { margin-bottom: 16rpx; }
.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
.relation-item { display: flex; flex-direction: row; align-items: center; padding: 12rpx 0; }
.relation-name { width: 100rpx; font-size: 26rpx; color: #333; }
.relation-bar-wrap { flex: 1; height: 16rpx; background: #f0f0f0; border-radius: 8rpx; margin: 0 16rpx; overflow: hidden; }
.relation-bar { height: 100%; border-radius: 8rpx; }
.relation-score { width: 60rpx; text-align: right; font-size: 24rpx; color: #666; }
.contact-item, .alert-item, .milestone-item { display: flex; flex-direction: row; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
.contact-name, .alert-title, .milestone-title { font-size: 28rpx; color: #333; }
.contact-relation, .milestone-date { font-size: 24rpx; color: #999; }
.loading { text-align: center; padding: 40rpx; color: #999; }
</style>
Files:
cfc-frontend/components/member-detail-sections/wisdom-section.vueReference: cfc-frontend/pages/wisdom/index.vue 的 loadCognitiveReport, getAssessmentHistory
[ ] Step 1: 创建 wisdom-section.vue 模板
<template>
<view class="wisdom-section">
<!-- 最新测评结果 -->
<view class="section-card" v-if="latestAssessment">
<view class="section-header">
<text class="section-title">🧠 最新测评结果</text>
</view>
<view class="assessment-card" @click="goAssessmentDetail">
<text class="assessment-date">测评日期:{{ formatDate(latestAssessment.createTime) }}</text>
<view class="assessment-score-row">
<view class="score-item" v-for="(score, key) in dimensionScores" :key="key">
<text class="score-num" :style="{ color: '#6366F1' }">{{ score }}</text>
<text class="score-label">{{ key }}</text>
</view>
</view>
</view>
</view>
<view class="section-card" v-else-if="!loading && !childId">
<view class="empty-state">
<text class="empty-text">该成员暂无测评数据</text>
</view>
</view>
<!-- 近期测评历史 -->
<view class="section-card" v-if="assessmentHistory.length > 0">
<view class="section-header">
<text class="section-title">📊 近期测评历史</text>
</view>
<view class="history-item" v-for="item in assessmentHistory" :key="item.id">
<text class="history-name">{{ item.assessmentName || '心智测评' }}</text>
<text class="history-score">{{ item.totalScore || '--' }}分</text>
<text class="history-date">{{ formatDate(item.createTime) }}</text>
</view>
</view>
<!-- 加载中 -->
<view class="loading" v-if="loading">加载中...</view>
</view>
</template>
[ ] Step 2: 添加 wisdom-section.vue script
<script>
import { getAssessmentLatestResult, getAssessmentHistory } from '../../utils/api.js'
export default {
props: {
memberId: [Number, String],
childId: [Number, String],
memberType: String,
memberInfo: Object,
sandboxData: Object,
energyMap: Object
},
data: function() {
return {
loading: true,
latestAssessment: null,
assessmentHistory: []
}
},
computed: {
dimensionScores: function() {
if (!this.latestAssessment) return {}
var result = {}
var keys = ['iq', 'eq', 'memory', 'logic', 'creativity', 'concentration']
for (var i = 0; i < keys.length; i++) {
var k = keys[i]
if (this.latestAssessment[k] != null) {
result[k === 'iq' ? '智商' : k === 'eq' ? '情商' : k === 'memory' ? '记忆' : k === 'logic' ? '逻辑' : k === 'creativity' ? '创造' : k === 'concentration' ? '专注' : k] = this.latestAssessment[k]
}
}
return result
}
},
mounted: function() {
if (this.childId) {
this.loadData()
} else {
this.loading = false
}
},
methods: {
loadData: function() {
var self = this
getAssessmentLatestResult(this.childId).then(function(res) {
if (res.code === 200 && res.data) {
self.latestAssessment = res.data
}
}).catch(function(e) { console.log('加载测评结果失败', e) })
getAssessmentHistory(this.childId, 3).then(function(res) {
if (res.code === 200 && res.data) {
self.assessmentHistory = Array.isArray(res.data) ? res.data : []
}
}).catch(function(e) { console.log('加载测评历史失败', e) })
this.loading = false
},
formatDate: function(dateStr) {
if (!dateStr) return ''
var d = new Date(dateStr)
return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
},
goAssessmentDetail: function() {
uni.navigateTo({ url: '/pages/assessment/report?childId=' + this.childId })
}
}
}
</script>
[ ] Step 3: 添加样式
<style scoped>
.wisdom-section {}
.section-card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06); }
.section-header { margin-bottom: 16rpx; }
.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
.assessment-card { padding: 10rpx 0; }
.assessment-date { font-size: 24rpx; color: #999; }
.assessment-score-row { display: flex; flex-direction: row; flex-wrap: wrap; margin-top: 16rpx; }
.score-item { display: flex; flex-direction: column; align-items: center; min-width: 80rpx; margin: 8rpx 16rpx 8rpx 0; }
.score-num { font-size: 36rpx; font-weight: bold; }
.score-label { font-size: 22rpx; color: #999; margin-top: 4rpx; }
.history-item { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
.history-name { font-size: 28rpx; color: #333; flex: 1; }
.history-score { font-size: 28rpx; color: #6366F1; font-weight: bold; margin: 0 16rpx; }
.history-date { font-size: 22rpx; color: #999; }
.empty-state { padding: 30rpx 0; text-align: center; }
.empty-text { font-size: 28rpx; color: #999; }
.loading { text-align: center; padding: 40rpx; color: #999; }
</style>
Files:
cfc-frontend/components/member-detail-sections/action-section.vueReference: cfc-frontend/pages/action/index.vue
[ ] Step 1: 创建 action-section.vue 模板
<template>
<view class="action-section">
<!-- 今日任务(仅孩子) -->
<view class="section-card" v-if="childId && tasks.length > 0">
<view class="section-header">
<text class="section-title">📋 今日任务</text>
</view>
<view class="task-item" v-for="task in tasks" :key="task.id" @click="goTaskDetail(task)">
<view class="task-left">
<text class="task-name">{{ task.title || task.name }}</text>
<text class="task-desc">{{ task.description || '' }}</text>
</view>
<text class="task-status" :class="'status-' + (task.status || 'pending')">{{ task.statusLabel || task.status }}</text>
</view>
</view>
<!-- 打卡统计 -->
<view class="section-card" v-if="checkinStats">
<view class="section-header">
<text class="section-title">🏃 打卡统计</text>
</view>
<view class="stats-row">
<view class="stat-item">
<text class="stat-num">{{ checkinStats.totalDays || 0 }}</text>
<text class="stat-label">累计天数</text>
</view>
<view class="stat-item">
<text class="stat-num">{{ checkinStats.currentStreak || 0 }}</text>
<text class="stat-label">连续天数</text>
</view>
<view class="stat-item">
<text class="stat-num">{{ checkinStats.completionRate || '0%' }}</text>
<text class="stat-label">完成率</text>
</view>
</view>
</view>
<!-- 最近活动 -->
<view class="section-card" v-if="activities.length > 0">
<view class="section-header">
<text class="section-title">🎪 最近活动</text>
</view>
<view class="activity-item" v-for="act in activities" :key="act.id" @click="goActivityDetail(act)">
<text class="activity-title">{{ act.title || act.name }}</text>
<text class="activity-date">{{ formatDate(act.startTime || act.createTime) }}</text>
</view>
</view>
<!-- 加载中 -->
<view class="loading" v-if="loading">加载中...</view>
</view>
</template>
[ ] Step 2: 添加 action-section.vue script
<script>
import { getTodayTasksByCategory, getCheckinStats, getActivityList } from '../../utils/api.js'
export default {
props: {
memberId: [Number, String],
childId: [Number, String],
memberType: String,
memberInfo: Object,
sandboxData: Object,
energyMap: Object
},
data: function() {
return {
loading: true,
tasks: [],
checkinStats: null,
activities: []
}
},
mounted: function() {
this.loadData()
},
methods: {
loadData: function() {
var self = this
if (this.childId) {
getTodayTasksByCategory(this.childId, 'all').then(function(res) {
if (res && res.data) {
self.tasks = Array.isArray(res.data) ? res.data.slice(0, 5) : []
}
}).catch(function() {})
getCheckinStats({ childId: this.childId }).then(function(res) {
if (res.code === 200 && res.data) {
self.checkinStats = res.data
}
}).catch(function() {})
}
getActivityList({ page: 1, size: 3 }).then(function(res) {
if (res && res.data) {
var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
self.activities = list.slice(0, 3)
}
}).catch(function() {})
this.loading = false
},
formatDate: function(dateStr) {
if (!dateStr) return ''
var d = new Date(dateStr)
return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
},
goTaskDetail: function(task) {
uni.navigateTo({ url: '/pages/tasks/tasks' })
},
goActivityDetail: function(act) {
if (act && act.id) {
uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
}
}
}
}
</script>
[ ] Step 3: 添加样式
<style scoped>
.action-section {}
.section-card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06); }
.section-header { margin-bottom: 16rpx; }
.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
.task-item { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
.task-left { flex: 1; }
.task-name { font-size: 28rpx; color: #333; }
.task-desc { font-size: 22rpx; color: #999; margin-top: 4rpx; }
.task-status { font-size: 24rpx; padding: 4rpx 12rpx; border-radius: 8rpx; }
.status-pending { background: #FEF3C7; color: #D97706; }
.status-completed { background: #D1FAE5; color: #059669; }
.stats-row { display: flex; flex-direction: row; justify-content: space-around; padding: 16rpx 0; }
.stat-item { display: flex; flex-direction: column; align-items: center; }
.stat-num { font-size: 40rpx; font-weight: bold; color: #10B981; }
.stat-label { font-size: 22rpx; color: #999; margin-top: 4rpx; }
.activity-item { display: flex; flex-direction: row; justify-content: space-between; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
.activity-title { font-size: 28rpx; color: #333; flex: 1; }
.activity-date { font-size: 22rpx; color: #999; }
.loading { text-align: center; padding: 40rpx; color: #999; }
</style>
Files:
Project root: cfc-backend/ (not needed, frontend only)
[ ] Step 1: 检查文件结构
ls -la cfc-frontend/components/member-detail-sections/
ls -la cfc-frontend/pages/member-detail/
[ ] Step 2: 检查 member-detail.vue 无语法错误
确认 imports、components、computed 等都已正确添加。
[ ] Step 3: 确认页面路由不冲突
grep -n 'member-detail' cfc-frontend/pages.json
Spec 覆盖度:
占位符检查: 无 "TBD", "TODO", "implement later"
类型一致性:
memberId, childId, memberType props 在 4 个子组件中命名一致energyMap prop 与 member-detail.vue 中的 energyMapForGraph 匹配Scope 检查: 范围聚焦于 member-detail 页面的内容区改造,不涉及导航、路由、后端改动。