|
|
@@ -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 页面的内容区改造,不涉及导航、路由、后端改动。
|