Просмотр исходного кода

docs: 成员详情维度分段 设计文档+实施计划

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sisyphus 3 месяцев назад
Родитель
Сommit
96804c0b77

+ 825 - 0
docs/superpowers/plans/2026-07-14-member-detail-dimension-sections.md

@@ -0,0 +1,825 @@
+# 家庭成员详情页维度专属内容区 — 实施计划
+
+> **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>` 闭合标签之后、`<!-- 底部占位 -->` 之前,添加:
+
+```html
+      <!-- 维度专属内容 -->
+      <view class="dimension-section">
+        <component :is="dimensionSection" />
+      </view>
+```
+
+- [ ] **Step 2: 在 script 中添加 imports**
+
+在现有 import 块末尾添加:
+
+```javascript
+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 → 组件**
+
+```javascript
+    dimensionSection: function() {
+      var map = {
+        body: BodySection,
+        mind: MindSection,
+        wisdom: WisdomSection,
+        action: ActionSection
+      }
+      return map[this.dimensionCode] || null
+    },
+```
+
+- [ ] **Step 4: 添加样式**
+
+```css
+.dimension-section {
+  margin: 20rpx 30rpx;
+}
+```
+
+- [ ] **Step 5: 确认 member-detail.vue 的 props 传递正确**
+
+在 `<component :is="dimensionSection">` 上添加 props 绑定:
+
+```html
+        <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.vue` 的 `loadHealthReport`, `loadDimensionData`, `getCheckinList`
+
+- [ ] **Step 1: 创建 body-section.vue 基本结构(template + script + style)**
+
+```html
+<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 逻辑**
+
+```html
+<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: 添加样式**
+
+```html
+<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.vue` 的 `loadContacts`, `loadHealthAlerts`, `loadMilestones`
+
+- [ ] **Step 1: 创建 mind-section.vue 模板**
+
+```html
+<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**
+
+```html
+<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 风格一致)**
+
+```html
+<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.vue` 的 `loadCognitiveReport`, `getAssessmentHistory`
+
+- [ ] **Step 1: 创建 wisdom-section.vue 模板**
+
+```html
+<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**
+
+```html
+<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: 添加样式**
+
+```html
+<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 模板**
+
+```html
+<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**
+
+```html
+<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: 添加样式**
+
+```html
+<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: 检查文件结构**
+
+```bash
+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: 确认页面路由不冲突**
+
+```bash
+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 页面的内容区改造,不涉及导航、路由、后端改动。

+ 113 - 0
docs/superpowers/specs/2026-07-14-member-detail-dimension-sections.md

