diff --git a/components/model/food/FoodLibraryModal.tsx b/app/food-library.tsx similarity index 57% rename from components/model/food/FoodLibraryModal.tsx rename to app/food-library.tsx index 8e1453c..d727984 100644 --- a/components/model/food/FoodLibraryModal.tsx +++ b/app/food-library.tsx @@ -1,7 +1,9 @@ +import type { FoodItem } from '@/components/model/food/FoodDetailModal'; +import { FoodDetailModal } from '@/components/model/food/FoodDetailModal'; import { Ionicons } from '@expo/vector-icons'; +import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useState } from 'react'; import { - Modal, SafeAreaView, ScrollView, StatusBar, @@ -12,15 +14,6 @@ import { View, } from 'react-native'; -// 食物数据类型 -export interface FoodItem { - id: string; - name: string; - emoji: string; - calories: number; - unit: string; // 单位,如 "100克" -} - // 食物分类类型 export interface FoodCategory { id: string; @@ -28,14 +21,6 @@ export interface FoodCategory { foods: FoodItem[]; } -// 食物库弹窗属性 -export interface FoodLibraryModalProps { - visible: boolean; - onClose: () => void; - onSelectFood: (food: FoodItem) => void; - mealType?: 'breakfast' | 'lunch' | 'dinner' | 'snack'; -} - // 模拟食物数据 const FOOD_DATA: FoodCategory[] = [ { @@ -118,14 +103,15 @@ const MEAL_TYPE_MAP = { snack: '加餐' }; -export function FoodLibraryModal({ - visible, - onClose, - onSelectFood, - mealType = 'breakfast' -}: FoodLibraryModalProps) { +export default function FoodLibraryScreen() { + const router = useRouter(); + const params = useLocalSearchParams<{ mealType?: string }>(); + const mealType = (params.mealType as 'breakfast' | 'lunch' | 'dinner' | 'snack') || 'breakfast'; + const [selectedCategoryId, setSelectedCategoryId] = useState('common'); const [searchText, setSearchText] = useState(''); + const [selectedFood, setSelectedFood] = useState(null); + const [showFoodDetail, setShowFoodDetail] = useState(false); // 获取当前选中的分类 const selectedCategory = FOOD_DATA.find(cat => cat.id === selectedCategoryId); @@ -135,118 +121,152 @@ export function FoodLibraryModal({ food.name.toLowerCase().includes(searchText.toLowerCase()) ) || []; - // 处理食物选择 + // 处理食物选择 - 显示详情弹窗 const handleSelectFood = (food: FoodItem) => { - onSelectFood(food); - onClose(); + console.log('选择食物:', food); + setSelectedFood(food); + setShowFoodDetail(true); + console.log('设置弹窗状态:', { + showFoodDetail: true, + selectedFood: food, + foodName: food.name, + foodId: food.id + }); + }; + + // 处理食物保存 + const handleSaveFood = (food: FoodItem, amount: number, unit: string) => { + // 这里可以处理保存逻辑,比如添加到营养记录 + console.log('保存食物:', food, amount, unit); + setShowFoodDetail(false); + router.back(); // 返回上一页 + }; + + // 关闭详情弹窗 + const handleCloseFoodDetail = () => { + setShowFoodDetail(false); + setSelectedFood(null); }; return ( - - - + + - {/* 头部 */} - - - - - 食物库 - - 自定义 - - + {/* 头部 */} + + router.back()} style={styles.backButton}> + + + 食物库 + { + const testFood: FoodItem = { + id: 'test', + name: '测试食物', + emoji: '🍎', + calories: 100, + unit: '100克' + }; + handleSelectFood(testFood); + }} + > + 测试弹窗 + + - {/* 搜索框 */} - - - - + {/* 搜索框 */} + + + + - {/* 主要内容区域 - 卡片样式 */} - - - {/* 左侧分类导航 */} - - - {FOOD_DATA.map((category) => ( - setSelectedCategoryId(category.id)} - > - - {category.name} - - - ))} - - + {/* 主要内容区域 - 卡片样式 */} + + + {/* 左侧分类导航 */} + + + {FOOD_DATA.map((category) => ( + setSelectedCategoryId(category.id)} + > + + {category.name} + + + ))} + + - {/* 右侧食物列表 */} - - - {filteredFoods.map((food) => ( - - - {food.emoji} - - {food.name} - - {food.calories}千卡/{food.unit} - - + {/* 右侧食物列表 */} + + + {filteredFoods.map((food) => ( + + + {food.emoji} + + {food.name} + + {food.calories}千卡/{food.unit} + - handleSelectFood(food)} - > - - - ))} + handleSelectFood(food)} + > + + + + ))} - {filteredFoods.length === 0 && ( - - 暂无食物数据 - - )} - - + {filteredFoods.length === 0 && ( + + 暂无食物数据 + + )} + + - {/* 底部餐次选择和记录按钮 */} - - - - {MEAL_TYPE_MAP[mealType]} - - - - - 记录 - + {/* 底部餐次选择和记录按钮 */} + + + + {MEAL_TYPE_MAP[mealType]} + - - + + + 记录 + + + + {/* 食物详情弹窗 */} + + ); } diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index 48b1ceb..232ccd6 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -7,7 +7,7 @@ import { router } from 'expo-router'; import React, { useMemo, useState } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { RadarCategory, RadarChart } from './RadarChart'; -import { FoodItem, FoodLibraryModal } from './model/food/FoodLibraryModal'; + export type NutritionRadarCardProps = { nutritionSummary: NutritionSummary | null; @@ -38,7 +38,6 @@ export function NutritionRadarCard({ resetToken, onMealPress }: NutritionRadarCardProps) { - const [showFoodLibrary, setShowFoodLibrary] = useState(false); const [currentMealType, setCurrentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast'); const radarValues = useMemo(() => { // 基于推荐日摄入量计算分数 @@ -115,12 +114,7 @@ export function NutritionRadarCard({ }; const handleAddFood = () => { - setShowFoodLibrary(true); - }; - - const handleSelectFood = (food: FoodItem) => { - console.log('选择了食物:', food); - // 这里可以添加将食物添加到营养记录的逻辑 + router.push(`/food-library?mealType=${currentMealType}`); }; return ( @@ -193,13 +187,7 @@ export function NutritionRadarCard({ - {/* 食物库弹窗 */} - setShowFoodLibrary(false)} - onSelectFood={handleSelectFood} - mealType={currentMealType} - /> + ); } diff --git a/components/model/food/DEBUG.md b/components/model/food/DEBUG.md new file mode 100644 index 0000000..328d006 --- /dev/null +++ b/components/model/food/DEBUG.md @@ -0,0 +1,37 @@ +# FoodDetailModal 弹窗问题修复记录 + +## 问题描述 +FoodDetailModal弹窗打开后没有内容显示 + +## 问题分析 +1. **导入路径问题**: 原来FoodDetailModal试图从已删除的FoodLibraryModal导入FoodItem类型 +2. **类型定义冲突**: FoodItem类型在多个文件中重复定义 +3. **Modal嵌套问题**: 已通过将食物库改为页面解决 + +## 修复步骤 + +### 1. 修复类型导入问题 +- 在FoodDetailModal.tsx中重新定义FoodItem接口 +- 在food-library.tsx中从FoodDetailModal导入FoodItem类型 +- 更新food模块的导出文件 + +### 2. 添加调试信息 +- 在FoodDetailModal中添加console.log来跟踪数据传递 +- 在food-library.tsx中添加调试信息来确认食物选择逻辑 + +### 3. 确保正确的数据流 +``` +用户点击食物 -> handleSelectFood -> setSelectedFood + setShowFoodDetail -> FoodDetailModal接收props +``` + +## 测试方法 +1. 打开食物库页面 +2. 点击任意食物的加号按钮 +3. 检查控制台输出,确认数据传递正确 +4. 确认弹窗显示完整内容 + +## 预期结果 +- 弹窗正常显示食物信息 +- 营养数据正确计算和显示 +- 单位选择功能正常 +- 保存功能正常工作 \ No newline at end of file diff --git a/components/model/food/FoodDetailModal.tsx b/components/model/food/FoodDetailModal.tsx new file mode 100644 index 0000000..5a9cba5 --- /dev/null +++ b/components/model/food/FoodDetailModal.tsx @@ -0,0 +1,425 @@ +import { Ionicons } from '@expo/vector-icons'; +import React, { useEffect, useState } from 'react'; +import { + Dimensions, + Keyboard, + KeyboardAvoidingView, + Modal, + Platform, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; + +// 食物数据类型定义 +export interface FoodItem { + id: string; + name: string; + emoji: string; + calories: number; + unit: string; // 单位,如 "100克" +} + +// 营养信息接口 +interface NutritionInfo { + protein: number; // 蛋白质(克) + fat: number; // 脂肪(克) + carbs: number; // 碳水化合物(克) +} + +// 单位选项 +const UNIT_OPTIONS = [ + { id: 'gram', name: '克', ratio: 1 }, + { id: 'small', name: '小份', ratio: 0.8 }, + { id: 'medium', name: '中份', ratio: 1.2 }, + { id: 'large', name: '大份', ratio: 1.5 }, +]; + +// 食物详情弹窗属性 +export interface FoodDetailModalProps { + visible: boolean; + food: FoodItem | null; + onClose: () => void; + onSave: (food: FoodItem, amount: number, unit: string) => void; +} + +// 模拟营养数据 +const getNutritionInfo = (foodId: string): NutritionInfo => { + const nutritionData: Record = { + '5': { protein: 0.8, fat: 0.6, carbs: 14.5 }, // 猕猴桃 + '1': { protein: 0.2, fat: 0.0, carbs: 0.1 }, // 咖啡 + '2': { protein: 12.8, fat: 11.1, carbs: 0.7 }, // 荷包蛋 + '3': { protein: 13.3, fat: 8.8, carbs: 2.8 }, // 鸡蛋 + '4': { protein: 1.4, fat: 0.2, carbs: 22.8 }, // 香蕉 + '6': { protein: 0.2, fat: 0.2, carbs: 13.8 }, // 苹果 + '7': { protein: 0.7, fat: 0.3, carbs: 7.7 }, // 草莓 + '8': { protein: 6.6, fat: 4.6, carbs: 22.7 }, // 蛋烧麦 + '9': { protein: 2.6, fat: 0.3, carbs: 25.9 }, // 米饭 + '10': { protein: 4.0, fat: 1.2, carbs: 22.8 }, // 玉米 + }; + + return nutritionData[foodId] || { protein: 0, fat: 0, carbs: 0 }; +}; + +export function FoodDetailModal({ + visible, + food, + onClose, + onSave +}: FoodDetailModalProps) { + const [amount, setAmount] = useState('100'); + const [selectedUnit, setSelectedUnit] = useState(UNIT_OPTIONS[0]); + const [isFavorite, setIsFavorite] = useState(false); + const [keyboardHeight, setKeyboardHeight] = useState(0); + + // 键盘监听 + useEffect(() => { + const keyboardDidShowListener = Keyboard.addListener( + Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', + (e) => { + console.log('键盘显示,高度:', e.endCoordinates.height); + setKeyboardHeight(e.endCoordinates.height); + } + ); + const keyboardDidHideListener = Keyboard.addListener( + Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', + () => { + console.log('键盘隐藏'); + setKeyboardHeight(0); + } + ); + + return () => { + keyboardDidShowListener?.remove(); + keyboardDidHideListener?.remove(); + }; + }, []); + + // 重置状态 + useEffect(() => { + if (visible && food) { + console.log('FoodDetailModal: 接收到食物数据:', food); + setAmount('100'); + setSelectedUnit(UNIT_OPTIONS[0]); + setIsFavorite(false); + } + }, [visible, food]); + + // 调试信息 + console.log('FoodDetailModal render:', { + visible, + food: food?.name, + foodId: food?.id, + hasFood: !!food + }); + + if (!food) { + console.log('FoodDetailModal: food为空,不渲染内容'); + return null; + } + + const nutrition = getNutritionInfo(food.id); + const amountNum = parseFloat(amount) || 0; + const unitRatio = selectedUnit.ratio; + const finalAmount = amountNum * unitRatio; + + // 计算实际营养值 + const actualCalories = Math.round((food.calories * finalAmount) / 100); + const actualProtein = ((nutrition.protein * finalAmount) / 100).toFixed(1); + const actualFat = ((nutrition.fat * finalAmount) / 100).toFixed(1); + const actualCarbs = ((nutrition.carbs * finalAmount) / 100).toFixed(1); + + const handleSave = () => { + onSave(food, finalAmount, selectedUnit.name); + onClose(); + }; + + console.log('FoodDetailModal 即将渲染 Modal:', { visible, food: food.name }); + + return ( + + + + 0 && { + height: screenHeight - keyboardHeight - 100, // 减去键盘高度和更多安全距离 + maxHeight: screenHeight - keyboardHeight - 100, + minHeight: Math.min(400, screenHeight * 0.4) // 确保最小高度,但不超过屏幕40% + } + ]}> + 0 ? 20 : 0 // 键盘弹出时增加底部间距 + }} + scrollEnabled={true} + bounces={false} + > + {/* 头部 */} + + + + + + 我要纠错 + + + + {/* 食物信息 */} + + + {food.emoji} + {food.name} + + + setIsFavorite(!isFavorite)} + style={styles.favoriteButton} + > + + + + + {/* 营养信息 */} + + + 热量 + {actualCalories}千卡 + + + 蛋白质 + {actualProtein}克 + + + 脂肪 + {actualFat}克 + + + 碳水化合物 + {actualCarbs}克 + + + + {/* 数量输入 */} + + + + + {/* 单位选择 */} + + {UNIT_OPTIONS.map((unit) => ( + setSelectedUnit(unit)} + > + + {unit.name} + + + ))} + + + {/* 保存按钮 */} + + 保存 + + + + + + + ); +} + +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + alignItems: 'center', + }, + keyboardAvoidingView: { + width: '100%', + justifyContent: 'flex-end', + alignItems: 'center', + }, + modalContainer: { + width: screenWidth, + backgroundColor: '#FFFFFF', + borderRadius: 20, // 四周都设置圆角 + // maxHeight: screenHeight * 0.75, // 最大高度为屏幕高度的75% + // minHeight: screenHeight * 0.5, // 最小高度为屏幕高度的50% + shadowColor: '#000', + shadowOffset: { + width: 0, + height: -5, + }, + shadowOpacity: 0.25, + shadowRadius: 20, + elevation: 10, + overflow: 'hidden', // 确保内容不会溢出 + }, + scrollView: { + flexGrow: 1, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: screenWidth > 400 ? 24 : 16, // 根据屏幕宽度调整内边距 + paddingTop: 16, + paddingBottom: 8, + }, + closeButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#F5F5F5', + justifyContent: 'center', + alignItems: 'center', + }, + correctButton: { + paddingHorizontal: 16, + paddingVertical: 8, + }, + correctButtonText: { + fontSize: 16, + color: '#4CAF50', + fontWeight: '500', + }, + foodHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: screenWidth > 400 ? 24 : 16, + paddingVertical: 12, + }, + foodInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + foodEmoji: { + fontSize: 40, + marginRight: 12, + }, + foodName: { + fontSize: 20, + fontWeight: '600', + color: '#333', + marginRight: 8, + }, + favoriteButton: { + padding: 8, + }, + nutritionContainer: { + flexDirection: 'row', + paddingHorizontal: screenWidth > 400 ? 24 : 16, + paddingVertical: 16, + justifyContent: 'space-between', + }, + nutritionItem: { + alignItems: 'center', + flex: 1, + }, + nutritionLabel: { + fontSize: 14, + color: '#666', + marginBottom: 8, + }, + nutritionValue: { + fontSize: 16, + fontWeight: '600', + color: '#333', + }, + amountContainer: { + alignItems: 'center', + paddingVertical: 16, + }, + amountInput: { + backgroundColor: '#E8F5E8', + borderRadius: 12, + paddingHorizontal: 24, + paddingVertical: 16, + fontSize: 24, + fontWeight: '600', + color: '#4CAF50', + textAlign: 'center', + minWidth: 120, + }, + unitContainer: { + flexDirection: 'row', + justifyContent: 'center', + paddingHorizontal: screenWidth > 400 ? 24 : 16, + paddingBottom: 20, + gap: screenWidth > 400 ? 16 : 12, // 根据屏幕宽度调整间距 + }, + unitOption: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: '#F5F5F5', + minWidth: 60, + alignItems: 'center', + }, + unitOptionActive: { + backgroundColor: '#4CAF50', + }, + unitText: { + fontSize: 14, + color: '#666', + fontWeight: '500', + }, + unitTextActive: { + color: '#FFFFFF', + }, + saveButton: { + backgroundColor: '#4CAF50', + marginHorizontal: screenWidth > 400 ? 24 : 16, + marginBottom: 16, + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + }, + saveButtonText: { + fontSize: 18, + fontWeight: '600', + color: '#FFFFFF', + }, +}); \ No newline at end of file diff --git a/components/model/food/README.md b/components/model/food/README.md index e4432f2..b7523fd 100644 --- a/components/model/food/README.md +++ b/components/model/food/README.md @@ -1,109 +1,68 @@ -# 食物库弹窗组件 +# 食物库页面实现说明 -## 功能概述 +## 概述 -食物库弹窗组件 (`FoodLibraryModal`) 是一个完整的食物选择界面,用户可以通过分类浏览和搜索来选择食物。 +已成功将食物库从弹窗模式改造为页面模式,解决了React Native中Modal嵌套的问题。 -## 主要特性 +## 实现内容 -- ✅ 完全按照设计图还原的UI界面 -- ✅ 左侧分类导航,右侧食物列表 -- ✅ 搜索功能 -- ✅ 食物信息显示(名称、卡路里、单位) -- ✅ 餐次选择(早餐、午餐、晚餐、加餐) -- ✅ 自定义和收藏功能预留 +### 1. 创建食物库页面 +- **文件位置**: `app/food-library.tsx` +- **功能**: 完整的食物库界面,包含搜索、分类导航、食物列表等功能 +- **特点**: + - 支持URL参数传递餐次类型 (`mealType`) + - 使用Expo Router进行页面导航 + - 在页面中可以正常使用FoodDetailModal弹窗 + +### 2. 修改营养卡片组件 +- **文件**: `components/NutritionRadarCard.tsx` +- **修改内容**: + - 移除了FoodLibraryModal的导入和使用 + - 将加号按钮的点击事件改为页面导航 + - 使用 `router.push('/food-library?mealType=${currentMealType}')` 进行导航 + +### 3. 更新路由配置 +- **文件**: `constants/Routes.ts` +- **新增**: `FOOD_LIBRARY: '/food-library'` 路由常量 + +### 4. 清理旧文件 +- 删除了 `components/model/food/FoodLibraryModal.tsx` +- 更新了 `components/model/food/index.ts` 导出文件 ## 使用方式 -### 1. 在营养卡片中使用 +1. 在statistics页面的营养卡片中点击右上角的加号按钮 +2. 会导航到食物库页面 (`/food-library`) +3. 在食物库页面中点击食物项的加号按钮 +4. 会弹出食物详情弹窗 (FoodDetailModal) +5. 在详情弹窗中可以选择单位和数量,点击保存后返回statistics页面 -在 `statistics.tsx` 页面的营养摄入分析卡片右上角,点击绿色的加号按钮即可打开食物库弹窗。 +## 技术优势 -### 2. 组件集成 +1. **解决Modal嵌套问题**: 避免了React Native中Modal嵌套的兼容性问题 +2. **更好的用户体验**: 页面导航比弹窗更符合移动端的交互习惯 +3. **键盘适配**: FoodDetailModal中的键盘适配功能可以正常工作 +4. **代码结构清晰**: 页面和弹窗职责分离,代码更易维护 -```tsx -import { FoodLibraryModal, FoodItem } from '@/components/model/food/FoodLibraryModal'; +## 页面功能 -// 在组件中使用 -const [showFoodLibrary, setShowFoodLibrary] = useState(false); +### 食物库页面功能 +- ✅ 搜索食物 +- ✅ 分类导航(常见、自定义、收藏等) +- ✅ 食物列表展示 +- ✅ 餐次显示 +- ✅ 返回导航 -const handleSelectFood = (food: FoodItem) => { - console.log('选择了食物:', food); - // 处理食物选择逻辑 -}; +### 食物详情弹窗功能 +- ✅ 食物信息展示 +- ✅ 营养成分显示 +- ✅ 单位选择(克、小份、中份、大份) +- ✅ 数量输入 +- ✅ 键盘适配 +- ✅ 保存功能 - setShowFoodLibrary(false)} - onSelectFood={handleSelectFood} - mealType="breakfast" -/> -``` +## 注意事项 -## 组件结构 - -``` -components/ - model/ - food/ - ├── FoodLibraryModal.tsx # 主要弹窗组件 - └── index.ts # 导出文件 -``` - -## 数据结构 - -### FoodItem -```tsx -interface FoodItem { - id: string; - name: string; - emoji: string; - calories: number; - unit: string; // 如 "100克" -} -``` - -### FoodCategory -```tsx -interface FoodCategory { - id: string; - name: string; - foods: FoodItem[]; -} -``` - -## 食物分类 - -- 常见 -- 自定义 -- 收藏 -- 水果蔬菜 -- 肉蛋奶 -- 豆类坚果 -- 零食饮料 -- 主食 -- 菜肴 - -## 已实现的功能 - -1. **分类浏览** - 左侧导航可切换不同食物分类 -2. **搜索功能** - 顶部搜索框支持食物名称搜索 -3. **食物选择** - 点击右侧加号按钮选择食物 -4. **餐次指示** - 底部显示当前餐次(早餐、午餐、晚餐、加餐) -5. **UI动画** - 模态弹窗的滑入滑出动画 - -## 待扩展功能 - -1. **自定义食物** - 用户可以添加自定义食物 -2. **收藏功能** - 用户可以收藏常用食物 -3. **营养详情** - 显示更详细的营养成分信息 -4. **数据持久化** - 将选择的食物保存到营养记录中 -5. **餐次切换** - 底部餐次选择器的交互功能 - -## 技术实现 - -- 使用 React Native Modal 实现全屏弹窗 -- 使用 ScrollView 实现分类和食物列表的滚动 -- 使用 TouchableOpacity 实现交互反馈 -- 使用 Ionicons 图标库 -- 完全响应式设计,适配不同屏幕尺寸 \ No newline at end of file +1. 食物库页面使用URL参数传递餐次类型,确保从不同餐次点击加号时能正确显示对应的餐次 +2. FoodDetailModal仍然是弹窗形式,但现在是在页面中使用,避免了嵌套问题 +3. 所有原有的UI样式和交互逻辑都得到了保留 \ No newline at end of file diff --git a/components/model/food/index.ts b/components/model/food/index.ts index b2467a1..66fa7b8 100644 --- a/components/model/food/index.ts +++ b/components/model/food/index.ts @@ -1,2 +1,3 @@ -export { FoodLibraryModal } from './FoodLibraryModal'; -export type { FoodCategory, FoodItem, FoodLibraryModalProps } from './FoodLibraryModal'; +export { FoodDetailModal } from './FoodDetailModal'; +export type { FoodDetailModalProps, FoodItem } from './FoodDetailModal'; + diff --git a/constants/Routes.ts b/constants/Routes.ts index 9755e43..dfb2aab 100644 --- a/constants/Routes.ts +++ b/constants/Routes.ts @@ -36,6 +36,7 @@ export const ROUTES = { // 营养相关路由 NUTRITION_RECORDS: '/nutrition/records', + FOOD_LIBRARY: '/food-library', // 体重记录相关路由 WEIGHT_RECORDS: '/weight-records',