import { CreateCustomFoodModal, type CustomFoodData } from '@/components/model/food/CreateCustomFoodModal'; import { FoodDetailModal } from '@/components/model/food/FoodDetailModal'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { DEFAULT_IMAGE_FOOD } from '@/constants/Image'; import { useAppDispatch } from '@/hooks/redux'; import { useFoodLibrary, useFoodSearch } from '@/hooks/useFoodLibrary'; import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { addDietRecord, type CreateDietRecordDto } from '@/services/dietRecords'; import { foodLibraryApi, type CreateCustomFoodDto } from '@/services/foodLibraryApi'; import { fetchDailyNutritionData } from '@/store/nutritionSlice'; import type { FoodItem, MealType, SelectedFoodItem } from '@/types/food'; import { saveNutritionToHealthKit } from '@/utils/health'; import { Ionicons } from '@expo/vector-icons'; import { Image } from 'expo-image'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, Dimensions, Modal, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const { width } = Dimensions.get('window'); export default function FoodLibraryScreen() { const { t } = useI18n(); const safeAreaTop = useSafeAreaTop(); const insets = useSafeAreaInsets(); const router = useRouter(); const params = useLocalSearchParams<{ mealType?: string }>(); const mealType = (params.mealType as MealType) || 'breakfast'; // Redux hooks const dispatch = useAppDispatch(); const { categories, loading, error, clearErrors, loadFoodLibrary } = useFoodLibrary(); const { searchResults, searchLoading, search, clearResults } = useFoodSearch(); // 本地状态 const [selectedCategoryId, setSelectedCategoryId] = useState('common'); const [searchText, setSearchText] = useState(''); const [selectedFood, setSelectedFood] = useState(null); const [showFoodDetail, setShowFoodDetail] = useState(false); const [selectedFoodItems, setSelectedFoodItems] = useState([]); const [showMealSelector, setShowMealSelector] = useState(false); const [currentMealType, setCurrentMealType] = useState(mealType); const [isRecording, setIsRecording] = useState(false); const [showCreateCustomFood, setShowCreateCustomFood] = useState(false); const getMealTypeLabel = (type: MealType) => { const labels: Record = { breakfast: t('foodLibrary.mealTypes.breakfast'), lunch: t('foodLibrary.mealTypes.lunch'), dinner: t('foodLibrary.mealTypes.dinner'), snack: t('foodLibrary.mealTypes.snack'), }; return labels[type] || type; }; // 获取当前选中的分类 const selectedCategory = categories.find(cat => cat.id === selectedCategoryId); // 过滤食物列表 - 优先显示搜索结果 const filteredFoods = useMemo(() => { if (searchText.trim() && searchResults.length > 0) { return searchResults; } if (selectedCategory) { return selectedCategory.foods } return []; }, [searchText, searchResults, selectedCategory]); // 处理搜索 useEffect(() => { const timeoutId = setTimeout(() => { if (searchText.trim()) { search(searchText); } else { clearResults(); } }, 300); // 防抖 return () => clearTimeout(timeoutId); }, [searchText, search, clearResults]); // 处理食物选择 - 显示详情弹窗 const handleSelectFood = (food: FoodItem) => { 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) => { // 计算实际热量 const actualCalories = Math.round((food.calories * amount) / 100); // 创建新的选择项目 const newSelectedItem: SelectedFoodItem = { id: `${food.id}_${Date.now()}`, // 使用时间戳确保唯一性 food, amount, unit, calories: actualCalories }; // 添加到已选择列表 setSelectedFoodItems(prev => [...prev, newSelectedItem]); console.log('保存食物:', food, amount, unit, '热量:', actualCalories); setShowFoodDetail(false); }; // 移除已选择的食物 const handleRemoveSelectedFood = (itemId: string) => { setSelectedFoodItems(prev => prev.filter(item => item.id !== itemId)); }; // 计算总热量 const totalCalories = selectedFoodItems.reduce((sum, item) => sum + item.calories, 0); // 关闭详情弹窗 const handleCloseFoodDetail = () => { setShowFoodDetail(false); setSelectedFood(null); }; // 处理删除自定义食物 const handleDeleteFood = async (foodId: string) => { try { await foodLibraryApi.deleteCustomFood(Number(foodId)); // 删除成功后重新加载食物库数据 await loadFoodLibrary(); // 关闭弹窗 handleCloseFoodDetail(); } catch (error) { console.error('删除食物失败:', error); Alert.alert( t('foodLibrary.alerts.deleteFailed.title'), t('foodLibrary.alerts.deleteFailed.message') ); } }; // 处理饮食记录 const handleRecordDiet = async () => { if (selectedFoodItems.length === 0) return; setIsRecording(true); try { // 逐个记录选中的食物 for (const item of selectedFoodItems) { const dietRecordData: CreateDietRecordDto = { mealType: currentMealType, foodName: item.food.name, foodDescription: item.food.description, portionDescription: `${item.amount}${item.unit}`, estimatedCalories: item.calories, proteinGrams: item.food.protein ? Number(((item.food.protein * item.amount) / 100).toFixed(2)) : undefined, carbohydrateGrams: item.food.carbohydrate ? Number(((item.food.carbohydrate * item.amount) / 100).toFixed(2)) : undefined, fatGrams: item.food.fat ? Number(((item.food.fat * item.amount) / 100).toFixed(2)) : undefined, fiberGrams: item.food.fiber ? Number(((item.food.fiber * item.amount) / 100).toFixed(2)) : undefined, sugarGrams: item.food.sugar ? Number(((item.food.sugar * item.amount) / 100).toFixed(2)) : undefined, sodiumMg: item.food.sodium ? Number(((item.food.sodium * item.amount) / 100).toFixed(2)) : undefined, additionalNutrition: item.food.additionalNutrition, source: 'manual', mealTime: new Date().toISOString(), imageUrl: item.food.imageUrl, }; // 先保存到后端 await addDietRecord(dietRecordData); // 然后尝试同步到 HealthKit(非阻塞) // 提取蛋白质、脂肪和碳水化合物数据 const { proteinGrams, fatGrams, carbohydrateGrams, mealTime } = dietRecordData; if (proteinGrams !== undefined || fatGrams !== undefined || carbohydrateGrams !== undefined) { // 使用 catch 确保 HealthKit 同步失败不影响后端记录 saveNutritionToHealthKit( { proteinGrams: proteinGrams || undefined, fatGrams: fatGrams || undefined, carbohydrateGrams: carbohydrateGrams || undefined }, mealTime ).catch(error => { // HealthKit 同步失败只记录日志,不影响用户体验 console.error('HealthKit 营养数据同步失败(不影响记录):', error); }); } } // 记录成功后,刷新当天的营养数据 const today = new Date(); await dispatch(fetchDailyNutritionData(today)); // 清空选择列表并返回 setSelectedFoodItems([]); router.back(); } catch (error) { console.error('记录饮食失败:', error); // 这里可以显示错误提示 } finally { setIsRecording(false); } }; // 处理餐次选择 const handleMealTypeSelect = (selectedMealType: MealType) => { setCurrentMealType(selectedMealType); setShowMealSelector(false); }; // 处理创建自定义食物 const handleCreateCustomFood = () => { setShowCreateCustomFood(true); }; // 处理保存自定义食物 const handleSaveCustomFood = async (customFoodData: CustomFoodData) => { try { // 转换数据格式以匹配API要求 const createData: CreateCustomFoodDto = { name: customFoodData.name, caloriesPer100g: customFoodData.calories, proteinPer100g: customFoodData.protein, carbohydratePer100g: customFoodData.carbohydrate, fatPer100g: customFoodData.fat, imageUrl: customFoodData.imageUrl, }; // 调用API创建自定义食物 const createdFood = await foodLibraryApi.createCustomFood(createData); // 需要拉取一遍最新的食物列表 await loadFoodLibrary(); // 创建FoodItem对象 const customFoodItem: FoodItem = { id: createdFood.id.toString(), name: createdFood.name, calories: createdFood.caloriesPer100g || 0, unit: 'g', description: createdFood.description || `自定义食物 - ${createdFood.name}`, imageUrl: createdFood.imageUrl, protein: createdFood.proteinPer100g, fat: createdFood.fatPer100g, carbohydrate: createdFood.carbohydratePer100g, }; // 添加到选择列表中 const newSelectedItem: SelectedFoodItem = { id: createdFood.id.toString(), food: customFoodItem, amount: customFoodData.defaultAmount, unit: 'g', calories: Math.round((customFoodItem.calories * customFoodData.defaultAmount) / 100) }; setSelectedFoodItems(prev => [...prev, newSelectedItem]); } catch (error) { console.error('创建自定义食物失败:', error); Alert.alert( t('foodLibrary.alerts.createFailed.title'), t('foodLibrary.alerts.createFailed.message') ); } }; // 关闭自定义食物弹窗 const handleCloseCreateCustomFood = () => { setShowCreateCustomFood(false); }; // 餐次选择选项 const mealOptions = [ { key: 'breakfast' as const, label: t('foodLibrary.mealTypes.breakfast'), color: '#FF6B35' }, { key: 'lunch' as const, label: t('foodLibrary.mealTypes.lunch'), color: '#4CAF50' }, { key: 'dinner' as const, label: t('foodLibrary.mealTypes.dinner'), color: '#2196F3' }, { key: 'snack' as const, label: t('foodLibrary.mealTypes.snack'), color: '#FF9800' }, ]; return ( {/* 头部 */} router.back()} variant="default" right={ {t('foodLibrary.custom')} } /> {/* 搜索框 */} {searchText.length > 0 && ( setSearchText('')}> )} {/* 主要内容区域 - Split View Card */} {loading && categories.length === 0 ? ( {t('foodLibrary.loading')} ) : error ? ( {error} { clearErrors(); // 这里可以重新加载数据 }} > {t('foodLibrary.retry')} ) : ( {/* 左侧分类导航 */} {categories.map((category) => ( { setSelectedCategoryId(category.id); if (searchText) { setSearchText(''); clearResults(); } }} > {category.name} ))} {/* 右侧食物列表 */} {searchLoading ? ( {t('foodLibrary.search.loading')} ) : ( {filteredFoods.map((food) => ( handleSelectFood(food)} activeOpacity={0.7} > {food.name} {food.calories} kcal/{food.unit} ))} {filteredFoods.length === 0 && !searchLoading && ( {searchText ? t('foodLibrary.search.empty') : t('foodLibrary.search.noData')} )} {/* 底部留白,防止被底部栏遮挡 */} )} )} {/* 底部操作栏 - 悬浮样式 */} {/* 已选择食物概览 (如果有选择) */} {selectedFoodItems.length > 0 && ( {selectedFoodItems.map((item) => ( handleRemoveSelectedFood(item.id)} > {item.amount}{item.unit} ))} {totalCalories} kcal )} setShowMealSelector(true)} > option.key === currentMealType)?.color || '#FF6B35' } ]} /> {getMealTypeLabel(currentMealType)} {isRecording ? ( ) : ( <> {t('foodLibrary.actions.record')} ({selectedFoodItems.length}) )} {/* 餐次选择弹窗 */} setShowMealSelector(false)} > setShowMealSelector(false)} /> {t('foodLibrary.actions.selectMeal')} setShowMealSelector(false)}> {mealOptions.map((option) => ( handleMealTypeSelect(option.key)} > {option.label} {currentMealType === option.key && ( )} ))} {/* 食物详情弹窗 */} {/* 创建自定义食物弹窗 */} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f3f4fb', // Matches sleep-detail background }, customButton: { flexDirection: 'row', alignItems: 'center', gap: 4, padding: 4, }, customButtonText: { fontSize: 14, color: Colors.light.primary, fontWeight: '600', fontFamily: 'AliBold', }, // Search Area searchWrapper: { paddingHorizontal: 20, paddingBottom: 16, zIndex: 10, }, searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFFFFF', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 20, shadowColor: 'rgba(30, 41, 59, 0.05)', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 1, shadowRadius: 12, elevation: 4, }, searchIcon: { marginRight: 10, }, searchInput: { flex: 1, fontSize: 15, color: '#1c1f3a', fontFamily: 'AliRegular', padding: 0, // Remove Android default padding }, // Main Content Card mainContentCard: { flex: 1, marginHorizontal: 20, backgroundColor: '#FFFFFF', borderTopLeftRadius: 28, borderTopRightRadius: 28, shadowColor: 'rgba(30, 41, 59, 0.08)', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 1, shadowRadius: 16, elevation: 5, overflow: 'hidden', }, splitViewContainer: { flex: 1, flexDirection: 'row', }, // Sidebar categorySidebar: { width: 90, backgroundColor: '#F8FAFC', borderRightWidth: 1, borderRightColor: '#F1F5F9', }, categoryListContent: { paddingVertical: 12, }, categoryItem: { paddingVertical: 16, paddingHorizontal: 8, alignItems: 'center', position: 'relative', }, categoryItemActive: { backgroundColor: '#FFFFFF', }, categoryIndicator: { position: 'absolute', left: 0, top: 12, bottom: 12, width: 3, borderTopRightRadius: 3, borderBottomRightRadius: 3, backgroundColor: 'transparent', }, categoryIndicatorActive: { backgroundColor: Colors.light.primary, }, categoryText: { fontSize: 13, color: '#64748B', textAlign: 'center', fontFamily: 'AliRegular', }, categoryTextActive: { color: Colors.light.primary, fontFamily: 'AliBold', fontWeight: '600', }, // Food List foodListContainer: { flex: 1, backgroundColor: '#FFFFFF', }, foodListContent: { padding: 16, }, foodItemCard: { flexDirection: 'row', alignItems: 'center', padding: 12, marginBottom: 12, backgroundColor: '#FFFFFF', borderRadius: 16, borderWidth: 1, borderColor: '#F1F5F9', shadowColor: 'rgba(148, 163, 184, 0.1)', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 1, shadowRadius: 6, elevation: 2, }, foodImage: { width: 48, height: 48, borderRadius: 12, marginRight: 12, backgroundColor: '#F8FAFC', }, foodInfo: { flex: 1, marginRight: 8, }, foodName: { fontSize: 15, color: '#1E293B', fontFamily: 'AliBold', marginBottom: 4, }, foodCalories: { fontSize: 13, color: Colors.light.primary, fontFamily: 'AliBold', }, unitText: { fontSize: 11, color: '#94A3B8', fontFamily: 'AliRegular', fontWeight: 'normal', }, addButton: { width: 28, height: 28, borderRadius: 14, backgroundColor: Colors.light.primary, alignItems: 'center', justifyContent: 'center', }, // Empty States loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, loadingText: { marginTop: 12, fontSize: 14, color: '#94A3B8', fontFamily: 'AliRegular', }, errorContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20, }, errorText: { fontSize: 14, color: '#EF4444', textAlign: 'center', marginBottom: 16, fontFamily: 'AliRegular', }, retryButton: { paddingHorizontal: 20, paddingVertical: 10, backgroundColor: Colors.light.primary, borderRadius: 20, }, retryButtonText: { color: '#FFF', fontSize: 14, fontFamily: 'AliBold', }, searchLoadingContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 20, }, searchLoadingText: { marginLeft: 8, fontSize: 14, color: '#94A3B8', fontFamily: 'AliRegular', }, emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingTop: 60, }, emptyText: { fontSize: 14, color: '#94A3B8', fontFamily: 'AliRegular', }, // Bottom Bar bottomBarContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(255, 255, 255, 0.95)', borderTopWidth: 1, borderTopColor: '#F1F5F9', paddingTop: 12, paddingHorizontal: 20, shadowColor: 'rgba(30, 41, 59, 0.1)', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 1, shadowRadius: 16, elevation: 8, }, selectedPreviewContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, selectedList: { flexGrow: 0, }, selectedChip: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#F1F5F9', borderRadius: 16, paddingLeft: 4, paddingRight: 8, paddingVertical: 4, marginRight: 8, }, selectedChipImage: { width: 24, height: 24, borderRadius: 12, marginRight: 6, }, selectedChipText: { fontSize: 12, color: '#475569', fontFamily: 'AliBold', marginRight: 6, }, selectedChipClose: { width: 16, height: 16, borderRadius: 8, backgroundColor: '#CBD5E1', alignItems: 'center', justifyContent: 'center', }, totalCaloriesBadge: { marginLeft: 'auto', backgroundColor: '#EEF2FF', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, }, totalCaloriesText: { fontSize: 12, color: Colors.light.primary, fontFamily: 'AliBold', }, actionRow: { flexDirection: 'row', gap: 12, }, mealSelectorButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: '#F8FAFC', borderRadius: 24, height: 48, borderWidth: 1, borderColor: '#E2E8F0', }, mealDot: { width: 8, height: 8, borderRadius: 4, marginRight: 8, }, mealSelectorText: { fontSize: 15, color: '#1E293B', fontFamily: 'AliBold', marginRight: 6, }, confirmButton: { flex: 2, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: Colors.light.primary, borderRadius: 24, height: 48, shadowColor: Colors.light.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 4, gap: 8, }, confirmButtonDisabled: { backgroundColor: '#94A3B8', shadowOpacity: 0, }, confirmButtonText: { fontSize: 16, color: '#FFF', fontFamily: 'AliBold', fontWeight: '600', }, // Modal Styles modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(15, 23, 42, 0.4)', }, modalBackdrop: { ...StyleSheet.absoluteFillObject, }, modalContent: { backgroundColor: '#FFFFFF', borderTopLeftRadius: 28, borderTopRightRadius: 28, padding: 24, }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24, }, modalTitle: { fontSize: 20, color: '#1E293B', fontFamily: 'AliBold', }, mealOptionsList: { gap: 12, }, mealOptionItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, backgroundColor: '#F8FAFC', borderWidth: 1, borderColor: '#F1F5F9', }, mealOptionItemActive: { backgroundColor: '#EEF2FF', borderColor: Colors.light.primary, }, mealOptionDot: { width: 12, height: 12, borderRadius: 6, marginRight: 16, }, mealOptionLabel: { flex: 1, fontSize: 16, color: '#475569', fontFamily: 'AliRegular', }, mealOptionLabelActive: { color: '#1E293B', fontFamily: 'AliBold', }, });