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

feat: add food recommendation API endpoint, FoodService.listByReportId, and frontend food display in health report

Sisyphus 2 месяцев назад
Родитель
Сommit
aaf29704f3

+ 16 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -16,6 +16,7 @@ import com.etotem.cfc.entity.NutritionDeficiencyRecord;
 import com.etotem.cfc.entity.NutritionIndicatorMapping;
 import com.etotem.cfc.mapper.FoodMapper;
 import com.etotem.cfc.service.ChildNutritionProfileService;
+import com.etotem.cfc.service.FoodService;
 import com.etotem.cfc.service.HealthAnalysisService;
 import com.etotem.cfc.service.HealthReportService;
 import com.etotem.cfc.service.NutritionDeficiencyService;
@@ -65,6 +66,9 @@ public class HealthReportController {
     @Resource
     private FoodMapper foodMapper;
 
+    @Resource
+    private FoodService foodService;
+
     /**
      * 创建健康报告(含指标明细)
      */
@@ -515,6 +519,18 @@ public class HealthReportController {
         return Result.success(null);
     }
 
+    @Operation(summary = "获取食材推荐列表")
+    @PostMapping("/foods")
+    public Result<List<Food>> listFoods(@RequestBody Map<String, Object> params) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        if (reportId == null) {
+            return Result.error("reportId不能为空");
+        }
+        List<Food> foods = foodService.listByReportId(reportId);
+        return Result.success(foods);
+    }
+
     /**
      * 归档报告
      */

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

@@ -47,4 +47,15 @@ public class FoodService {
     public void delete(Long id) {
         foodMapper.deleteById(id);
     }
+
+    public List<Food> listByReportId(Long reportId) {
+        if (reportId == null) {
+            return new java.util.ArrayList<>();
+        }
+        return foodMapper.selectList(
+                new LambdaQueryWrapper<Food>()
+                        .eq(Food::getReportId, reportId)
+                        .orderByAsc(Food::getSortOrder)
+        );
+    }
 }

+ 132 - 0
cfc-frontend/pages/body/health-report.vue

@@ -125,6 +125,45 @@
       </view>
     </view>
 
+    <!-- ===== 食材推荐 ===== -->
+    <view class="section" v-if="foods && foods.length > 0">
+      <view class="section-title">
+        <text>🥗 食材推荐</text>
+        <text class="section-sub">共 {{ foods.length }} 项</text>
+      </view>
+      <view class="food-list">
+        <view class="food-card" v-for="food in foods" :key="food.id">
+          <view class="food-header">
+            <text class="food-name">{{ food.name }}</text>
+            <view class="food-score" :class="getFoodScoreClass(food.score)">
+              <text>{{ food.score || '--' }}</text>
+            </view>
+          </view>
+          <view class="food-meta" v-if="food.category">
+            <text class="food-category">{{ food.category }}</text>
+          </view>
+          <view class="food-nutrition" v-if="food.energyKj || food.protein || food.fat || food.carbs">
+            <view class="nutrition-item" v-if="food.energyKj">
+              <text class="n-label">能量</text>
+              <text class="n-value">{{ food.energyKj }} kJ</text>
+            </view>
+            <view class="nutrition-item" v-if="food.protein">
+              <text class="n-label">蛋白质</text>
+              <text class="n-value">{{ food.protein }}g</text>
+            </view>
+            <view class="nutrition-item" v-if="food.fat">
+              <text class="n-label">脂肪</text>
+              <text class="n-value">{{ food.fat }}g</text>
+            </view>
+            <view class="nutrition-item" v-if="food.carbs">
+              <text class="n-label">碳水</text>
+              <text class="n-value">{{ food.carbs }}g</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
     <!-- ===== 五维雷达图 (菌群报告) ===== -->
     <view class="section" v-if="report && report.reportType === 'gut_flora' && radarDimensions.length > 0">
       <view class="section-title">
@@ -184,6 +223,7 @@ export default {
       reportDetail: null,
       indicators: [],
       deficiencyRecords: [],
+      foods: [],
       loading: false
     }
   },
@@ -339,6 +379,13 @@ export default {
             }).catch(function(e) {
               console.log('获取报告详情失败', e)
             })
+            healthRequest('/api/health/foods', { reportId: self.reportId }).then(function(res) {
+              if (res.data) {
+                self.foods = res.data
+              }
+            }).catch(function(e) {
+              console.log('获取食材推荐失败', e)
+            })
           }
         }
       }).catch(function(e) {
@@ -445,6 +492,12 @@ export default {
 
       ctx.draw()
     },
+    getFoodScoreClass: function(score) {
+      if (score == null) return 'score-unknown'
+      if (score >= 80) return 'score-high'
+      if (score >= 60) return 'score-mid'
+      return 'score-low'
+    },
     formatDate: function(dateStr) {
       if (!dateStr) return '--'
       try {
@@ -867,4 +920,83 @@ export default {
   height: 300px;
   margin: 0 auto;
 }
+
+/* ===== 食材推荐 ===== */
+.food-list {
+  display: flex;
+  flex-direction: column;
+}
+.food-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  margin-bottom: 12rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.food-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8rpx;
+}
+.food-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+.food-score {
+  font-size: 24rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 16rpx;
+  font-weight: 600;
+}
+.score-high {
+  background: #DCFCE7;
+  color: #16A34A;
+}
+.score-mid {
+  background: #FEF3C7;
+  color: #D97706;
+}
+.score-low {
+  background: #FEE2E2;
+  color: #DC2626;
+}
+.score-unknown {
+  background: #F3F4F6;
+  color: #6B7280;
+}
+.food-meta {
+  margin-bottom: 8rpx;
+}
+.food-category {
+  font-size: 22rpx;
+  color: #6B7280;
+  background: #F3F4F6;
+  padding: 2rpx 12rpx;
+  border-radius: 12rpx;
+}
+.food-nutrition {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  margin-top: 8rpx;
+}
+.nutrition-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  min-width: 100rpx;
+}
+.n-label {
+  font-size: 20rpx;
+  color: #9CA3AF;
+}
+.n-value {
+  font-size: 24rpx;
+  color: #374151;
+  font-weight: 500;
+}
 </style>