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

feat(diet): 饮食模块整合-去食材查询合并食材推荐

- 食材查询页(food-query)能力并入食材推荐页,作为「推荐食材」分区(成员切换/搜索/分类/筛选/排序/家庭总览/个人列表)
- 删除 pages/diet/food-query.vue 及 pages.json 路由,recommendation 标题改「食材推荐」
- 饮食首页:移除「食材查询」入口,「食谱推荐」改名「食材推荐」指向 recommendation
- 成长页:仅保留「饮食推荐」入口(指向 recommendation),移除「食材戒备匹配」卡片及相关数据/方法/接口导入
- 肠道菌群报告详情:移除「食材戒备」卡片、goToCaution、cautionCount 及 getFoodCautionList 导入

任务:饮食模块整合-去食材查询合并食材推荐
验证:verify-vue-compile 全站 PASS(389/390,mp-html 跳过);audit-mp-antipatterns ERROR 0;audit-duplicate-api-calls 0 error;npm run build:mp-weixin 构建成功
Agnes Agent 2 недель назад
Родитель
Сommit
258df1886a

+ 1 - 7
cfc-frontend/pages.json

@@ -1900,12 +1900,6 @@
             "navigationBarTitleText": "饮食偏好调研"
             "navigationBarTitleText": "饮食偏好调研"
           }
           }
         },
         },
