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 { useColorScheme } from '@/hooks/useColorScheme'; 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 { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date'; import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FlatList, RefreshControl, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; type ViewMode = 'daily' | 'all'; export default function NutritionRecordsScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); // 日期相关状态 - 使用与统计页面相同的日期逻辑 const days = getMonthDaysZh(); const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); const monthTitle = getMonthTitleZh(); // 获取当前选中日期 - 使用 useMemo 避免每次渲染都创建新对象 const currentSelectedDate = useMemo(() => { return days[selectedIndex]?.date?.toDate() ?? new Date(); }, [selectedIndex, days]); const currentSelectedDateString = useMemo(() => { return dayjs(currentSelectedDate).format('YYYY-MM-DD'); }, [currentSelectedDate]); // 从 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 [showFoodOverlay, setShowFoodOverlay] = useState(false); // 根据视图模式选择使用的数据 const displayRecords = viewMode === 'daily' ? nutritionRecords : allRecords; const loading = viewMode === 'daily' ? nutritionLoading.records : allRecordsLoading; // 页面聚焦时自动刷新数据 useFocusEffect( useCallback(() => { console.log('营养记录页面聚焦,刷新数据...'); 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]) ); // 当选中日期或视图模式变化时重新加载数据 useEffect(() => { 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 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 renderDateSelector = () => { if (viewMode !== 'daily') return null; return ( setSelectedIndex(index)} showMonthTitle={true} disableFutureDates={true} showCalendarIcon={true} containerStyle={{ paddingHorizontal: 16 }} /> ); }; const renderEmptyState = () => ( {viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'} {viewMode === 'daily' ? '开始记录今日营养摄入' : '开始记录你的营养摄入吧'} ); const renderRecord = ({ item, index }: { item: DietRecord; index: number }) => ( handleRecordPress(item)} onDelete={() => handleDeleteRecord(item.id)} /> ); const renderFooter = () => { if (!hasMoreData) { return ( 没有更多数据了 ); } if (viewMode === 'all' && displayRecords.length > 0) { return ( 加载更多 ); } return null; }; // 根据当前时间智能判断餐次类型 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 = () => ( ); return ( router.back()} right={renderRightButton()} /> {/* {renderViewModeToggle()} */} {renderDateSelector()} {/* Calorie Ring Chart */} {( renderRecord({ item, index })} keyExtractor={(item) => item.id.toString()} contentContainerStyle={[ styles.listContainer, { paddingBottom: 40, paddingTop: 16 } ]} showsVerticalScrollIndicator={false} refreshControl={ } ListEmptyComponent={renderEmptyState} ListFooterComponent={renderFooter} onEndReached={viewMode === 'all' ? loadMoreRecords : undefined} onEndReachedThreshold={0.1} /> )} {/* 食物添加悬浮窗 */} setShowFoodOverlay(false)} mealType={getCurrentMealType()} /> ); } const styles = StyleSheet.create({ container: { flex: 1, }, viewModeContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, marginBottom: 8, }, monthTitle: { fontSize: 22, fontWeight: '800', }, toggleContainer: { flexDirection: 'row', borderRadius: 20, padding: 2, }, toggleButton: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 18, minWidth: 80, alignItems: 'center', }, toggleText: { fontSize: 14, fontWeight: '600', }, daysContainer: { marginBottom: 12, }, daysScrollContainer: { paddingHorizontal: 16, paddingVertical: 8, }, dayPill: { width: 48, height: 48, borderRadius: 34, marginRight: 12, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 4, elevation: 3, }, dayNumber: { fontSize: 18, textAlign: 'center', }, dayLabel: { fontSize: 12, marginTop: 2, textAlign: 'center', }, addButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, loadingText: { marginTop: 12, fontSize: 16, fontWeight: '500', }, listContainer: { paddingHorizontal: 16, paddingTop: 8, }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingVertical: 60, paddingHorizontal: 16, }, emptyContent: { alignItems: 'center', maxWidth: 320, }, emptyTitle: { fontSize: 18, fontWeight: '700', marginTop: 16, marginBottom: 8, textAlign: 'center', }, emptySubtitle: { fontSize: 14, fontWeight: '500', textAlign: 'center', lineHeight: 20, }, footerContainer: { paddingVertical: 20, alignItems: 'center', }, footerText: { fontSize: 14, fontWeight: '500', }, loadMoreButton: { paddingVertical: 16, alignItems: 'center', }, loadMoreText: { fontSize: 16, fontWeight: '600', }, });