Prechádzať zdrojové kódy

feat(报告): 优化谷禾菌群报告详情页展示与饮食推荐

- 营养指标分组提升为一级手风琴(主要营养评估/氨基酸评估等)
- 疾病风险内联展示并颜色区分(注意/低风险),修复详情页与风险页统计不一致
- 疾病风险页知识库入口改为 ? 图标放疾病名之前
- 菌群详情页修复分类 tab 过滤(中文 category 映射英文 key)
- 菌群风险分组(肥胖/抑郁/过敏等)改手风琴展示
- 饮食推荐页:去重标题、按推荐值分级背景色(<-30红/>30绿/每10一档)、去蛋白能量固定信息、增加食材搜索、复用知识库弹窗
- 成长页饮食推荐复用食材页,不传 reportId 取最新建议
iwt 2 mesiacov pred
rodič
commit
ee1bf9db1f

+ 1 - 1
cfc-frontend/pages/growth-main/index.vue

@@ -309,7 +309,7 @@ export default {
     goToTasks: function() { uni.navigateTo({ url: '/pages/child/tasks' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
     goToCaution: function() { uni.navigateTo({ url: '/pages/growth/caution-detail' }) },
-    goToFoods: function() { uni.navigateTo({ url: '/pages/diet/index' }) },
+    goToFoods: function() { uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail' }) },
     goToArticle: function(article) { var id = article && article.id ? article.id : article; if (!id) return; uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id }) },
     goToProduct: function(prod) { var id = prod && prod.id ? prod.id : prod; if (!id) return; uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id }) },
     goMoreProducts: function() { uni.navigateTo({ url: '/pages/shop/index/index' }) },

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

@@ -106,15 +106,27 @@
           <text class="func-dim-summary">{{ analysisData.dietaryPattern.detail }}</text>
         </view>
 
-        <!-- 风险提示 -->
-        <view class="analysis-card">
-          <text class="card-label">风险提示</text>
-          <view class="risk-item" v-for="(risk, ri) in analysisData.riskIndicators" :key="ri">
-            <text class="risk-name">{{ risk.name }}</text>
-            <text :class="'risk-level risk-' + statusKey(risk.level)">{{ risk.level }}</text>
-            <text class="risk-bacteria" v-if="risk.relatedBacteria && risk.relatedBacteria.length > 0">
-              {{ risk.relatedBacteria.join('、') }}
-            </text>
+        <!-- 菌群风险分组手风琴(肥胖/抑郁/过敏等) -->
+        <view class="accordion-section" v-if="analysisData && analysisData.riskIndicators && analysisData.riskIndicators.length > 0">
+          <view class="accordion-header" @tap="toggleAccordion('riskGroup')">
+            <text class="accordion-title">菌群风险分组</text>
+            <text class="accordion-stats">共 {{ analysisData.riskIndicators.length }} 组</text>
+            <text class="accordion-arrow">{{ accordionOpen.riskGroup ? '收起' : '展开' }}</text>
+          </view>
+          <view class="accordion-body" v-if="accordionOpen.riskGroup">
+            <view class="risk-group-item" v-for="(risk, ri) in analysisData.riskIndicators" :key="ri">
+              <view class="risk-group-header" @tap="toggleRiskGroup(ri)">
+                <text class="risk-group-name">{{ risk.name }}</text>
+                <text :class="'risk-level risk-' + statusKey(risk.level)">{{ risk.level }}</text>
+                <text class="risk-group-arrow">{{ riskGroupOpen[ri] ? '收起' : '展开' }}</text>
+              </view>
+              <view class="risk-group-body" v-if="riskGroupOpen[ri]">
+                <text class="risk-bacteria" v-if="risk.relatedBacteria && risk.relatedBacteria.length > 0">
+                  相关菌群:{{ risk.relatedBacteria.join('、') }}
+                </text>
+                <text class="risk-bacteria" v-else>暂无异常相关菌群</text>
+              </view>
+            </view>
           </view>
         </view>
 
@@ -164,26 +176,25 @@
         </view>
       </view>
 
-      <!-- 营养指标手风琴 -->
-      <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">异常 <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">
-          <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="ind-help-btn" @tap.stop="showKnowledge(indicatorType(item), item.indicatorName)">?</text>
-                  <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">
+      <!-- 营养分组手风琴(每个分组独立一级展示) -->
+      <view class="nutrition-group-section" v-for="(group, gIdx) in nutritionGroups" :key="gIdx">
+        <view class="accordion-section">
+          <view class="accordion-header" @tap="toggleNutriGroup(gIdx)">
+            <text class="accordion-title">{{ group.category }}</text>
+            <text class="accordion-stats" v-if="group.abnormalCount > 0">异常 <text class="abnormal-num">{{ group.abnormalCount }}</text> 项</text>
+            <text class="accordion-arrow">{{ nutriGroupOpen[gIdx] ? '收起' : '展开' }}</text>
+          </view>
+          <view class="accordion-body" v-if="nutriGroupOpen[gIdx]">
+            <view class="indicator-card" v-for="(item, idx) in group.items" :key="idx">
+              <view class="indicator-row">
+                <text class="ind-help-btn" @tap.stop="showKnowledge(indicatorType(item), item.indicatorName)">?</text>
+                <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._cssClass || 'normal')">{{ item.indicatorValue || '--' }}</text>
                   <text class="indicator-unit" v-if="item.unit">{{ item.unit }}</text>
                 </view>
