Jelajahi Sumber

feat: 疾病风险内联编辑 + fromMap diseaseRisks 映射 + 全量编译验证

Xiaogang Liao 1 bulan lalu
induk
melakukan
9b63eb97d4

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ReportBlockAssembler.java

@@ -40,6 +40,20 @@ public class ReportBlockAssembler {
         s.setGutAge(str(map.get("gutAge")));
         s.setGutType(str(map.get("gutType")));
         p.setSummary(s);
+        // 编辑时携带 diseaseRisks(使风险编辑后重新组装能保留风险块)
+        Object risksRaw = map.get("diseaseRisks");
+        if (risksRaw instanceof List) {
+            List<Map<String, Object>> riskList = (List<Map<String, Object>>) risksRaw;
+            List<ParsedReportPayload.DiseaseRisk> risks = new ArrayList<>();
+            for (Map<String, Object> r : riskList) {
+                ParsedReportPayload.DiseaseRisk dr = new ParsedReportPayload.DiseaseRisk();
+                dr.setDiseaseName(str(r.get("diseaseName")));
+                dr.setRiskValue(str(r.get("riskValue")));
+                dr.setRiskLevel(str(r.get("riskLevel")));
+                risks.add(dr);
+            }
+            p.setDiseaseRisks(risks);
+        }
         return p;
     }
 

+ 17 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ReportBlockAssemblerTest.java

@@ -78,6 +78,23 @@ class ReportBlockAssemblerTest {
         assertEquals(85, p.getSummary().getCoreGenusScore());
     }
 
+    @Test
+    void fromMap_shouldCarryDiseaseRisks() {
+        java.util.Map<String, Object> map = new java.util.HashMap<>();
+        java.util.List<Map<String, Object>> risks = new java.util.ArrayList<>();
+        Map<String, Object> r1 = new java.util.HashMap<>();
+        r1.put("diseaseName", "心脑血管疾病");
+        r1.put("riskValue", "0.37");
+        r1.put("riskLevel", "注意");
+        risks.add(r1);
+        map.put("diseaseRisks", risks);
+        ParsedReportPayload.Payload p = assembler.fromMap(map);
+        assertNotNull(p.getDiseaseRisks(), "fromMap 应携带 diseaseRisks");
+        assertEquals(1, p.getDiseaseRisks().size());
+        assertEquals("心脑血管疾病", p.getDiseaseRisks().get(0).getDiseaseName());
+        assertEquals("注意", p.getDiseaseRisks().get(0).getRiskLevel());
+    }
+
     @Test
     void assembleDan_shouldExtractScoreListAndText() {
         String itemsJson = "[{\"code\":\"total_score\",\"name\":\"总得分\",\"value\":\"112\",\"category\":\"\"},"

+ 79 - 2
cfc-frontend/pages/health/report-detail.vue

@@ -3,6 +3,9 @@
     <view class="nav-bar">
       <view class="nav-back" @tap="goBack"><text class="back-text">‹ 返回</text></view>
       <text class="nav-title">{{ typeLabel }}</text>
+      <view class="nav-edit" v-if="isGutFlora && hasRiskBlocks" @tap="toggleRiskEdit">
+        <text>{{ isEditing ? '完成' : '编辑' }}</text>
+      </view>
     </view>
     <scroll-view class="content" scroll-y>
       <!-- 报告头 -->
@@ -16,13 +19,27 @@
       <!-- blocks 渲染 -->
       <report-blocks-renderer v-if="blocks && blocks.length > 0" :blocks="blocks" />
       <view class="empty-hint" v-else>暂无报告内容</view>
+      <!-- 风险编辑面板 -->
+      <view class="edit-panel" v-if="isEditing">
+        <view class="edit-card" v-for="(risk, ei) in editRisks" :key="'e' + ei">
+          <text class="edit-name">{{ risk.diseaseName }}</text>
+          <view class="edit-row">
+            <text class="edit-label">风险值:</text>
+            <input class="edit-input" type="text" v-model="risk.riskValue" />
+            <text class="edit-label">等级:</text>
+            <picker :value="riskLevelIndex(risk.riskLevel)" :range="riskLevels" @change="onRiskLevelChange($event, risk)">
+              <text class="edit-picker">{{ risk.riskLevel || '请选择' }}</text>
+            </picker>
+          </view>
+        </view>
+      </view>
     </scroll-view>
   </view>
 </template>
 
 <script>
 import ReportBlocksRenderer from '../../components/report-blocks-renderer.vue'
-import { getReportDetail, getDanReportDetail } from '../../utils/api.js'
+import { getReportDetail, getDanReportDetail, editHealthReport } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
@@ -32,7 +49,10 @@ export default {
       reportType: '',
       reportId: null,
       blocks: [],
-      headInfo: null
+      headInfo: null,
+      isEditing: false,
+      editRisks: [],
+      riskLevels: ['低风险', '需注意', '高风险', '异常']
     }
   },
   computed: {
@@ -69,6 +89,53 @@ export default {
       }).catch(function() {
         uni.showToast({ title: '加载失败', icon: 'none' })
       })
