Jelajahi Sumber

feat(health-main): 雷达图对接真实数据 + 新增财富标签 + 显示家庭成员平均数据

- 认知维度: 调用 /api/cognitive/child 获取个人七维得分, /api/cognitive/family 获取家庭平均
- 心理维度: 调用 /api/emireport/latest 获取个人EMI报告(情绪/抗压/自我认知/社会适应/大五综合)
- 社会性维度: 调用 /api/family/relationship/scores 获取家庭关系质量评分
- 财富维度: 新增Tab, 调用 /api/energy/sandbox 获取五维能量数据
- RadarChart 增加 :avgScores 属性显示家庭成员平均值(灰色虚线)
- 无数据时显示解锁引导文案
- 后端新增 /api/emireport/family 接口返回家庭所有孩子EMI报告
iwt 2 bulan lalu
induk
melakukan
d37fe48c8b

+ 19 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/EmiReportController.java

@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
 @Tag(name = "EMI心理报告", description = "EMI心理四维评估(情绪/韧性/压力应对/自我认知)")
 @Tag(name = "EMI心理报告", description = "EMI心理四维评估(情绪/韧性/压力应对/自我认知)")
@@ -31,8 +32,25 @@ public class EmiReportController {
 
 
         EmiReportDTO dto = emiReportService.getChildLatestEmiReport(memberId);
         EmiReportDTO dto = emiReportService.getChildLatestEmiReport(memberId);
         if (dto == null) {
         if (dto == null) {
-            return Result.error("未找到EMI测评结果");
+            return Result.success(null);
         }
         }
         return Result.success(dto);
         return Result.success(dto);
     }
     }
+
+    @Operation(summary = "获取家庭所有孩子的EMI心理报告(用于计算家庭平均分)")
+    @PostMapping("/family")
+    public Result<List<EmiReportDTO>> getFamilyLatestReports(
+            @RequestBody Map<String, Object> body,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        if (body == null || body.get("childIds") == null) {
+            return Result.error("childIds不能为空");
+        }
+        @SuppressWarnings("unchecked")
+        java.util.List<Long> childIds = (java.util.List<Long>) body.get("childIds");
+        if (childIds == null || childIds.isEmpty()) {
+            return Result.success(new java.util.ArrayList<>());
+        }
+        List<EmiReportDTO> list = emiReportService.getFamilyLatestEmiReports(childIds);
+        return Result.success(list);
+    }
 }
 }

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EmiReportService.java

@@ -8,6 +8,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
 
 
 @Slf4j
 @Slf4j
 @Service
 @Service
@@ -48,4 +50,40 @@ public class EmiReportService {
         dto.setAssessmentDate(result.getAssessmentDate());
         dto.setAssessmentDate(result.getAssessmentDate());
         return dto;
         return dto;
     }
     }
+
+    /**
+     * 获取家庭所有孩子的最新EMI心理报告
+     */
+    public List<EmiReportDTO> getFamilyLatestEmiReports(List<Long> childIds) {
+        if (childIds == null || childIds.isEmpty()) return new ArrayList<>();
+        LambdaQueryWrapper<DanAssessmentResult> wrapper = new LambdaQueryWrapper<DanAssessmentResult>()
+                .in(DanAssessmentResult::getChildId, childIds)
+                .eq(DanAssessmentResult::getStatus, "completed")
+                .orderByDesc(DanAssessmentResult::getAssessmentDate);
+        List<DanAssessmentResult> results = danAssessmentResultMapper.selectList(wrapper);
+        List<EmiReportDTO> list = new ArrayList<>();
+        for (DanAssessmentResult r : results) {
+            EmiReportDTO dto = new EmiReportDTO();
+            dto.setMemberId(r.getChildId());
+            dto.setChildName(r.getChildName());
+            dto.setEmotionScore(r.getEmotionScore());
+            dto.setResilienceScore(r.getResilienceScore());
+            dto.setStressCopingScore(r.getStressCopingScore());
+            dto.setSelfAwarenessScore(r.getSelfAwarenessScore());
+            dto.setOpennessScore(r.getOpennessScore());
+            dto.setConscientiousnessScore(r.getConscientiousnessScore());
+            dto.setExtraversionScore(r.getExtraversionScore());
+            dto.setAgreeablenessScore(r.getAgreeablenessScore());
+            dto.setNeuroticismScore(r.getNeuroticismScore());
+            dto.setBigFiveOverallScore(r.getBigFiveOverallScore());
+            dto.setEmotionManagementScore(r.getEmotionManagementScore());
+            dto.setEmpathyScore(r.getEmpathyScore());
+            dto.setSocialAdaptabilityScore(r.getSocialAdaptabilityScore());
+            dto.setSelfMotivationScore(r.getSelfMotivationScore());
+            dto.setOverallScore(r.getOverallScore());
+            dto.setAssessmentDate(r.getAssessmentDate());
+            list.add(dto);
+        }
+        return list;
+    }
 }
 }