-        {
-          "path": "food-query",
-          "style": {
-            "navigationBarTitleText": "食材查询"
-          }
-        },
         {
         {
           "path": "meal-config",
           "path": "meal-config",
           "style": {
           "style": {
@@ -1915,7 +1909,7 @@
         {
         {
           "path": "recommendation",
           "path": "recommendation",
           "style": {
           "style": {
-            "navigationBarTitleText": "食谱推荐"
+            "navigationBarTitleText": "食材推荐"
           }
           }
         },
         },
         {
         {

+ 0 - 769
cfc-frontend/pages/diet/food-query.vue

@@ -1,769 +0,0 @@
-<template>
-  <view class="food-query-page" :style="{ '--font-scale': fontScale }">
-    <!-- 成员切换条(统一组件 MemberSelector:家庭总览 / 个人 / 各成员,同一用户只出现一次) -->
-    <!-- T28:报告入口(有 reportId)不渲染成员切换,只取当前报告 -->
-    <MemberSelector
-      variant="minimal"
-      v-if="!reportId && memberList.length > 1"
-      :members="memberList"
-      :selected-member-id="currentMemberId"
-      :show-overview="true"
-      :overview-active="currentView === 'family'"
-      @overview="switchView('family')"
-      @select="switchMember" />
-
-    <!-- 模式切换(仅家庭模式展示;报告入口不显示) -->
-    <view class="mode-bar" v-if="!reportId && familyMode">
-      <view class="mode-btn" :class="{'active': sortType === 'desc'}" @tap="setSort('desc')">
-        <text>推荐值 ↓</text>
-      </view>
-      <view class="mode-btn" :class="{'active': sortType === 'asc'}" @tap="setSort('asc')">
-        <text>推荐值 ↑</text>
-      </view>
-    </view>
-
-    <!-- 搜索栏(固定顶部,不随列表滚动) -->
-      <view class="search-bar">
-        <input class="search-input" placeholder="搜索食材..." v-model="searchQuery" @confirm="searchFood" />
-        <view class="search-btn" @tap="searchFood">搜索</view>
-      </view>
-
-      <!-- 分类标签 -->
-      <view class="category-tabs">
-        <view class="tab-item" :class="{'tab-active': activeCategory === 'all'}" @tap="switchCategory('all')">
-          <text>全部</text>
-        </view>
-        <view class="tab-item" :class="{'tab-active': activeCategory === '蔬菜'}" @tap="switchCategory('蔬菜')">
-          <text>蔬菜</text>
-        </view>
-        <view class="tab-item" :class="{'tab-active': activeCategory === '水果'}" @tap="switchCategory('水果')">
-          <text>水果</text>
-        </view>
-        <view class="tab-item" :class="{'tab-active': activeCategory === '肉禽'}" @tap="switchCategory('肉禽')">
-          <text>肉禽</text>
-        </view>
-        <view class="tab-item" :class="{'tab-active': activeCategory === '水产'}" @tap="switchCategory('水产')">
-          <text>水产</text>
-        </view>
-        <view class="tab-item" :class="{'tab-active': activeCategory === '谷物'}" @tap="switchCategory('谷物')">
-          <text>谷物</text>
-        </view>
-        <view class="tab-item" :class="{'tab-active': activeCategory === '蛋奶'}" @tap="switchCategory('蛋奶')">
-          <text>蛋奶</text>
-        </view>
-      </view>
-
-      <!-- 筛选栏 -->
-      <view class="filter-bar">
-        <view class="filter-item" :class="{'filter-active': filterType === 'suitable'}" @tap="setFilter('suitable')">
-          <text>✅ 适合</text>
-        </view>
-        <view class="filter-item" :class="{'filter-active': filterType === 'unsuitable'}" @tap="setFilter('unsuitable')">
-          <text>❌ 戒备</text>
-        </view>
-        <view class="filter-item" :class="{'filter-active': filterType === 'all'}" @tap="setFilter('all')">
-          <text>📋 全部</text>
-        </view>
-      </view>
-
-      <!-- 列表区(仅此区域滚动) -->
-      <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
-      <!-- 家庭总览(读后端持久化 overview:家庭成员该项食材最新推荐指数的最小值) -->
-      <view class="family-overview" v-if="currentView === 'family' && familyOverview && familyOverview.length > 0">
-        <view class="overview-title">
-          <text>🏠 家庭推荐概况(共 {{ familyMembers.length }} 人 · 取家庭最保守值)</text>
-        </view>
-        <view class="overview-item" v-for="(item, idx) in familyOverview" :key="idx">
-          <view class="overview-header">
-            <text class="food-name">{{ item.name }}</text>
-            <view class="score-badge" :class="item.color">
-              <text>{{ item.score }}</text>
-            </view>
-          </view>
-          <view class="overview-detail">
-            <text class="category-label">{{ item.category }}</text>
-            <view class="member-scores">
-              <text v-for="(s, mi) in item.memberScores" :key="mi"
-                class="member-score"
-                :class="s.score >= 0 ? 'score-green' : 'score-red'">
-                {{ s.name }}:{{ s.score >= 0 ? '+' : '' }}{{ s.score }}
-              </text>
-            </view>
-          </view>
-        </view>
-      </view>
-
-      <!-- 食材列表 -->
-      <view class="food-list" v-if="currentView === 'personal'">
-        <view class="food-card" v-for="(item, idx) in filteredFoods" :key="getFoodKey(item, idx)">
-          <view class="food-left">
-            <view class="ingredient-badge" :class="item.scoreClass">
-              <text class="ingredient-badge-text">{{ idx + 1 }}</text>
-            </view>
-            <view class="food-info">
-              <text class="food-name">{{ item.name }}</text>
-              <view class="food-meta" v-if="item.category">
-                <text class="meta-item">{{ item.category }}</text>
-              </view>
-            </view>
-          </view>
-          <view class="food-right">
-            <text class="score-value" :class="item.scoreClass">{{ item.score || '--' }}</text>
-            <view class="status-badge" :class="'badge-' + item.status">
-              <text>{{ item.statusText }}</text>
-            </view>
-          </view>
-        </view>
-      </view>
-
-      <view class="empty-state" v-if="!loading && filteredFoods.length === 0 && currentView === 'personal'">
-        <text class="empty-icon">🥗</text>
-        <text class="empty-text">暂无匹配食材</text>
-      </view>
-
-      <view class="loading-state" v-if="loading">
-        <text class="loading-text">加载中...</text>
-      </view>
-
-      <view class="bottom-spacer"></view>
-    </scroll-view>
-  </view>
-</template>
-
-<script>
-import config from '@/config.js'
-import fontScaleMixin from '@/mixins/font-scale.js'
-import MemberSelector from '@/components/MemberSelector.vue'
-import { memberIdOf } from '@/utils/memberNormalize.js'
-
-export default {
-  mixins: [fontScaleMixin],
-  components: { MemberSelector },
-  data() {
-    return {
-      searchQuery: '',
-      activeCategory: 'all',
-      filterType: 'suitable',
-      sortType: 'desc',
-      currentView: 'personal',
-      foods: [],
-      allFoods: [],
-      familyData: null,
-      familyMembers: [],
-      familyOverview: [],
-      currentMemberId: null,
-      memberList: [],
-      loading: false,
-      refreshing: false,
-      reportId: null
-    }
-  },
-  computed: {
-    filteredFoods: function() {
-      var self = this
-      var result = self.currentView === 'family'
-        ? self.familyOverview
-        : self.allFoods
-
-      // 分类筛选(食材分类为中文,用关键词集合匹配不同数据源的分类命名)
-      if (self.activeCategory !== 'all') {
-        var keywords = self.getCategoryKeywords(self.activeCategory)
-        result = result.filter(function(f) {
-          if (!f.category) return false
-          for (var ki = 0; ki < keywords.length; ki++) {
-            if (f.category.indexOf(keywords[ki]) !== -1) return true
-          }
-          return false
-        })
-      }
-
-      // 搜索筛选
-      if (self.searchQuery) {
-        result = result.filter(function(f) {
-          return (f.name && f.name.indexOf(self.searchQuery) !== -1) ||
-                 (f.category && f.category.indexOf(self.searchQuery) !== -1)
-        })
-      }
-
-      // 适合/戒备筛选(戒备 = 推荐指数 < 0)
-      if (self.filterType === 'suitable') {
-        result = result.filter(function(f) {
-          return f.score >= 0
-        })
-      } else if (self.filterType === 'unsuitable') {
-        result = result.filter(function(f) {
-          return f.score < 0
-        })
-      }
-
-      // 排序(戒备模式默认升序,见 setFilter / onLoad 中 sortType 默认值)
-      if (self.sortType === 'asc') {
-        result = result.slice().sort(function(a, b) {
-          return (a.score || 0) - (b.score || 0)
-        })
-      } else {
-        result = result.slice().sort(function(a, b) {
-          return (b.score || 0) - (a.score || 0)
-        })
-      }
-
-      return result
-    }
-  },
-  onLoad: function(options) {
-    this.reportId = options.reportId ? parseInt(options.reportId) : null
-    this.currentMemberId = options.memberId ? parseInt(options.memberId) : null
-    
-    // 从报告入口进来时,默认显示戒备模式(升序,数值最小的排最前)
-    if (this.reportId) {
-      this.filterType = 'unsuitable'
-      this.sortType = 'asc'
-    } else {
-      this.loadMembers()
-      this.loadFamilyData()
-    }
-    
-    this.loadFoods()
-  },
-  methods: {
-    getFoodKey: function(item, idx) {
-      return item.id || idx
-    },
-    goBack: function() {
-      uni.navigateBack()
-    },
-    loadMembers: function() {
-      var self = this
-      var token = uni.getStorageSync('token')
-      if (!token || self.reportId) return
-      
-      uni.request({
-        url: config.API_BASE_URL + '/api/food/recommendation/members',
-        method: 'POST',
-        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-        success: function(res) {
-          if (res.data && res.data.code === 200 && res.data.data) {
-            self.memberList = res.data.data
-            // 默认选中「个人」= isSelf 行(统一身份键 familyMemberId,不再用 userId)
-            if (!self.currentMemberId && self.memberList.length > 0) {
-              var defaultMember = self.getSelfRow() || self.memberList[0]
-              self.currentMemberId = memberIdOf(defaultMember)
-            }
-          }
-        }
-      })
-    },
-    loadFoods: function() {
-      var self = this
-      var token = uni.getStorageSync('token')
-      if (!token) return
-      
-      self.loading = true
-      var url = config.API_BASE_URL + '/api/health/foods'
-      var data = {}
-      // T28:报告入口只传 reportId(不传 memberId);非报告入口传 memberId
-      if (self.reportId) {
-        data.reportId = self.reportId
-      } else if (self.currentMemberId) {
-        data.memberId = self.currentMemberId
-      }
-      
-      uni.request({
-        url: url,
-        method: 'POST',
-        data: data,
-        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-        success: function(res) {
-          self.loading = false
-          if (res.data && res.data.code === 200 && res.data.data) {
-            self.allFoods = res.data.data.map(function(f) {
-              f.scoreClass = self.getScoreClass(f.score)
-              f.statusText = self.getStatusText(f.score)
-              f.status = f.score >= 0 ? 'suitable' : 'unsuitable'
-              return f
-            })
-          }
-        },
-        fail: function() {
-          self.loading = false
-        }
-      })
-    },
-    loadFamilyData: function() {
-      var self = this
-      var token = uni.getStorageSync('token')
-      if (!token || self.reportId) return
-      
-      var familyId = uni.getStorageSync('familyId')
-      if (!familyId) return
-      
-      uni.request({
-        url: config.API_BASE_URL + '/api/health/foods/by-family',
-        method: 'POST',
-        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-        success: function(res) {
-          if (res.data && res.data.code === 200 && res.data.data) {
-            self.familyData = res.data.data
-            self.familyMembers = Object.keys(res.data.data.byMember || {}).map(function(key) {
-              return { userId: key, nickname: res.data.data.memberNames[key] || '成员' }
-            })
-            // T28:优先使用后端持久化总览(overview,minIndexScore = 家庭成员该项食材最新推荐指数的最小值)
-            if (res.data.data.overview && res.data.data.overview.length > 0) {
-              self.familyOverview = res.data.data.overview.map(function(o) {
-                var min = o.minIndexScore
-                return {
-                  id: o.foodId,
-                  name: o.foodName,
-                  category: o.category,
-                  score: min,
-                  scoreClass: self.getScoreClass(min),
-                  color: self.getScoreClass(min),
-                  statusText: self.getStatusText(min),
-                  status: min >= 0 ? 'suitable' : 'unsuitable',
-                  memberCount: o.memberCount || 0,
-                  memberScores: (o.memberScores || []).map(function(s) {
-                    return { name: s.nickname || '', score: s.score }
-                  })
-                }
-              })
-            } else {
-              // 后端未返回 overview 时的降级路径:前端按 min 计算(比 avg 更安全)
-              self.buildFamilyOverview(res.data.data.total || res.data.data.totalList, res.data.data.byMember, res.data.data.memberNames)
-            }
-          }
-        }
-      })
-    },
-    buildFamilyOverview: function(totalList, byMember, memberNames) {
-      var self = this
-      var overview = []
-      var list = totalList || []
-      for (var i = 0; i < list.length; i++) {
-        var food = list[i]
-        var memberScores = []
-        
-        for (var j = 0; j < self.familyMembers.length; j++) {
-          var member = self.familyMembers[j]
-          var memberFoods = byMember[member.userId]
-          if (memberFoods) {
-            for (var k = 0; k < memberFoods.length; k++) {
-              if (memberFoods[k].foodId === food.id) {
-                var score = memberFoods[k].indexScore || 0
-                memberScores.push({ name: member.nickname, score: score })
-                break
-              }
-            }
-          }
-        }
-        
-        // T28 降级路径:取家庭成员该项食材的最小值(最保守值),比 avg 更安全
-        if (memberScores.length > 0) {
-          var minScore = memberScores[0].score
-          for (var mi = 1; mi < memberScores.length; mi++) {
-            if (memberScores[mi].score < minScore) minScore = memberScores[mi].score
-          }
-          overview.push({
-            id: food.id,
-            name: food.name,
-            category: food.category,
-            score: minScore,
-            scoreClass: self.getScoreClass(minScore),
-            color: self.getScoreClass(minScore),
-            statusText: self.getStatusText(minScore),
-            status: minScore >= 0 ? 'suitable' : 'unsuitable',
-            memberCount: memberScores.length,
-            memberScores: memberScores
-          })
-        }
-      }
-      
-      // 按最小推荐值排序(从高到低)
-      overview.sort(function(a, b) {
-        return b.score - a.score
-      })
-      
-      self.familyOverview = overview
-    },
-    getScoreClass: function(score) {
-      if (score === undefined || score === null) return 'neutral'
-      if (score >= 0) return 'good'
-      return 'bad'
-    },
-    getStatusText: function(score) {
-      if (score === undefined || score === null) return '未知'
-      if (score >= 0) return '适合'
-      return '戒备'
-    },
-    searchFood: function() {
-      // 搜索由 computed 自动处理
-    },
-    switchCategory: function(category) {
-      this.activeCategory = category
-    },
-    getCategoryKeywords: function(category) {
-      var map = {
-        '蔬菜': ['蔬菜'],
-        '水果': ['水果'],
-        '肉禽': ['肉类', '肉禽'],
-        '水产': ['水产', '水产品'],
-        '谷物': ['谷物', '主食', '全谷物'],
-        '蛋奶': ['蛋类', '蛋奶', '乳制品']
-      }
-      return map[category] || []
-    },
-    setFilter: function(type) {
-      this.filterType = type
-      // 适合/全部:倒序,推荐值大的排前面;戒备:升序,推荐值小的排前面
-      if (type === 'unsuitable') {
-        this.sortType = 'asc'
-      } else {
-        this.sortType = 'desc'
-      }
-    },
-    setSort: function(type) {
-      this.sortType = type
-    },
-    switchView: function(view) {
-      this.currentView = view
-    },
-    // 取「本人」行(isSelf):后端已按 user_id 折叠,此处仅取第一行 isSelf
-    getSelfRow: function() {
-      var members = this.memberList || []
-      for (var i = 0; i < members.length; i++) {
-        if (members[i] && members[i].isSelf) return members[i]
-      }
-      return null
-    },
-    switchMember: function(member) {
-      // 统一身份键:familyMemberId(= family_members.id);userId 仅用于折叠,不再作主键
-      this.currentMemberId = memberIdOf(member)
-      this.currentView = 'personal'
-      this.loadFoods()
-    },
-    onRefresh: function() {
-      this.refreshing = true
-      this.loadFoods()
-      this.loadFamilyData()
-      setTimeout(function() {
-        this.refreshing = false
-      }.bind(this), 500)
-    }
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.food-query-page {
-  height: 100vh;
-  overflow: hidden;
-  background-color: $app-bg;
-  display: flex;
-  flex-direction: column;
-}
-
-/* 成员切换条 */
-.member-bar {
-  display: flex;
-  overflow-x: auto;
-  padding: 16rpx $uni-space-page 8rpx;
-  background: $app-surface;
-  border-bottom: 1rpx solid $facade-fill;
-  gap: 16rpx;
-  -webkit-overflow-scrolling: touch;
-}
-.member-tab {
-  flex-shrink: 0;
-  padding: 8rpx 20rpx;
-  border-radius: 24rpx;
-  background: $facade-fill-2;
-  color: $facade-text-body;
-  font-size: calc(26rpx * var(--font-scale, 1));
-  font-weight: 500;
-}
-.member-tab.active {
-  background: linear-gradient(135deg, $dim-body, $dim-body-light);
-  color: $app-surface;
-}
-
-/* 模式切换(排序) */
-.mode-bar {
-  display: flex;
-  padding: 16rpx $uni-space-page;
-  background: $app-surface;
-  border-bottom: 1rpx solid $facade-fill;
-  gap: 20rpx;
-}
-.mode-btn {
-  flex: 1;
-  padding: 12rpx 0;
-  text-align: center;
-  border-radius: 24rpx;
-  background: $facade-fill-2;
-  color: $facade-text-body;
-  font-size: calc(26rpx * var(--font-scale, 1));
-}
-.mode-btn.active {
-  background: linear-gradient(135deg, $status-success, $status-success-light-3);
-  color: $app-surface;
-}
-
-/* 搜索栏 */
-.search-bar {
-  display: flex;
-  padding: 20rpx $uni-space-page;
-  background-color: $app-surface;
-}
-.search-input {
-  flex: 1;
-  border: 1rpx solid $facade-border-soft;
-  border-radius: 24rpx;
-  padding: 16rpx 24rpx;
-  font-size: calc(26rpx * var(--font-scale, 1));
-  background-color: $facade-fill-ghost;
-}
-.search-btn {
-  margin-left: 20rpx;
-  padding: 16rpx 32rpx;
-  background-color: $dim-body;
-  color: $app-surface;
-  border-radius: 24rpx;
-  font-size: calc(26rpx * var(--font-scale, 1));
-}
-
-.content {
-  flex: 1;
-  min-height: 0;
-  overflow: hidden;
-  padding: 20rpx $uni-space-page;
-  box-sizing: border-box;
-}
-
-/* 分类标签 */
-.category-tabs {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 16rpx;
-  margin-bottom: 20rpx;
-}
-.tab-item {
-  padding: 12rpx 24rpx;
-  border: 1rpx solid $facade-border-soft;
-  border-radius: 24rpx;
-  font-size: calc(24rpx * var(--font-scale, 1));
-  color: $facade-text-body;
-  background-color: $app-surface;
-}
-.tab-active {
-  border-color: $dim-body;
-  color: $dim-body;
-  background-color: $flat-card;
-}
-
-/* 筛选栏 */
-.filter-bar {
-  display: flex;
-  gap: 20rpx;
-  margin-bottom: 20rpx;
-}
-.filter-item {
-  padding: 12rpx 24rpx;
-  border: 1rpx solid $facade-border-soft;
-  border-radius: 24rpx;
-  font-size: calc(24rpx * var(--font-scale, 1));
-  color: $facade-text-body;
-  background-color: $app-surface;
-}
-.filter-active {
-  border-color: $mat-green;
-  color: $mat-green;
-  background-color: $mat-green-tint;
-}
-
-/* 家庭总览 */
-.family-overview {
-  background: $app-surface;
-  border-radius: 16rpx;
-  padding: 20rpx;
-  margin-bottom: 20rpx;
-}
-.overview-title {
-  font-size: calc(28rpx * var(--font-scale, 1));
-  font-weight: 600;
-  color: $facade-text-strong;
-  margin-bottom: 16rpx;
-  padding-bottom: 16rpx;
-  border-bottom: 1rpx solid $facade-fill;
-}
-.overview-item {
-  padding: 16rpx 0;
-  border-bottom: 1rpx solid $facade-fill-2;
-}
-.overview-item:last-child {
-  border-bottom: none;
-}
-.overview-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 8rpx;
-}
-.overview-detail {
-  display: flex;
-  flex-direction: column;
-  gap: 8rpx;
-}
-.category-label {
-  font-size: calc(22rpx * var(--font-scale, 1));
-  color: $facade-text-muted;
-}
-.member-scores {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 12rpx;
-}
-.member-score {
-  font-size: calc(20rpx * var(--font-scale, 1));
-  padding: 4rpx 12rpx;
-  border-radius: 16rpx;
-  background: $facade-fill-2;
-}
-.score-green {
-  color: $status-success;
-  background: $mat-green-tint;
-}
-.score-red {
-  color: $status-danger;
-  background: $status-danger-soft;
-}
-
-/* 食材列表 */
-.food-list {
-  display: flex;
-  flex-direction: column;
-  gap: 16rpx;
-}
-.food-card {
-  background: $app-surface;
-  border-radius: 16rpx;
-  padding: 18rpx 20rpx;
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  width: 100%;
-  box-sizing: border-box;
-}
-.food-left {
-  display: flex;
-  align-items: center;
-  flex: 1;
-  min-width: 0;
-}
-.ingredient-badge {
-  width: 36rpx;
-  height: 36rpx;
-  border-radius: 8rpx;
-  background: linear-gradient(135deg, $dim-body, $dim-body-light);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-right: 16rpx;
-  flex-shrink: 0;
-}
-.ingredient-badge.good {
-  background: linear-gradient(135deg, $status-success, $status-success-light-3);
-}
-.ingredient-badge.bad {
-  background: linear-gradient(135deg, $status-danger, $status-danger-deep);
-}
-.ingredient-badge-text {
-  font-size: calc(18rpx * var(--font-scale, 1));
-  color: $app-surface;
-  font-weight: 600;
-}
-.food-info {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  min-width: 0;
-  overflow: hidden;
-}
-.food-name {
-  font-size: calc(28rpx * var(--font-scale, 1));
-  font-weight: 600;
-  color: $text-charcoal;
-  display: block;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-.food-meta {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8rpx;
-  margin-top: 6rpx;
-}
-.meta-item {
-  flex-shrink: 0;
-  font-size: calc(20rpx * var(--font-scale, 1));
-  color: $facade-text-faint;
-  background: $facade-fill-2;
-  padding: 4rpx 14rpx;
-  border-radius: 8rpx;
-}
-.food-right {
-  display: flex;
-  align-items: center;
-  gap: 12rpx;
-  flex-shrink: 0;
-}
-.score-value {
-  font-size: calc(28rpx * var(--font-scale, 1));
-  font-weight: 700;
-  min-width: 60rpx;
-  text-align: right;
-}
-.score-value.good { color: $status-success; }
-.score-value.bad { color: $status-danger; }
-.status-badge {
-  padding: 6rpx 16rpx;
-  border-radius: 24rpx;
-  font-size: calc(22rpx * var(--font-scale, 1));
-  font-weight: 500;
-}
-.badge-suitable {
-  background: $mat-green-tint;
-  color: $mat-green;
-}
-.badge-unsuitable {
-  background: $fortune-0-tint;
-  color: $mat-red;
-}
-
-/* 空状态 */
-.empty-state {
-  text-align: center;
-  padding: 100rpx 40rpx;
-}
-.empty-icon {
-  font-size: calc(80rpx * var(--font-scale, 1));
-  display: block;
-  margin-bottom: 20rpx;
-}
-.empty-text {
-  font-size: calc(30rpx * var(--font-scale, 1));
-  color: $facade-text-strong;
-  font-weight: 500;
-  display: block;
-  margin-bottom: 12rpx;
-}
-.loading-state {
-  text-align: center;
-  padding: 80rpx 0;
-}
-.loading-text {
-  font-size: calc(26rpx * var(--font-scale, 1));
-  color: $facade-text-muted;
-}
-.bottom-spacer { height: 40rpx; }
-</style>

+ 1 - 7
cfc-frontend/pages/diet/index.vue

@@ -72,12 +72,6 @@
         <text class="section-label-text">常用功能</text>
         <text class="section-label-text">常用功能</text>
       </view>
       </view>
       <view class="nav-grid">
       <view class="nav-grid">
-        <view class="nav-item" @tap="navTo('/pages/diet/food-query')">
-          <view class="nav-icon-wrap nav-icon-green">
-            <text class="nav-icon">🥗</text>
-          </view>
-          <text class="nav-label">食材查询</text>
-        </view>
         <view class="nav-item" @tap="navTo('/pages/diet/records')">
         <view class="nav-item" @tap="navTo('/pages/diet/records')">
           <view class="nav-icon-wrap nav-icon-blue">
           <view class="nav-icon-wrap nav-icon-blue">
             <text class="nav-icon">📝</text>
             <text class="nav-icon">📝</text>
@@ -100,7 +94,7 @@
           <view class="nav-icon-wrap nav-icon-red">
           <view class="nav-icon-wrap nav-icon-red">
             <text class="nav-icon">🍽️</text>
             <text class="nav-icon">🍽️</text>
           </view>
           </view>
-          <text class="nav-label">食谱推荐</text>
+          <text class="nav-label">食材推荐</text>
         </view>
         </view>
       </view>
       </view>
 
 

+ 741 - 3
cfc-frontend/pages/diet/recommendation.vue

@@ -1,7 +1,142 @@
 <template>
 <template>
   <view class="recommendation-page" :style="{ '--font-scale': fontScale }">
   <view class="recommendation-page" :style="{ '--font-scale': fontScale }">
 
 
-    <scroll-view class="content" scroll-y>
+    <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
+      <!-- ===== 推荐食材 ===== -->
+      <view class="section-title-bar">
+        <text class="section-title">推荐食材</text>
+        <text class="section-subtitle">查看家庭成员适合与戒备的食材</text>
+      </view>
+
+      <view class="food-section">
+        <!-- 成员切换条(统一组件 MemberSelector:家庭总览 / 个人 / 各成员) -->
+        <MemberSelector
+          variant="minimal"
+          v-if="memberList.length > 1"
+          :members="memberList"
+          :selected-member-id="currentMemberId"
+          :show-overview="true"
+          :overview-active="currentView === 'family'"
+          @overview="switchView('family')"
+          @select="switchMember" />
+
+        <!-- 模式切换(仅家庭模式展示) -->
+        <view class="mode-bar" v-if="familyMode">
+          <view class="mode-btn" :class="{'active': sortType === 'desc'}" @tap="setSort('desc')">
+            <text>推荐值 ↓</text>
+          </view>
+          <view class="mode-btn" :class="{'active': sortType === 'asc'}" @tap="setSort('asc')">
+            <text>推荐值 ↑</text>
+          </view>
+        </view>
+
+        <!-- 搜索栏 -->
+        <view class="search-bar">
+          <input class="search-input" placeholder="搜索食材..." v-model="searchQuery" @confirm="searchFood" />
+          <view class="search-btn" @tap="searchFood">搜索</view>
+        </view>
+
+        <!-- 分类标签 -->
+        <view class="category-tabs">
+          <view class="tab-item" :class="{'tab-active': activeCategory === 'all'}" @tap="switchCategory('all')">
+            <text>全部</text>
+          </view>
+          <view class="tab-item" :class="{'tab-active': activeCategory === '蔬菜'}" @tap="switchCategory('蔬菜')">
+            <text>蔬菜</text>
+          </view>
+          <view class="tab-item" :class="{'tab-active': activeCategory === '水果'}" @tap="switchCategory('水果')">
+            <text>水果</text>
+          </view>
+          <view class="tab-item" :class="{'tab-active': activeCategory === '肉禽'}" @tap="switchCategory('肉禽')">
+            <text>肉禽</text>
+          </view>
+          <view class="tab-item" :class="{'tab-active': activeCategory === '水产'}" @tap="switchCategory('水产')">
+            <text>水产</text>
+          </view>
+          <view class="tab-item" :class="{'tab-active': activeCategory === '谷物'}" @tap="switchCategory('谷物')">
+            <text>谷物</text>
+          </view>
+          <view class="tab-item" :class="{'tab-active': activeCategory === '蛋奶'}" @tap="switchCategory('蛋奶')">
+            <text>蛋奶</text>
+          </view>
+        </view>
+
+        <!-- 筛选栏 -->
+        <view class="filter-bar">
+          <view class="filter-item" :class="{'filter-active': filterType === 'suitable'}" @tap="setFilter('suitable')">
+            <text>✅ 适合</text>
+          </view>
+          <view class="filter-item" :class="{'filter-active': filterType === 'unsuitable'}" @tap="setFilter('unsuitable')">
+            <text>❌ 戒备</text>
+          </view>
+          <view class="filter-item" :class="{'filter-active': filterType === 'all'}" @tap="setFilter('all')">
+            <text>📋 全部</text>
+          </view>
+        </view>
+
+        <!-- 家庭总览(取家庭成员该项食材推荐指数的最小值) -->
+        <view class="family-overview" v-if="currentView === 'family' && familyOverview && familyOverview.length > 0">
+          <view class="overview-title">
+            <text>🏠 家庭推荐概况(共 {{ familyMembers.length }} 人 · 取家庭最保守值)</text>
+          </view>
+          <view class="overview-item" v-for="(item, idx) in familyOverview" :key="idx">
+            <view class="overview-header">
+              <text class="food-name">{{ item.name }}</text>
+              <view class="score-badge" :class="item.color">
+                <text>{{ item.score }}</text>
+              </view>
+            </view>
+            <view class="overview-detail">
+              <text class="category-label">{{ item.category }}</text>
+              <view class="member-scores">
+                <text v-for="(s, mi) in item.memberScores" :key="mi"
+                  class="member-score"
+                  :class="s.score >= 0 ? 'score-green' : 'score-red'">
+                  {{ s.name }}:{{ s.score >= 0 ? '+' : '' }}{{ s.score }}
+                </text>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- 食材列表 -->
+        <view class="food-list" v-if="currentView === 'personal'">
+          <view class="food-card" v-for="(item, idx) in filteredFoods" :key="getFoodKey(item, idx)">
+            <view class="food-left">
+              <view class="ingredient-badge" :class="item.scoreClass">
+                <text class="ingredient-badge-text">{{ idx + 1 }}</text>
+              </view>
+              <view class="food-info">
+                <text class="food-name">{{ item.name }}</text>
+                <view class="food-meta" v-if="item.category">
+                  <text class="meta-item">{{ item.category }}</text>
+                </view>
+              </view>
+            </view>
+            <view class="food-right">
+              <text class="score-value" :class="item.scoreClass">{{ item.score || '--' }}</text>
+              <view class="food-status-badge" :class="'badge-' + item.status">
+                <text>{{ item.statusText }}</text>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <view class="food-empty-state" v-if="!foodLoading && filteredFoods.length === 0 && currentView === 'personal'">
+          <text class="food-empty-icon">🥗</text>
+          <text class="food-empty-text">暂无匹配食材</text>
+        </view>
+
+        <view class="loading-state" v-if="foodLoading">
+          <text class="loading-text">加载中...</text>
+        </view>
+      </view>
+
+      <!-- ===== 今日食谱 ===== -->
+      <view class="section-title-bar">
+        <text class="section-title">今日食谱</text>
+      </view>
+
       <!-- 无推荐状态 -->
       <!-- 无推荐状态 -->
       <view class="empty-state" v-if="!recommendation">
       <view class="empty-state" v-if="!recommendation">
         <view class="empty-icon-wrap"><text class="empty-icon">🍽️</text></view>
         <view class="empty-icon-wrap"><text class="empty-icon">🍽️</text></view>
@@ -104,17 +239,35 @@
 
 
 <script>
 <script>
 import { getDietRecommendation, generateDietRecommendation, completeDietRecommendation, regenerateDietRecommendation } from '@/utils/api.js'
 import { getDietRecommendation, generateDietRecommendation, completeDietRecommendation, regenerateDietRecommendation } from '@/utils/api.js'
+import config from '@/config.js'
+import MemberSelector from '@/components/MemberSelector.vue'
+import { memberIdOf } from '@/utils/memberNormalize.js'
 
 
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
+  components: { MemberSelector },
   data() {
   data() {
     return {
     return {
       recommendation: null,
       recommendation: null,
       meals: [],
       meals: [],
       summary: {},
       summary: {},
-      loading: false
+      loading: false,
+      // ===== 推荐食材(原「食材查询」页迁移)=====
+      searchQuery: '',
+      activeCategory: 'all',
+      filterType: 'suitable',
+      sortType: 'desc',
+      currentView: 'personal',
+      allFoods: [],
+      familyData: null,
+      familyMembers: [],
+      familyOverview: [],
+      currentMemberId: null,
+      memberList: [],
+      foodLoading: false,
+      refreshing: false
     }
     }
   },
   },
   computed: {
   computed: {
@@ -125,10 +278,68 @@ export default {
         'completed': '已完成'
         'completed': '已完成'
       }
       }
       return map[this.recommendation && this.recommendation.status] || '待确认'
       return map[this.recommendation && this.recommendation.status] || '待确认'
+    },
+    familyMode: function() {
+      return this.currentView === 'family'
+    },
+    filteredFoods: function() {
+      var self = this
+      var result = self.currentView === 'family'
+        ? self.familyOverview
+        : self.allFoods
+
+      // 分类筛选(食材分类为中文,用关键词集合匹配不同数据源的分类命名)
+      if (self.activeCategory !== 'all') {
+        var keywords = self.getCategoryKeywords(self.activeCategory)
+        result = result.filter(function(f) {
+          if (!f.category) return false
+          for (var ki = 0; ki < keywords.length; ki++) {
+            if (f.category.indexOf(keywords[ki]) !== -1) return true
+          }
+          return false
+        })
+      }
+
+      // 搜索筛选
+      if (self.searchQuery) {
+        result = result.filter(function(f) {
+          return (f.name && f.name.indexOf(self.searchQuery) !== -1) ||
+                 (f.category && f.category.indexOf(self.searchQuery) !== -1)
+        })
+      }
+
+      // 适合/戒备筛选(戒备 = 推荐指数 < 0)
+      if (self.filterType === 'suitable') {
+        result = result.filter(function(f) {
+          return f.score >= 0
+        })
+      } else if (self.filterType === 'unsuitable') {
+        result = result.filter(function(f) {
+          return f.score < 0
+        })
+      }
+
+      // 排序(戒备模式默认升序,见 setFilter)
+      if (self.sortType === 'asc') {
+        result = result.slice().sort(function(a, b) {
+          return (a.score || 0) - (b.score || 0)
+        })
+      } else {
+        result = result.slice().sort(function(a, b) {
+          return (b.score || 0) - (a.score || 0)
+        })
+      }
+
+      return result
     }
     }
   },
   },
-  onLoad: function() {
+  onLoad: function(options) {
+    var opts = options || {}
+    this.currentMemberId = opts.memberId ? parseInt(opts.memberId) : null
     this.loadRecommendation()
     this.loadRecommendation()
+    this.loadMembers()
+    this.loadFamilyData()
+    this.loadFoods()
   },
   },
   onShow: function() {
   onShow: function() {
     this.loadRecommendation()
     this.loadRecommendation()
@@ -251,6 +462,224 @@ export default {
       var m = (d.getMonth() + 1).toString().padStart(2, '0')
       var m = (d.getMonth() + 1).toString().padStart(2, '0')
       var day = d.getDate().toString().padStart(2, '0')
       var day = d.getDate().toString().padStart(2, '0')
       return d.getFullYear() + '-' + m + '-' + day
       return d.getFullYear() + '-' + m + '-' + day
+    },
+    // ===== 推荐食材(原「食材查询」页迁移)=====
+    getFoodKey: function(item, idx) {
+      return item.id || idx
+    },
+    loadMembers: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      if (!token) return
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/food/recommendation/members',
+        method: 'POST',
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            self.memberList = res.data.data
+            // 默认选中「个人」= isSelf 行(统一身份键 familyMemberId,不再用 userId)
+            if (!self.currentMemberId && self.memberList.length > 0) {
+              var defaultMember = self.getSelfRow() || self.memberList[0]
+              self.currentMemberId = memberIdOf(defaultMember)
+            }
+          }
+        }
+      })
+    },
+    loadFoods: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      if (!token) return
+
+      self.foodLoading = true
+      var url = config.API_BASE_URL + '/api/health/foods'
+      var data = {}
+      if (self.currentMemberId) {
+        data.memberId = self.currentMemberId
+      }
+
+      uni.request({
+        url: url,
+        method: 'POST',
+        data: data,
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          self.foodLoading = false
+          if (res.data && res.data.code === 200 && res.data.data) {
+            self.allFoods = res.data.data.map(function(f) {
+              f.scoreClass = self.getScoreClass(f.score)
+              f.statusText = self.getStatusText(f.score)
+              f.status = f.score >= 0 ? 'suitable' : 'unsuitable'
+              return f
+            })
+          }
+        },
+        fail: function() {
+          self.foodLoading = false
+        }
+      })
+    },
+    loadFamilyData: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      if (!token) return
+
+      var familyId = uni.getStorageSync('familyId')
+      if (!familyId) return
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/health/foods/by-family',
+        method: 'POST',
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            self.familyData = res.data.data
+            self.familyMembers = Object.keys(res.data.data.byMember || {}).map(function(key) {
+              return { userId: key, nickname: res.data.data.memberNames[key] || '成员' }
+            })
+            // 优先使用后端持久化总览(overview,minIndexScore = 家庭成员该项食材最新推荐指数的最小值)
+            if (res.data.data.overview && res.data.data.overview.length > 0) {
+              self.familyOverview = res.data.data.overview.map(function(o) {
+                var min = o.minIndexScore
+                return {
+                  id: o.foodId,
+                  name: o.foodName,
+                  category: o.category,
+                  score: min,
+                  scoreClass: self.getScoreClass(min),
+                  color: self.getScoreClass(min),
+                  statusText: self.getStatusText(min),
+                  status: min >= 0 ? 'suitable' : 'unsuitable',
+                  memberCount: o.memberCount || 0,
+                  memberScores: (o.memberScores || []).map(function(s) {
+                    return { name: s.nickname || '', score: s.score }
+                  })
+                }
+              })
+            } else {
+              // 后端未返回 overview 时的降级路径:前端按 min 计算(比 avg 更安全)
+              self.buildFamilyOverview(res.data.data.total || res.data.data.totalList, res.data.data.byMember, res.data.data.memberNames)
+            }
+          }
+        }
+      })
+    },
+    buildFamilyOverview: function(totalList, byMember, memberNames) {
+      var self = this
+      var overview = []
+      var list = totalList || []
+      for (var i = 0; i < list.length; i++) {
+        var food = list[i]
+        var memberScores = []
+
+        for (var j = 0; j < self.familyMembers.length; j++) {
+          var member = self.familyMembers[j]
+          var memberFoods = byMember[member.userId]
+          if (memberFoods) {
+            for (var k = 0; k < memberFoods.length; k++) {
+              if (memberFoods[k].foodId === food.id) {
+                var score = memberFoods[k].indexScore || 0
+                memberScores.push({ name: member.nickname, score: score })
+                break
+              }
+            }
+          }
+        }
+
+        // 降级路径:取家庭成员该项食材的最小值(最保守值),比 avg 更安全
+        if (memberScores.length > 0) {
+          var minScore = memberScores[0].score
+          for (var mi = 1; mi < memberScores.length; mi++) {
+            if (memberScores[mi].score < minScore) minScore = memberScores[mi].score
+          }
+          overview.push({
+            id: food.id,
+            name: food.name,
+            category: food.category,
+            score: minScore,
+            scoreClass: self.getScoreClass(minScore),
+            color: self.getScoreClass(minScore),
+            statusText: self.getStatusText(minScore),
+            status: minScore >= 0 ? 'suitable' : 'unsuitable',
+            memberCount: memberScores.length,
+            memberScores: memberScores
+          })
+        }
+      }
+
+      // 按最小推荐值排序(从高到低)
+      overview.sort(function(a, b) {
+        return b.score - a.score
+      })
+
+      self.familyOverview = overview
+    },
+    getScoreClass: function(score) {
+      if (score === undefined || score === null) return 'neutral'
+      if (score >= 0) return 'good'
+      return 'bad'
+    },
+    getStatusText: function(score) {
+      if (score === undefined || score === null) return '未知'
+      if (score >= 0) return '适合'
+      return '戒备'
+    },
+    searchFood: function() {
+      // 搜索由 computed 自动处理
+    },
+    switchCategory: function(category) {
+      this.activeCategory = category
+    },
+    getCategoryKeywords: function(category) {
+      var map = {
+        '蔬菜': ['蔬菜'],
+        '水果': ['水果'],
+        '肉禽': ['肉类', '肉禽'],
+        '水产': ['水产', '水产品'],
+        '谷物': ['谷物', '主食', '全谷物'],
+        '蛋奶': ['蛋类', '蛋奶', '乳制品']
+      }
+      return map[category] || []
+    },
+    setFilter: function(type) {
+      this.filterType = type
+      // 适合/全部:倒序,推荐值大的排前面;戒备:升序,推荐值小的排前面
+      if (type === 'unsuitable') {
+        this.sortType = 'asc'
+      } else {
+        this.sortType = 'desc'
+      }
+    },
+    setSort: function(type) {
+      this.sortType = type
+    },
+    switchView: function(view) {
+      this.currentView = view
+    },
+    // 取「本人」行(isSelf):后端已按 user_id 折叠,此处仅取第一行 isSelf
+    getSelfRow: function() {
+      var members = this.memberList || []
+      for (var i = 0; i < members.length; i++) {
+        if (members[i] && members[i].isSelf) return members[i]
+      }
+      return null
+    },
+    switchMember: function(member) {
+      // 统一身份键:familyMemberId(= family_members.id);userId 仅用于折叠,不再作主键
+      this.currentMemberId = memberIdOf(member)
+      this.currentView = 'personal'
+      this.loadFoods()
+    },
+    onRefresh: function() {
+      var self = this
+      self.refreshing = true
+      self.loadFoods()
+      self.loadFamilyData()
+      setTimeout(function() {
+        self.refreshing = false
+      }, 500)
     }
     }
   }
   }
 }
 }
