import { CalorieRingChart } from '@/components/CalorieRingChart'; import { DateSelector } from '@/components/DateSelector'; import { FloatingFoodOverlay } from '@/components/FloatingFoodOverlay'; import { NutritionRecordCard } from '@/components/NutritionRecordCard'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { DietRecord } from '@/services/dietRecords'; import { type FoodRecognitionResponse } from '@/services/foodRecognition'; import { saveRecognitionResult } from '@/store/foodRecognitionSlice'; import { selectHealthDataByDate } from '@/store/healthSlice'; import { deleteNutritionRecord, fetchDailyNutritionData, fetchNutritionRecords, selectNutritionLoading, selectNutritionRecordsByDate, selectNutritionSummaryByDate } from '@/store/nutritionSlice'; import { getTodayIndexInMonth } from '@/utils/date'; import { fetchBasalEnergyBurned } from '@/utils/health'; import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useState } from 'react'; import { FlatList, RefreshControl, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; type ViewMode = 'daily' | 'all'; export default function NutritionRecordsScreen() { const { t } = useI18n(); const safeAreaTop = useSafeAreaTop(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); const isGlassAvailable = isLiquidGlassAvailable(); const { isLoggedIn } = useAuthGuard(); // 日期相关状态 const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); // 直接使用 state 管理当前选中日期,而不是从 days 数组派生,以支持 DateSelector 内部月份切换 const [currentSelectedDate, setCurrentSelectedDate] = useState(new Date()); const currentSelectedDateString = dayjs(currentSelectedDate).format('YYYY-MM-DD'); // 从 Redux 获取数据 const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString)); const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(currentSelectedDateString)); const userProfile = useAppSelector((state) => state.user.profile); // 从 Redux 获取营养记录数据 const nutritionRecords = useAppSelector(selectNutritionRecordsByDate(currentSelectedDateString)); const nutritionLoading = useAppSelector(selectNutritionLoading); // 视图模式:按天查看 vs 全部查看 const [viewMode, setViewMode] = useState('daily'); // 全部记录模式的本地状态 const [allRecords, setAllRecords] = useState([]); const [allRecordsLoading, setAllRecordsLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [hasMoreData, setHasMoreData] = useState(true); const [page, setPage] = useState(1); // 基础代谢数据状态 const [basalMetabolism, setBasalMetabolism] = useState(1482); // 食物添加弹窗状态 const [showFoodOverlay, setShowFoodOverlay] = useState(false); // 根据视图模式选择使用的数据 const displayRecords = viewMode === 'daily' ? nutritionRecords : allRecords; const loading = viewMode === 'daily' ? nutritionLoading.records : allRecordsLoading; // 页面聚焦时自动刷新数据 useFocusEffect( useCallback(() => { if (!isLoggedIn) return; if (viewMode === 'daily') { dispatch(fetchDailyNutritionData(currentSelectedDate)); } else { // 全部记录模式:重新加载数据 const loadAllRecords = async () => { try { setAllRecordsLoading(true); const response = await dispatch(fetchNutritionRecords({ page: 1, limit: 10, append: false, })); if (fetchNutritionRecords.fulfilled.match(response)) { const { records } = response.payload; setAllRecords(records); setHasMoreData(records.length === 10); setPage(1); } setAllRecordsLoading(false); } catch (error) { console.error('加载全部记录失败:', error); setAllRecordsLoading(false); } }; loadAllRecords(); } }, [viewMode, currentSelectedDateString, dispatch, isLoggedIn]) ); // 当选中日期或视图模式变化时重新加载数据 useEffect(() => { fetchBasalMetabolismData(); if (viewMode === 'daily') { dispatch(fetchDailyNutritionData(currentSelectedDate)); } else { setPage(1); // 重置分页 setAllRecords([]); // 清空记录 // 全部记录模式:加载数据 const loadAllRecords = async () => { try { setAllRecordsLoading(true); const response = await dispatch(fetchNutritionRecords({ page: 1, limit: 10, append: false, })); if (fetchNutritionRecords.fulfilled.match(response)) { const { records } = response.payload; setAllRecords(records); setHasMoreData(records.length === 10); } setAllRecordsLoading(false); } catch (error) { console.error('加载全部记录失败:', error); setAllRecordsLoading(false); } }; loadAllRecords(); } }, [viewMode, currentSelectedDateString, dispatch]); // 获取基础代谢数据 const fetchBasalMetabolismData = useCallback(async () => { try { const options = { startDate: dayjs(currentSelectedDate).startOf('day').toDate().toISOString(), endDate: dayjs(currentSelectedDate).endOf('day').toDate().toISOString() }; const basalEnergy = await fetchBasalEnergyBurned(options); setBasalMetabolism(basalEnergy || 1482); } catch (error) { console.error('获取基础代谢数据失败:', error); setBasalMetabolism(1482); // 失败时使用默认值 } }, [currentSelectedDate]); const onRefresh = useCallback(async () => { try { setRefreshing(true); if (viewMode === 'daily') { await dispatch(fetchDailyNutritionData(currentSelectedDate)); } else { // 全部记录模式:刷新数据 setPage(1); const response = await dispatch(fetchNutritionRecords({ page: 1, limit: 10, append: false, })); if (fetchNutritionRecords.fulfilled.match(response)) { const { records } = response.payload; setAllRecords(records); setHasMoreData(records.length === 10); } } } catch (error) { console.error('刷新数据失败:', error); } finally { setRefreshing(false); } }, [viewMode, currentSelectedDateString, dispatch]); // 计算营养目标 const calculateNutritionGoals = () => { const weight = parseFloat(userProfile?.weight || '70'); // 默认70kg const height = parseFloat(userProfile?.height || '170'); // 默认170cm const age = userProfile?.birthDate ? dayjs().diff(dayjs(userProfile.birthDate), 'year') : 25; // 默认25岁 const isWoman = userProfile?.gender === 'female'; // 基础代谢率计算(Mifflin-St Jeor Equation) let bmr; if (isWoman) { bmr = 10 * weight + 6.25 * height - 5 * age - 161; } else { bmr = 10 * weight + 6.25 * height - 5 * age + 5; } // 总热量需求(假设轻度活动) const totalCalories = bmr * 1.375; // 计算营养素目标 const proteinGoal = weight * 1.6; // 1.6g/kg const fatGoal = totalCalories * 0.25 / 9; // 25%来自脂肪,9卡/克 const carbsGoal = (totalCalories - proteinGoal * 4 - fatGoal * 9) / 4; // 剩余来自碳水 return { proteinGoal: Math.round(proteinGoal * 10) / 10, fatGoal: Math.round(fatGoal * 10) / 10, carbsGoal: Math.round(carbsGoal * 10) / 10, }; }; const nutritionGoals = calculateNutritionGoals(); const loadMoreRecords = useCallback(async () => { if (hasMoreData && !loading && !refreshing && viewMode === 'all') { try { const nextPage = page + 1; const response = await dispatch(fetchNutritionRecords({ page: nextPage, limit: 10, append: true, })); if (fetchNutritionRecords.fulfilled.match(response)) { const { records } = response.payload; setAllRecords(prev => [...prev, ...records]); setHasMoreData(records.length === 10); setPage(nextPage); } } catch (error) { console.error('加载更多记录失败:', error); } } }, [hasMoreData, loading, refreshing, viewMode, page, dispatch]); // 删除记录 const handleDeleteRecord = async (recordId: number) => { try { if (viewMode === 'daily') { // 按天查看模式,使用 Redux 删除 await dispatch(deleteNutritionRecord({ recordId, dateKey: currentSelectedDateString })); } else { // 全部记录模式,从本地状态中移除 await dispatch(deleteNutritionRecord({ recordId, dateKey: currentSelectedDateString })); setAllRecords(prev => prev.filter(record => record.id !== recordId)); } } catch (error) { console.error('删除营养记录失败:', error); } }; // 处理营养记录卡片点击 const handleRecordPress = (record: DietRecord) => { // 将 DietRecord 转换为 FoodRecognitionResponse 格式 const recognitionResult: FoodRecognitionResponse = { items: [{ id: record.id.toString(), label: record.foodName, foodName: record.foodName, portion: record.portionDescription || `${record.estimatedCalories || 0}g`, calories: record.estimatedCalories || 0, mealType: record.mealType, nutritionData: { proteinGrams: record.proteinGrams || 0, carbohydrateGrams: record.carbohydrateGrams || 0, fatGrams: record.fatGrams || 0, fiberGrams: 0, // DietRecord 中没有纤维数据,设为0 } }], analysisText: record.foodDescription || `${record.foodName} - ${record.portionDescription}`, confidence: 95, // 设置一个默认置信度 isFoodDetected: true, nonFoodMessage: undefined }; // 生成唯一的识别ID const recognitionId = `record-${record.id}-${Date.now()}`; // 保存到 Redux dispatch(saveRecognitionResult({ id: recognitionId, result: recognitionResult })); // 跳转到分析结果页面 router.push({ pathname: '/food/analysis-result', params: { imageUri: record.imageUrl || '', mealType: record.mealType, recognitionId: recognitionId, hideRecordBar: 'true' } }); }; // 根据当前时间智能判断餐次类型 const getCurrentMealType = (): 'breakfast' | 'lunch' | 'dinner' | 'snack' => { const hour = new Date().getHours(); if (hour >= 5 && hour < 11) { return 'breakfast'; // 5:00-10:59 早餐 } else if (hour >= 11 && hour < 14) { return 'lunch'; // 11:00-13:59 午餐 } else if (hour >= 17 && hour < 21) { return 'dinner'; // 17:00-20:59 晚餐 } else { return 'snack'; // 其他时间默认为零食 } }; // 添加食物的处理函数 const handleAddFood = () => { setShowFoodOverlay(true); }; // 渲染右侧添加按钮 const renderRightButton = () => ( {isGlassAvailable ? ( ) : ( )} ); const renderEmptyState = () => ( {t('nutritionRecords.empty.title')} {t('nutritionRecords.empty.action')} ); const renderRecord = ({ item }: { item: DietRecord }) => ( handleRecordPress(item)} onDelete={() => handleDeleteRecord(item.id)} /> ); const renderFooter = () => { if (!hasMoreData) { if (displayRecords.length === 0) return null; return ( {t('nutritionRecords.footer.end')} ); } if (viewMode === 'all' && displayRecords.length > 0) { return ( {t('nutritionRecords.footer.loadMore')} ); } return null; }; const ListHeader = () => ( {viewMode === 'daily' && ( { setSelectedIndex(index); setCurrentSelectedDate(date); }} showMonthTitle={true} disableFutureDates={true} showCalendarIcon={true} containerStyle={styles.dateSelectorContainer} /> )} {t('nutritionRecords.listTitle')} {displayRecords.length > 0 && ( {t('nutritionRecords.recordCount', { count: displayRecords.length })} )} ); return ( {/* 顶部柔和渐变背景 */} router.back()} right={renderRightButton()} transparent={true} /> item.id.toString()} contentContainerStyle={[ styles.listContainer, { paddingTop: safeAreaTop } ]} showsVerticalScrollIndicator={false} refreshControl={ } ListHeaderComponent={ListHeader} ListEmptyComponent={renderEmptyState} ListFooterComponent={renderFooter} onEndReached={viewMode === 'all' ? loadMoreRecords : undefined} onEndReachedThreshold={0.1} /> {/* 食物添加悬浮窗 */} setShowFoodOverlay(false)} mealType={getCurrentMealType()} /> ); } const styles = StyleSheet.create({ container: { flex: 1, }, topGradient: { position: 'absolute', left: 0, right: 0, top: 0, height: 320, }, listContainer: { paddingBottom: 100, // 留出底部空间防止遮挡 }, headerContent: { marginBottom: 16, }, dateSelectorContainer: { paddingHorizontal: 16, marginBottom: 16, }, chartWrapper: { marginBottom: 24, shadowColor: 'rgba(30, 41, 59, 0.05)', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 4, }, listTitleContainer: { flexDirection: 'row', alignItems: 'baseline', paddingHorizontal: 24, marginBottom: 12, gap: 8, }, listTitle: { fontSize: 18, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', }, listSubtitle: { fontSize: 13, color: '#6f7ba7', fontFamily: 'AliRegular', }, glassAddButton: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', borderRadius: 20, overflow: 'hidden', }, fallbackAddButton: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', borderRadius: 20, borderWidth: 1, borderColor: 'rgba(0,0,0,0.05)', }, emptySimpleContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, }, emptySimpleImage: { width: 48, height: 48, opacity: 0.4, marginBottom: 12, }, emptySimpleText: { fontSize: 14, color: '#94A3B8', fontFamily: 'AliRegular', marginBottom: 8, }, emptyActionText: { fontSize: 14, fontWeight: '600', fontFamily: 'AliBold', }, footerContainer: { paddingVertical: 24, alignItems: 'center', }, footerText: { fontSize: 12, fontWeight: '500', opacity: 0.6, fontFamily: 'AliRegular', }, loadMoreButton: { paddingVertical: 16, alignItems: 'center', }, loadMoreText: { fontSize: 14, fontWeight: '600', fontFamily: 'AliBold', }, });