瀏覽代碼

feat(miniprogram): 食材戒备匹配功能

- 后端: FoodRecommendService.getCautionFoods() + /api/food/recommendation/caution 接口
- 前端: 新建 gut-flora-caution-detail.vue 页面,倒序展示 indexScore < 0 的食材
- 前端: gut-flora-detail.vue 新增'食材戒备'入口卡片(有戒备食材时显示)
- 前端: api.js 新增 getFoodCautionList()
iwt 1 月之前
父節點
當前提交
c065ec5d30

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/FoodRecommendController.java

@@ -53,4 +53,13 @@ public class FoodRecommendController {
         Map<Long, List<FoodRecommendIndex>> result = foodRecommendService.getByFamily(familyId);
         return Result.success(result);
     }
+
+    @Operation(summary = "获取戒备食材列表(推荐指数 < 0)")
+    @PostMapping("/caution")
+    public Result<List<FoodRecommendIndex>> getCautionFoods(
+            @RequestAttribute("userId") Long userId) {
+        if (userId == null) return Result.error("请先登录");
+        List<FoodRecommendIndex> foods = foodRecommendService.getCautionFoods(userId);
+        return Result.success(foods);
+    }
 }

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FoodRecommendService.java

@@ -165,4 +165,15 @@ public class FoodRecommendService {
         }
         return result;
     }
+    /**
+     * 获取戒备食材列表(推荐指数 < 0),按指数升序排列(最需戒备的排最前)
+     */
+    public List<FoodRecommendIndex> getCautionFoods(Long userId) {
+        return foodRecommendIndexMapper.selectList(
+                new LambdaQueryWrapper<FoodRecommendIndex>()
+                        .eq(FoodRecommendIndex::getUserId, userId)
+                        .lt(FoodRecommendIndex::getIndexScore, 0)
+                        .orderByAsc(FoodRecommendIndex::getIndexScore)
+        );
+    }
 }

+ 7 - 1
cfc-frontend/pages.json

@@ -906,6 +906,12 @@
           "style": {
             "navigationBarTitleText": "报告详情"
           }
+        },
+        {
+          "path": "gut-flora-caution-detail",
+          "style": {
+            "navigationBarTitleText": "食材戒备匹配"
+          }
         }
       ]
     },
@@ -1571,4 +1577,4 @@
       "pages/*": "components/@dcloudio/uni-$2/components/uni-$1/uni-$1.vue"
     }
   }
-}
+}

+ 300 - 0
cfc-frontend/pages/health/gut-flora-caution-detail.vue