@@ -432,4 +861,313 @@ export default {
   font-size: calc(28rpx * var(--font-scale, 1)); color: $app-surface; font-weight: 600;
   font-size: calc(28rpx * var(--font-scale, 1)); color: $app-surface; font-weight: 600;
   box-shadow: 0 6rpx 20rpx rgba(255, 140, 66, 0.25);
   box-shadow: 0 6rpx 20rpx rgba(255, 140, 66, 0.25);
 }
 }
+
+/* ===== 推荐食材(原「食材查询」页迁移)===== */
+.section-title-bar {
+  display: flex;
+  flex-direction: column;
+  padding: 8rpx 0 16rpx;
+}
+.section-title {
+  font-size: calc(34rpx * var(--font-scale, 1));
+  font-weight: 700;
+  color: $facade-text-strong;
+}
+.section-subtitle {
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+  margin-top: 6rpx;
+}
+.food-section {
+  background-color: $app-surface;
+  border-radius: 24rpx;
+  padding: 20rpx;
+  margin-bottom: 32rpx;
+}
+
+/* 模式切换(排序) */
+.mode-bar {
+  display: flex;
+  padding: 16rpx 0;
+  gap: 20rpx;
+}
+.mode-btn {
+  flex: 1;
+  padding: 12rpx 0;
+  text-align: center;
+  border-radius: 24rpx;
+  background: $facade-fill-2;
+  color: $facade-text-body;
+  font-size: calc(26rpx * var(--font-scale, 1));
+}
+.mode-btn.active {
+  background: linear-gradient(135deg, $status-success, $status-success-light-3);
+  color: $app-surface;
+}
+
+/* 搜索栏 */
+.search-bar {
+  display: flex;
+  padding: 20rpx 0;
+}
+.search-input {
+  flex: 1;
+  border: 1rpx solid $facade-border-soft;
+  border-radius: 24rpx;
+  padding: 16rpx 24rpx;
+  font-size: calc(26rpx * var(--font-scale, 1));
+  background-color: $facade-fill-ghost;
+}
+.search-btn {
+  margin-left: 20rpx;
+  padding: 16rpx 32rpx;
+  background-color: $dim-body;
+  color: $app-surface;
+  border-radius: 24rpx;
+  font-size: calc(26rpx * var(--font-scale, 1));
+}
+
+/* 分类标签 */
+.category-tabs {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+.tab-item {
+  padding: 12rpx 24rpx;
+  border: 1rpx solid $facade-border-soft;
+  border-radius: 24rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $facade-text-body;
+  background-color: $app-surface;
+}
+.tab-active {
+  border-color: $dim-body;
+  color: $dim-body;
+  background-color: $flat-card;
+}
+
+/* 筛选栏 */
+.filter-bar {
+  display: flex;
+  gap: 20rpx;
+  margin-bottom: 20rpx;
+}
+.filter-item {
+  padding: 12rpx 24rpx;
+  border: 1rpx solid $facade-border-soft;
+  border-radius: 24rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $facade-text-body;
+  background-color: $app-surface;
+}
+.filter-active {
+  border-color: $mat-green;
+  color: $mat-green;
+  background-color: $mat-green-tint;
+}
+
+/* 家庭总览 */
+.family-overview {
+  background: $facade-fill-ghost-2;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  margin-bottom: 20rpx;
+}
+.overview-title {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  font-weight: 600;
+  color: $facade-text-strong;
+  margin-bottom: 16rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid $facade-fill;
+}
+.overview-item {
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid $facade-fill-2;
+}
+.overview-item:last-child {
+  border-bottom: none;
+}
+.overview-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8rpx;
+}
+.overview-detail {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.category-label {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+}
+.member-scores {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+.member-score {
+  font-size: calc(20rpx * var(--font-scale, 1));
+  padding: 4rpx 12rpx;
+  border-radius: 16rpx;
+  background: $facade-fill-2;
+}
+.score-green {
+  color: $status-success;
+  background: $mat-green-tint;
+}
+.score-red {
+  color: $status-danger;
+  background: $status-danger-soft;
+}
+.score-badge {
+  min-width: 72rpx;
+  text-align: center;
+  padding: 4rpx 14rpx;
+  border-radius: 16rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  font-weight: 700;
+  color: $app-surface;
+  background: linear-gradient(135deg, $dim-body, $dim-body-light);
+}
+.score-badge.good {
+  background: linear-gradient(135deg, $status-success, $status-success-light-3);
+}
+.score-badge.bad {
+  background: linear-gradient(135deg, $status-danger, $status-danger-deep);
+}
+
+/* 食材列表 */
+.food-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.food-card {
+  background: $facade-fill-ghost-2;
+  border-radius: 16rpx;
+  padding: 18rpx 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  box-sizing: border-box;
+}
+.food-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+.ingredient-badge {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 8rpx;
+  background: linear-gradient(135deg, $dim-body, $dim-body-light);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+.ingredient-badge.good {
+  background: linear-gradient(135deg, $status-success, $status-success-light-3);
+}
+.ingredient-badge.bad {
+  background: linear-gradient(135deg, $status-danger, $status-danger-deep);
+}
+.ingredient-badge-text {
+  font-size: calc(18rpx * var(--font-scale, 1));
+  color: $app-surface;
+  font-weight: 600;
+}
+.food-info {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+}
+.food-name {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  font-weight: 600;
+  color: $text-charcoal;
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.food-meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8rpx;
+  margin-top: 6rpx;
+}
+.meta-item {
+  flex-shrink: 0;
+  font-size: calc(20rpx * var(--font-scale, 1));
+  color: $facade-text-faint;
+  background: $facade-fill-2;
+  padding: 4rpx 14rpx;
+  border-radius: 8rpx;
+}
+.food-right {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  flex-shrink: 0;
+}
+.score-value {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  font-weight: 700;
+  min-width: 60rpx;
+  text-align: right;
+}
+.score-value.good { color: $status-success; }
+.score-value.bad { color: $status-danger; }
+.food-status-badge {
+  padding: 6rpx 16rpx;
+  border-radius: 24rpx;
+  font-size: calc(22rpx * var(--font-scale, 1));
+  font-weight: 500;
+}
+.badge-suitable {
+  background: $mat-green-tint;
+  color: $mat-green;
+}
+.badge-unsuitable {
+  background: $fortune-0-tint;
+  color: $mat-red;
+}
+
+/* 空状态 */
+.food-empty-state {
+  text-align: center;
+  padding: 60rpx 40rpx;
+}
+.food-empty-icon {
+  font-size: calc(80rpx * var(--font-scale, 1));
+  display: block;
+  margin-bottom: 20rpx;
+}
+.food-empty-text {
+  font-size: calc(30rpx * var(--font-scale, 1));
+  color: $facade-text-strong;
+  font-weight: 500;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.loading-state {
+  text-align: center;
+  padding: 60rpx 0;
+}
+.loading-text {
+  font-size: calc(26rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+}
 </style>
 </style>

+ 4 - 29
cfc-frontend/pages/growth-main/index.vue

@@ -207,20 +207,6 @@
         </view>
         </view>
       </view>
       </view>
 
 
-      <!-- 食材戒备匹配 -->
-      <view class="section" v-if="cautionCount > 0">
-        <view class="action-card" @click="goToCaution">
-          <view class="action-icon-wrap danger">
-            <text class="action-icon">⚠️</text>
-          </view>
-          <view class="action-info">
-            <text class="action-title">食材戒备匹配</text>
-            <text class="action-desc">{{ cautionCount }} 种食材需关注,点击查看戒备详情</text>
-          </view>
-          <text class="action-arrow">›</text>
-        </view>
-      </view>
-
       <!-- 饮食推荐 -->
       <!-- 饮食推荐 -->
       <view class="section">
       <view class="section">
         <view class="action-card" @click="goToFoods">
         <view class="action-card" @click="goToFoods">
@@ -287,14 +273,14 @@ import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
-import { healthCheckinList, getHealthMealList, getFoodCautionList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList, getTodayTasks, reportAppEnter } from '../../utils/api.js'
+import { healthCheckinList, getHealthMealList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList, getTodayTasks, reportAppEnter } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import shareMixin from '../../components/share-mixin.js'
 import shareMixin from '../../components/share-mixin.js'
 export default {
 export default {
   mixins: [fontScaleMixin, shareMixin],
   mixins: [fontScaleMixin, shareMixin],
   components: { LoginGuideCard, DimensionActivities, DimensionProducts, ArticleBookshelf },
   components: { LoginGuideCard, DimensionActivities, DimensionProducts, ArticleBookshelf },
-  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], recentTasks: [], todayTaskList: [], streakDays: 0, activeChallenges: 0, completeness: 0, cautionCount: 0,
+  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], recentTasks: [], todayTaskList: [], streakDays: 0, activeChallenges: 0, completeness: 0,
       healthArticles: [],
       healthArticles: [],
       growthArticles: [],
       growthArticles: [],
       isLoggedIn: false } },
       isLoggedIn: false } },
@@ -376,12 +362,9 @@ export default {
     goMoreActivities: function() { uni.navigateTo({ url: '/pages/activity/index' }) },
     goMoreActivities: function() { uni.navigateTo({ url: '/pages/activity/index' }) },
     goToTasks: function() { uni.navigateTo({ url: '/pages/child/tasks' }) },
     goToTasks: function() { uni.navigateTo({ url: '/pages/child/tasks' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
-    goToCaution: function() {
-      var mid = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || ''
-      uni.navigateTo({ url: '/pages/diet/food-query' + (mid ? '?memberId=' + mid : '') })
-    },
     goToFoods: function() {
     goToFoods: function() {
-      uni.navigateTo({ url: '/pages/diet/food-query' })
+      var mid = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || ''
+      uni.navigateTo({ url: '/pages/diet/recommendation' + (mid ? '?memberId=' + mid : '') })
     },
     },
     goToArticle: function(article) { var id = article && article.id ? article.id : article; if (!id) return; uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id }) },
     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/discover-detail/product-detail/product-detail?id=' + id }) },
     goToProduct: function(prod) { var id = prod && prod.id ? prod.id : prod; if (!id) return; uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + id }) },
@@ -425,7 +408,6 @@ export default {
       var self = this
       var self = this
       try { await self.loadProgressItems() } catch(e) {}
       try { await self.loadProgressItems() } catch(e) {}
       try { await self.loadRecentTasks() } catch(e) {}
       try { await self.loadRecentTasks() } catch(e) {}
-      try { await self.loadCautionData() } catch(e) {}
       try { await self.loadGrowthRecommendations() } catch(e) {}
       try { await self.loadGrowthRecommendations() } catch(e) {}
       try { await self.loadMilestones() } catch(e) {}
       try { await self.loadMilestones() } catch(e) {}
     },
     },