@@ -194,11 +205,31 @@
                   </picker>
                 </view>
               </view>
-<text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
-            <text class="indicator-symptoms" v-if="item.symptoms">症状: {{ item.symptoms }}</text>
+              <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
+              <text class="indicator-symptoms" v-if="item.symptoms">症状: {{ item.symptoms }}</text>
             </view>
+            <view class="empty-hint" v-if="group.items.length === 0">暂无数据</view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 疾病风险内联展示(颜色区分) -->
+      <view class="accordion-section" v-if="diseaseRisks.length > 0">
+        <view class="accordion-header" @tap="toggleAccordion('risks')">
+          <text class="accordion-title">疾病风险</text>
+          <text class="accordion-stats" v-if="diseaseRisksAbnormal > 0">需关注 <text class="abnormal-num">{{ diseaseRisksAbnormal }}</text> 项</text>
+          <text class="accordion-arrow">{{ accordionOpen.risks ? '收起' : '展开' }}</text>
+        </view>
+        <view class="accordion-body" v-if="accordionOpen.risks">
+          <view class="risk-card-inline" v-for="(risk, ri) in diseaseRisks" :key="ri"
+                :class="'risk-level-' + (risk._cssClass || 'low')">
+            <text class="risk-name-inline">{{ risk.diseaseName }}</text>
+            <text class="risk-badge-inline" :class="'badge-' + (risk._cssClass || 'low')">
+              {{ risk.riskLevel || '--' }}
+            </text>
+            <text class="risk-value-inline" v-if="risk.riskValue">风险值 {{ risk.riskValue }}</text>
           </view>
-          <view class="empty-hint" v-if="nutritionGroups.length === 0">暂无数据</view>
+          <view class="empty-hint" v-if="diseaseRisks.length === 0">暂无疾病风险数据</view>
         </view>
       </view>
 
@@ -264,11 +295,14 @@ export default {
       report: {},
       reportInterpretation: null,
       isEditing: false,
-      accordionOpen: { gut: false, nutri: false },
+      accordionOpen: { gut: false, risks: false, riskGroup: false },
+      riskGroupOpen: [],
+      nutriGroupOpen: [],
       scoreItems: [],
       allIndicators: [],
       gutIndicators: [],
       nutritionGroups: [],
+      diseaseRisks: [],
       floraCount: 0,
       floraAbnormalCount: 0,
       riskCount: 0,
@@ -314,6 +348,15 @@ export default {
         if (this.isAbnormalStatus(item.status)) abnormal++
       }
       return { total: list.length, valid: valid, abnormal: abnormal }
+    },
+    diseaseRisksAbnormal: function() {
+      var list = this.diseaseRisks || []
+      var count = 0
+      for (var i = 0; i < list.length; i++) {
+        var level = list[i].riskLevel || ''
+        if (level !== '低风险') count++
+      }
+      return count
     }
   },
   onLoad: function(options) {
@@ -372,8 +415,12 @@ export default {
             return self.isAbnormalStatus(i.status)
           }).length
           self.riskCount = (res.data.diseaseRisks || []).length
-          // 疾病风险:统计需关注的数量(风险等级非"低风险"即视为需关注)
+          // 疾病风险:内联展示 + 颜色区分,统计需关注的数量(风险等级非"低风险"即视为需关注)
           var risks = res.data.diseaseRisks || []
