Browse Source

feat(frontend): DAN 测评报告专用详情页

- 新增 pages/dan-assessment/report-result.vue,展示认知雷达图+心理素养+大五人格+成长建议
- 后端 DanReportUploadVO 增加 result 字段,getDetail 返回关联的 DanAssessmentResult 分数
- 更新 4 处入口跳转:report-list、mind-detail、wisdom-detail、health-main
Sisyphus 1 month ago
parent
commit
ba26f8d845

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/DanReportUploadVO.java

@@ -28,6 +28,8 @@ public class DanReportUploadVO {
     private Long confirmedBy;
     private Integer editCount;
     private Long linkedResultId;
+    /** 关联的测评结果(含各项分数,可为 null) */
+    private Map<String, Object> result;
     private Date createdAt;
     private Date updatedAt;
 

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java

@@ -334,6 +334,37 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
         }
         DanReportUploadVO vo = DanReportUploadVO.fromEntity(upload);
         vo.setBlocks(reportBlockService.getBlocks("dan", upload.getId()));
+        if (upload.getLinkedResultId() != null) {
+            DanAssessmentResult r = resultMapper.selectById(upload.getLinkedResultId());
+            if (r != null) {
+                Map<String, Object> rm = new LinkedHashMap<>();
+                rm.put("id", r.getId());
+                rm.put("childName", r.getChildName());
+                rm.put("assessmentDate", r.getAssessmentDate());
+                rm.put("overallScore", r.getOverallScore());
+                rm.put("attentionScore", r.getAttentionScore());
+                rm.put("focusScore", r.getFocusScore());
+                rm.put("memoryScore", r.getMemoryScore());
+                rm.put("logicScore", r.getLogicScore());
+                rm.put("perceptionScore", r.getPerceptionScore());
+                rm.put("spatialScore", r.getSpatialScore());
+                rm.put("processingSpeedScore", r.getProcessingSpeedScore());
+                rm.put("emotionScore", r.getEmotionScore());
+                rm.put("resilienceScore", r.getResilienceScore());
+                rm.put("stressCopingScore", r.getStressCopingScore());
+                rm.put("selfAwarenessScore", r.getSelfAwarenessScore());
+                rm.put("opennessScore", r.getOpennessScore());
+                rm.put("conscientiousnessScore", r.getConscientiousnessScore());
+                rm.put("extraversionScore", r.getExtraversionScore());
+                rm.put("agreeablenessScore", r.getAgreeablenessScore());
+                rm.put("neuroticismScore", r.getNeuroticismScore());
+                rm.put("bigFiveOverallScore", r.getBigFiveOverallScore());
+                rm.put("analysisReport", r.getAnalysisReport());
+                rm.put("growthSuggestions", r.getGrowthSuggestions());
+                rm.put("structuredAnalysis", r.getStructuredAnalysis());
+                vo.setResult(rm);
+            }
+        }
         return Result.success(vo);
     }
 }

+ 7 - 0
cfc-frontend/pages.json

@@ -393,6 +393,13 @@
             "navigationBarTitleText": "DAN 报告上传",
             "navigationStyle": "custom"
           }
+        },
+        {
+          "path": "report-result",
+          "style": {
+            "navigationBarTitleText": "DAN 测评报告",
+            "navigationStyle": "custom"
+          }
         }
       ]
     },

+ 228 - 0
cfc-frontend/pages/dan-assessment/report-result.vue

