2026-07-14-member-detail-dimension-sections.md 26 KB

家庭成员详情页维度专属内容区 — 实施计划

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), 微信小程序


Task 1: 改造 member-detail.vue — 动态组件插槽

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>
  • Step 2: 在 script 中添加 imports

在现有 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" />

Task 2: 创建 body-section.vue

Files:

  • Create: cfc-frontend/components/member-detail-sections/body-section.vue
  • Reference: cfc-frontend/pages/body/index.vueloadHealthReport, 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>
    

Task 3: 创建 mind-section.vue

Files:

  • Create: cfc-frontend/components/member-detail-sections/mind-section.vue
  • Reference: cfc-frontend/pages/mind/index.vueloadContacts, 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>
    

Task 4: 创建 wisdom-section.vue

Files:

  • Create: cfc-frontend/components/member-detail-sections/wisdom-section.vue
  • Reference: cfc-frontend/pages/wisdom/index.vueloadCognitiveReport, 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>
    

Task 5: 创建 action-section.vue

Files:

  • Create: cfc-frontend/components/member-detail-sections/action-section.vue
  • Reference: 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>
    

Task 6: 验证编译

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
    

自检清单

  1. Spec 覆盖度:

    • 4 个子组件都对应到 spec 中的各维度内容定义
    • 动态组件机制已实现(Task 1)
    • 共用部分保持不变
    • childId 可用性处理(非孩子成员显示空状态)
  2. 占位符检查: 无 "TBD", "TODO", "implement later"

  3. 类型一致性:

    • memberId, childId, memberType props 在 4 个子组件中命名一致
    • energyMap prop 与 member-detail.vue 中的 energyMapForGraph 匹配
  4. Scope 检查: 范围聚焦于 member-detail 页面的内容区改造,不涉及导航、路由、后端改动。