Просмотр исходного кода

feat(health): 美化菌群报告疾病风险页并补充疾病知识库,手风琴标题去掉有效保留异常/共

liaoxg 2 месяцев назад
Родитель
Сommit
767c29ce9c

+ 3 - 2
cfc-frontend/pages/health/gut-flora-detail.vue

@@ -133,7 +133,7 @@
       <view class="accordion-section">
         <view class="accordion-header" @tap="toggleAccordion('gut')">
           <text class="accordion-title">肠道功能与代谢物</text>
-          <text class="accordion-stats" v-if="gutStats.total > 0">有效 {{ gutStats.valid }} / 异常 {{ gutStats.abnormal }} / 共 {{ gutStats.total }} 项</text>
+          <text class="accordion-stats" v-if="gutStats.total > 0">异常 <text class="abnormal-num">{{ gutStats.abnormal }}</text> / 共 {{ gutStats.total }} 项</text>
           <text class="accordion-arrow">{{ accordionOpen.gut ? '收起' : '展开' }}</text>
         </view>
         <view class="accordion-body" v-if="accordionOpen.gut">
@@ -168,7 +168,7 @@
       <view class="accordion-section">
         <view class="accordion-header" @tap="toggleAccordion('nutri')">
           <text class="accordion-title">营养指标</text>
-          <text class="accordion-stats" v-if="nutriStats.total > 0">有效 {{ nutriStats.valid }} / 异常 {{ nutriStats.abnormal }} / 共 {{ nutriStats.total }} 项</text>
+          <text class="accordion-stats" v-if="nutriStats.total > 0">异常 <text class="abnormal-num">{{ nutriStats.abnormal }}</text> / 共 {{ nutriStats.total }} 项</text>
           <text class="accordion-arrow">{{ accordionOpen.nutri ? '收起' : '展开' }}</text>
         </view>
         <view class="accordion-body" v-if="accordionOpen.nutri">