@@ -0,0 +1,113 @@
+# 家庭成员详情页 — 维度专属内容区设计
+
+## 概述
+
+当前 `pages/member-detail/member-detail.vue` 从身/心/智/行四个维度页面的 FamilyRelationGraph 点击成员进入,但展示内容不区分入口维度。本设计让该页面根据 `dimensionCode` 参数展示与维度相关的专属内容,实现"从哪个页面进,显示该页面相关的内容"。
+
+## 现状
+
+- `member-detail.vue` 已接收 `dimensionCode` 参数(body/mind/wisdom/action)
+- 页面现有结构:导航栏 + 成员信息卡 + 五维能量条 + 关系图谱 + 通用任务/活动/商品
+- 通用内容(活动/商品推荐)不区分维度,始终展示 body 域内容
+
+## 设计
+
+### 页面结构(所有维度共用)
+
+```
+┌─ 顶部导航栏(返回按钮 + "成员名 · 维度名")
+├─ 成员信息卡(头像 + 昵称/角色 + 综合能量分)
+├─ 五维能量条 (FamilyEnergyBar)
+├─ 家庭成员关系图谱 (FamilyRelationGraph)
+├─ ══ 维度专属内容区(根据 dimensionCode 切换)══
+│   └─ (详见各维度内容定义)
+└─ 底部占位
+```
+
+### 架构
+
+- `member-detail.vue` 作为容器页面,负责所有共用部分的渲染
+- 在关系图谱下方使用 `<component :is="dimensionSection" />` 动态组件
+- 4 个独立子组件放在 `components/member-detail-sections/` 目录下
+
+### 各维度专属内容
+
+#### 身 (body-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 最新健康报告卡片 | `getHealthAnalysis(childId)` | 总分 + 关键指标 + 报告日期,无报告时显示"暂无健康报告" |
+| 七维健康雷达图 | `getDimensionOverview({ childId })` | 与 body/index.vue 相同的 RadarChart 组件 |
+| 健康指标列表 | `getHealthIndicatorList(childId)` | 关键指标展示 |
+| 最近打卡记录 | `getCheckinList({ childId, page:1, size:3 })` | 最近3条健康打卡 |
+
+#### 心 (mind-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 亲密关系评分 | `intimacyMap` + `energyMap` | 用进度条或评分卡展示该成员与各家庭成员的关系质量 |
+| 联系人列表 | `getContactList({ memberId })` | 该成员的联系人列表 |
+| 健康告警 | `getHealthAlerts(familyId)` | 家庭健康提醒 |
+| 里程碑 | `getMilestones(familyId)` | 近期待办里程碑 |
+
+#### 智 (wisdom-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 最新测评结果 | `getAssessmentLatestResult(childId)` | 总分 + 各维度分数,与 wisdom/index.vue 风格一致 |
+| 认知能力维度图 | 从测评结果提取 | 用雷达图或柱状图展示各认知维度得分 |
+| 近期测评历史 | `getAssessmentHistory(childId, 3)` | 最近3次测评简表 |
+| 认知趋势 | `getCognitiveTrend(childId, 5)` | 趋势图(如有需要) |
+
+#### 行 (action-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 今日任务 | `getTodayTasksByCategory(childId, 'all')` | 已存在的 DimensionTasks 组件,仅孩子类型显示 |
+| 打卡统计 | `getCheckinStats({ childId })` | 行为打卡汇总统计 |
+| 最近活动 | `getActivityList({ page:1, size:3 })` | 推荐活动 |
+
+### 数据加载策略
+
+- `member-detail.vue` 的 `onLoad` 仍加载共用数据(沙盘、成员信息)
+- 维度专属数据在 `dimensionSection` 子组件的 `onShow`/`mounted` 中各自加载
+- 子组件通过 props 接收:`memberId`, `childId`, `memberType`, `sandboxData`, `energyMap`
+- `familyId` 从 `uni.getStorageSync('familyId')` 获取(项目内统一模式)
+- **childId 可用性**:`childId` 仅在 `memberType === 'child'` 时有值。身/智维度依赖健康数据和测评数据,这些数据仅对孩子成员有意义——非孩子成员时显示"该成员暂无维度数据"的提示。心/行维度的部分内容(联系人、告警、里程碑)对全年龄段成员可用。
+
+### 错误处理
+
+- 各子组件独立处理自己的加载失败,不阻塞共用部分的渲染
+- 加载中显示骨架屏或 loading 文本
+- 无数据时显示空状态提示
+
+### 文件清单(新增)
+
+```
+cfc-frontend/components/member-detail-sections/
+├── body-section.vue       # 身维度专属内容
+├── mind-section.vue       # 心维度专属内容
+├── wisdom-section.vue     # 智维度专属内容
+└── action-section.vue     # 行维度专属内容
+```
+
+### 修改文件
+
+```
+cfc-frontend/pages/member-detail/member-detail.vue  # 引入动态组件区
+```
+
+## 排除范围
+
+- 不修改各维度页面(parent-index、body、mind、wisdom、action)的 `goMemberDetail`——它们已经传了 `dimensionCode`
+- 不修改 FamilyRelationGraph 组件
+- 不改动共用部分(导航、成员卡、能量条、关系图谱)的现有逻辑
+
+## 成功标准
+
+1. 从**身**页面点击成员 → 进入后看到健康相关数据(报告/雷达图/指标)
+2. 从**心**页面点击成员 → 进入后看到关系/联系人/告警数据
+3. 从**智**页面点击成员 → 进入后看到测评/认知数据
+4. 从**行**页面点击成员 → 进入后看到任务/打卡/活动数据
+5. 共用部分在所有维度下表现一致
+6. 各专属内容区独立加载,单个模块失败不影响其他区域