@@ -0,0 +1,300 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">食材戒备匹配</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <view class="warning-banner">
+      <text class="banner-icon">⚠️</text>
+      <text class="banner-text">以下食材推荐指数低于 0,建议谨慎食用或避免</text>
+    </view>
+
+    <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
+      <view class="food-card" v-for="(item, idx) in foods" :key="idx">
+        <view class="food-header">
+          <text class="food-name">{{ item.foodName }}</text>
+          <view class="caution-badge">戒备</view>
+        </view>
+        <view class="food-meta">
+          <text class="meta-item" v-if="item.category">{{ item.category }}</text>
+        </view>
+        <view class="food-score-row">
+          <view class="score-bar-wrap">
+            <view class="score-bar-bg">
+              <view class="score-bar-fill danger" :style="'width:' + Math.min(Math.abs(item.indexScore) / 50 * 100, 100) + '%'"></view>
+            </view>
+            <text class="score-value danger">{{ item.indexScore }}</text>
+          </view>
+          <text class="score-label">推荐指数</text>
+        </view>
+        <view class="food-nutrition" v-if="item.nutrition">
+          <text class="nut-item">蛋白 {{ item.nutrition.protein }}g</text>
+          <text class="nut-item">脂肪 {{ item.nutrition.fat }}g</text>
+          <text class="nut-item">碳水 {{ item.nutrition.carbs }}g</text>
+        </view>
+      </view>
+
+      <view class="empty-state" v-if="!loading && foods.length === 0">
+        <text class="empty-icon">✅</text>
+        <text class="empty-text">暂无戒备食材</text>
+        <text class="empty-hint">您的菌群报告未显示需要戒备的食材</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 { getFoodCautionList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      foods: [],
+      loading: true,
+      refreshing: false,
+      nutritionCache: {}
+    }
+  },
+  onLoad() {
+    this.loadFoods()
+  },
+  methods: {
+    goBack() {
+      uni.navigateBack()
+    },
+    async loadFoods() {
+      this.loading = true
+      try {
+        var res = await getFoodCautionList()
+        if (res && res.code === 200 && res.data) {
+          this.foods = res.data
+          // 批量加载营养数据
+          this.loadNutrition()
+        } else {
+          this.foods = []
+        }
+      } catch (e) {
+        console.error('加载戒备食材失败', e)
+        this.foods = []
+      } finally {
+        this.loading = false
+        this.refreshing = false
+      }
+    },
+    async loadNutrition() {
+      if (!this.foods.length) return
+      try {
+        var kbRes = await this.getKnowledgeSection('食物库')
+        if (kbRes && kbRes.data && kbRes.data['数据']) {
+          var kbFoods = kbRes.data['数据']
+          var cache = {}
+          for (var i = 0; i < kbFoods.length; i++) {
+            var f = kbFoods[i]
+            cache[f['名称']] = {
+              protein: f['蛋白g'] || null,
+              fat: f['脂肪g'] || null,
+              carbs: f['碳水化合物g'] || null,
+              fiber: f['总膳食纤维g'] || null,
+              energy: f['能量KJ'] || null,
+              category: f['分类'] || ''
+            }
+          }
+          this.nutritionCache = cache
+          this.foods = this.foods.map(function(item) {
+            var n = cache[item.foodName] || {}
+            return Object.assign({}, item, {
+              nutrition: n,
+              category: item.category || n.category || ''
+            })
+          })
+          this.$forceUpdate()
+        }
+      } catch (e) {
+        console.warn('加载营养数据失败', e)
+      }
+    },
+    getKnowledgeSection(key) {
+      return new Promise(function(resolve) {
+        uni.request({
+          url: getApp().globalData.baseApi + '/api/kb/section',
+          method: 'POST',
+          header: {
+            'Content-Type': 'application/json',
+            'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '')
+          },
+          data: { key: key },
+          success: function(res) { resolve(res.data) },
+          fail: function() { resolve({}) }
+        })
+      })
+    },
+    onRefresh() {
+      this.refreshing = true
+      this.loadFoods()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  background: #f5f6fa;
+}
+.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-placeholder { width: 80rpx; }
+
+.warning-banner {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  padding: 20rpx 30rpx;
+  background: #FFF3CD;
+  border-bottom: 1rpx solid #FFECB5;
+}
+.banner-icon { font-size: 32rpx; }
+.banner-text { font-size: 24rpx; color: #856404; flex: 1; }
+
+.content {
+  padding: 20rpx 30rpx;
+  height: calc(100vh - 160rpx);
+}
+
+.food-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  border-left: 6rpx solid #EF4444;
+  box-shadow: 0 2rpx 8rpx rgba(239, 68, 68, 0.08);
+}
+.food-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8rpx;
+}
+.food-name {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+}
+.caution-badge {
+  padding: 4rpx 16rpx;
+  background: #EF4444;
+  color: #fff;
+  border-radius: 20rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+}
+.food-meta {
+  margin-bottom: 16rpx;
+}
+.meta-item {
+  font-size: 24rpx;
+  color: #888;
+  background: #f5f5f5;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+}
+.food-score-row {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  margin-bottom: 12rpx;
+}
+.score-bar-wrap {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.score-bar-bg {
+  flex: 1;
+  height: 12rpx;
+  background: #fee2e2;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.score-bar-fill {
+  height: 100%;
+  border-radius: 6rpx;
+  transition: width 0.3s ease;
+}
+.score-bar-fill.danger {
+  background: linear-gradient(90deg, #EF4444, #DC2626);
+}
+.score-value {
+  font-size: 28rpx;
+  font-weight: 700;
+  color: #EF4444;
+  min-width: 60rpx;
+  text-align: right;
+}
+.score-label {
+  font-size: 24rpx;
+  color: #999;
+  white-space: nowrap;
+}
+.food-nutrition {
+  display: flex;
+  gap: 20rpx;
+  flex-wrap: wrap;
+}
+.nut-item {
+  font-size: 22rpx;
+  color: #999;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 100rpx 40rpx;
+}
+.empty-icon {
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.empty-text {
+  font-size: 30rpx;
+  color: #333;
+  font-weight: 500;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.empty-hint {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+}
+
+.loading-state {
+  text-align: center;
+  padding: 80rpx 0;
+}
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+
+.bottom-spacer { height: 40rpx; }
+</style>

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

@@ -67,6 +67,11 @@
           <text class="nav-label">饮食推荐</text>
           <text class="nav-count">{{ foodCount }}项</text>
         </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 class="nav-card" @tap="goToIndicators">
           <text class="nav-icon">📋</text>
           <text class="nav-label">全部指标</text>
@@ -257,6 +262,7 @@ export default {
       floraCount: 0,
       riskCount: 0,
       foodCount: 0,
+      cautionCount: 0,
       indicatorCount: 0,
       pathogenCount: 0,
       knowledgeVisible: false,
@@ -295,6 +301,15 @@ export default {
           self.floraCount = (res.data.gutFlora || []).length
           self.riskCount = (res.data.diseaseRisks || []).length
           self.foodCount = (res.data.foods || []).length
+          // 异步加载戒备食材数量
+          var userId = uni.getStorageSync('userId')
+          if (userId) {
+            getFoodCautionList().then(function(cautionRes) {
+              if (cautionRes && cautionRes.code === 200) {
+                self.cautionCount = (cautionRes.data || []).length
+              }
+            }).catch(function() {})
+          }
           self.indicatorCount = (res.data.indicators || []).length
           // 计算病原菌总数
           var flora = res.data.gutFlora || []
@@ -464,6 +479,9 @@ export default {
     goToFoods: function() {
       uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail?reportId=' + this.reportId })
     },
+    goToCaution: function() {
+      uni.navigateTo({ url: '/pages/health/gut-flora-caution-detail' })
+    },
     goToIndicators: function() {
       this.accordionOpen.nutri = true
     },
@@ -495,6 +513,12 @@ export default {
 
 .quick-nav { display: flex; padding: 20rpx; background: #fff; margin-bottom: 16rpx; }
 .nav-card { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 16rpx 0; }
+.caution-card {
+  background: #FEF2F2;
+  border-radius: 16rpx;
+  border: 2rpx solid #FECACA;
+}
+.caution-count { color: #DC2626; font-weight: 700; }
 .nav-icon { font-size: 40rpx; margin-bottom: 6rpx; }
 .nav-label { font-size: 24rpx; color: #333; }
 .nav-count { font-size: 20rpx; color: #999; }

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

@@ -2604,3 +2604,6 @@ export const saveMealConfig = (data) => request('/api/diet/meals/config/save', '
 
 // ===== 北京菌群报告 — 菌群功能分析 =====
 export const getGutFloraAnalysis = (reportId) => request('/api/nutrition/beijing/analysis', 'POST', { reportId })
+
+// ===== 食材戒备匹配 =====
+export const getFoodCautionList = () => request('/api/food/recommendation/caution', 'POST', {})

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-393f72de88b0dce758152fdad4031e99d56754d9
+c991b876cff41beab4e089a89539776d407108fb

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

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

+ 1 - 1
cfc-web/package.json

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

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

@@ -4,6 +4,35 @@
 
 ---
 
+## v1.0.982 (2026-08-11)
+
+### 新功能
+- 新增成就页面 achievement/index.vue
+- 实现服务角色申请、报告详情、成就页面,修复占位按钮
+
+
+## v1.0.981 (2026-08-11)
+
+### 新功能
+- 新增应季食材管理后台页面
+
+### 其他
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.980 (2026-08-11)
+
+### 新功能
+- 新增子维度自检题库(行6维/心自驱力自我概念/智成长型思维/富家族传承)及计分
+- 心13维分组雷达图+行6维关系域雷达图+智成长型思维加入认知雷达
+- 五维体系新增心13维/行6维/智成长型思维/富家族传承子维度字段与计算逻辑
+
+### Bug 修复
+- 自驱力/自我概念/成长型思维解析改为嵌套Map+中文key的structuredAnalysis真实结构
+
+
 ## v1.0.979 (2026-08-11)
 
 ### Bug 修复

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.979
+> 当前版本: v1.0.982
 
 ## 历史版本
 
@@ -8,6 +8,35 @@
 
 ---
 
+## v1.0.982 (2026-08-11)
+
+### 新功能
+- 新增成就页面 achievement/index.vue
+- 实现服务角色申请、报告详情、成就页面,修复占位按钮
+
+
+## v1.0.981 (2026-08-11)
+
+### 新功能
+- 新增应季食材管理后台页面
+
+### 其他
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.980 (2026-08-11)
+
+### 新功能
+- 新增子维度自检题库(行6维/心自驱力自我概念/智成长型思维/富家族传承)及计分
+- 心13维分组雷达图+行6维关系域雷达图+智成长型思维加入认知雷达
+- 五维体系新增心13维/行6维/智成长型思维/富家族传承子维度字段与计算逻辑
+
+### Bug 修复
+- 自驱力/自我概念/成长型思维解析改为嵌套Map+中文key的structuredAnalysis真实结构
+
+
 ## v1.0.979 (2026-08-11)
 
 ### Bug 修复