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,
};
});
}