@@ -0,0 +1,228 @@
+<template>
+  <view class="container">
+    <!-- 报告头部 -->
+    <view class="report-head" v-if="headInfo">
+      <text class="head-name">{{ headInfo.personName || headInfo.childName || 'DAN 测评报告' }}</text>
+      <text class="head-date">{{ headInfo.dateText }}</text>
+      <text class="head-dim" v-if="dimensionLabel">{{ dimensionLabel }}</text>
+    </view>
+
+    <view class="loading-wrap" v-if="loading">
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <template v-else-if="result">
+      <!-- 综合得分 -->
+      <view class="section overview-card">
+        <view class="overview-header">
+          <text class="overview-title">综合评估</text>
+          <text class="overview-score">{{ result.overallScore || '--' }}</text>
+        </view>
+        <view class="overview-desc" v-if="result.analysisReport">
+          <text>{{ result.analysisReport }}</text>
+        </view>
+      </view>
+
+      <!-- 认知七维雷达图 -->
+      <view class="section radar-section">
+        <view class="section-header">
+          <text class="section-title">🧠 认知能力</text>
+        </view>
+        <RadarChart
+          :dimensions="cognitiveDimensions"
+          :scores="cognitiveScores"
+          fillColor="#6366F1"
+          gridColor="#E0E7FF"
+          labelColor="#3730A3"
+          :width="560"
+          :height="460" />
+        <view class="score-row" v-for="(dim, i) in cognitiveDimensions" :key="i">
+          <text class="score-label">{{ dim.label }}</text>
+          <view class="score-bar-bg">
+            <view class="score-bar-fill" :style="'width:' + Math.min(100, result[dim.key] || 0) + '%;background:' + dim.color"></view>
+          </view>
+          <text class="score-num">{{ result[dim.key] || 0 }}</text>
+        </view>
+      </view>
+
+      <!-- EMI 心理维度 -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-title">💖 心理素养</text>
+        </view>
+        <view class="score-row" v-for="(dim, i) in emiDimensions" :key="i">
+          <text class="score-label">{{ dim.label }}</text>
+          <view class="score-bar-bg">
+            <view class="score-bar-fill" :style="'width:' + Math.min(100, result[dim.key] || 0) + '%;background:' + dim.color"></view>
+          </view>
+          <text class="score-num">{{ result[dim.key] || 0 }}</text>
+        </view>
+      </view>
+
+      <!-- 大五人格 -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-title">🌈 人格特质</text>
+        </view>
+        <view class="score-row" v-for="(dim, i) in bigFiveDimensions" :key="i">
+          <text class="score-label">{{ dim.label }}</text>
+          <view class="score-bar-bg">
+            <view class="score-bar-fill" :style="'width:' + Math.min(100, result[dim.key] || 0) + '%;background:' + dim.color"></view>
+          </view>
+          <text class="score-num">{{ result[dim.key] || 0 }}</text>
+        </view>
+        <view class="bigfive-total" v-if="result.bigFiveOverallScore != null">
+          <text>大五综合分:</text>
+          <text class="bigfive-val">{{ result.bigFiveOverallScore }}</text>
+        </view>
+      </view>
+
+      <!-- 成长建议 -->
+      <view class="section suggestion-card" v-if="result.growthSuggestions">
+        <view class="section-title">🌱 成长建议</view>
+        <text class="suggestion-text">{{ result.growthSuggestions }}</text>
+      </view>
+
+      <!-- 报告内容块 -->
+      <view class="section" v-if="blocks && blocks.length > 0">
+        <view class="section-header">
+          <text class="section-title">📋 报告内容</text>
+        </view>
+        <view class="block-item" v-for="(block, i) in blocks" :key="i">
+          <text class="block-title">{{ block.title || '' }}</text>
+          <text class="block-content">{{ block.content || '' }}</text>
+        </view>
+      </view>
+
+    </template>
+
+    <view class="empty-wrap" v-else>
+      <text class="empty-text">暂无数据</text>
+    </view>
+
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import RadarChart from '../../components/RadarChart.vue'
+import { getDanReportDetail } from '../../utils/api.js'
+
+export default {
+  components: { RadarChart },
+  data() {
+    return {
+      reportId: null,
+      loading: true,
+      headInfo: null,
+      result: null,
+      blocks: [],
+      cognitiveDimensions: [
+        { key: 'attentionScore', label: '注意力', color: '#6366F1' },
+        { key: 'focusScore', label: '专注力', color: '#818CF8' },
+        { key: 'memoryScore', label: '记忆力', color: '#A5B4FC' },
+        { key: 'logicScore', label: '逻辑思维', color: '#C7D2FE' },
+        { key: 'perceptionScore', label: '感知能力', color: '#6366F1' },
+        { key: 'spatialScore', label: '空间思维', color: '#818CF8' },
+        { key: 'processingSpeedScore', label: '加工速度', color: '#A5B4FC' }
+      ],
+      emiDimensions: [
+        { key: 'emotionScore', label: '情绪商数', color: '#FF6B9D' },
+        { key: 'resilienceScore', label: '心理韧性', color: '#F472B6' },
+        { key: 'stressCopingScore', label: '压力应对', color: '#EC4899' },
+        { key: 'selfAwarenessScore', label: '自我认知', color: '#DB2777' }
+      ],
+      bigFiveDimensions: [
+        { key: 'opennessScore', label: '开放性', color: '#F59E0B' },
+        { key: 'conscientiousnessScore', label: '尽责性', color: '#FBBF24' },
+        { key: 'extraversionScore', label: '外向性', color: '#FCD34D' },
+        { key: 'agreeablenessScore', label: '宜人性', color: '#FDE68A' },
+        { key: 'neuroticismScore', label: '神经质', color: '#F59E0B' }
+      ]
+    }
+  },
+  computed: {
+    dimensionLabel() {
+      if (!this.result) return ''
+      var dim = this.result.dimension
+      return dim === 'mind' ? '心维度 (A2)' : dim === 'wisdom' ? '智维度 (B4)' : ''
+    }
+  },
+  onLoad(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) this.loadData()
+  },
+  methods: {
+    loadData() {
+      var self = this
+      this.loading = true
+      getDanReportDetail(this.reportId).then(function(res) {
+        self.loading = false
+        if (res.code !== 200 || !res.data) {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
+          return
+        }
+        var data = res.data
+        // head info
+        self.headInfo = {
+          personName: data.personName || data.childName || '',
+          dateText: data.assessmentDate ? data.assessmentDate.substring(0, 10) : (data.confirmedAt ? data.confirmedAt.substring(0, 10) : ''),
+          dimension: data.dimension
+        }
+        // linked result scores
+        self.result = data.result || null
+        // blocks
+        self.blocks = (data.blocks || []).filter(function(b) { return b.title || b.content })
+      }).catch(function(e) {
+        self.loading = false
+        uni.showToast({ title: '加载异常', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #F8F8F8; padding-bottom: 40rpx; }
+.loading-wrap { display: flex; justify-content: center; padding: 120rpx 0; }
+.loading-text { font-size: 28rpx; color: #999; }
+.empty-wrap { display: flex; justify-content: center; padding: 120rpx 0; }
+.empty-text { font-size: 28rpx; color: #999; }
+
+.report-head { background: linear-gradient(135deg, #6366F1, #8B5CF6); padding: 32rpx 28rpx 28rpx; color: #fff; }
+.head-name { font-size: 36rpx; font-weight: bold; display: block; }
+.head-date { font-size: 24rpx; opacity: 0.85; display: block; margin-top: 8rpx; }
+.head-dim { font-size: 22rpx; background: rgba(255,255,255,0.2); border-radius: 20rpx; padding: 4rpx 16rpx; margin-top: 12rpx; display: inline-block; }
+
+.section { margin: 20rpx 24rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.section-header { display: flex; align-items: center; margin-bottom: 20rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
+
+.overview-card { text-align: center; }
+.overview-header { display: flex; align-items: baseline; justify-content: center; gap: 12rpx; margin-bottom: 16rpx; }
+.overview-title { font-size: 28rpx; color: #666; }
+.overview-score { font-size: 56rpx; font-weight: bold; color: #6366F1; }
+.overview-desc { font-size: 26rpx; color: #666; line-height: 1.6; }
+
+.radar-section { padding-bottom: 12rpx; }
+
+.score-row { display: flex; align-items: center; margin-bottom: 16rpx; }
+.score-label { font-size: 24rpx; color: #555; width: 140rpx; flex-shrink: 0; }
+.score-bar-bg { flex: 1; height: 16rpx; background: #F0F0F0; border-radius: 8rpx; margin: 0 16rpx; overflow: hidden; }
+.score-bar-fill { height: 100%; border-radius: 8rpx; transition: width 0.3s; }
+.score-num { font-size: 24rpx; color: #666; width: 60rpx; text-align: right; flex-shrink: 0; }
+
+.bigfive-total { margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #F0F0F0; font-size: 26rpx; color: #666; }
+.bigfive-val { font-size: 32rpx; font-weight: bold; color: #F59E0B; margin-left: 8rpx; }
+
+.suggestion-card { background: linear-gradient(135deg, #FFF7ED, #FEF3C7); }
+.suggestion-card .section-title { margin-bottom: 16rpx; }
+.suggestion-text { font-size: 26rpx; color: #92400E; line-height: 1.8; }
+
+.block-item { padding: 20rpx 0; border-bottom: 1rpx solid #F5F5F5; }
+.block-item:last-child { border-bottom: none; }
+.block-title { font-size: 28rpx; font-weight: 600; color: #333; display: block; margin-bottom: 8rpx; }
+.block-content { font-size: 26rpx; color: #666; line-height: 1.6; }
+
+.bottom-spacer { height: 40rpx; }
+</style>

+ 4 - 1
cfc-frontend/pages/health-main/index.vue

@@ -483,8 +483,11 @@ export default {
     goReportDetail: function(report) {
       if (!report || !report.id) return
       var type = report.reportType || 'gut_flora'
+      if (type === 'dan') {
+        uni.navigateTo({ url: '/pages/dan-assessment/report-result?reportId=' + report.id })
+        return
+      }
       var url = '/pages/health/report-detail?reportType=' + type + '&reportId=' + report.id
-      // 肠道菌群报告:从列表点击直接进入编辑模式,跳过查看过渡页
       if (type === 'gut_flora') {
         url += '&mode=edit'
       }

+ 1 - 1
cfc-frontend/pages/health/report-list.vue

@@ -75,7 +75,7 @@ export default {
 	goToDetail(reportId, reportType) {
 		var detailPages = {
 		'gut_flora': '/pages/health/report-detail?reportType=gut_flora',
-		'dan': '/pages/health/report-detail?reportType=dan',
+		'dan': '/pages/dan-assessment/report-result',
 		'physical_exam': '/pages/health/physical-exam-detail',
 		'tongue': '/pages/health/tongue-index'
 	  }

+ 1 - 1
cfc-frontend/pages/mind-detail/index.vue

@@ -916,7 +916,7 @@ var descList = descMap[this.dominantElement] || descMap.wood
     },
     viewDanReport: function(report) {
       var reportId = report.sourceReportId || report.id
-      uni.navigateTo({ url: '/pages/health/report-detail?reportType=dan&reportId=' + reportId })
+      uni.navigateTo({ url: '/pages/dan-assessment/report-result?reportId=' + reportId })
     },
     formatDate: function(dateStr) {
       if (!dateStr) return ''

+ 1 - 1
cfc-frontend/pages/wisdom-detail/index.vue

@@ -534,7 +534,7 @@ export default {
     },
     viewDanReport: function(report) {
       var reportId = report.sourceReportId || report.id
-      uni.navigateTo({ url: '/pages/health/report-detail?reportType=dan&reportId=' + reportId })
+      uni.navigateTo({ url: '/pages/dan-assessment/report-result?reportId=' + reportId })
     },
     formatDate: function(dateStr) {
       if (!dateStr) return ''