diff --git a/app/_layout.tsx b/app/_layout.tsx index cd2f645..3a99913 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -16,7 +16,6 @@ import Toast from 'react-native-toast-message'; import { DialogProvider } from '@/components/ui/DialogProvider'; import { Provider } from 'react-redux'; -import { ROUTES } from '@/constants/Routes'; function Bootstrapper({ children }: { children: React.ReactNode }) { const dispatch = useAppDispatch(); @@ -29,7 +28,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { await dispatch(rehydrateUser()); setUserDataLoaded(true); }; - + loadUserData(); // 冷启动时清空 AI 教练会话缓存 clearAiCoachSessionCache(); @@ -48,7 +47,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { }; const handlePrivacyDisagree = () => { - RNExitApp.exitApp(); + RNExitApp.exitApp(); }; return ( @@ -93,6 +92,7 @@ export default function RootLayout() { + diff --git a/app/nutrition/_layout.tsx b/app/nutrition/_layout.tsx new file mode 100644 index 0000000..ba5aa56 --- /dev/null +++ b/app/nutrition/_layout.tsx @@ -0,0 +1,9 @@ +import { Stack } from 'expo-router'; + +export default function NutritionLayout() { + return ( + + + + ); +} diff --git a/app/nutrition/records.tsx b/app/nutrition/records.tsx new file mode 100644 index 0000000..1664a56 --- /dev/null +++ b/app/nutrition/records.tsx @@ -0,0 +1,466 @@ +import { NutritionRecordCard } from '@/components/NutritionRecordCard'; +import { HeaderBar } from '@/components/ui/HeaderBar'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import { DietRecord } from '@/services/dietRecords'; +import { getMockDietRecords } from '@/services/mockDietRecords'; +import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date'; +import { Ionicons } from '@expo/vector-icons'; +// import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import dayjs from 'dayjs'; +import { router } from 'expo-router'; +import React, { useEffect, useRef, useState } from 'react'; +import { + ActivityIndicator, + FlatList, + RefreshControl, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +type ViewMode = 'daily' | 'all'; + +export default function NutritionRecordsScreen() { + const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; + const colorTokens = Colors[theme]; + // const tabBarHeight = useBottomTabBarHeight(); + const insets = useSafeAreaInsets(); + + // 日期相关状态 - 使用与统计页面相同的日期逻辑 + const days = getMonthDaysZh(); + const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); + const monthTitle = getMonthTitleZh(); + + // 视图模式:按天查看 vs 全部查看 + const [viewMode, setViewMode] = useState('daily'); + + // 数据状态 + const [records, setRecords] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [hasMoreData, setHasMoreData] = useState(true); + const [page, setPage] = useState(1); + + // 日期滚动相关 + const daysScrollRef = useRef(null); + const [scrollWidth, setScrollWidth] = useState(0); + const DAY_PILL_WIDTH = 68; + const DAY_PILL_SPACING = 12; + + // 日期滚动控制 + const scrollToIndex = (index: number, animated = true) => { + const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING); + const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2)); + daysScrollRef.current?.scrollTo({ x: centerOffset, animated }); + }; + + useEffect(() => { + if (scrollWidth > 0) { + scrollToIndex(selectedIndex, false); + } + }, [scrollWidth, selectedIndex]); + + // 加载记录数据 + const loadRecords = async (isRefresh = false, loadMore = false) => { + try { + if (isRefresh) { + setRefreshing(true); + setPage(1); + } else if (loadMore) { + // 加载更多时不显示loading + } else { + setLoading(true); + } + + const currentPage = isRefresh ? 1 : (loadMore ? page + 1 : 1); + + let startDate: string | undefined; + let endDate: string | undefined; + + if (viewMode === 'daily') { + // 按天查看时,获取选中日期的数据 + const selectedDate = days[selectedIndex]?.date?.format('YYYY-MM-DD') ?? dayjs().format('YYYY-MM-DD'); + startDate = selectedDate; + endDate = selectedDate; + } + // viewMode === 'all' 时不设置日期范围,获取所有数据 + + // 使用模拟数据进行测试 + // const data = await getDietRecords({ + // startDate, + // endDate, + // page: currentPage, + // limit: 10, + // }); + + // 模拟网络延迟 + await new Promise(resolve => setTimeout(resolve, 800)); + + const data = getMockDietRecords({ + startDate, + endDate, + page: currentPage, + limit: 10, + }); + + if (isRefresh || currentPage === 1) { + setRecords(data.records); + } else { + setRecords(prev => [...prev, ...data.records]); + } + + setHasMoreData(data.records.length === 10); // 如果返回的记录数少于limit,说明没有更多数据 + setPage(currentPage); + } catch (error) { + console.error('加载营养记录失败:', error); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + // 当选中日期或视图模式变化时重新加载数据 + useEffect(() => { + loadRecords(); + }, [selectedIndex, viewMode]); + + const onRefresh = () => { + loadRecords(true); + }; + + const loadMoreRecords = () => { + if (hasMoreData && !loading && !refreshing) { + loadRecords(false, true); + } + }; + + // 渲染视图模式切换器 + const renderViewModeToggle = () => ( + + {monthTitle} + + setViewMode('daily')} + > + + 按天查看 + + + setViewMode('all')} + > + + 全部记录 + + + + + ); + + // 渲染日期选择器(仅在按天查看模式下显示) + const renderDateSelector = () => { + if (viewMode !== 'daily') return null; + + return ( + + setScrollWidth(e.nativeEvent.layout.width)} + > + {days.map((day, index) => { + const isSelected = index === selectedIndex; + const isToday = day.isToday; + const isDisabled = day.date?.isAfter(dayjs(), 'day') ?? false; + + return ( + { + if (!isDisabled) { + setSelectedIndex(index); + scrollToIndex(index); + } + }} + disabled={isDisabled} + > + + {day.date?.date() ?? ''} + + + {day.dayAbbr} + + + ); + })} + + + ); + }; + + const renderEmptyState = () => ( + + + + {viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'} + + + {viewMode === 'daily' ? '点击右上角添加今日营养摄入' : '开始记录你的营养摄入吧'} + + + ); + + const renderRecord = ({ item }: { item: DietRecord }) => ( + + ); + + const renderFooter = () => { + if (!hasMoreData) { + return ( + + + 没有更多数据了 + + + ); + } + + if (viewMode === 'all' && records.length > 0) { + return ( + + + 加载更多 + + + ); + } + + return null; + }; + + return ( + + router.back()} + right={ + { + // TODO: 跳转到添加营养记录页面 + console.log('添加营养记录'); + }} + > + + + } + /> + + {renderViewModeToggle()} + {renderDateSelector()} + + {loading ? ( + + + + 加载中... + + + ) : ( + item.id.toString()} + contentContainerStyle={[ + styles.listContainer, + { paddingBottom: 40 } + ]} + showsVerticalScrollIndicator={false} + refreshControl={ + + } + ListEmptyComponent={renderEmptyState} + ListFooterComponent={renderFooter} + onEndReached={viewMode === 'all' ? loadMoreRecords : undefined} + onEndReachedThreshold={0.1} + /> + )} + + ); +} + +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: 68, + height: 68, + 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', + }, + 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, + }, + emptyTitle: { + fontSize: 20, + fontWeight: '700', + marginTop: 16, + marginBottom: 8, + }, + emptySubtitle: { + fontSize: 16, + fontWeight: '500', + textAlign: 'center', + lineHeight: 22, + }, + footerContainer: { + paddingVertical: 20, + alignItems: 'center', + }, + footerText: { + fontSize: 14, + fontWeight: '500', + }, + loadMoreButton: { + paddingVertical: 16, + alignItems: 'center', + }, + loadMoreText: { + fontSize: 16, + fontWeight: '600', + }, +}); diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index 464d3dc..db5396d 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -1,8 +1,10 @@ +import { ROUTES } from '@/constants/Routes'; import { NutritionSummary } from '@/services/dietRecords'; import Feather from '@expo/vector-icons/Feather'; import dayjs from 'dayjs'; +import { router } from 'expo-router'; import React, { useMemo } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { RadarCategory, RadarChart } from './RadarChart'; export type NutritionRadarCardProps = { @@ -55,8 +57,12 @@ export function NutritionRadarCard({ nutritionSummary, isLoading = false }: Nutr ]; }, [nutritionSummary]); + const handleNavigateToRecords = () => { + router.push(ROUTES.NUTRITION_RECORDS); + }; + return ( - + 营养摄入分析 @@ -92,7 +98,7 @@ export function NutritionRadarCard({ nutritionSummary, isLoading = false }: Nutr )} - + ); } diff --git a/components/NutritionRecordCard.tsx b/components/NutritionRecordCard.tsx new file mode 100644 index 0000000..0d2ae48 --- /dev/null +++ b/components/NutritionRecordCard.tsx @@ -0,0 +1,358 @@ +import { RadarChart } from '@/components/RadarChart'; +import { ThemedText } from '@/components/ThemedText'; +import { useThemeColor } from '@/hooks/useThemeColor'; +import { DietRecord, calculateNutritionSummary, convertToRadarData } from '@/services/dietRecords'; +import { Ionicons } from '@expo/vector-icons'; +import dayjs from 'dayjs'; +import React, { useMemo } from 'react'; +import { Image, StyleSheet, TouchableOpacity, View } from 'react-native'; + +export type NutritionRecordCardProps = { + record: DietRecord; + onPress?: () => void; +}; + +const NUTRITION_DIMENSIONS = [ + { key: 'calories', label: '热量' }, + { key: 'protein', label: '蛋白质' }, + { key: 'carbohydrate', label: '碳水' }, + { key: 'fat', label: '脂肪' }, + { key: 'fiber', label: '纤维' }, + { key: 'sodium', label: '钠' }, +]; + +const MEAL_TYPE_LABELS = { + breakfast: '早餐', + lunch: '午餐', + dinner: '晚餐', + snack: '加餐', + other: '其他', +} as const; + +const MEAL_TYPE_ICONS = { + breakfast: 'sunny-outline', + lunch: 'partly-sunny-outline', + dinner: 'moon-outline', + snack: 'cafe-outline', + other: 'restaurant-outline', +} as const; + +const MEAL_TYPE_COLORS = { + breakfast: '#FFB366', + lunch: '#4ECDC4', + dinner: '#5D5FEF', + snack: '#FF6B6B', + other: '#9AA3AE', +} as const; + +export function NutritionRecordCard({ record, onPress }: NutritionRecordCardProps) { + const surfaceColor = useThemeColor({}, 'surface'); + const textColor = useThemeColor({}, 'text'); + const textSecondaryColor = useThemeColor({}, 'textSecondary'); + const primaryColor = useThemeColor({}, 'primary'); + + // 计算单条记录的营养摘要 + const nutritionSummary = useMemo(() => { + return calculateNutritionSummary([record]); + }, [record]); + + // 计算雷达图数据 + const radarValues = useMemo(() => { + return convertToRadarData(nutritionSummary); + }, [nutritionSummary]); + + // 营养维度数据 + const nutritionStats = useMemo(() => { + return [ + { + label: '热量', + value: record.estimatedCalories ? `${Math.round(record.estimatedCalories)} 千卡` : '-', + color: '#FF6B6B' + }, + { + label: '蛋白质', + value: record.proteinGrams ? `${record.proteinGrams.toFixed(1)} g` : '-', + color: '#4ECDC4' + }, + { + label: '碳水', + value: record.carbohydrateGrams ? `${record.carbohydrateGrams.toFixed(1)} g` : '-', + color: '#45B7D1' + }, + { + label: '脂肪', + value: record.fatGrams ? `${record.fatGrams.toFixed(1)} g` : '-', + color: '#FFA07A' + }, + { + label: '纤维', + value: record.fiberGrams ? `${record.fiberGrams.toFixed(1)} g` : '-', + color: '#98D8C8' + }, + { + label: '钠', + value: record.sodiumMg ? `${Math.round(record.sodiumMg)} mg` : '-', + color: '#F7DC6F' + }, + ]; + }, [record]); + + const mealTypeColor = MEAL_TYPE_COLORS[record.mealType]; + const mealTypeIcon = MEAL_TYPE_ICONS[record.mealType]; + const mealTypeLabel = MEAL_TYPE_LABELS[record.mealType]; + + return ( + + {/* 卡片头部 */} + + + + + + + + {mealTypeLabel} + + + {record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '时间未设置'} + + + + + + + + + + {/* 食物信息 */} + + + {record.imageUrl ? ( + + ) : ( + + )} + + + + {record.foodName} + + {record.foodDescription && ( + + {record.foodDescription} + + )} + {(record.weightGrams || record.portionDescription) && ( + + {record.weightGrams ? `${record.weightGrams}g` : ''} + {record.weightGrams && record.portionDescription ? ' • ' : ''} + {record.portionDescription || ''} + + )} + + + + {/* 营养分析区域 */} + + + + + + + {nutritionStats.slice(0, 4).map((stat) => ( + + + + {stat.label} + + + {stat.value} + + + ))} + + + + {/* 额外的营养信息 */} + + {nutritionStats.slice(4).map((stat) => ( + + + + {stat.label} + + + {stat.value} + + + ))} + + + {/* 备注信息 */} + {record.notes && ( + + + 备注 + + + {record.notes} + + + )} + + ); +} + +const styles = StyleSheet.create({ + card: { + borderRadius: 22, + padding: 20, + marginBottom: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 6, + elevation: 3, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 16, + }, + mealInfo: { + flexDirection: 'row', + alignItems: 'center', + }, + mealTypeIndicator: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + mealDetails: { + justifyContent: 'center', + }, + mealType: { + fontSize: 16, + fontWeight: '700', + }, + mealTime: { + fontSize: 13, + fontWeight: '500', + marginTop: 2, + }, + moreButton: { + padding: 4, + }, + foodSection: { + flexDirection: 'row', + marginBottom: 20, + }, + foodImageContainer: { + width: 60, + height: 60, + borderRadius: 12, + marginRight: 12, + overflow: 'hidden', + }, + foodImage: { + width: '100%', + height: '100%', + }, + foodImagePlaceholder: { + backgroundColor: '#F5F5F5', + justifyContent: 'center', + alignItems: 'center', + }, + foodInfo: { + flex: 1, + justifyContent: 'center', + }, + foodName: { + fontSize: 18, + fontWeight: '700', + marginBottom: 4, + }, + foodDescription: { + fontSize: 14, + fontWeight: '500', + lineHeight: 20, + marginBottom: 4, + }, + portionInfo: { + fontSize: 13, + fontWeight: '600', + }, + nutritionSection: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + }, + radarContainer: { + marginRight: 16, + }, + statsContainer: { + flex: 1, + }, + statItem: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + statDot: { + width: 8, + height: 8, + borderRadius: 4, + marginRight: 8, + }, + statLabel: { + fontSize: 13, + fontWeight: '600', + flex: 1, + }, + statValue: { + fontSize: 13, + fontWeight: '700', + }, + additionalStats: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 12, + }, + additionalStatItem: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + notesSection: { + marginTop: 12, + paddingTop: 16, + borderTopWidth: 1, + borderTopColor: 'rgba(0,0,0,0.08)', + }, + notesLabel: { + fontSize: 12, + fontWeight: '600', + marginBottom: 6, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + notesText: { + fontSize: 14, + fontWeight: '500', + lineHeight: 20, + }, +}); diff --git a/constants/Routes.ts b/constants/Routes.ts index 12ef5cb..37c8c5f 100644 --- a/constants/Routes.ts +++ b/constants/Routes.ts @@ -36,6 +36,9 @@ export const ROUTES = { // 引导页路由 ONBOARDING: '/onboarding', ONBOARDING_PERSONAL_INFO: '/onboarding/personal-info', + + // 营养相关路由 + NUTRITION_RECORDS: '/nutrition/records', } as const; // 路由参数常量 diff --git a/services/dietRecords.ts b/services/dietRecords.ts index a882252..0ce811a 100644 --- a/services/dietRecords.ts +++ b/services/dietRecords.ts @@ -40,16 +40,26 @@ export type NutritionSummary = { export async function getDietRecords({ startDate, endDate, + page = 1, + limit = 10, }: { - startDate: string; - endDate: string; + startDate?: string; + endDate?: string; + page?: number; + limit?: number; }): Promise<{ records: DietRecord[] total: number page: number limit: number }> { - const params = startDate && endDate ? `?startDate=${startDate}&endDate=${endDate}` : ''; + const searchParams = new URLSearchParams(); + if (startDate) searchParams.append('startDate', startDate); + if (endDate) searchParams.append('endDate', endDate); + searchParams.append('page', page.toString()); + searchParams.append('limit', limit.toString()); + + const params = searchParams.toString() ? `?${searchParams.toString()}` : ''; return await api.get<{ records: DietRecord[] total: number diff --git a/services/mockDietRecords.ts b/services/mockDietRecords.ts new file mode 100644 index 0000000..5ad0c3b --- /dev/null +++ b/services/mockDietRecords.ts @@ -0,0 +1,211 @@ +import dayjs from 'dayjs'; +import { DietRecord } from './dietRecords'; + +// 模拟营养记录数据,用于测试UI效果 +export const mockDietRecords: DietRecord[] = [ + // 今天的记录 + { + id: 1, + mealType: 'breakfast', + foodName: '燕麦粥配蓝莓', + foodDescription: '有机燕麦片,新鲜蓝莓,低脂牛奶', + weightGrams: 300, + portionDescription: '1大碗', + estimatedCalories: 280, + proteinGrams: 12.5, + carbohydrateGrams: 45.2, + fatGrams: 6.8, + fiberGrams: 8.5, + sugarGrams: 15.3, + sodiumMg: 120, + source: 'manual', + mealTime: dayjs().hour(7).minute(30).toISOString(), + imageUrl: 'https://images.unsplash.com/photo-1511690743698-d9d85f2fbf38?w=300&h=300&fit=crop', + notes: '营养丰富的早餐,富含膳食纤维和抗氧化物质', + createdAt: dayjs().hour(7).minute(35).toISOString(), + updatedAt: dayjs().hour(7).minute(35).toISOString(), + }, + { + id: 2, + mealType: 'lunch', + foodName: '鸡胸肉沙拉', + foodDescription: '烤鸡胸肉,混合蔬菜,橄榄油调味', + weightGrams: 250, + portionDescription: '1份', + estimatedCalories: 320, + proteinGrams: 35.6, + carbohydrateGrams: 8.4, + fatGrams: 15.2, + fiberGrams: 6.2, + sugarGrams: 5.8, + sodiumMg: 480, + source: 'manual', + mealTime: dayjs().hour(12).minute(15).toISOString(), + imageUrl: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=300&h=300&fit=crop', + notes: '高蛋白低碳水,适合健身人群', + createdAt: dayjs().hour(12).minute(20).toISOString(), + updatedAt: dayjs().hour(12).minute(20).toISOString(), + }, + { + id: 3, + mealType: 'snack', + foodName: '混合坚果', + foodDescription: '杏仁,核桃,腰果混合装', + weightGrams: 30, + portionDescription: '1小包', + estimatedCalories: 180, + proteinGrams: 6.5, + carbohydrateGrams: 6.8, + fatGrams: 15.5, + fiberGrams: 3.2, + sugarGrams: 2.1, + sodiumMg: 5, + source: 'manual', + mealTime: dayjs().hour(15).minute(30).toISOString(), + notes: '健康的下午茶零食', + createdAt: dayjs().hour(15).minute(35).toISOString(), + updatedAt: dayjs().hour(15).minute(35).toISOString(), + }, + { + id: 4, + mealType: 'dinner', + foodName: '三文鱼配蒸蔬菜', + foodDescription: '挪威三文鱼,西兰花,胡萝卜', + weightGrams: 350, + portionDescription: '1份', + estimatedCalories: 420, + proteinGrams: 42.3, + carbohydrateGrams: 12.5, + fatGrams: 22.8, + fiberGrams: 5.6, + sugarGrams: 8.2, + sodiumMg: 380, + source: 'vision', + mealTime: dayjs().hour(19).minute(0).toISOString(), + imageUrl: 'https://images.unsplash.com/photo-1467003909585-2f8a72700288?w=300&h=300&fit=crop', + notes: '富含Omega-3脂肪酸,有益心血管健康', + createdAt: dayjs().hour(19).minute(10).toISOString(), + updatedAt: dayjs().hour(19).minute(10).toISOString(), + }, + + // 昨天的记录 + { + id: 5, + mealType: 'breakfast', + foodName: '希腊酸奶杯', + foodDescription: '无糖希腊酸奶,草莓,燕麦片', + weightGrams: 200, + portionDescription: '1杯', + estimatedCalories: 220, + proteinGrams: 20.4, + carbohydrateGrams: 18.6, + fatGrams: 8.2, + fiberGrams: 4.1, + sugarGrams: 12.5, + sodiumMg: 85, + source: 'manual', + mealTime: dayjs().subtract(1, 'day').hour(8).minute(0).toISOString(), + imageUrl: 'https://images.unsplash.com/photo-1488477181946-6428a0291777?w=300&h=300&fit=crop', + notes: '高蛋白早餐,饱腹感强', + createdAt: dayjs().subtract(1, 'day').hour(8).minute(5).toISOString(), + updatedAt: dayjs().subtract(1, 'day').hour(8).minute(5).toISOString(), + }, + + // 更多历史记录,用于测试分页 + { + id: 6, + mealType: 'lunch', + foodName: '牛肉面', + foodDescription: '手拉面条,牛肉汤底,青菜', + weightGrams: 400, + portionDescription: '1碗', + estimatedCalories: 580, + proteinGrams: 28.0, + carbohydrateGrams: 65.0, + fatGrams: 18.5, + fiberGrams: 4.8, + sugarGrams: 6.2, + sodiumMg: 1200, + source: 'manual', + mealTime: dayjs().subtract(2, 'day').hour(13).minute(0).toISOString(), + notes: '传统中式午餐', + createdAt: dayjs().subtract(2, 'day').hour(13).minute(10).toISOString(), + updatedAt: dayjs().subtract(2, 'day').hour(13).minute(10).toISOString(), + }, + { + id: 7, + mealType: 'breakfast', + foodName: '全麦吐司配牛油果', + foodDescription: '全麦面包,新鲜牛油果,煎蛋', + weightGrams: 180, + portionDescription: '2片吐司', + estimatedCalories: 350, + proteinGrams: 15.2, + carbohydrateGrams: 28.5, + fatGrams: 22.0, + fiberGrams: 8.0, + sugarGrams: 3.5, + sodiumMg: 220, + source: 'manual', + mealTime: dayjs().subtract(3, 'day').hour(8).minute(30).toISOString(), + imageUrl: 'https://images.unsplash.com/photo-1541519227354-08fa5d50c44d?w=300&h=300&fit=crop', + notes: '健康脂肪和蛋白质的完美组合', + createdAt: dayjs().subtract(3, 'day').hour(8).minute(35).toISOString(), + updatedAt: dayjs().subtract(3, 'day').hour(8).minute(35).toISOString(), + }, + { + id: 8, + mealType: 'dinner', + foodName: '蒸蛋羹', + foodDescription: '鸡蛋,温水,少许盐', + weightGrams: 150, + portionDescription: '1小碗', + estimatedCalories: 140, + proteinGrams: 12.0, + carbohydrateGrams: 1.0, + fatGrams: 10.0, + fiberGrams: 0, + sugarGrams: 1.0, + sodiumMg: 180, + source: 'manual', + mealTime: dayjs().subtract(4, 'day').hour(18).minute(45).toISOString(), + notes: '清淡易消化的晚餐', + createdAt: dayjs().subtract(4, 'day').hour(19).minute(0).toISOString(), + updatedAt: dayjs().subtract(4, 'day').hour(19).minute(0).toISOString(), + }, +]; + +// 模拟API响应,支持分页和日期过滤 +export function getMockDietRecords({ + startDate, + endDate, + page = 1, + limit = 10, +}: { + startDate?: string; + endDate?: string; + page?: number; + limit?: number; +} = {}) { + let filteredRecords = mockDietRecords; + + // 如果有日期范围,则过滤 + if (startDate && endDate) { + filteredRecords = mockDietRecords.filter(record => { + const recordDate = dayjs(record.mealTime).format('YYYY-MM-DD'); + return recordDate >= startDate && recordDate <= endDate; + }); + } + + // 分页 + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedRecords = filteredRecords.slice(startIndex, endIndex); + + return { + records: paginatedRecords, + total: filteredRecords.length, + page, + limit, + }; +} diff --git a/utils/date.ts b/utils/date.ts index afad423..0abd44e 100644 --- a/utils/date.ts +++ b/utils/date.ts @@ -29,10 +29,14 @@ export function getMonthTitleZh(date: Dayjs = dayjs()): string { export type MonthDay = { /** 中文星期:日/一/二/三/四/五/六 */ weekdayZh: string; + /** 简化的星期,用于显示 */ + dayAbbr: string; /** 月内第几日(1-31) */ dayOfMonth: number; /** 对应的 dayjs 对象 */ date: Dayjs; + /** 是否是今天 */ + isToday: boolean; }; /** 获取某月的所有日期(中文星期+日号) */ @@ -41,12 +45,18 @@ export function getMonthDaysZh(date: Dayjs = dayjs()): MonthDay[] { const monthIndex = date.month(); const daysInMonth = date.daysInMonth(); const zhWeek = ['日', '一', '二', '三', '四', '五', '六']; + const today = dayjs(); + return Array.from({ length: daysInMonth }, (_, i) => { const d = dayjs(new Date(year, monthIndex, i + 1)); + const isToday = d.isSame(today, 'day'); + return { weekdayZh: zhWeek[d.day()], + dayAbbr: zhWeek[d.day()], dayOfMonth: i + 1, date: d, + isToday, }; }); }