Selaa lähdekoodia

chore: auto bump version and changelog [skip ci]

iwt 3 kuukautta sitten
vanhempi
sitoutus
b93c781df7

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

@@ -4,380 +4,380 @@
       <view class="nav-back" @tap="goBack">
         <text class="back-text">‹ 返回</text>
       </view>
-      <text class="nav-title">菌群报告详情</text>
+      <text class="nav-title">肠道菌群报告</text>
     </view>
 
     <scroll-view class="content" scroll-y>
       <!-- 报告头部 -->
       <view class="report-header">
         <text class="report-title">肠道菌群检测报告</text>
-        <text class="report-date" v-if="report.reportDate">报告日期: {{ report.reportDate }}</text>
-        <text class="report-number" v-if="report.reportNumber">报告编号: {{ report.reportNumber }}</text>
+        <view class="report-meta">
+          <text class="meta-item" v-if="report.personName">{{ report.personName }}</text>
+          <text class="meta-item" v-if="report.gender">{{ report.gender }}</text>
+          <text class="meta-item" v-if="report.age">{{ report.age }}岁</text>
+        </view>
+        <view class="report-meta">
+          <text class="meta-item" v-if="report.reportDate">日期: {{ report.reportDate }}</text>
+          <text class="meta-item" v-if="report.reportNumber">编号: {{ report.reportNumber }}</text>
+        </view>
+        <view class="badge-row" v-if="report.gutAge || report.gutType">
+          <text class="badge" v-if="report.gutAge">肠龄: {{ report.gutAge }}</text>
+          <text class="badge" v-if="report.gutType">肠型: {{ report.gutType }}</text>
+        </view>
       </view>
 
-      <!-- 5个评分圆环 -->
-      <view class="scores-section">
-        <view class="score-ring" v-for="(score, idx) in scoreRings" :key="idx">
-          <view class="ring-wrapper">
-            <canvas class="ring-canvas" :canvas-id="'ring_' + idx" style="width:120rpx;height:120rpx;"></canvas>
-            <text class="ring-value">{{ score.value }}</text>
+      <!-- 评分圆环 -->
+      <scroll-view class="scores-scroll" scroll-x>
+        <view class="score-item" v-for="(score, idx) in scoreItems" :key="idx">
+          <view class="score-ring" :style="'border-color: ' + score.color + '; color: ' + score.color">
+            <text class="score-val">{{ score.value }}</text>
           </view>
-          <text class="ring-label">{{ score.label }}</text>
+          <text class="score-label">{{ score.label }}</text>
+        </view>
+      </scroll-view>
+
+      <!-- 快速跳转 -->
+      <view class="quick-nav">
+        <view class="nav-card" @tap="goToSpecies">
+          <text class="nav-icon">🦠</text>
+          <text class="nav-label">菌群详情</text>
+          <text class="nav-count">{{ floraCount }}种</text>
+        </view>
+        <view class="nav-card" @tap="goToRisks">
+          <text class="nav-icon">⚠️</text>
+          <text class="nav-label">疾病风险</text>
+          <text class="nav-count">{{ riskCount }}项</text>
+        </view>
+        <view class="nav-card" @tap="goToFoods">
+          <text class="nav-icon">🍽️</text>
+          <text class="nav-label">饮食推荐</text>
+          <text class="nav-count">{{ foodCount }}项</text>
+        </view>
+        <view class="nav-card" @tap="goToIndicators">
+          <text class="nav-icon">📋</text>
+          <text class="nav-label">全部指标</text>
+          <text class="nav-count">{{ indicatorCount }}项</text>
         </view>
       </view>
 
-      <!-- 菌群分类 -->
-      <view class="section-card" v-for="(group, gIdx) in bacteriaGroups" :key="gIdx">
-        <view class="section-title">
-          <text>{{ group.name }}</text>
-          <text class="section-count">{{ group.items.length }}种</text>
+      <!-- 肠道功能手风琴 -->
+      <view class="accordion-section">
+        <view class="accordion-header" @tap="toggleAccordion('gut')">
+          <text class="accordion-title">肠道功能与代谢物</text>
+          <text class="accordion-arrow">{{ accordionOpen.gut ? '收起' : '展开' }}</text>
         </view>
-        <view class="bacteria-item" v-for="(item, iIdx) in group.items" :key="iIdx">
-          <view class="bacteria-header">
-            <text class="bacteria-name">{{ item.name }}</text>
-            <text class="bacteria-status" :class="'status-' + (item.status || 'normal')">{{ statusText(item.status) }}</text>
-          </view>
-          <view class="bar-track">
-            <view class="bar-fill" :style="'width:' + (item.abundance || 0) + '%'"></view>
+        <view class="accordion-body" v-if="accordionOpen.gut">
+          <view class="indicator-card" v-for="(item, idx) in gutIndicators" :key="idx">
+            <view class="indicator-row">
+              <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
+                {{ item.indicatorName }}
+                <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
+                <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
+                <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
+              </text>
+              <view class="indicator-value" v-if="!isEditing">
+                <text :class="'val-text val-' + item.status">{{ item.indicatorValue || '--' }}</text>
+              </view>
+              <view class="indicator-edit" v-if="isEditing">
+                <input class="edit-input" type="text" v-model="item.indicatorValue" />
+                <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
+                  <text class="edit-picker">{{ item.status || '正常' }}</text>
+                </picker>
+              </view>
+            </view>
+            <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
           </view>
