Răsfoiți Sursa

feat(report): 报告详情优化-新增payload按需加载接口,blocks渲染器增加知识库?查询

E2E Test Bot 3 săptămâni în urmă
părinte
comite
95c1a864b4

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -403,6 +403,21 @@ public class HealthReportController {
         return Result.success(detail);
     }
 
+    /**
+     * 获取报告 payload(编辑回填用,按需加载,减少首屏性能开销)
+     */
+    @Operation(summary = "获取报告payload(编辑回填)")
+    @PostMapping("/report/payload")
+    public Result<Map<String, Object>> getReportPayload(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = ParamUtils.getLong(params.get("reportId"));
+        if (reportId == null) {
+            return Result.error("reportId不能为空");
+        }
+        Map<String, Object> payload = healthReportService.getReportPayload(reportId);
+        return Result.success(payload);
+    }
+
     /**
      * 编辑已发布报告 — 修正 AI 解析错误的值
      */

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -500,6 +500,25 @@ public class HealthReportService {
         return detail;
     }
 
+    /**
+     * 获取报告 payload(编辑回填用,按需加载)
+     */
+    public Map<String, Object> getReportPayload(Long reportId) {
+        Map<String, Object> result = new HashMap<>();
+        HealthReport report = healthReportMapper.selectById(reportId);
+        if (report == null) {
+            return result;
+        }
+        if (report.getPayloadJson() != null && !report.getPayloadJson().isEmpty()) {
+            try {
+                result.put("payload", objectMapper.readValue(report.getPayloadJson(), ParsedReportPayload.Payload.class));
+            } catch (Exception e) {
+                log.warn("解析payload_json失败 reportId={}: {}", reportId, e.getMessage());
+            }
+        }
+        return result;
+    }
+
     private int clampScore(Integer score) {
         if (score == null) return 0;
         return Math.min(Math.max(score, 0), 100);

+ 21 - 13
cfc-frontend/components/report-blocks-renderer.vue

@@ -5,18 +5,18 @@
       <view v-if="block.type === 'score'" class="block score-block">
         <view class="block-title">{{ block.title }}</view>
         <view class="score-grid">
-<view class="score-item" v-for="(item, si) in block.items" :key="getScoreKey(si)">
-              <view class="score-circle" :style="'border-color:' + (item.color || '#4A9BD7')">
-                <text class="score-value">{{ item.value }}</text>
-              </view>
-              <text class="score-label">{{ item.label }}</text>
-              <view class="sub-items" v-if="item.subItems && item.subItems.length" style="margin-top:8rpx; display:flex; flex-direction:column; align-items:center;">
-                <view class="sub-item" v-for="(sub, ssi) in item.subItems" :key="getSubKey(ssi)" style="display:flex; flex-direction:row; align-items:center;">
-                  <text class="sub-label" style="font-size:20rpx; color:#666; margin-right:4rpx;">{{ sub.label }}:</text>
-                  <text class="sub-value" style="font-size:20rpx; color:#333;">{{ sub.value }}</text>
-                </view>
+          <view class="score-item" v-for="(item, si) in block.items" :key="getScoreKey(si)">
+            <view class="score-circle" :style="'border-color:' + (item.color || '#4A9BD7')">
+              <text class="score-value">{{ item.value }}</text>
+            </view>
+            <text class="score-label">{{ item.label }}</text>
+            <view class="sub-items" v-if="item.subItems && item.subItems.length" style="margin-top:8rpx; display:flex; flex-direction:column; align-items:center;">
+              <view class="sub-item" v-for="(sub, ssi) in item.subItems" :key="getSubKey(ssi)" style="display:flex; flex-direction:row; align-items:center;">
+                <text class="sub-label" style="font-size:20rpx; color:#666; margin-right:4rpx;">{{ sub.label }}:</text>
+                <text class="sub-value" style="font-size:20rpx; color:#333;">{{ sub.value }}</text>
               </view>
             </view>
+          </view>
         </view>
       </view>
 
@@ -28,6 +28,7 @@
           <text class="risk-name">{{ item.name }}</text>
           <text class="risk-value">风险值: {{ item.value || '--' }}</text>
           <text class="risk-badge" :class="'badge-' + riskCss(item.level)">{{ riskText(item.level) }}</text>
+          <text class="ind-help-btn" @tap.stop="onHelp(item, 'indicator')">?</text>
         </view>
       </view>
 
@@ -41,19 +42,22 @@
           <text class="ind-name">{{ item.name }}</text>
           <text class="ind-value">{{ item.value }} {{ item.unit }}</text>
           <text class="ind-status" :class="'status-' + statusCss(item.status)">{{ item.status }}</text>
+          <text class="ind-help-btn" @tap.stop="onHelp(item, 'indicator')">?</text>
         </view>
       </view>
 
-      <!-- list: 通用列表(columns 驱动) -->
+      <!-- list: 通用列表(columns 驱动,含 ? 按钮) -->
       <view v-else-if="block.type === 'list'" class="block list-block">
         <view class="block-title">{{ block.title }}</view>
         <view class="list-head" v-if="block.columns">
           <text class="list-cell head-cell" v-for="(colItem, ci) in block.columns" :key="getColumnKey(ci)"
                 :style="'flex:' + (ci === 0 ? 2 : 1)">{{ colItem.label }}</text>
+          <text class="list-cell head-cell" style="flex:0 0 60rpx;">帮助</text>
         </view>
         <view class="list-row" v-for="(item, li) in block.items" :key="getListItemKey(li)">
           <text class="list-cell" :style="'flex:' + (ci === 0 ? 2 : 1)"
                 v-for="(colItem, ci) in block.columns" :key="getListColumnKey(ci)">{{ item[colItem.key] || '--' }}</text>
+          <text class="list-cell ind-help-btn" style="flex:0 0 60rpx; text-align:center;" @tap.stop="onHelp(item, 'bacteria')">?</text>
         </view>
       </view>
 
@@ -82,7 +86,6 @@ export default {
     getBlockKey: function(bi) { return 'b' + bi },
     getSubKey: function(ssi) { return 'sub' + ssi },
     getScoreKey: function(si) { return 's' + si },
-  getSubKey: function(ssi) { return 'sub' + ssi },
     getRiskKey: function(ri) { return 'r' + ri },
     getIndicatorKey: function(ii) { return 'i' + ii },
     getColumnKey: function(ci) { return 'c' + ci },
@@ -99,6 +102,9 @@ export default {
     statusCss: function(status) {
       var map = { '偏高': 'high', '偏低': 'low', '缺乏': 'low', '不足': 'low', '过多': 'high', '异常': 'high' }
       return map[status] || 'normal'
+    },
+    onHelp: function(item, blockType) {
+      this.$emit('help', item, blockType)
     }
   }
 }
@@ -140,4 +146,6 @@ export default {
 /* text */
 .text-content { font-size: 26rpx; color: #555; line-height: 1.7; white-space: pre-wrap; }
 .chart-placeholder { font-size: 24rpx; color: #ccc; }
-</style>
+/* help button */
+.ind-help-btn { display: inline-flex; align-items: center; justify-content: center; width: 36rpx; height: 36rpx; border-radius: 50%; background: #E3F2FD; color: #1976D2; font-size: 22rpx; font-weight: 600; margin-left: 8rpx; flex-shrink: 0; }
+</style>

+ 75 - 7
cfc-frontend/pages/health/report-detail.vue

@@ -13,7 +13,7 @@
 
       <!-- view mode -->
       <view v-if="pageMode==='view'">
-        <report-blocks-renderer v-if="blocks && blocks.length > 0" :blocks="blocks" />
+        <report-blocks-renderer v-if="blocks && blocks.length > 0" :blocks="blocks" @help="onBlockHelp" />
         <view class="empty-hint" v-else>暂无报告内容</view>
         <view class="edit-btn" v-if="isGutFlora && pageMode==='view'">
           <view class="nav-edit" @tap="enterEditMode"><text>{{ backendBlocksExist ? '编辑' : '查看完整数据 ›' }}</text></view>
@@ -391,7 +391,7 @@
 
 <script>
 import ReportBlocksRenderer from '../../components/report-blocks-renderer.vue'
-import { getReportDetail, getDanReportDetail, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMemberList, addFamilyMember } from '../../utils/api.js'
+import { getReportDetail, getDanReportDetail, getReportPayload, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMemberList, addFamilyMember, queryIndicatorKnowledge, queryBacteriaKnowledge } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
@@ -447,7 +447,8 @@ export default {
       groupExpandList: [],
       collapseState: { gutFlora: false, diseaseRisk: false, probiotic: false, taxonomy: false, pathogen: false },
       showHelpPopup: false,
-      helpData: {}
+      helpData: {},
+      _kbCache: {}
     }
   },
   computed: {
@@ -634,10 +635,23 @@ export default {
         pathogenDetection: []
       }
     },
-    enterEditMode() {
-      this.pageMode = 'edit'
-      this.directEdit = false
-      this.showMemberPanel = false
+    enterEditMode: function() {
+      var self = this
+      self.pageMode = 'edit'
+      self.directEdit = false
+      self.showMemberPanel = false
+      // 按需加载 payload(编辑回填)
+      if (self.reportId && self.reportType !== 'dan') {
+        uni.showLoading({ title: '加载编辑数据...' })
+        getReportPayload(self.reportId).then(function(res) {
+          uni.hideLoading()
+          if (res.code === 200 && res.data && res.data.payload) {
+            self.payload = Object.assign({}, self.defaultPayload(), res.data.payload)
+          }
+        }).catch(function() {
+          uni.hideLoading()
+        })
+      }
     },
     cancelEdit() {
       // 从列表 mode=edit 直入编辑模式时,取消等同返回上一页(报告卡片/报告列表)
@@ -817,6 +831,60 @@ export default {
       }
       this.showHelpPopup = true
     },
+    /** blocks renderer ? 按钮点击 — 按需查询知识库(页面级缓存) */
+    onBlockHelp: function(item, blockType) {
+      var self = this
+      var name = item.name || ''
+      if (!name) return
+      var cacheKey = blockType + ':' + name
+      if (self._kbCache[cacheKey]) {
+        self.helpData = self._kbCache[cacheKey]
+        self.showHelpPopup = true
+        return
+      }
+      uni.showLoading({ title: '加载说明...' })
+      var queryFn = blockType === 'bacteria' ? queryBacteriaKnowledge : queryIndicatorKnowledge
+      queryFn(name).then(function(res) {
+        uni.hideLoading()
+        if (res.code === 200 && res.data) {
+          var v3 = res.data
+          var high = v3['偏高影响'] || ''
+          var low = v3['偏低影响'] || ''
+          var suggestion = v3['调整建议'] || ''
+          var desc = ''
+          if (high) desc += '偏高影响:' + high + '\n'
+          if (low) desc += '偏低影响:' + low
+          if (suggestion) desc += (desc ? '\n' : '') + '调整建议:' + suggestion
+          self.helpData = {
+            name: v3['名称'] || name,
+            value: item.value || '',
+            refRange: item.refRange || '',
+            status: item.status || 'normal',
+            description: desc || v3['说明'] || self.getDefaultDescription(name, item.status)
+          }
+        } else {
+          self.helpData = {
+            name: name,
+            value: item.value || '',
+            refRange: item.refRange || '',
+            status: item.status || 'normal',
+            description: self.getDefaultDescription(name, item.status)
+          }
+        }
+        self._kbCache[cacheKey] = self.helpData
+        self.showHelpPopup = true
+      }).catch(function() {
+        uni.hideLoading()
+        self.helpData = {
+          name: name,
+          value: item.value || '',
+          refRange: item.refRange || '',
+          status: item.status || 'normal',
+          description: self.getDefaultDescription(name, item.status)
+        }
+        self.showHelpPopup = true
+      })
+    },
     closeHelp() {
       this.showHelpPopup = false
     },

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

@@ -1955,6 +1955,8 @@ export const getFamilyReports = () => request('/api/health/reports/family', 'POS
 export const getReportDetail = (reportId) => request('/api/health/report/detail', 'POST', { reportId })
 // DAN 报告详情(含 blocks)
 export const getDanReportDetail = (id) => request('/api/dan-report/' + id, 'POST', {})
+// 报告 payload(编辑回填按需加载)
+export const getReportPayload = (reportId) => request('/api/health/report/payload', 'POST', { reportId })
 
 export const editHealthReport = (reportId, payload, subjectId) => request('/api/health/report/edit', 'POST', { reportId, payload, subjectId })