+    },
+    isGutFlora: function() {
+      return this.reportType === 'gut_flora'
+    },
+    hasRiskBlocks: function() {
+      var blocks = this.blocks || []
+      for (var i = 0; i < blocks.length; i++) {
+        if (blocks[i].type === 'risk_group' && blocks[i].items && blocks[i].items.length > 0) return true
+      }
+      return false
+    },
+    toggleRiskEdit: function() {
+      var self = this
+      if (this.isEditing) {
+        editHealthReport(this.reportId, { diseaseRisks: this.editRisks }, 0).then(function(res) {
+          if (res.code === 200) {
+            uni.showToast({ title: '已保存', icon: 'success' })
+            self.isEditing = false
+            self.loadData()
+          } else {
+            uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+          }
+        })
+      } else {
+        var risks = []
+        var blocks = this.blocks || []
+        for (var i = 0; i < blocks.length; i++) {
+          if (blocks[i].type === 'risk_group' && blocks[i].items) {
+            for (var j = 0; j < blocks[i].items.length; j++) {
+              risks.push({
+                diseaseName: blocks[i].items[j].name,
+                riskValue: blocks[i].items[j].value,
+                riskLevel: blocks[i].items[j].level
+              })
+            }
+          }
+        }
+        this.editRisks = JSON.parse(JSON.stringify(risks))
+        this.isEditing = true
+      }
+    },
+    riskLevelIndex: function(level) {
+      var idx = this.riskLevels.indexOf(level)
+      return idx >= 0 ? idx : 0
+    },
+    onRiskLevelChange: function(e, risk) {
+      risk.riskLevel = this.riskLevels[e.detail.value]
     }
   }
 }
@@ -80,6 +147,8 @@ export default {
 .nav-back { padding: 10rpx 0; }
 .back-text { font-size: 28rpx; color: #4A9BD7; }
 .nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; }
+.nav-edit { padding: 10rpx 0; }
+.nav-edit text { font-size: 28rpx; color: #4A9BD7; }
 .content { padding: 20rpx 30rpx; height: calc(100vh - 100rpx); }
 .report-head { background: #fff; border-radius: 16rpx; padding: 28rpx; margin-bottom: 20rpx; }
 .head-name { font-size: 34rpx; font-weight: 600; color: #333; }
@@ -87,4 +156,12 @@ export default {
 .type-badge { font-size: 22rpx; color: #2E7D32; background: #E8F5E9; padding: 4rpx 16rpx; border-radius: 20rpx; margin-right: 16rpx; }
 .head-date { font-size: 24rpx; color: #999; }
 .empty-hint { text-align: center; color: #ccc; padding: 80rpx 0; font-size: 28rpx; }
+/* edit panel */
+.edit-panel { margin-top: 20rpx; }
+.edit-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
+.edit-name { font-size: 28rpx; font-weight: 600; color: #333; display: block; margin-bottom: 12rpx; }
+.edit-row { display: flex; align-items: center; gap: 10rpx; }
+.edit-label { font-size: 26rpx; color: #666; }
+.edit-input { flex: 1; border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; }
+.edit-picker { font-size: 26rpx; color: #333; padding: 8rpx 16rpx; border: 1rpx solid #ddd; border-radius: 8rpx; }
 </style>