-          <text class="bar-label">{{ (item.abundance || 0).toFixed(1) }}%</text>
+          <view class="empty-hint" v-if="gutIndicators.length === 0">暂无数据</view>
         </view>
-        <view class="empty-hint" v-if="group.items.length === 0">暂无数据</view>
       </view>
 
-      <!-- 饮食推荐 -->
-      <view class="section-card" v-if="foods.length > 0">
-        <view class="section-title">
-          <text>饮食推荐</text>
-          <text class="section-count">{{ foods.length }}项</text>
+      <!-- 营养指标手风琴 -->
+      <view class="accordion-section">
+        <view class="accordion-header" @tap="toggleAccordion('nutri')">
+          <text class="accordion-title">营养指标</text>
+          <text class="accordion-arrow">{{ accordionOpen.nutri ? '收起' : '展开' }}</text>
         </view>
-        <view class="food-item" v-for="(food, fIdx) in foods" :key="fIdx">
-          <text class="food-name">{{ food.foodName || food.name }}</text>
-          <text class="food-category" v-if="food.category">{{ food.category }}</text>
-          <text class="food-reason" v-if="food.reason">{{ food.reason }}</text>
+        <view class="accordion-body" v-if="accordionOpen.nutri">
+          <view class="sub-group" v-for="(group, gIdx) in nutritionGroups" :key="gIdx">
+            <text class="sub-group-title">{{ group.category }}</text>
+            <view class="indicator-card" v-for="(item, idx) in group.items" :key="idx">
+              <view class="indicator-row">
+                <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
+                  {{ item.indicatorName }}
+                  <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
+                  <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
+                  <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
+                </text>
+                <view class="indicator-value" v-if="!isEditing">
+                  <text :class="'val-text val-' + item.status">{{ item.indicatorValue || '--' }}</text>
+                </view>
+                <view class="indicator-edit" v-if="isEditing">
+                  <input class="edit-input" type="text" v-model="item.indicatorValue" />
+                  <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
+                    <text class="edit-picker">{{ item.status || '正常' }}</text>
+                  </picker>
+                </view>
+              </view>
+              <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
+            </view>
+          </view>
+          <view class="empty-hint" v-if="nutritionGroups.length === 0">暂无数据</view>
         </view>
       </view>
 
       <view class="bottom-spacer"></view>
     </scroll-view>
 
-    <!-- 营养咨询 FAB -->
-    <view class="fab" @tap="goToNutritionChat">
-      <text class="fab-icon">💬</text>
+    <health-knowledge-popup :visible="knowledgeVisible" :data="knowledgeData" @close="closeKnowledge" />
+
+    <view class="fab" v-if="!isEditing" @tap="toggleEdit">
+      <text class="fab-icon">✏️</text>
+    </view>
+    <view class="bottom-bar" v-if="isEditing">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
     </view>
   </view>
 </template>
 
 <script>