@@ -759,6 +759,7 @@ report.gutType = s.gutType || ''
 .accordion-header { display: flex; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
 .accordion-title { font-size: 30rpx; font-weight: 600; color: #333; }
 .accordion-stats { font-size: 22rpx; color: #999; margin-left: 16rpx; }
+.abnormal-num { color: #C62828; font-weight: 700; }
 .accordion-arrow { font-size: 24rpx; color: #999; }
 .accordion-body { padding: 10rpx 30rpx 20rpx; }
 

+ 315 - 49
cfc-frontend/pages/health/gut-flora-risks-detail.vue

@@ -1,20 +1,56 @@
 <template>
   <view class="page">
-<text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+    <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+
+    <!-- 头部摘要 -->
+    <view class="header-summary" v-if="!isEditing && risks.length > 0">
+      <view class="summary-stat">
+        <text class="stat-num stat-abnormal">{{ importantRisks.length }}</text>
+        <text class="stat-label">需关注</text>
+      </view>
+      <view class="summary-stat">
+        <text class="stat-num stat-low">{{ normalRisks.length }}</text>
+        <text class="stat-label">低风险</text>
+      </view>
+      <view class="summary-stat">
+        <text class="stat-num stat-total">{{ risks.length }}</text>
+        <text class="stat-label">总计</text>
+      </view>
+    </view>
 
     <scroll-view class="content" scroll-y>
-      <view class="section-label">重要风险</view>
-      <view class="risk-card" v-for="(item, idx) in importantRisks" :key="idx"
+      <!-- 重要风险 -->
+      <view class="section-header" v-if="importantRisks.length > 0">
+        <view class="section-line"></view>
+        <text class="section-label">⚠ 需重点关注</text>
+      </view>
+      <view class="risk-card important-card" v-for="(item, idx) in importantRisks" :key="'imp-' + idx"
             :class="'risk-level-' + (item._cssClass || 'low')">
-        <view class="risk-header">
-          <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
-          <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
-            {{ riskBadgeText(item.riskLevel) }}
-          </text>
+        <view class="card-top">
+          <view class="card-title-row">
+            <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+            <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
+              {{ riskBadgeText(item.riskLevel) }}
+            </text>
+          </view>
+          <view class="risk-progress-wrap" v-if="item.riskValue != null">
+            <view class="risk-progress-bg">
+              <view class="risk-progress-fill" :class="'fill-' + (item._cssClass || 'low')"
+                    :style="'width:' + calcProgress(item.riskValue) + '%'"></view>
+            </view>
+            <text class="risk-value-text">{{ item.riskValue }}</text>
+          </view>
+        </view>
+        <view class="card-desc" v-if="item.diseaseDesc">
+          <text class="desc-label">说明:</text>
+          <text class="desc-content">{{ item.diseaseDesc }}</text>
         </view>
         <view class="risk-detail-row" v-if="!isEditing">
-          <text class="risk-value">风险值: {{ item.riskValue || '--' }}</text>
-          <text class="risk-level-text">等级: {{ item.riskLevel || '--' }}</text>
+          <text class="risk-tag" :class="'tag-' + (item._cssClass || 'low')">
+            {{ item.riskLevel || '--' }}
+          </text>
+          <text class="risk-value" v-if="item.riskValue">风险值 {{ item.riskValue }}</text>
+          <text class="risk-more" @tap.stop="showKnowledge('disease', item.diseaseName)">查看知识库 ›</text>
         </view>
         <view class="risk-edit-row" v-if="isEditing">
           <text class="edit-label">风险值:</text>
@@ -26,18 +62,28 @@
         </view>
       </view>
 
-      <view class="section-label">其它风险</view>
-      <view class="risk-card" v-for="(item, idx) in normalRisks" :key="idx"
+      <!-- 其它风险 -->
+      <view class="section-header" v-if="normalRisks.length > 0">
+        <view class="section-line"></view>
+        <text class="section-label">✓ 低风险项</text>
+      </view>
+      <view class="risk-card normal-card" v-for="(item, idx) in normalRisks" :key="'norm-' + idx"
             :class="'risk-level-' + (item._cssClass || 'low')">
-        <view class="risk-header">
-          <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
-          <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
-            {{ riskBadgeText(item.riskLevel) }}
-          </text>
+        <view class="card-top">
+          <view class="card-title-row">
+            <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+            <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
+              {{ riskBadgeText(item.riskLevel) }}
+            </text>
+          </view>
+        </view>
+        <view class="card-desc" v-if="item.diseaseDesc">
+          <text class="desc-label">说明:</text>
+          <text class="desc-content">{{ item.diseaseDesc }}</text>
         </view>
         <view class="risk-detail-row" v-if="!isEditing">
-          <text class="risk-value">风险值: {{ item.riskValue || '--' }}</text>
-          <text class="risk-level-text">等级: {{ item.riskLevel || '--' }}</text>
+          <text class="risk-tag tag-low">{{ item.riskLevel || '--' }}</text>
+          <text class="risk-more" @tap.stop="showKnowledge('disease', item.diseaseName)">查看知识库 ›</text>
         </view>
         <view class="risk-edit-row" v-if="isEditing">
           <text class="edit-label">风险值:</text>
@@ -48,10 +94,12 @@
           </picker>
         </view>
       </view>
+
       <view class="empty-hint" v-if="risks.length === 0">暂无疾病风险评估</view>
       <view class="bottom-spacer"></view>
     </scroll-view>
 
+    <!-- 疾病知识库弹窗 -->
     <health-knowledge-popup :visible="knowledgeVisible" :data="knowledgeData" @close="closeKnowledge" />
 
     <view class="bottom-bar" v-if="isEditing">
@@ -62,7 +110,7 @@
 </template>
 
 <script>
-import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge } from '../../utils/api.js'
+import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, queryDiseaseKnowledge } from '../../utils/api.js'
 import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
 
 export default {
@@ -106,6 +154,12 @@ export default {
       var map = { '低风险': 'low', '需注意': 'warning', '高风险': 'high', '异常': 'abnormal' }
       return map[level] || 'low'
     },
+    /** 风险值归一化进度(0~1范围映射到百分比) */
+    calcProgress: function(value) {
+      var v = parseFloat(value)
+      if (isNaN(v)) return 0
+      return Math.min(100, Math.max(0, v * 100))
+    },
     getRisks: function() {
       return this.isEditing ? this.editRisks : this.risks
     },
@@ -114,10 +168,29 @@ export default {
       getReportDetail(this.reportId).then(function(res) {
         if (res.code === 200 && res.data) {
           self.reportData = res.data
-          self.risks = (res.data.diseaseRisks || []).map(function(item) {
+          var rawRisks = res.data.diseaseRisks || []
+          self.risks = rawRisks.map(function(item) {
             item._cssClass = self._cssForRiskLevel(item.riskLevel)
             return item
           })
+          // 批量预加载疾病知识库
+          self.preloadDiseaseKnowledge(self.risks)
+        }
+      })
+    },
+    /** 批量加载疾病知识库,补充到 risks 数据中 */
+    preloadDiseaseKnowledge: function(risks) {
+      var self = this
+      risks.forEach(function(item) {
+        if (item.diseaseName) {
+          queryDiseaseKnowledge(item.diseaseName).then(function(res) {
+            if (res.code === 200 && res.data) {
+              var d = res.data
+              // 兼容不同字段名
+              item.diseaseDesc = d.description || d.说明 || d.category || ''
+              item.diseaseRelatedBacteria = d.relatedBacteria || d.关联菌种 || ''
+            }
+          }).catch(function() {})
         }
       })
     },
@@ -151,26 +224,42 @@ export default {
     },
     onRiskLevelChange: function(e, item) {
       item.riskLevel = this.riskLevels[e.detail.value]
+      item._cssClass = this._cssForRiskLevel(item.riskLevel)
     },
     showKnowledge: function(type, name) {
       var self = this
-      // 优先使用 KB v3 查询指标调整建议
-      queryIndicatorKnowledge(name).then(function(res) {
+      // 优先查询疾病知识库
+      queryDiseaseKnowledge(name).then(function(res) {
         if (res.code === 200 && res.data) {
-          var v3 = res.data
-          var high = v3['偏高影响'] || ''
-          var low = v3['偏低影响'] || ''
+          var d = res.data
           self.knowledgeData = {
-            itemName: v3['名称'] || name,
-            category: v3['说明'] || '',
-            description: (high ? '偏高影响:' + high + '\n' : '') + (low ? '偏低影响:' + low : ''),
-            suggestion: v3['调整建议'] || ''
+            itemName: d.name || d.名称 || name,
+            category: d.category || d.分类 || d.description || d.说明 || '',
+            description: d.description || d.说明 || d.relatedBacteria || d.关联菌种 || '',
+            suggestion: d.suggestion || d.调整建议 || ''
           }
           self.knowledgeVisible = true
-        } else {
-          // 回退旧版 DB 查询
-          self.queryOldKnowledgeBase(type, name)
+          return
         }
+        // 回退到指标知识库
+        queryIndicatorKnowledge(name).then(function(res2) {
+          if (res2.code === 200 && res2.data) {
+            var v3 = res2.data
+            var high = v3['偏高影响'] || ''
+            var low = v3['偏低影响'] || ''
+            self.knowledgeData = {
+              itemName: v3['名称'] || name,
+              category: v3['说明'] || '',
+              description: (high ? '偏高影响:' + high + '\n' : '') + (low ? '偏低影响:' + low : ''),
+              suggestion: v3['调整建议'] || ''
+            }
+            self.knowledgeVisible = true
+          } else {
+            self.queryOldKnowledgeBase(type, name)
+          }
+        }).catch(function() {
+          self.queryOldKnowledgeBase(type, name)
+        })
       }).catch(function() {
         self.queryOldKnowledgeBase(type, name)
       })
@@ -196,30 +285,207 @@ export default {
 </script>
 
 <style scoped>
-.page { min-height: 100vh; background: #f8f8f8; }
-.nav-edit { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 0; }
-.content { padding: 20rpx 30rpx; height: calc(100vh - 100rpx); }
-.section-label { font-size: 26rpx; color: #999; margin: 10rpx 0 16rpx; padding-left: 8rpx; }
-.risk-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; border-left: 8rpx solid #10B981; }
+.page { min-height: 100vh; background: #f5f6fa; }
+.nav-edit { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 30rpx; text-align: right; display: block; }
+
+/* ===== 头部摘要 ===== */
+.header-summary {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 32rpx 30rpx;
+  margin: 0 0 16rpx;
+}
+.summary-stat {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.stat-num {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: #fff;
+}
+.stat-abnormal { color: #FFD6D6; }
+.stat-low { color: #d4edda; }
+.stat-total { color: #fff; }
+.stat-label {
+  font-size: 22rpx;
+  color: rgba(255,255,255,0.8);
+  margin-top: 6rpx;
+}
+
+.content { padding: 0 24rpx; height: calc(100vh - 200rpx); }
+
+/* ===== 区块标题 ===== */
+.section-header {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin: 24rpx 0 16rpx;
+}
+.section-line {
+  width: 6rpx;
+  height: 28rpx;
+  border-radius: 3rpx;
+  background: #4A9BD7;
+}
+.section-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+/* ===== 风险卡片 ===== */
+.risk-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 2rpx 16rpx rgba(0,0,0,0.06);
+  border-left: 10rpx solid #10B981;
+  transition: all 0.2s;
+}
 .risk-level-warning { border-left-color: #F59E0B; }
-.risk-level-high { border-left-color: #C62828; }
+.risk-level-high { border-left-color: #EF4444; }
 .risk-level-abnormal { border-left-color: #C62828; }
-.risk-header { display: flex; align-items: center; margin-bottom: 12rpx; }
-.risk-name { font-size: 30rpx; font-weight: 600; color: #333; flex: 1; }
-.risk-badge { padding: 4rpx 16rpx; border-radius: 20rpx; font-size: 22rpx; }
+
+/* 重要风险卡片背景微 tint */
+.important-card.risk-level-warning { background: linear-gradient(135deg, #fff 60%, #FFF8E1 100%); }
+.important-card.risk-level-high { background: linear-gradient(135deg, #fff 60%, #FFEBEE 100%); }
+.important-card.risk-level-abnormal { background: linear-gradient(135deg, #fff 60%, #FFEBEE 100%); }
+
+.card-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 12rpx;
+}
+.card-title-row {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  gap: 12rpx;
+  min-width: 0;
+}
+.risk-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.risk-badge {
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+  font-size: 22rpx;
+  flex-shrink: 0;
+}
 .badge-low { background: #E8F5E9; color: #2E7D32; }
 .badge-warning { background: #FFF3E0; color: #E65100; }
 .badge-high { background: #FFEBEE; color: #C62828; }
-.badge-abnormal { background: #FFEBEE; color: #C62828; }
-.risk-detail-row { display: flex; gap: 24rpx; }
-.risk-value, .risk-level-text { font-size: 24rpx; color: #999; }
-.risk-edit-row { display: flex; align-items: center; gap: 12rpx; flex-wrap: wrap; }
+.badge-abnormal { background: #C62828; color: #fff; }
+
+/* 风险进度条 */
+.risk-progress-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 6rpx;
+  flex-shrink: 0;
+  margin-left: 16rpx;
+  width: 160rpx;
+}
+.risk-progress-bg {
+  width: 100%;
+  height: 10rpx;
+  background: #f0f0f0;
+  border-radius: 5rpx;
+  overflow: hidden;
+}
+.risk-progress-fill {
+  height: 100%;
+  border-radius: 5rpx;
+  transition: width 0.6s ease;
+}
+.fill-low { background: #10B981; }
+.fill-warning { background: #F59E0B; }
+.fill-high { background: #EF4444; }
+.fill-abnormal { background: #C62828; }
+.risk-value-text {
+  font-size: 22rpx;
+  color: #999;
+}
+
+/* 疾病说明 */
+.card-desc {
+  display: flex;
+  gap: 6rpx;
+  margin-bottom: 14rpx;
+  padding: 12rpx 16rpx;
+  background: #f8f9fc;
+  border-radius: 10rpx;
+}
+.desc-label {
+  font-size: 22rpx;
+  color: #999;
+  flex-shrink: 0;
+}
+.desc-content {
+  font-size: 22rpx;
+  color: #666;
+  line-height: 1.5;
+  flex: 1;
+}
+
+/* 底部标签行 */
+.risk-detail-row {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  flex-wrap: wrap;
+}
+.risk-tag {
+  font-size: 22rpx;
+  padding: 4rpx 14rpx;
+  border-radius: 8rpx;
+}
+.tag-low { background: #E8F5E9; color: #2E7D32; }
+.tag-warning { background: #FFF3E0; color: #E65100; }
+.tag-high { background: #FFEBEE; color: #C62828; }
+.tag-abnormal { background: #C62828; color: #fff; }
+.risk-value {
+  font-size: 24rpx;
+  color: #666;
+}
+.risk-more {
+  font-size: 24rpx;
+  color: #4A9BD7;
+  margin-left: auto;
+}
+
+/* 编辑行 */
+.risk-edit-row {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  flex-wrap: wrap;
+  padding-top: 12rpx;
+  border-top: 1rpx solid #f0f0f0;
+  margin-top: 4rpx;
+}
 .edit-label { font-size: 26rpx; color: #666; }
 .edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 120rpx; }
 .edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; color: #333; }
-.empty-hint { text-align: center; color: #ccc; padding: 60rpx 0; font-size: 28rpx; }
-.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; }
+
+.empty-hint { text-align: center; color: #ccc; padding: 80rpx 0; font-size: 28rpx; }
+.bottom-spacer { height: 40rpx; }
+
+/* 底栏 */
+.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; z-index: 100; }
 .btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
 .btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
-.bottom-spacer { height: 40rpx; }
 </style>

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

@@ -2460,6 +2460,11 @@ export const queryIndicatorKnowledge = (name) => {
   return request('/api/health/knowledge/v3/indicator/query', 'POST', { name })
 }
 
+// 查询疾病知识库(统一知识库 disease/query)
+export const queryDiseaseKnowledge = (name) => {
+  return request('/api/health/knowledge/unified/disease/query', 'POST', { name })
+}
+
 // 查询食物营养数据
 export const queryFoodKnowledge = (name) => {
   return request('/api/health/knowledge/v3/food/query', 'POST', { name })