+          self.diseaseRisks = risks.map(function(r) {
+            r._cssClass = self._cssForRiskLevel(r.riskLevel)
+            return r
+          })
           self.riskAttentionCount = risks.filter(function(r) {
             return r.riskLevel && r.riskLevel !== '低风险'
           }).length
@@ -541,18 +588,40 @@ report.gutType = s.gutType || ''
           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
-          }
+      for (var g = 0; g < groups.length; g++) {
+        if (groups[g].category === item.category) {
+          groups[g].items.push(item)
+          break
+        }
+      }
+      }
+      for (var gi = 0; gi < groups.length; gi++) {
+        var abnormal = 0
+        for (var ii = 0; ii < groups[gi].items.length; ii++) {
+          if (this.isAbnormalStatus(groups[gi].items[ii].status)) abnormal++
         }
+        groups[gi].abnormalCount = abnormal
       }
       this.nutritionGroups = groups
     },
     toggleAccordion: function(key) {
       this.accordionOpen[key] = !this.accordionOpen[key]
     },
+    toggleNutriGroup: function(idx) {
+      var arr = this.nutriGroupOpen
+      arr[idx] = !arr[idx]
+      this.nutriGroupOpen = [].concat(arr)
+    },
+    toggleRiskGroup: function(idx) {
+      var arr = this.riskGroupOpen
+      arr[idx] = !arr[idx]
+      this.riskGroupOpen = [].concat(arr)
+    },
+    /** 中文风险等级 → CSS安全英文 */
+    _cssForRiskLevel: function(level) {
+      var map = { '低风险': 'low', '注意': 'warning', '需注意': 'warning', '高风险': 'high', '异常': 'abnormal' }
+      return map[level] || 'low'
+    },
     indicatorType: function(item) {
       return INDICATOR_TYPE_MAP[item.category] || 'nutrient'
     },
@@ -719,7 +788,11 @@ report.gutType = s.gutType || ''
       uni.navigateTo({ url: '/pages/growth/caution-detail' })
     },
     goToIndicators: function() {
-      this.accordionOpen.nutri = true
+      // 展开所有营养分组手风琴
+      var arr = []
+      for (var i = 0; i < this.nutritionGroups.length; i++) arr.push(true)
+      this.nutriGroupOpen = arr
+      if (this.nutritionGroups.length === 0) uni.showToast({ title: '暂无营养指标', icon: 'none' })
     },
     // 问答功能已屏蔽
     goAIChat: function() {}