-import { getReportDetail } from '../../utils/api.js'
-import { createReportSurvey } from '../../utils/api.js'
+import { getReportDetail, editHealthReport, queryKnowledgeBase } from '../../utils/api.js'
+import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+
+var INDICATOR_TYPE_MAP = {
+  '维生素': 'vitamin',
+  '氨基酸': 'amino_acid',
+  '微量元素': 'indicator',
+  '肠道屏障与代谢物': 'indicator',
+  '短链脂肪酸': 'indicator',
+  '神经递质与激素': 'indicator',
+  '抗生素风险评估': 'indicator'
+}
 
 export default {
+  components: { HealthKnowledgePopup },
   data() {
     return {
       reportId: null,
+      reportData: null,
       report: {},
-      scoreRings: [],
-      bacteriaGroups: [
-        { name: '有益菌', items: [] },
-        { name: '有害菌', items: [] },
-        { name: '中性菌', items: [] }
-      ],
-      foods: []
+      isEditing: false,
+      accordionOpen: { gut: false, nutri: false },
+      scoreItems: [],
+      allIndicators: [],
+      gutIndicators: [],
+      nutritionGroups: [],
+      floraCount: 0,
+      riskCount: 0,
+      foodCount: 0,
+      indicatorCount: 0,
+      knowledgeVisible: false,
+      knowledgeData: {},
+      indStatuses: ['正常', '偏低', '偏高', '缺乏', '不足', '过多', '异常']
     }
   },
   onLoad: function(options) {
     this.reportId = options.reportId ? parseInt(options.reportId) : null
     if (this.reportId) {
-      this.loadReportDetail()
+      this.loadData()
     }
   },
-  onReady: function() {
-    // canvas需要DOM就绪后绘制
-    setTimeout(() => {
-      this.drawRings()
-    }, 500)
-  },
   methods: {
-    goBack: function() {
-      uni.navigateBack()
-    },
-    loadReportDetail: function() {
+    goBack: function() { uni.navigateBack() },
+    loadData: function() {
       var self = this
-      uni.showLoading({ title: '加载中...' })
       getReportDetail(this.reportId).then(function(res) {
-        uni.hideLoading()
-        if (res && res.code === 200 && res.data) {
-          var data = res.data
-          self.report = {
-            reportDate: data.reportDate || '',
-            reportNumber: data.reportNumber || '',
-            personName: data.personName || ''
-          }
-          // 解析评分
-          self.scoreRings = [
-            { label: '综合评分', value: data.overallScore || 0, max: 100 },
-            { label: '肠道健康', value: data.gutHealthScore || 0, max: 100 },
-            { label: '营养评分', value: data.nutritionScore || 0, max: 100 },
-            { label: '有益菌', value: data.beneficialScore || 0, max: 100 },
-            { label: '多样性', value: data.diversityScore || 0, max: 100 }
-          ]
-          // 解析 payload 中的菌群数据
-          var payload = data.payload
-          if (payload) {
-            self.parseBacteria(payload)
-            if (payload.foods) {
-              self.foods = payload.foods
-            }
-          }
-          // 延迟绘制canvas
-          self.$nextTick(function() {
-            setTimeout(function() { self.drawRings() }, 300)
-          })
-        } else {
-          uni.showToast({ title: '加载失败', icon: 'none' })
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.report = res.data.report || res.data
+          self.buildScores(res.data)
+          self.buildIndicators(res.data)
+          self.floraCount = (res.data.gutFlora || []).length
+          self.riskCount = (res.data.diseaseRisks || []).length
+          self.foodCount = (res.data.foods || []).length
+          self.indicatorCount = (res.data.indicators || []).length
         }
-      }).catch(function() {
-        uni.hideLoading()
-        uni.showToast({ title: '加载失败', icon: 'none' })
       })
     },
-    parseBacteria: function(payload) {
-      var species = payload.probioticSpecies || payload.gutFlora || []
-      var groups = [
-        { name: '有益菌', items: [] },
-        { name: '有害菌', items: [] },
-        { name: '中性菌', items: [] }
+    buildScores: function(data) {
+      var r = data.report || data
+      var items = []
+      var scoreFields = [
+        { key: 'overallScore', label: '综合', color: '#4A9BD7' },
+        { key: 'gutHealthScore', label: '菌群健康', color: '#10B981' },
+        { key: 'chronicDiseaseScore', label: '慢病控制', color: '#F59E0B' },
+        { key: 'nutritionScore', label: '营养均衡', color: '#FF8C42' },
+        { key: 'gutBalanceScore', label: '平衡', color: '#6366F1' },
+        { key: 'gutDiversityScore', label: '多样性', color: '#FF6B9D' },
+        { key: 'beneficialBacteriaScore', label: '有益菌', color: '#10B981' },
+        { key: 'harmfulBacteriaScore', label: '有害菌', color: '#C62828' },
+        { key: 'coreSpeciesScore', label: '核心菌属', color: '#4A9BD7' }
       ]
-      for (var i = 0; i < species.length; i++) {
-        var item = species[i]
-        var category = item.category || 'beneficial'
-        var groupIdx = 0
-        if (category === 'beneficial') groupIdx = 0
-        else if (category === 'harmful') groupIdx = 1
-        else groupIdx = 2
-        groups[groupIdx].items.push({
-          name: item.name || item.speciesName || '未知',
-          abundance: item.abundance || item.relativeAbundance || 0,
-          status: item.status || 'normal'
-        })
+      for (var i = 0; i < scoreFields.length; i++) {
+        var f = scoreFields[i]
+        if (r[f.key] != null) {
+          items.push({ value: r[f.key], label: f.label, color: f.color })
+        }
+      }
+      this.scoreItems = items
+    },
+    buildIndicators: function(data) {
+      var indicators = data.indicators || []
+      this.allIndicators = indicators
+      var gutCategories = ['肠道屏障与代谢物', '短链脂肪酸', '神经递质与激素', '抗生素风险评估']
+      this.gutIndicators = indicators.filter(function(item) {
+        return gutCategories.indexOf(item.category) >= 0
+      })
+      var seen = {}
+      var groups = []
+      for (var i = 0; i < indicators.length; i++) {
+        var item = indicators[i]
+        if (gutCategories.indexOf(item.category) >= 0) continue
+        if (!seen[item.category]) {
+          seen[item.category] = true
+          groups.push({ category: item.category, items: [] })
+        }
+        for (var g = 0; g < groups.length; g++) {
+          if (groups[g].category === item.category) {
+            groups[g].items.push(item)
+            break
+          }
+        }
       }
-      this.bacteriaGroups = groups
+      this.nutritionGroups = groups
     },
-    drawRings: function() {
+    toggleAccordion: function(key) {
+      this.accordionOpen[key] = !this.accordionOpen[key]
+    },
+    indicatorType: function(item) {
+      return INDICATOR_TYPE_MAP[item.category] || 'nutrient'
+    },
+    indStatusIndex: function(status) {
+      var idx = this.indStatuses.indexOf(status)
+      return idx >= 0 ? idx : 0
+    },
+    onIndStatusChange: function(e, item) {
+      item.status = this.indStatuses[e.detail.value]
+    },
+    showKnowledge: function(type, name) {
       var self = this
-      this.scoreRings.forEach(function(score, idx) {
-        var ctx = uni.createCanvasContext('ring_' + idx, self)
-        var size = 120
-        var cx = size / 2
-        var cy = size / 2
-        var radius = 40
-        var lineWidth = 10
-        var percent = Math.min(score.value / (score.max || 100), 1)
-
-        // 背景圆环
-        ctx.setStrokeStyle('#E8E8E8')
-        ctx.setLineWidth(lineWidth)
-        ctx.setLineCap('round')
-        ctx.beginPath()
-        ctx.arc(cx, cy, radius, 0, 2 * Math.PI)
-        ctx.stroke()
-
-        // 前景圆环
-        var colors = ['#5B9BD5', '#FF8C42', '#10B981', '#6366F1', '#FF6B9D']
-        ctx.setStrokeStyle(colors[idx % colors.length])
-        ctx.setLineWidth(lineWidth)
-        ctx.setLineCap('round')
-        ctx.beginPath()
-        ctx.arc(cx, cy, radius, -Math.PI / 2, -Math.PI / 2 + 2 * Math.PI * percent)
-        ctx.stroke()
-
-        ctx.draw()
+      queryKnowledgeBase(type, name).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.knowledgeData = res.data
+          self.knowledgeVisible = true
+        } else {
+          self.knowledgeData = { itemName: name }
+          self.knowledgeVisible = true
+        }
       })
     },
-    statusText: function(status) {
-      if (status === 'high') return '偏高'
-      if (status === 'low') return '偏低'
-      if (status === 'abnormal') return '异常'
-      return '正常'
+    closeKnowledge: function() {
+      this.knowledgeVisible = false
+      this.knowledgeData = {}
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.loadData()
     },
-    goToNutritionChat: function() {
-      if (!this.reportId) return
-      createReportSurvey(this.reportId, '').then(function(res) {
-        if (res && res.code === 200) {
-          uni.navigateTo({
-            url: '/pages/health/nutrition-chat?reportId=' + (res.data && res.data.surveyId ? res.data.surveyId : '')
-          })
+    saveEdit: function() {
+      var self = this
+      var payload = {
+        overallScore: this.report.overallScore,
+        gutHealthScore: this.report.gutHealthScore,
+        nutritionScore: this.report.nutritionScore,
+        indicators: this.allIndicators
+      }
+      editHealthReport(this.reportId, payload, 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' })
         }
-      }).catch(function() {
-        uni.navigateTo({
-          url: '/pages/health/nutrition-chat?reportId=' + this.reportId
-        })
       })
+    },
+    goToSpecies: function() {
+      uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId })
+    },
+    goToRisks: function() {
+      uni.navigateTo({ url: '/pages/health/gut-flora-risks-detail?reportId=' + this.reportId })
+    },
+    goToFoods: function() {
+      uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail?reportId=' + this.reportId })
+    },
+    goToIndicators: function() {
+      this.accordionOpen.nutri = true
     }
   }
 }
 </script>
 
 <style scoped>
-.page {
-  min-height: 100vh;
-  background-color: #F5F7FA;
-}
-.nav-bar {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: 88rpx;
-  padding-top: var(--status-bar-height, 0);
-  background-color: #FFFFFF;
-  position: relative;
-  border-bottom: 1rpx solid #f0f0f0;
-}
-.nav-back {
-  position: absolute;
-  left: 30rpx;
-  top: 50%;
-  transform: translateY(-50%);
-  padding: 10rpx;
-}
-.back-text { font-size: 28rpx; color: #333; }
-.nav-title { font-size: 32rpx; font-weight: 600; color: #333; }
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.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; }
+.content { height: calc(100vh - 100rpx); }
 
-.content {
-  flex: 1;
-  padding: 30rpx;
-}
+.report-header { background: linear-gradient(135deg, #4A9BD7, #6366F1); padding: 40rpx 30rpx; color: #fff; }
+.report-title { font-size: 36rpx; font-weight: 700; display: block; margin-bottom: 16rpx; }
+.report-meta { display: flex; gap: 20rpx; margin-bottom: 6rpx; }
+.meta-item { font-size: 24rpx; opacity: 0.9; }
+.badge-row { display: flex; gap: 16rpx; margin-top: 12rpx; }
+.badge { padding: 6rpx 20rpx; background: rgba(255,255,255,0.2); border-radius: 20rpx; font-size: 22rpx; }
 
-.report-header {
-  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
-  border-radius: 20rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-}
-.report-title { font-size: 34rpx; font-weight: bold; color: #fff; display: block; margin-bottom: 12rpx; }
-.report-date { font-size: 24rpx; color: rgba(255,255,255,0.85); display: block; margin-bottom: 6rpx; }
-.report-number { font-size: 22rpx; color: rgba(255,255,255,0.7); display: block; }
+.scores-scroll { padding: 20rpx 0; white-space: nowrap; background: #fff; }
+.score-item { display: inline-flex; flex-direction: column; align-items: center; margin: 0 20rpx; }
+.score-ring { width: 100rpx; height: 100rpx; border-radius: 50%; border: 6rpx solid; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 700; margin-bottom: 8rpx; }
+.score-label { font-size: 22rpx; color: #666; }
 
-.scores-section {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-around;
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx 20rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
-}
-.score-ring {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  min-width: 120rpx;
-}
-.ring-wrapper {
-  position: relative;
-  width: 120rpx;
-  height: 120rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.ring-canvas {
-  position: absolute;
-  top: 0;
-  left: 0;
-}
-.ring-value {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #333;
-  z-index: 1;
-}
-.ring-label {
-  font-size: 22rpx;
-  color: #666;
-  margin-top: 10rpx;
-}
+.quick-nav { display: flex; padding: 20rpx; background: #fff; margin-bottom: 16rpx; }
+.nav-card { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 16rpx 0; }
+.nav-icon { font-size: 40rpx; margin-bottom: 6rpx; }
+.nav-label { font-size: 24rpx; color: #333; }
+.nav-count { font-size: 20rpx; color: #999; }
 
-.section-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
-}
-.section-title {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 24rpx;
-  padding-bottom: 16rpx;
-  border-bottom: 2rpx solid #f0f0f0;
-}
-.section-count { font-size: 24rpx; color: #999; font-weight: normal; }
+.accordion-section { background: #fff; margin-bottom: 16rpx; }
+.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-arrow { font-size: 24rpx; color: #999; }
+.accordion-body { padding: 10rpx 30rpx 20rpx; }
 
-.bacteria-item {
-  margin-bottom: 20rpx;
-}
-.bacteria-header {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 8rpx;
-}
-.bacteria-name { font-size: 26rpx; color: #333; font-weight: 500; }
-.bacteria-status { font-size: 22rpx; padding: 2rpx 12rpx; border-radius: 6rpx; }
-.status-normal { background: #E8F5E9; color: #2E7D32; }
-.status-high { background: #FFEBEE; color: #C62828; }
-.status-low { background: #FFF3E0; color: #E65100; }
-.status-abnormal { background: #FFF3E0; color: #E65100; }
+.sub-group { margin-bottom: 16rpx; }
+.sub-group-title { font-size: 26rpx; color: #4A9BD7; font-weight: 600; display: block; margin: 12rpx 0 8rpx; }
 
-.bar-track {
-  height: 12rpx;
-  background: #F0F0F0;
-  border-radius: 6rpx;
-  overflow: hidden;
-}
-.bar-fill {
-  height: 100%;
-  background: linear-gradient(90deg, #5B9BD5, #3A7CC4);
-  border-radius: 6rpx;
-}
-.bar-label { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
-.empty-hint { font-size: 26rpx; color: #999; text-align: center; padding: 40rpx 0; }
+.indicator-card { padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.indicator-row { display: flex; align-items: center; justify-content: space-between; }
+.indicator-name { font-size: 28rpx; color: #333; flex: 1; }
+.arrow-up { color: #C62828; font-size: 22rpx; }
+.arrow-down { color: #E65100; font-size: 22rpx; }
+.arrow-warn { color: #F59E0B; font-size: 22rpx; }
 
-.food-item {
-  display: flex;
-  flex-direction: column;
-  padding: 16rpx 0;
-  border-bottom: 1rpx solid #f5f5f5;
-}
-.food-item:last-child { border-bottom: none; }
-.food-name { font-size: 26rpx; color: #333; font-weight: 500; }
-.food-category { font-size: 22rpx; color: #5B9BD5; margin-top: 4rpx; }
-.food-reason { font-size: 22rpx; color: #666; margin-top: 4rpx; }
+.val-text { font-size: 28rpx; font-weight: 600; color: #333; }
+.val-偏高 { color: #C62828; }
+.val-过多 { color: #C62828; }
+.val-偏低 { color: #E65100; }
+.val-缺乏 { color: #E65100; }
+.val-不足 { color: #E65100; }
+.val-异常 { color: #C62828; }
+.val-注意 { color: #F59E0B; }
 
-.bottom-spacer { height: 40rpx; }
+.indicator-range { font-size: 22rpx; color: #bbb; display: block; margin-top: 4rpx; }
 
-.fab {
-  position: fixed;
-  right: 30rpx;
-  bottom: 100rpx;
-  width: 100rpx;
-  height: 100rpx;
-  border-radius: 50%;
-  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow: 0 4rpx 20rpx rgba(91, 155, 213, 0.4);
-  z-index: 100;
-}
-.fab-icon { font-size: 44rpx; }
+.indicator-edit { display: flex; gap: 8rpx; align-items: center; }
+.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 26rpx; width: 100rpx; }
+.edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 24rpx; color: #333; }
+
+.fab { position: fixed; right: 30rpx; bottom: 40rpx; width: 100rpx; height: 100rpx; background: #4A9BD7; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(74,155,215,0.4); }
+.fab-icon { font-size: 40rpx; color: #fff; }
+
+.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>

+ 169 - 0
cfc-frontend/pages/health/gut-flora-foods-detail.vue

@@ -0,0 +1,169 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">饮食推荐</text>
+      <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+    </view>
+
+    <scroll-view class="filter-bar" scroll-x>
+      <view class="filter-item" v-for="(cat, idx) in categories" :key="idx"
+            :class="'filter-' + (selectedCategory === cat ? 'active' : '')"
+            @tap="selectedCategory = cat">
+        <text>{{ cat }}</text>
+      </view>
+    </scroll-view>
+
+    <view class="sort-bar">
+      <text class="sort-text" @tap="sortDesc = !sortDesc">排序: {{ sortDesc ? '推荐↓' : '推荐↑' }}</text>
+      <text class="sort-text" @tap="showRecommendedOnly = !showRecommendedOnly">{{ showRecommendedOnly ? '全部' : '仅推荐' }}</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <view class="food-card" v-for="(item, idx) in filteredFoods" :key="idx">
+        <view class="food-header">
+          <text class="food-name">{{ item.foodName || item.name }}</text>
+          <view class="food-cat-tag">
+            <text>{{ item.category }}</text>
+          </view>
+        </view>
+        <view class="food-detail-row" v-if="!isEditing">
+          <text class="food-nutrition">蛋白{{ item.protein || '--' }} | 脂肪{{ item.fat || '--' }} | 碳水{{ item.carb || '--' }} | 纤维{{ item.fiber || '--' }}</text>
+        </view>
+        <view class="food-detail-row" v-if="!isEditing && item.energy">
+          <text class="food-nutrition">能量 {{ item.energy }}KJ</text>
+        </view>
+        <view class="food-edit-row" v-if="isEditing">
+          <text class="edit-label">推荐指数:</text>
+          <input class="edit-input" type="number" v-model="item.score" />
+        </view>
+        <view class="food-score" :class="(item.score || 0) >= 5 ? 'score-good' : 'score-normal'">
+          <text>推荐: {{ item.score }}</text>
+        </view>
+      </view>
+      <view class="empty-hint" v-if="filteredFoods.length === 0">暂无数据</view>
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+    <view class="bottom-bar" v-if="isEditing">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getReportDetail, editHealthReport } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      reportId: null,
+      reportData: null,
+      foods: [],
+      isEditing: false,
+      editFoods: [],
+      selectedCategory: '全部',
+      sortDesc: true,
+      showRecommendedOnly: false,
+      categories: ['全部', '主食', '蔬菜', '水果', '肉类', '其它']
+    }
+  },
+  computed: {
+    filteredFoods: function() {
+      var list = this.isEditing ? this.editFoods : this.foods
+      if (this.selectedCategory !== '全部') {
+        list = list.filter(function(item) {
+          return (item.category || '') === this.selectedCategory
+        }.bind(this))
+      }
+      if (this.showRecommendedOnly) {
+        list = list.filter(function(item) {
+          return (item.score || 0) >= 5
+        })
+      }
+      list = [].concat(list)
+      list.sort(function(a, b) {
+        var sa = a.score || 0
+        var sb = b.score || 0
+        return this.sortDesc ? sb - sa : sa - sb
+      }.bind(this))
+      return list
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) {
+      this.loadData()
+    }
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadData: function() {
+      var self = this
+      getReportDetail(this.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.foods = res.data.foods || []
+        }
+      })
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+      this.editFoods = JSON.parse(JSON.stringify(this.foods))
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.editFoods = []
+    },
+    saveEdit: function() {
+      var self = this
+      editHealthReport(self.reportId, { foods: self.editFoods }, 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' })
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.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 { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 0; }
+.filter-bar { background: #fff; padding: 10rpx 20rpx; white-space: nowrap; }
+.filter-item { display: inline-block; padding: 12rpx 28rpx; margin: 0 10rpx; border-radius: 30rpx; font-size: 26rpx; background: #f0f0f0; color: #666; }
+.filter-active { background: #4A9BD7; color: #fff; }
+.sort-bar { display: flex; justify-content: space-between; padding: 16rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; }
+.sort-text { font-size: 24rpx; color: #999; }
+.content { padding: 20rpx 30rpx; height: calc(100vh - 200rpx); }
+.food-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
+.food-header { display: flex; align-items: center; margin-bottom: 12rpx; }
+.food-name { font-size: 30rpx; font-weight: 600; color: #333; flex: 1; }
+.food-cat-tag { padding: 4rpx 16rpx; background: #f0f0f0; border-radius: 20rpx; font-size: 22rpx; color: #666; }
+.food-detail-row { margin-bottom: 6rpx; }
+.food-nutrition { font-size: 24rpx; color: #999; }
+.food-edit-row { display: flex; align-items: center; margin-bottom: 6rpx; }
+.edit-label { font-size: 26rpx; color: #666; margin-right: 12rpx; }
+.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 120rpx; }
+.food-score { margin-top: 8rpx; font-size: 24rpx; }
+.score-good { color: #10B981; }
+.score-normal { color: #999; }
+.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; }
+.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>

+ 204 - 0
cfc-frontend/pages/health/gut-flora-risks-detail.vue

@@ -0,0 +1,204 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">疾病风险</text>
+      <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <view class="section-label">重要风险</view>
+      <view class="risk-card" v-for="(item, idx) in importantRisks" :key="'imp-' + idx"
+            :class="'risk-level-' + (item.riskLevel || 'low')">
+        <view class="risk-header">
+          <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+          <text class="risk-badge" :class="'badge-' + (item.riskLevel || 'low')">
+            {{ riskBadgeText(item.riskLevel) }}
+          </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>
+        </view>
+        <view class="risk-edit-row" v-if="isEditing">
+          <text class="edit-label">风险值:</text>
+          <input class="edit-input" type="text" v-model="item.riskValue" />
+          <text class="edit-label">等级:</text>
+          <picker :value="riskLevelIndex(item.riskLevel)" :range="riskLevels" @change="onRiskLevelChange($event, item)">
+            <text class="edit-picker">{{ item.riskLevel || '请选择' }}</text>
+          </picker>
+        </view>
+      </view>
+
+      <view class="section-label">其它风险</view>
+      <view class="risk-card" v-for="(item, idx) in normalRisks" :key="'norm-' + idx"
+            :class="'risk-level-' + (item.riskLevel || 'low')">
+        <view class="risk-header">
+          <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+          <text class="risk-badge" :class="'badge-' + (item.riskLevel || 'low')">
+            {{ riskBadgeText(item.riskLevel) }}
+          </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>
+        </view>
+        <view class="risk-edit-row" v-if="isEditing">
+          <text class="edit-label">风险值:</text>
+          <input class="edit-input" type="text" v-model="item.riskValue" />
+          <text class="edit-label">等级:</text>
+          <picker :value="riskLevelIndex(item.riskLevel)" :range="riskLevels" @change="onRiskLevelChange($event, item)">
+            <text class="edit-picker">{{ item.riskLevel || '请选择' }}</text>
+          </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">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getReportDetail, editHealthReport, queryKnowledgeBase } from '../../utils/api.js'
+import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+
+export default {
+  components: { HealthKnowledgePopup },
+  data() {
+    return {
+      reportId: null,
+      reportData: null,
+      risks: [],
+      isEditing: false,
+      editRisks: [],
+      riskLevels: ['低风险', '需注意', '高风险', '异常'],
+      knowledgeVisible: false,
+      knowledgeData: {}
+    }
+  },
+  computed: {
+    importantRisks: function() {
+      return this.getRisks().filter(function(item) {
+        var level = item.riskLevel || ''
+        return level === '需注意' || level === '高风险' || level === '异常'
+      })
+    },
+    normalRisks: function() {
+      return this.getRisks().filter(function(item) {
+        var level = item.riskLevel || ''
+        return level !== '需注意' && level !== '高风险' && level !== '异常'
+      })
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) {
+      this.loadData()
+    }
+  },
+  methods: {
+    goBack: function() { uni.navigateBack() },
+    getRisks: function() {
+      return this.isEditing ? this.editRisks : this.risks
+    },
+    loadData: function() {
+      var self = this
+      getReportDetail(this.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.risks = res.data.diseaseRisks || []
+        }
+      })
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+      this.editRisks = JSON.parse(JSON.stringify(this.risks))
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.editRisks = []
+    },
+    saveEdit: function() {
+      var self = this
+      editHealthReport(self.reportId, { diseaseRisks: self.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' })
+        }
+      })
+    },
+    riskBadgeText: function(level) {
+      var map = { '低风险': '✓ 低', '需注意': '⚠ 注意', '高风险': '⚠ 高', '异常': '⚠ 异常' }
+      return map[level] || level || ''
+    },
+    riskLevelIndex: function(level) {
+      var idx = this.riskLevels.indexOf(level)
+      return idx >= 0 ? idx : 0
+    },
+    onRiskLevelChange: function(e, item) {
+      item.riskLevel = this.riskLevels[e.detail.value]
+    },
+    showKnowledge: function(type, name) {
+      var self = this
+      queryKnowledgeBase(type, name).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.knowledgeData = res.data
+          self.knowledgeVisible = true
+        } else {
+          self.knowledgeData = { itemName: name }
+          self.knowledgeVisible = true
+        }
+      })
+    },
+    closeKnowledge: function() {
+      this.knowledgeVisible = false
+      this.knowledgeData = {}
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.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 { 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; }
+.risk-level-需注意 { border-left-color: #F59E0B; }
+.risk-level-高风险 { border-left-color: #C62828; }
+.risk-level-异常 { 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; }
+.badge-低风险 { background: #E8F5E9; color: #2E7D32; }
+.badge-需注意 { background: #FFF3E0; color: #E65100; }
+.badge-高风险 { background: #FFEBEE; color: #C62828; }
+.badge-异常 { 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; }
+.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; }
+.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>

+ 201 - 0
cfc-frontend/pages/health/gut-flora-species-detail.vue

@@ -0,0 +1,201 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">菌群详情</text>
+      <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+    </view>
+
+    <scroll-view class="tabs" scroll-x>
+      <view class="tab-item" v-for="(tab, idx) in tabs" :key="idx"
+            :class="'tab-' + (selectedTab === tab.key ? 'active' : '')"
+            @tap="selectedTab = tab.key">
+        <text>{{ tab.label }}</text>
+      </view>
+    </scroll-view>
+
+    <scroll-view class="content" scroll-y>
+      <view class="flora-card" v-for="(item, idx) in filteredFlora" :key="idx">
+        <view class="flora-header">
+          <text class="flora-name" :class="'status-' + (item.status || '正常')"
+                @tap="showKnowledge('bacteria', item.bacteriaName)">
+            {{ item.bacteriaName }}
+            <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
+            <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
+          </text>
+          <text class="flora-level-tag" v-if="item.level && item.level !== 'GENUS'">{{ item.level }}</text>
+        </view>
+        <view class="flora-details" v-if="!isEditing">
+          <text class="detail-text">丰度 {{ item.bacteriaValue || '--' }} | 正常 {{ item.normalRange || '--' }}</text>
+          <text class="detail-text">人群 {{ item.populationLevel || '--' }} | 检出 {{ item.detectionRate || '--' }}</text>
+        </view>
+        <view class="flora-edit-row" v-if="isEditing">
+          <text class="edit-label">丰度:</text>
+          <input class="edit-input" type="text" v-model="item.bacteriaValue" />
+          <text class="edit-label">状态:</text>
+          <picker :value="floraStatusIndex(item.status)" :range="floraStatuses" @change="onFloraStatusChange($event, item)">
+            <text class="edit-picker">{{ item.status || '正常' }}</text>
+          </picker>
+        </view>
+      </view>
+      <view class="empty-hint" v-if="filteredFlora.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">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getReportDetail, editHealthReport, queryKnowledgeBase } from '../../utils/api.js'
+import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+
+export default {
+  components: { HealthKnowledgePopup },
+  data() {
+    return {
+      reportId: null,
+      reportData: null,
+      gutFlora: [],
+      probioticSpecies: [],
+      isEditing: false,
+      editFlora: [],
+      selectedTab: 'core',
+      floraStatuses: ['正常', '偏低', '偏高', '缺乏', '不足', '过多'],
+      knowledgeVisible: false,
+      knowledgeData: {},
+      tabs: [
+        { key: 'core', label: '核心菌属' },
+        { key: 'probiotic', label: '益生菌' },
+        { key: 'harmful', label: '有害菌' },
+        { key: 'other', label: '其它菌属' },
+        { key: 'pathogen', label: '病原菌' },
+        { key: 'obesity', label: '肥胖相关' },
+        { key: 'constipation', label: '便秘相关' },
+        { key: 'depression', label: '抑郁相关' },
+        { key: 'allergy', label: '过敏相关' },
+        { key: 'bloating', label: '腹胀相关' },
+        { key: 'insomnia', label: '失眠相关' },
+        { key: 'all', label: '全部' }
+      ]
+    }
+  },
+  computed: {
+    filteredFlora: function() {
+      var list = this.isEditing ? this.editFlora : this.gutFlora
+      if (this.selectedTab === 'all') return list
+      if (this.selectedTab === 'probiotic') {
+        return this.isEditing
+          ? this.editFlora.filter(function(i) { return i.category === 'probiotic' })
+          : this.probioticSpecies
+      }
+      return list.filter(function(item) {
+        return item.category === this.selectedTab
+      }.bind(this))
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) {
+      this.loadData()
+    }
+  },
+  methods: {
+    goBack: function() { uni.navigateBack() },
+    loadData: function() {
+      var self = this
+      getReportDetail(this.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.gutFlora = res.data.gutFlora || []
+          self.probioticSpecies = res.data.probioticSpecies || []
+        }
+      })
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+      this.editFlora = JSON.parse(JSON.stringify(this.gutFlora))
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.editFlora = []
+    },
+    saveEdit: function() {
+      var self = this
+      editHealthReport(self.reportId, { gutFlora: self.editFlora }, 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' })
+        }
+      })
+    },
+    floraStatusIndex: function(status) {
+      var idx = this.floraStatuses.indexOf(status)
+      return idx >= 0 ? idx : 0
+    },
+    onFloraStatusChange: function(e, item) {
+      item.status = this.floraStatuses[e.detail.value]
+    },
+    showKnowledge: function(type, name) {
+      var self = this
+      queryKnowledgeBase(type, name).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.knowledgeData = res.data
+          self.knowledgeVisible = true
+        } else {
+          self.knowledgeData = { itemName: name }
+          self.knowledgeVisible = true
+        }
+      })
+    },
+    closeKnowledge: function() {
+      this.knowledgeVisible = false
+      this.knowledgeData = {}
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.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 { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 0; }
+.tabs { background: #fff; padding: 10rpx 20rpx; white-space: nowrap; border-bottom: 1rpx solid #eee; }
+.tab-item { display: inline-block; padding: 12rpx 24rpx; margin: 0 6rpx; border-radius: 30rpx; font-size: 24rpx; background: #f0f0f0; color: #666; }
+.tab-active { background: #4A9BD7; color: #fff; }
+.content { padding: 20rpx 30rpx; height: calc(100vh - 180rpx); }
+.flora-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
+.flora-header { display: flex; align-items: center; margin-bottom: 10rpx; }
+.flora-name { font-size: 28rpx; font-weight: 600; color: #333; flex: 1; }
+.flora-name.status-偏高 { color: #C62828; }
+.flora-name.status-过多 { color: #C62828; }
+.flora-name.status-偏低 { color: #E65100; }
+.flora-name.status-缺乏 { color: #E65100; }
+.flora-name.status-不足 { color: #E65100; }
+.arrow-up { color: #C62828; font-size: 22rpx; }
+.arrow-down { color: #E65100; font-size: 22rpx; }
+.flora-level-tag { padding: 2rpx 12rpx; background: #E3F2FD; border-radius: 16rpx; font-size: 20rpx; color: #4A9BD7; }
+.detail-text { font-size: 24rpx; color: #999; display: block; line-height: 1.8; }
+.flora-edit-row { display: flex; align-items: center; gap: 12rpx; flex-wrap: wrap; }
+.edit-label { font-size: 26rpx; color: #666; }
+.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 140rpx; }
+.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; }
+.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>

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-0ca63ac87e84ca9ef17c074fd95d98e7ee41c531
+c985f71ac5f46b3431b59eca1437c0be79cd83b3

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.384",
+  "version": "1.0.385",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.384",
+      "version": "1.0.385",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.385",
+  "version": "1.0.386",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 12 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,18 @@
 
 ---
 
+## v1.0.386 (2026-07-17)
+
+### 新功能
+- add api methods, knowledge-popup component, register 4 pages in pages.json
+- add KnowledgeBaseDataImporter + ref JSONs + import endpoint
+- add POST /api/health/report/edit + updateReportFromPayload
+- add health_knowledge_base (entity+mapper+service+controller+migration98)
+
+### Bug 修复
+- 修复管理端多处模板标签闭合并将本地开发接口指向 127.0.0.1:9082 代理
+
+
 ## v1.0.385 (2026-07-17)
 
 ### Bug 修复

+ 13 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.385
+> 当前版本: v1.0.386
 
 ## 历史版本
 
@@ -8,6 +8,18 @@
 
 ---
 
+## v1.0.386 (2026-07-17)
+
+### 新功能
+- add api methods, knowledge-popup component, register 4 pages in pages.json
+- add KnowledgeBaseDataImporter + ref JSONs + import endpoint
+- add POST /api/health/report/edit + updateReportFromPayload
+- add health_knowledge_base (entity+mapper+service+controller+migration98)
+
+### Bug 修复
+- 修复管理端多处模板标签闭合并将本地开发接口指向 127.0.0.1:9082 代理
+
+
 ## v1.0.385 (2026-07-17)
 
 ### Bug 修复