@@ -526,13 +508,6 @@ export default {
         }
         }
       } catch(e) {}
       } catch(e) {}
     },
     },
-    async loadCautionData() {
-      var self = this
-      try {
-        var res = await getFoodCautionList(self.memberId)
-        if (res && res.code === 200) self.cautionCount = (res.data || []).length
-      } catch(e) {}
-    },
     async loadMilestones() {
     async loadMilestones() {
       var self = this
       var self = this
       try {
       try {

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

@@ -65,11 +65,6 @@
           <text class="nav-label">饮食推荐</text>
           <text class="nav-label">饮食推荐</text>
           <text class="nav-count">{{ foodCount }}项</text>
           <text class="nav-count">{{ foodCount }}项</text>
         </view>
         </view>
-        <view class="nav-card caution-card" @tap="goToCaution" v-if="cautionCount > 0">
-          <text class="nav-icon">⚠️</text>
-          <text class="nav-label">食材戒备</text>
-          <text class="nav-count caution-count">{{ cautionCount }}项</text>
-        </view>
       </view>
       </view>
 
 
       <!-- 指标变化(最近两次报告) -->
       <!-- 指标变化(最近两次报告) -->
@@ -372,7 +367,7 @@
 
 
 <script>
 <script>
 import { normalizeMembers, memberIdOf } from '../../utils/memberNormalize.js'
 import { normalizeMembers, memberIdOf } from '../../utils/memberNormalize.js'
-import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList, getFamilyMemberList, addFamilyMember, getIndicatorChangeRanking } from '../../utils/api.js'
+import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFamilyMemberList, addFamilyMember, getIndicatorChangeRanking } from '../../utils/api.js'
 import HealthKnowledgePopup from '@/pages/health/components/health-knowledge-popup.vue'
 import HealthKnowledgePopup from '@/pages/health/components/health-knowledge-popup.vue'
 import { BrandColors } from '../../utils/brand-colors.js'
 import { BrandColors } from '../../utils/brand-colors.js'
 import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
 import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
@@ -415,7 +410,6 @@ export default {
       riskCount: 0,
       riskCount: 0,
       riskAttentionCount: 0,
       riskAttentionCount: 0,
       foodCount: 0,
       foodCount: 0,
-      cautionCount: 0,
       indicatorCount: 0,
       indicatorCount: 0,
       knowledgeVisible: false,
       knowledgeVisible: false,
       knowledgeData: {},
       knowledgeData: {},
@@ -685,15 +679,6 @@ riskGroupAbnormalCount: function() {
               self.foodCount = (Array.isArray(foodRes.data) ? foodRes.data : []).length
               self.foodCount = (Array.isArray(foodRes.data) ? foodRes.data : []).length
             }
             }
           }).catch(function() {})
           }).catch(function() {})