@@ -763,6 +836,38 @@ report.gutType = s.gutType || ''
 .accordion-arrow { font-size: 24rpx; color: #999; }
 .accordion-body { padding: 10rpx 30rpx 20rpx; }
 
+/* 疾病风险内联卡片 */
+.risk-card-inline {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 16rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+  border-left: 8rpx solid #10B981;
+  margin-bottom: 8rpx;
+}
+.risk-card-inline.risk-level-warning { border-left-color: #F59E0B; }
+.risk-card-inline.risk-level-high { border-left-color: #EF4444; }
+.risk-card-inline.risk-level-abnormal { border-left-color: #C62828; }
+.risk-name-inline { font-size: 28rpx; color: #333; flex: 1; }
+.risk-badge-inline { padding: 4rpx 16rpx; border-radius: 20rpx; font-size: 22rpx; flex-shrink: 0; }
+.risk-badge-inline.badge-low { background: #E8F5E9; color: #2E7D32; }
+.risk-badge-inline.badge-warning { background: #FFF3E0; color: #E65100; }
+.risk-badge-inline.badge-high { background: #FFEBEE; color: #C62828; }
+.risk-badge-inline.badge-abnormal { background: #C62828; color: #fff; }
+.risk-value-inline { font-size: 22rpx; color: #999; margin-left: 16rpx; }
+
+/* 菌群风险分组手风琴 */
+.risk-group-item { padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.risk-group-header { display: flex; align-items: center; gap: 12rpx; padding: 8rpx 0; }
+.risk-group-name { font-size: 28rpx; color: #333; flex: 1; }
+.risk-group-arrow { font-size: 24rpx; color: #999; }
+.risk-group-body { padding: 8rpx 0 12rpx; }
+.risk-level { font-size: 22rpx; padding: 4rpx 14rpx; border-radius: 8rpx; }
+.risk-level.risk-good { background: #E8F5E9; color: #2E7D32; }
+.risk-level.risk-warning { background: #FFF3E0; color: #E65100; }
+.risk-level.risk-danger { background: #FFEBEE; color: #C62828; }
+.risk-bacteria { font-size: 24rpx; color: #666; line-height: 1.6; }
+
 .sub-group { margin-bottom: 16rpx; }
 .sub-group-title { font-size: 26rpx; color: #4A9BD7; font-weight: 600; display: block; margin: 12rpx 0 8rpx; }
 

+ 208 - 82
cfc-frontend/pages/health/gut-flora-foods-detail.vue

@@ -1,52 +1,83 @@
 <template>
   <view class="page">
-    <view class="nav-bar">
-      <view class="nav-back" @tap="goBack">
-        <text class="back-text">‹ 返回</text>
+    <!-- 搜索框 -->
+    <view class="search-bar">
+      <view class="search-input-wrap">
+        <text class="search-icon">🔍</text>
+        <input class="search-input" v-model="searchText" placeholder="搜索食材名称,查看推荐值" confirm-type="search" />
+        <text class="search-clear" v-if="searchText" @tap="searchText = ''">✕</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-card"
+        v-for="(item, idx) in filteredFoods"
+        :key="idx"
+        :class="'score-band-' + _scoreBand(item.score)"
+        @tap="showFoodKnowledge(item)"
+      >
         <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 class="food-score-row">
+          <text class="food-score-label">推荐值</text>
+          <text class="food-score-value">{{ item.score }}</text>
         </view>
         <view class="food-edit-row" v-if="isEditing">
-          <text class="edit-label">推荐指数:</text>
+          <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="knowledge-overlay" v-if="knowledgeVisible" @tap="closeKnowledge">
+      <view class="knowledge-popup" @tap.stop>
+        <view class="knowledge-header">
+          <text class="knowledge-title">{{ knowledgeData.itemName || '食材详情' }}</text>
+          <text class="knowledge-close" @tap="closeKnowledge">✕</text>
+        </view>
+        <view class="knowledge-body">
+          <view class="knowledge-row" v-if="knowledgeData.category">
+            <text class="knowledge-label">分类</text>
+            <text class="knowledge-value">{{ knowledgeData.category }}</text>
+          </view>
+          <view class="knowledge-row" v-if="knowledgeData.protein != null">
+            <text class="knowledge-label">蛋白</text>
+            <text class="knowledge-value">{{ knowledgeData.protein }}g</text>
+          </view>
+          <view class="knowledge-row" v-if="knowledgeData.fat != null">
+            <text class="knowledge-label">脂肪</text>
+            <text class="knowledge-value">{{ knowledgeData.fat }}g</text>
+          </view>
+          <view class="knowledge-row" v-if="knowledgeData.carbs != null">
+            <text class="knowledge-label">碳水</text>
+            <text class="knowledge-value">{{ knowledgeData.carbs }}g</text>
+          </view>
+          <view class="knowledge-row" v-if="knowledgeData.fiber != null">
+            <text class="knowledge-label">纤维</text>
+            <text class="knowledge-value">{{ knowledgeData.fiber }}g</text>
+          </view>
+          <view class="knowledge-row" v-if="knowledgeData.energy != null">
+            <text class="knowledge-label">能量</text>
+            <text class="knowledge-value">{{ knowledgeData.energy }}KJ</text>
+          </view>
+          <view class="knowledge-desc" v-if="knowledgeData.description">
+            <text class="knowledge-desc-text">{{ knowledgeData.description }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <view class="edit-fab" v-if="!isEditing && reportId" @tap="toggleEdit">
+      <text>编辑</text>
+    </view>
     <view class="bottom-bar" v-if="isEditing">
       <view class="btn-cancel" @tap="cancelEdit">取消</view>
       <view class="btn-save" @tap="saveEdit">保存</view>
@@ -55,7 +86,7 @@
 </template>
 
 <script>
-import { getReportDetail, editHealthReport, getHealthFoods } from '../../utils/api.js'
+import { getHealthFoods, editHealthReport, queryFoodKnowledge } from '../../utils/api.js'
 
 export default {
   data() {
@@ -65,66 +96,124 @@ export default {
       foods: [],
       isEditing: false,
       editFoods: [],
-      selectedCategory: '全部',
-      sortDesc: true,
-      showRecommendedOnly: false,
-      categories: ['全部', '主食', '蔬菜', '水果', '肉类', '其它']
+      searchText: '',
+      knowledgeVisible: false,
+      knowledgeData: {},
+      _kbCache: {}
     }
   },
   computed: {
     filteredFoods: function() {
       var list = this.isEditing ? this.editFoods : this.foods
-      if (this.selectedCategory !== '全部') {
+      var kw = (this.searchText || '').trim()
+      if (kw) {
         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
+          var name = item.foodName || item.name || ''
+          return name.indexOf(kw) >= 0
         })
       }
-      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) {
+    // 不传 reportId 时取当前用户最新报告的食材推荐
     this.reportId = options.reportId ? parseInt(options.reportId) : null
-    if (this.reportId) {
-      this.loadData()
-    }
+    this.loadData()
   },
   methods: {
     goBack: function() {
       uni.navigateBack()
     },
+    /** 推荐值 → 背景色分级:<-30 红,>30 绿,中间每 10 换一档 */
+    _scoreBand: function(score) {
+      var s = parseFloat(score)
+      if (isNaN(s)) return 'null'
+      if (s < -30) return 'neg3'
+      if (s < -20) return 'neg2'
+      if (s < -10) return 'neg1'
+      if (s < 0) return 'neg0'
+      if (s < 10) return 'pos0'
+      if (s < 20) return 'pos1'
+      if (s < 30) return 'pos2'
+      return 'pos3'
+    },
     loadData: function() {
       var self = this
-      getHealthFoods(this.reportId).then(function(res) {
+      getHealthFoods(self.reportId).then(function(res) {
         if (res.code === 200 && res.data) {
-          // 后端 Food 实体字段:name, category, score, protein, fat, carbs, fiber, energyKj, status
-          // 兼容 foodName(旧 draft)和 name(实体),carb/carbs 双字段
-          self.foods = (res.data || []).map(function(item) {
+          // 后端 Food 实体字段:name, category, score;兼容 foodName(旧 draft)
+          self.foods = (Array.isArray(res.data) ? res.data : []).map(function(item) {
             return {
-              foodName: item.name || item.foodName || item.name,
+              foodName: item.name || item.foodName || '',
               name: item.name || item.foodName || '',
               score: item.score != null ? parseInt(item.score, 10) : 0,
               category: item.category || '其它',
-              protein: item.protein != null ? parseFloat(item.protein) : 0,
-              fat: item.fat != null ? parseFloat(item.fat) : 0,
-              carb: item.carbs != null ? parseFloat(item.carbs) : (item.carb != null ? parseFloat(item.carb) : 0),
-              fiber: item.fiber != null ? parseFloat(item.fiber) : 0,
-              energy: item.energyKj != null ? parseFloat(item.energyKj) : (item.energy != null ? parseFloat(item.energy) : 0)
+              protein: item.protein != null ? parseFloat(item.protein) : null,
+              fat: item.fat != null ? parseFloat(item.fat) : null,
+              carb: item.carbs != null ? parseFloat(item.carbs) : (item.carb != null ? parseFloat(item.carb) : null),
+              fiber: item.fiber != null ? parseFloat(item.fiber) : null,
+              energy: item.energyKj != null ? parseFloat(item.energyKj) : (item.energy != null ? parseFloat(item.energy) : null)
             }
           })
         }
       })
     },
+    showFoodKnowledge: function(item) {
+      var self = this
+      var name = item.foodName || item.name || ''
+      if (!name) return
+      // 本地营养数据优先(已在列表里)
+      var local = {
+        itemName: name,
+        category: item.category || '',
+        protein: item.protein,
+        fat: item.fat,
+        carbs: item.carb,
+        fiber: item.fiber,
+        energy: item.energy,
+        description: ''
+      }
+      // 有固定营养数据直接展示,否则查知识库
+      if (item.protein != null || item.fat != null || item.carbs != null) {
+        self.knowledgeData = local
+        self.knowledgeVisible = true
+        return
+      }
+      // 查知识库
+      var cacheKey = 'food:' + name
+      if (self._kbCache[cacheKey]) {
+        self.knowledgeData = self._kbCache[cacheKey]
+        self.knowledgeVisible = true
+        return
+      }
+      queryFoodKnowledge(name).then(function(res) {
+        if (res.code === 200 && res.data) {
+          var d = res.data
+          var kd = {
+            itemName: d['名称'] || name,
+            category: d['分类'] || local.category || '',
+            protein: d['蛋白g'] != null ? d['蛋白g'] : null,
+            fat: d['脂肪g'] != null ? d['脂肪g'] : null,
+            carbs: d['碳水化合物g'] != null ? d['碳水化合物g'] : null,
+            fiber: d['总膳食纤维g'] != null ? d['总膳食纤维g'] : null,
+            energy: d['能量KJ'] != null ? d['能量KJ'] : null,
+            description: d['说明'] || ''
+          }
+          self._kbCache[cacheKey] = kd
+          self.knowledgeData = kd
+        } else {
+          self.knowledgeData = local
+        }
+        self.knowledgeVisible = true
+      }).catch(function() {
+        self.knowledgeData = local
+        self.knowledgeVisible = true
+      })
+    },
+    closeKnowledge: function() {
+      this.knowledgeVisible = false
+      this.knowledgeData = {}
+    },
     toggleEdit: function() {
       this.isEditing = true
       this.editFoods = JSON.parse(JSON.stringify(this.foods))
@@ -151,30 +240,67 @@ export default {
 
 <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; }
+
+/* 搜索框 */
+.search-bar { padding: 20rpx 30rpx; background: #fff; }
+.search-input-wrap {
+  display: flex;
+  align-items: center;
+  background: #f5f5f5;
+  border-radius: 40rpx;
+  padding: 12rpx 24rpx;
+}
+.search-icon { font-size: 28rpx; margin-right: 12rpx; }
+.search-input { flex: 1; font-size: 28rpx; }
+.search-clear { font-size: 28rpx; color: #999; padding: 0 8rpx; }
+
+.content { padding: 20rpx 30rpx; height: calc(100vh - 120rpx); }
+
+.food-card {
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+/* 推荐值背景色分级:<-30 深红,>30 深绿,中间每10换一档 */
+.score-band-neg3 { background: #C62828; }
+.score-band-neg2 { background: #E65100; }
+.score-band-neg1 { background: #F57C00; }
+.score-band-neg0 { background: #F9A825; }
+.score-band-pos0 { background: #AFB42B; }
+.score-band-pos1 { background: #7CB342; }
+.score-band-pos2 { background: #43A047; }
+.score-band-pos3 { background: #1B5E20; }
+.score-band-null { background: #f0f0f0; }
+
+.food-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
+.food-name { font-size: 30rpx; font-weight: 600; color: #fff; flex: 1; }
+.food-cat-tag { padding: 4rpx 16rpx; background: rgba(255,255,255,0.3); border-radius: 20rpx; font-size: 22rpx; color: #fff; }
+
+.food-score-row { display: flex; align-items: baseline; gap: 12rpx; }
+.food-score-label { font-size: 22rpx; color: rgba(255,255,255,0.85); }
+.food-score-value { font-size: 40rpx; font-weight: 700; color: #fff; }
+
+.food-edit-row { display: flex; align-items: center; margin-top: 8rpx; }
+.edit-label { font-size: 26rpx; color: #fff; margin-right: 12rpx; }
+.edit-input { border: 1rpx solid #fff; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 140rpx; color: #fff; background: rgba(255,255,255,0.2); }
+
 .empty-hint { text-align: center; color: #ccc; padding: 60rpx 0; font-size: 28rpx; }
+
+/* 知识库弹窗 */
+.knowledge-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 999; }
+.knowledge-popup { width: 80%; background: #fff; border-radius: 20rpx; overflow: hidden; }
+.knowledge-header { display: flex; align-items: center; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
+.knowledge-title { font-size: 32rpx; font-weight: 600; color: #333; }
+.knowledge-close { font-size: 32rpx; color: #999; padding: 0 8rpx; }
+.knowledge-body { padding: 30rpx; max-height: 60vh; overflow-y: auto; }
+.knowledge-row { display: flex; align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.knowledge-label { font-size: 26rpx; color: #999; width: 120rpx; }
+.knowledge-value { font-size: 26rpx; color: #333; flex: 1; }
+.knowledge-desc { margin-top: 16rpx; }
+.knowledge-desc-text { font-size: 26rpx; color: #666; line-height: 1.6; }
+
+.edit-fab { position: fixed; right: 30rpx; bottom: 40rpx; background: #4A9BD7; color: #fff; border-radius: 40rpx; padding: 16rpx 32rpx; font-size: 26rpx; box-shadow: 0 4rpx 16rpx rgba(74,155,215,0.4); }
 .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; }