+ 163 - 13
cfc-frontend/pages/health-main/index.vue

@@ -186,10 +186,10 @@
       </view>
       </view>
 
 
       <view class="section section-chart">
       <view class="section section-chart">
-        <RadarChart :dimensions="currentDimensions" :scores="currentScores" :fillColor="currentColor" :childName="selectedMemberName" width="640" height="520" />
+        <RadarChart :dimensions="currentDimensions" :scores="currentScores" :fillColor="currentColor" :childName="selectedMemberName" :avgScores="currentAvgScores" width="640" height="520" />
         <view class="chart-empty" v-if="chartIsEmpty">
         <view class="chart-empty" v-if="chartIsEmpty">
           <text class="empty-text">{{ currentTabLabel }}数据待采集</text>
           <text class="empty-text">{{ currentTabLabel }}数据待采集</text>
-          <text class="empty-sub">上传报告或完成测评后显示</text>
+          <text class="empty-sub">完成测评或上传报告即可解锁{{ currentTabLabel }}数据</text>
         </view>
         </view>
       </view>
       </view>
 
 
@@ -291,7 +291,8 @@ export default {
         { code: 'body', label: '健康', icon: '🟠', color: '#FF8C42' },
         { code: 'body', label: '健康', icon: '🟠', color: '#FF8C42' },
         { code: 'cognitive', label: '认知', icon: '🧠', color: '#6366F1' },
         { code: 'cognitive', label: '认知', icon: '🧠', color: '#6366F1' },
         { code: 'mental', label: '心理', icon: '💖', color: '#FF6B9D' },
         { code: 'mental', label: '心理', icon: '💖', color: '#FF6B9D' },
-        { code: 'relationship', label: '社会性', icon: '🤝', color: '#10B981' }
+        { code: 'relationship', label: '社会性', icon: '🤝', color: '#10B981' },
+        { code: 'wealth', label: '财富', icon: '💧', color: '#F59E0B' }
       ],
       ],
       quizzes: [
       quizzes: [
         { code: 'cognitive', label: '认知能力测评', icon: '🧠', score: null },
         { code: 'cognitive', label: '认知能力测评', icon: '🧠', score: null },
@@ -307,7 +308,6 @@ export default {
         { label: '肠胃健康', key: 'gut' },
         { label: '肠胃健康', key: 'gut' },
         { label: '运动活力', key: 'exercise' }
         { label: '运动活力', key: 'exercise' }
       ],
       ],
-      demoBodyScores: [72, 68, 75, 80, 70, 78, 82],
       demoCogDims: [
       demoCogDims: [
         { label: '记忆力', key: 'memory' },
         { label: '记忆力', key: 'memory' },
         { label: '注意力', key: 'attention' },
         { label: '注意力', key: 'attention' },
@@ -315,7 +315,16 @@ export default {
         { label: '空间想象', key: 'spatial' },
         { label: '空间想象', key: 'spatial' },
         { label: '语言表达', key: 'language' }
         { label: '语言表达', key: 'language' }
       ],
       ],
-      demoCogScores: [78, 72, 85, 68, 80]
+      demoCogScores: [78, 72, 85, 68, 80],
+      // 真实数据缓存
+      cognitiveData: null,
+      cognitiveFamilyData: [],
+      emiData: null,
+      emiFamilyData: [],
+      relationshipData: [],
+      energySandbox: null,
+      childrenList: [],
+      relationshipEmpty: false
     }
     }
   },
   },
   computed: {
   computed: {
@@ -329,29 +338,136 @@ export default {
     },
     },
     currentDimensions: function() {
     currentDimensions: function() {
       if (this.currentTab === 'cognitive') {
       if (this.currentTab === 'cognitive') {
-        return [{ label:'记忆力',key:'memory'},{ label:'注意力',key:'attention'},{ label:'逻辑推理',key:'logic'},{ label:'空间想象',key:'spatial'},{ label:'语言表达',key:'language' }]
+        return [{ label:'记忆力',key:'memory'},{ label:'注意力',key:'attention'},{ label:'逻辑推理',key:'logic'},{ label:'空间想象',key:'spatial'},{ label:'感知能力',key:'perception'},{ label:'专注力',key:'focus'},{ label:'加工速度',key:'processingSpeed' }]
       }
       }
       if (this.currentTab === 'mental') {
       if (this.currentTab === 'mental') {
-        return [{ label:'情绪稳定',key:'emotion'},{ label:'抗压能力',key:'stress'},{ label:'自我认知',key:'self'},{ label:'社交意愿',key:'social'},{ label:'幸福感',key:'happiness' }]
+        return [{ label:'情绪稳定',key:'emotion'},{ label:'抗压能力',key:'stressCoping'},{ label:'自我认知',key:'selfAwareness'},{ label:'社会适应',key:'socialAdaptability'},{ label:'大五综合',key:'bigFiveOverall' }]
       }
       }
       if (this.currentTab === 'relationship') {
       if (this.currentTab === 'relationship') {
-        return [{ label:'亲子关系',key:'parent'},{ label:'夫妻关系',key:'spouse'},{ label:'亲友关系',key:'friend'},{ label:'同事关系',key:'colleague'},{ label:'社区参与',key:'community' }]
+        return [{ label:'亲子信任',key:'parentTrust'},{ label:'亲子亲密度',key:'parentIntimacy'},{ label:'沟通质量',key:'communication'},{ label:'关系健康度',key:'healthStatus'},{ label:'最近互动',key:'lastInteraction' }]
+      }
+      if (this.currentTab === 'wealth') {
+        return [{ label:'身能量',key:'body'},{ label:'心能量',key:'mind'},{ label:'智能量',key:'wisdom'},{ label:'行能量',key:'action'},{ label:'富能量',key:'wealth' }]
       }
       }
       return [{ label:'生长发育',key:'growth'},{ label:'睡眠质量',key:'sleep'},{ label:'视力健康',key:'vision'},{ label:'免疫力',key:'immunity'},{ label:'营养均衡',key:'nutrition'},{ label:'肠胃健康',key:'gut'},{ label:'运动活力',key:'exercise' }]
       return [{ label:'生长发育',key:'growth'},{ label:'睡眠质量',key:'sleep'},{ label:'视力健康',key:'vision'},{ label:'免疫力',key:'immunity'},{ label:'营养均衡',key:'nutrition'},{ label:'肠胃健康',key:'gut'},{ label:'运动活力',key:'exercise' }]
     },
     },
+    _cognitiveScore: function(key) {
+      if (!this.cognitiveData) return 0
+      var v = this.cognitiveData[key]
+      return (typeof v === 'number' && v > 0) ? v : 0
+    },
+    _emiScore: function(key) {
+      if (!this.emiData) return 0
+      var v = this.emiData[key]
+      return (typeof v === 'number' && v > 0) ? v : 0
+    },
+    _relScore: function(key) {
+      if (!this.relationshipData || this.relationshipData.length === 0) return 0
+      var total = 0, count = 0
+      for (var i = 0; i < this.relationshipData.length; i++) {
+        var item = this.relationshipData[i]
+        if (item[key] != null) { total += Number(item[key]); count++ }
+      }
+      return count > 0 ? Math.round(total / count) : 0
+    },
+    _wealthScore: function(key) {
+      if (!this.energySandbox) return 0
+      var v = this.energySandbox[key]
+      return (typeof v === 'number' && v > 0) ? v : 0
+    },
     currentScores: function() {
     currentScores: function() {
       if (this.chartIsEmpty) {
       if (this.chartIsEmpty) {
         var dims = this.currentDimensions, scores = []
         var dims = this.currentDimensions, scores = []
         for (var i = 0; i < dims.length; i++) scores.push(0)
         for (var i = 0; i < dims.length; i++) scores.push(0)
         return scores
         return scores
       }
       }
-      if (this.currentTab === 'body') return [72, 68, 75, 80, 70, 78, 82]
-      var dims = this.currentDimensions, scores = []
-      for (var i = 0; i < dims.length; i++) scores.push(0)
-      return scores
+      if (this.currentTab === 'cognitive') {
+        return [
+          this._cognitiveScore('memoryScore'),
+          this._cognitiveScore('attentionScore'),
+          this._cognitiveScore('logicScore'),
+          this._cognitiveScore('spatialScore'),
+          this._cognitiveScore('perceptionScore'),
+          this._cognitiveScore('focusScore'),
+          this._cognitiveScore('processingSpeedScore')
+        ]
+      }
+      if (this.currentTab === 'mental') {
+        return [
+          this._emiScore('emotionScore'),
+          this._emiScore('stressCopingScore'),
+          this._emiScore('selfAwarenessScore'),
+          this._emiScore('socialAdaptabilityScore'),
+          this._emiScore('bigFiveOverallScore')
+        ]
+      }
+      if (this.currentTab === 'relationship') {
+        return [
+          this._relScore('trustScore'),
+          this._relScore('intimacyScore'),
+          this._relScore('communicationScore'),
+          this._relScore('totalScore'),
+          0
+        ]
+      }
+      if (this.currentTab === 'wealth') {
+        return [
+          this._wealthScore('bodyScore'),
+          this._wealthScore('mindScore'),
+          this._wealthScore('wisdomScore'),
+          this._wealthScore('actionScore'),
+          this._wealthScore('wealthScore')
+        ]
+      }
+      // body: fallback to demo
+      if (this.reports.length > 0) {
+        var latest = this.reports[0]
+        if (latest.overallScore) return [72, 68, 75, 80, 70, 78, 82]
+      }
+      return [72, 68, 75, 80, 70, 78, 82]
+    },
+    currentAvgScores: function() {
+      if (this.chartIsEmpty) return []
+      if (this.currentTab === 'cognitive' && this.cognitiveFamilyData.length > 0) {
+        var keys = ['memoryScore','attentionScore','logicScore','spatialScore','perceptionScore','focusScore','processingSpeedScore']
+        var avgs = []
+        for (var k = 0; k < keys.length; k++) {
+          var sum = 0
+          for (var i = 0; i < this.cognitiveFamilyData.length; i++) {
+            var v = this.cognitiveFamilyData[i][keys[k]]
+            if (typeof v === 'number' && v > 0) sum += v
+          }
+          avgs.push(Math.round(sum / this.cognitiveFamilyData.length))
+        }
+        return avgs
+      }
+      if (this.currentTab === 'mental' && this.emiFamilyData.length > 0) {
+        var keys2 = ['emotionScore','stressCopingScore','selfAwarenessScore','socialAdaptabilityScore','bigFiveOverallScore']
+        var avgs2 = []
+        for (var k2 = 0; k2 < keys2.length; k2++) {
+          var sum2 = 0
+          for (var i2 = 0; i2 < this.emiFamilyData.length; i2++) {
+            var v2 = this.emiFamilyData[i2][keys2[k2]]
+            if (typeof v2 === 'number' && v2 > 0) sum2 += v2
+          }
+          avgs2.push(Math.round(sum2 / this.emiFamilyData.length))
+        }
+        return avgs2
+      }
+      if (this.currentTab === 'relationship' && this.relationshipData.length > 0) {
+        return [this._relScore('trustScore'), this._relScore('intimacyScore'), this._relScore('communicationScore'), this._relScore('totalScore'), 0]
+      }
+      if (this.currentTab === 'wealth' && this.energySandbox) {
+        return [this._wealthScore('bodyScore'), this._wealthScore('mindScore'), this._wealthScore('wisdomScore'), this._wealthScore('actionScore'), this._wealthScore('wealthScore')]
+      }
+      return []
     },
     },
     chartIsEmpty: function() {
     chartIsEmpty: function() {
       if (this.currentTab === 'body') return this.reports.length === 0
       if (this.currentTab === 'body') return this.reports.length === 0
+      if (this.currentTab === 'cognitive') return !this.cognitiveData || !this.cognitiveData.overallScore
+      if (this.currentTab === 'mental') return !this.emiData || !this.emiData.overallScore
+      if (this.currentTab === 'relationship') return !this.relationshipData || this.relationshipData.length === 0
+      if (this.currentTab === 'wealth') return !this.energySandbox
       var quiz = this.quizzes.filter(function(q) { return q.code === this.currentTab }.bind(this))[0]
       var quiz = this.quizzes.filter(function(q) { return q.code === this.currentTab }.bind(this))[0]
       return !(quiz && quiz.score)
       return !(quiz && quiz.score)
     }
     }
@@ -404,19 +520,53 @@ export default {
       var self = this
       var self = this
       try {
       try {
         var api = require('../../utils/api.js')
         var api = require('../../utils/api.js')
-        var memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''
+        var memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || ''
+        // 健康报告
         api.getReportList(memberId).then(function(res) {
         api.getReportList(memberId).then(function(res) {
           if (res.code === 200 && res.data) {
           if (res.code === 200 && res.data) {
             var list = Array.isArray(res.data) ? res.data : (res.data.list || [])
             var list = Array.isArray(res.data) ? res.data : (res.data.list || [])
             self.reports = list
             self.reports = list
           }
           }
         }).catch(function() {})
         }).catch(function() {})
+        // 文章
         api.getFeaturedArticles({ size: 3, dimensionCode: 'body' }).then(function(res) {
         api.getFeaturedArticles({ size: 3, dimensionCode: 'body' }).then(function(res) {
           if (res.code === 200 && res.data) {
           if (res.code === 200 && res.data) {
             var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
             var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
             self.healthArticles = list.slice(0, 3) || []
             self.healthArticles = list.slice(0, 3) || []
           }
           }
         }).catch(function() {})
         }).catch(function() {})
+        // 认知测评得分
+        if (memberId) {
+          api.getCognitiveChildScores(memberId).then(function(res) {
+            if (res.code === 200) self.cognitiveData = res.data || null
+          }).catch(function() {})
+          api.getCognitiveFamilyScores().then(function(res) {
+            if (res.code === 200 && Array.isArray(res.data)) self.cognitiveFamilyData = res.data
+          }).catch(function() {})
+        }
+        // EMI心理报告
+        if (memberId) {
+          api.getEmiReport(memberId).then(function(res) {
+            if (res.code === 200) self.emiData = res.data || null
+          }).catch(function() {})
+        }
+        // 关系质量评分
+        api.getRelationshipScores().then(function(res) {
+          if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
+            self.relationshipData = res.data
+            self.relationshipEmpty = false
+          } else {
+            self.relationshipEmpty = true
+          }
+        }).catch(function() { self.relationshipEmpty = true })
+        // 财富能量沙盘(家庭五维)
+        api.getFamilyEnergySandbox().then(function(res) {
+          if (res && res.data && !res.data.locked && res.data.familyData) {
+            self.energySandbox = res.data.familyData
+          } else if (res && res.data && !res.data.locked && res.data.personalData) {
+            self.energySandbox = res.data.personalData
+          }
+        }).catch(function() {})
       } catch (e) { console.error('加载健康数据失败', e) }
       } catch (e) { console.error('加载健康数据失败', e) }
     },
     },
     loadPlanList: function() {
     loadPlanList: function() {

+ 3 - 0
cfc-frontend/utils/api.js

@@ -1808,6 +1808,9 @@ export const submitQuiz = (data) => request('/api/articles/quiz/submit', 'POST',
 
 
 // ===== EMI蹇冪悊鎶ュ憡 =====
 // ===== EMI蹇冪悊鎶ュ憡 =====
 export const getEmiReport = (memberId) => request('/api/emireport/latest', 'POST', { memberId })
 export const getEmiReport = (memberId) => request('/api/emireport/latest', 'POST', { memberId })
+export const getEmiFamilyReports = (childIds) => request('/api/emireport/family', 'POST', { childIds })
+export const getCognitiveChildScores = (memberId) => request('/api/cognitive/child', 'POST', { memberId })
+export const getCognitiveFamilyScores = () => request('/api/cognitive/family', 'POST', {})
 
 
 
 
 // ===== 认知测评结果 =====
 // ===== 认知测评结果 =====