-          // 异步加载戒备食材数量(按报告 subjectId 查,本人 userId 查不到成员的推荐指数)
-          var cautionUserId = (self.report && self.report.subjectId) || uni.getStorageSync('userId')
-          if (cautionUserId) {
-            getFoodCautionList(cautionUserId).then(function(cautionRes) {
-              if (cautionRes && cautionRes.code === 200) {
-                self.cautionCount = (cautionRes.data || []).length
-              }
-            }).catch(function() {})
-          }
           self.indicatorCount = (res.data.indicators || []).length
           self.indicatorCount = (res.data.indicators || []).length
         }
         }
       })
       })
@@ -1054,13 +1039,6 @@ applyDraftPayload: function(payload) {
     goToFoods: function() {
     goToFoods: function() {
       uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail?reportId=' + this.reportId })
       uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail?reportId=' + this.reportId })
     },
     },
-    goToCaution: function() {
-      // 报告详情入口:传 reportId(按报告快照返回戒备食材)+ memberId(报告绑定主体 subjectId)
-      var mid = (this.report && this.report.subjectId) || uni.getStorageSync('currentMemberId') || ''
-      var url = '/pages/diet/food-query?reportId=' + this.reportId
-      if (mid) url += '&memberId=' + mid
-      uni.navigateTo({ url: url })
-    },
     goToIndicators: function() {
     goToIndicators: function() {
       // 展开所有营养分组手风琴
       // 展开所有营养分组手风琴
       var arr = []
       var arr = []
@@ -1161,12 +1139,6 @@ $c-gf-teal-ink:  #004D40; // teal-900 深文字
 
 
 .quick-nav { display: flex; padding: 20rpx; background: $app-surface; margin-bottom: 16rpx; }
 .quick-nav { display: flex; padding: 20rpx; background: $app-surface; margin-bottom: 16rpx; }
 .nav-card { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 16rpx 0; }
 .nav-card { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 16rpx 0; }
-.caution-card {
-  background: $status-danger-tint-2;
-  border-radius: 16rpx;
-  border: 2rpx solid $danger-line;
-}
-.caution-count { color: $status-danger-deep; font-weight: 700; }
 .nav-icon { font-size: calc(40rpx * var(--font-scale, 1)); margin-bottom: 6rpx; }
 .nav-icon { font-size: calc(40rpx * var(--font-scale, 1)); margin-bottom: 6rpx; }
 .nav-label { font-size: calc(24rpx * var(--font-scale, 1)); color: $facade-text-strong; }
 .nav-label { font-size: calc(24rpx * var(--font-scale, 1)); color: $facade-text-strong; }
 .nav-count { font-size: calc(20rpx * var(--font-scale, 1)); color: $facade-text-muted; }
 .nav-count { font-size: calc(20rpx * var(--font-scale, 1)); color: $facade-text-muted; }