diff --git a/app.json b/app.json index 0f4f770..fa5fa0f 100644 --- a/app.json +++ b/app.json @@ -1,10 +1,10 @@ { "expo": { - "name": "Health Bot", + "name": "Sealife", "slug": "digital-pilates", "version": "1.0.4", "orientation": "portrait", - "icon": "./assets/images/logo.png", + "icon": "./assets/images/Sealife.jpeg", "scheme": "digitalpilates", "userInterfaceStyle": "light", "newArchEnabled": true, @@ -21,7 +21,7 @@ }, "android": { "adaptiveIcon": { - "foregroundImage": "./assets/images/logo.png", + "foregroundImage": "./assets/images/Sealife.jpeg", "backgroundColor": "#ffffff" }, "edgeToEdgeEnabled": true, @@ -30,14 +30,14 @@ "web": { "bundler": "metro", "output": "static", - "favicon": "./assets/images/logo.png" + "favicon": "./assets/images/Sealife.jpeg" }, "plugins": [ "expo-router", [ "expo-splash-screen", { - "image": "./assets/images/logo.png", + "image": "./assets/images/Sealife.jpeg", "imageWidth": 200, "resizeMode": "contain", "backgroundColor": "#ffffff" diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index 36aeead..28fa6f0 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -172,7 +172,7 @@ export default function CoachScreen() { const generateWelcomeMessage = useCallback(() => { const hour = new Date().getHours(); const name = userProfile?.name || '朋友'; - const botName = (params?.name || 'Health Bot').toString(); + const botName = (params?.name || '海豹助手').toString(); // 时段问候 let timeGreeting = ''; @@ -197,7 +197,7 @@ export default function CoachScreen() { messages: [ `${timeGreeting},${name}!我是${botName},你的专属健康管理助手。新的一天开始了,让我们一起为你的健康目标努力吧!`, `${timeGreeting}!早晨是制定健康计划的最佳时机,我是${botName},可以帮你管理营养摄入、运动计划和生活作息。`, - `${timeGreeting},${name}!作为你的Health Bot,我很高兴能陪伴你的健康之旅。无论是饮食营养、健身锻炼还是生活管理,我都能为你提供专业建议。` + `${timeGreeting},${name}!作为你的海豹助手,我很高兴能陪伴你的健康之旅。无论是饮食营养、健身锻炼还是生活管理,我都能为你提供专业建议。` ] }, { @@ -213,7 +213,7 @@ export default function CoachScreen() { messages: [ `${timeGreeting},${name}!午餐时间很关键呢,合理的营养搭配能为下午提供充足能量。我是${botName},可以为你分析饮食营养和热量管理。`, `${timeGreeting}!忙碌的上午结束了,该关注一下身体需求啦。我是你的健康助手${botName},无论是饮食调整、运动安排还是休息建议,都可以找我。`, - `${timeGreeting},${name}!午间是调整状态的好时机。作为你的Health Bot,我建议关注饮食均衡和适度放松~` + `${timeGreeting},${name}!午间是调整状态的好时机。作为你的海豹助手,我建议关注饮食均衡和适度放松~` ] }, { @@ -229,7 +229,7 @@ export default function CoachScreen() { messages: [ `${timeGreeting},${name}!忙碌了一天,现在是时候关注身心平衡了。我是${botName},可以为你提供放松建议、营养补充和恢复方案。`, `${timeGreeting}!夜幕降临,这是一天中最适合总结和调整的时刻。我是你的健康伙伴${botName},让我们一起回顾今天的健康表现,规划明天的目标。`, - `${timeGreeting},${name}!晚间时光属于你自己,也是关爱身体的珍贵时间。作为你的Health Bot,我想陪你聊聊如何更好地管理健康生活。` + `${timeGreeting},${name}!晚间时光属于你自己,也是关爱身体的珍贵时间。作为你的海豹助手,我想陪你聊聊如何更好地管理健康生活。` ] }, { @@ -250,7 +250,7 @@ export default function CoachScreen() { }, { condition: () => userProfile && (!userProfile.pilatesPurposes || userProfile.pilatesPurposes.length === 0), - message: `${timeGreeting},${name}!作为你的Health Bot,我想更好地了解你的健康需求。告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~` + message: `${timeGreeting},${name}!作为你的海豹助手,我想更好地了解你的健康需求。告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~` } ]; @@ -1233,7 +1233,7 @@ export default function CoachScreen() { - 选择合适的方式记录您的饮食,Health Bot会根据您的饮食情况给出专业的营养建议。 + 选择合适的方式记录您的饮食,海豹助手会根据您的饮食情况给出专业的营养建议。 ); } @@ -1275,7 +1275,7 @@ export default function CoachScreen() { 发送记录 - 详细描述您的饮食内容和分量,有助于Health Bot给出更精准的营养分析和建议。 + 详细描述您的饮食内容和分量,有助于海豹助手给出更精准的营养分析和建议。 ); } diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 44b557d..d9422f2 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -30,6 +30,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function ExploreScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; + const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000; const userProfile = useAppSelector((s) => s.user.profile); @@ -68,6 +69,12 @@ export default function ExploreScreen() { // HealthKit: 每次页面聚焦都拉取今日数据 const [stepCount, setStepCount] = useState(null); const [activeCalories, setActiveCalories] = useState(null); + // 睡眠时长(分钟) + const [sleepDuration, setSleepDuration] = useState(null); + // HRV数据 + const [hrvValue, setHrvValue] = useState(69); + const [hrvStatus, setHrvStatus] = useState<'放松' | '正常' | '紧张'>('正常'); + const [isLoading, setIsLoading] = useState(false); // 用于触发动画重置的 token(当日期或数据变化时更新) const [animToken, setAnimToken] = useState(0); @@ -108,6 +115,21 @@ export default function ExploreScreen() { if (latestRequestKeyRef.current === requestKey) { setStepCount(data.steps); setActiveCalories(Math.round(data.activeEnergyBurned)); + setSleepDuration(data.sleepDuration); + + // 模拟HRV数据(实际应用中应从HealthKit获取) + const simulatedHrv = Math.floor(Math.random() * 80) + 30; // 30-110ms范围 + setHrvValue(simulatedHrv); + + // 根据HRV值判断状态 + if (simulatedHrv >= 70) { + setHrvStatus('放松'); + } else if (simulatedHrv >= 40) { + setHrvStatus('正常'); + } else { + setHrvStatus('紧张'); + } + setAnimToken((t) => t + 1); } else { console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current); @@ -187,6 +209,61 @@ export default function ExploreScreen() { 健康数据 + {/* HRV压力监测卡片 */} + + + + + + 压力监测 + + + + + {hrvValue} + 毫秒 + + + + {hrvStatus} + + {hrvStatus === '放松' ? '😌' : + hrvStatus === '正常' ? '😊' : + '😰'} + + + + + + + + + + 放松 + 正常 + 紧张 + + + + + {/* 查看更多 */} + + 查看更多 + + + {/* 标题与日期选择 */} {monthTitle} setTimeout(resolve, 800)); - - const data = getMockDietRecords({ + const data = await getDietRecords({ startDate, endDate, page: currentPage, @@ -140,7 +122,7 @@ export default function NutritionRecordsScreen() { // 渲染视图模式切换器 const renderViewModeToggle = () => ( - + {monthTitle} ( - - - {viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'} - - - {viewMode === 'daily' ? '点击右上角添加今日营养摄入' : '开始记录你的营养摄入吧'} - + + + + + + + + + + {viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'} + + + {viewMode === 'daily' ? '开始记录今日营养摄入' : '开始记录你的营养摄入吧'} + + + ); - const renderRecord = ({ item }: { item: DietRecord }) => ( - + const renderRecord = ({ item, index }: { item: DietRecord; index: number }) => ( + ); const renderFooter = () => { @@ -296,17 +292,6 @@ export default function NutritionRecordsScreen() { router.back()} - right={ - { - // TODO: 跳转到添加营养记录页面 - console.log('添加营养记录'); - }} - > - - - } /> {renderViewModeToggle()} @@ -322,11 +307,11 @@ export default function NutritionRecordsScreen() { ) : ( renderRecord({ item, index })} keyExtractor={(item) => item.id.toString()} contentContainerStyle={[ styles.listContainer, - { paddingBottom: 40 } + { paddingBottom: 40, paddingTop: 16 } ]} showsVerticalScrollIndicator={false} refreshControl={ @@ -387,8 +372,8 @@ const styles = StyleSheet.create({ paddingVertical: 8, }, dayPill: { - width: 68, - height: 68, + width: 48, + height: 48, borderRadius: 34, marginRight: 12, alignItems: 'center', @@ -434,18 +419,47 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', paddingVertical: 60, + paddingHorizontal: 16, + }, + emptyTimelineContainer: { + flexDirection: 'row', + alignItems: 'center', + maxWidth: 320, + }, + emptyTimeline: { + width: 64, + alignItems: 'center', + paddingTop: 8, + }, + emptyTimelineDot: { + width: 32, + height: 32, + borderRadius: 16, + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + emptyContent: { + flex: 1, + alignItems: 'center', + marginLeft: 16, }, emptyTitle: { - fontSize: 20, + fontSize: 18, fontWeight: '700', marginTop: 16, marginBottom: 8, + textAlign: 'center', }, emptySubtitle: { - fontSize: 16, + fontSize: 14, fontWeight: '500', textAlign: 'center', - lineHeight: 22, + lineHeight: 20, }, footerContainer: { paddingVertical: 20, diff --git a/assets/images/Sealife.jpeg b/assets/images/Sealife.jpeg new file mode 100644 index 0000000..2d9574b Binary files /dev/null and b/assets/images/Sealife.jpeg differ diff --git a/components/NutritionRecordCard.tsx b/components/NutritionRecordCard.tsx index 0d2ae48..800f20e 100644 --- a/components/NutritionRecordCard.tsx +++ b/components/NutritionRecordCard.tsx @@ -1,7 +1,6 @@ -import { RadarChart } from '@/components/RadarChart'; import { ThemedText } from '@/components/ThemedText'; import { useThemeColor } from '@/hooks/useThemeColor'; -import { DietRecord, calculateNutritionSummary, convertToRadarData } from '@/services/dietRecords'; +import { DietRecord } from '@/services/dietRecords'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import React, { useMemo } from 'react'; @@ -10,17 +9,11 @@ import { Image, StyleSheet, TouchableOpacity, View } from 'react-native'; export type NutritionRecordCardProps = { record: DietRecord; onPress?: () => void; + showTimeline?: boolean; + isFirst?: boolean; + isLast?: boolean; }; -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: '午餐', @@ -45,55 +38,45 @@ const MEAL_TYPE_COLORS = { other: '#9AA3AE', } as const; -export function NutritionRecordCard({ record, onPress }: NutritionRecordCardProps) { +export function NutritionRecordCard({ + record, + onPress, + showTimeline = false, + isFirst = false, + isLast = false +}: 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)} 千卡` : '-', + icon: 'flame-outline' as const, color: '#FF6B6B' }, { label: '蛋白质', value: record.proteinGrams ? `${record.proteinGrams.toFixed(1)} g` : '-', + icon: 'fitness-outline' as const, color: '#4ECDC4' }, { label: '碳水', value: record.carbohydrateGrams ? `${record.carbohydrateGrams.toFixed(1)} g` : '-', + icon: 'leaf-outline' as const, color: '#45B7D1' }, { label: '脂肪', value: record.fatGrams ? `${record.fatGrams.toFixed(1)} g` : '-', + icon: 'water-outline' as const, 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]); @@ -102,170 +85,173 @@ export function NutritionRecordCard({ record, onPress }: NutritionRecordCardProp const mealTypeLabel = MEAL_TYPE_LABELS[record.mealType]; return ( - - {/* 卡片头部 */} - - - - - - - - {mealTypeLabel} - - - {record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '时间未设置'} + + {/* 时间轴 */} + {showTimeline && ( + + + + {record.createdAt ? dayjs(record.createdAt).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} - + {!isLast && ( + + )} - ))} - - - {/* 备注信息 */} - {record.notes && ( - - - 备注 - - - {record.notes} - )} - + + {/* 卡片内容 */} + + {/* 主要内容区域 */} + + {/* 左侧:食物图片 */} + + {record.imageUrl ? ( + + ) : ( + + )} + + + {/* 右侧:食物信息 */} + + {/* 餐次和操作按钮 */} + + + + + {mealTypeLabel} + + + {!showTimeline && ( + + {record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '时间未设置'} + + )} + + + + + + + {/* 食物名称和分量 */} + + + {record.foodName} + + {(record.weightGrams || record.portionDescription) && ( + + {record.portionDescription || `${record.weightGrams}g`} + + )} + + + {/* 营养信息网格 */} + + {nutritionStats.map((stat) => ( + + + + + {stat.label} + + + + {stat.value} + + + ))} + + + {/* 备注信息 */} + {record.notes && ( + + + {record.notes} + + + )} + + + + ); } const styles = StyleSheet.create({ - card: { - borderRadius: 22, - padding: 20, + timelineContainer: { + flexDirection: 'row', marginBottom: 12, + }, + timelineColumn: { + width: 64, + alignItems: 'center', + paddingTop: 8, + }, + timeContainer: { + marginBottom: 8, + }, + timeText: { + fontSize: 12, + fontWeight: '600', + textAlign: 'center', + }, + timelineNode: { + alignItems: 'center', + flex: 1, + }, + timelineDot: { + width: 24, + height: 24, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.08, - shadowRadius: 6, - elevation: 3, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, }, - cardHeader: { + timelineLine: { + width: 2, + flex: 1, + marginTop: 8, + opacity: 0.3, + }, + card: { + flex: 1, + borderRadius: 16, + padding: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, + }, + cardWithTimeline: { + marginLeft: 8, + }, + mainContent: { 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, + width: 80, + height: 80, + borderRadius: 16, + marginRight: 16, overflow: 'hidden', }, foodImage: { @@ -273,86 +259,88 @@ const styles = StyleSheet.create({ height: '100%', }, foodImagePlaceholder: { - backgroundColor: '#F5F5F5', + backgroundColor: '#F8F9FA', justifyContent: 'center', alignItems: 'center', }, - foodInfo: { + foodInfoContainer: { flex: 1, - justifyContent: 'center', + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 8, + }, + mealTypeContainer: { + flex: 1, + }, + mealTypeBadge: { + alignSelf: 'flex-start', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 8, + marginBottom: 4, + }, + mealTypeText: { + fontSize: 12, + fontWeight: '600', + }, + mealTime: { + fontSize: 11, + fontWeight: '500', + }, + moreButton: { + padding: 4, + marginLeft: 8, + }, + foodNameSection: { + marginBottom: 12, }, foodName: { fontSize: 18, fontWeight: '700', - marginBottom: 4, - }, - foodDescription: { - fontSize: 14, - fontWeight: '500', - lineHeight: 20, - marginBottom: 4, + lineHeight: 24, + marginBottom: 2, }, 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, + }, + nutritionGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, + nutritionItem: { + width: '50%', + marginBottom: 8, + paddingRight: 8, + }, + nutritionItemHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 2, + }, + nutritionLabel: { + fontSize: 12, + fontWeight: '500', + marginLeft: 4, + }, + nutritionValue: { + fontSize: 14, + fontWeight: '700', + }, + notesSection: { + marginTop: 8, + paddingTop: 12, + borderTopWidth: 1, + borderTopColor: 'rgba(0,0,0,0.06)', + }, + notesText: { + fontSize: 13, + fontWeight: '500', + lineHeight: 18, + fontStyle: 'italic', }, }); diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj index 755430f..4658dfe 100644 --- a/ios/digitalpilates.xcodeproj/project.pbxproj +++ b/ios/digitalpilates.xcodeproj/project.pbxproj @@ -330,6 +330,7 @@ "FB_SONARKIT_ENABLED=1", ); INFOPLIST_FILE = digitalpilates/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Sealife; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -367,6 +368,7 @@ DEVELOPMENT_TEAM = 756WVXJ6MT; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64; INFOPLIST_FILE = digitalpilates/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Sealife; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -447,10 +449,7 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -505,10 +504,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = false; diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index b7b771b..6fc1c63 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Health Bot + Sealife CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/services/mockDietRecords.ts b/services/mockDietRecords.ts deleted file mode 100644 index 5ad0c3b..0000000 --- a/services/mockDietRecords.ts +++ /dev/null @@ -1,211 +0,0 @@ -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/health.ts b/utils/health.ts index c92490d..a1fe3f5 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -6,6 +6,8 @@ const PERMISSIONS: HealthKitPermissions = { read: [ AppleHealthKit.Constants.Permissions.StepCount, AppleHealthKit.Constants.Permissions.ActiveEnergyBurned, + AppleHealthKit.Constants.Permissions.SleepAnalysis, + AppleHealthKit.Constants.Permissions.HeartRateVariability, ], write: [], }, @@ -14,6 +16,8 @@ const PERMISSIONS: HealthKitPermissions = { export type TodayHealthData = { steps: number; activeEnergyBurned: number; // kilocalories + sleepDuration: number; // 睡眠时长(分钟) + hrv: number | null; // 心率变异性 (ms) }; export async function ensureHealthPermissions(): Promise { @@ -46,46 +50,147 @@ export async function fetchHealthDataForDate(date: Date): Promise((resolve) => { - AppleHealthKit.getStepCount(options, (err, res) => { - if (err) { - console.error('获取步数失败:', err); - return resolve(0); - } - if (!res) { - console.warn('步数数据为空'); - return resolve(0); - } - console.log('步数数据:', res); - resolve(res.value || 0); - }); - }); + // 并行获取所有健康数据 + const [steps, calories, sleepDuration, hrv] = await Promise.all([ + // 获取步数 + new Promise((resolve) => { + AppleHealthKit.getStepCount(options, (err, res) => { + if (err) { + console.error('获取步数失败:', err); + return resolve(0); + } + if (!res) { + console.warn('步数数据为空'); + return resolve(0); + } + console.log('步数数据:', res); + resolve(res.value || 0); + }); + }), - const calories = await new Promise((resolve) => { - AppleHealthKit.getActiveEnergyBurned(options, (err, res) => { - if (err) { - console.error('获取消耗卡路里失败:', err); - return resolve(0); - } - if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('卡路里数据为空或格式错误'); - return resolve(0); - } - console.log('卡路里数据:', res); - // 求和该日内的所有记录(单位:千卡) - const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); - resolve(total); - }); - }); + // 获取消耗卡路里 + new Promise((resolve) => { + AppleHealthKit.getActiveEnergyBurned(options, (err, res) => { + if (err) { + console.error('获取消耗卡路里失败:', err); + return resolve(0); + } + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn('卡路里数据为空或格式错误'); + return resolve(0); + } + console.log('卡路里数据:', res); + // 求和该日内的所有记录(单位:千卡) + const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); + resolve(total); + }); + }), - console.log('指定日期健康数据获取完成:', { steps, calories }); - return { steps, activeEnergyBurned: calories }; + // 获取睡眠时长 + new Promise((resolve) => { + AppleHealthKit.getSleepSamples(options, (err, res) => { + if (err) { + console.error('获取睡眠数据失败:', err); + return resolve(0); + } + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn('睡眠数据为空或格式错误'); + return resolve(0); + } + console.log('睡眠数据:', res); + + // 计算总睡眠时间(单位:分钟) + let totalSleepDuration = 0; + res.forEach((sample: any) => { + if (sample && sample.startDate && sample.endDate) { + const startTime = new Date(sample.startDate).getTime(); + const endTime = new Date(sample.endDate).getTime(); + const durationMinutes = (endTime - startTime) / (1000 * 60); + totalSleepDuration += durationMinutes; + } + }); + + resolve(totalSleepDuration); + }); + }), + + // 获取HRV数据 + new Promise((resolve) => { + AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { + if (err) { + console.error('获取HRV数据失败:', err); + return resolve(null); + } + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn('HRV数据为空或格式错误'); + return resolve(null); + } + console.log('HRV数据:', res); + + // 获取最新的HRV值 + const latestHrv = res[res.length - 1]; + if (latestHrv && latestHrv.value) { + resolve(latestHrv.value); + } else { + resolve(null); + } + }); + }) + ]); + + console.log('指定日期健康数据获取完成:', { steps, calories, sleepDuration, hrv }); + return { steps, activeEnergyBurned: calories, sleepDuration, hrv }; } export async function fetchTodayHealthData(): Promise { return fetchHealthDataForDate(new Date()); +} + +// 新增:专门获取HRV数据的函数 +export async function fetchHRVForDate(date: Date): Promise { + console.log('开始获取指定日期HRV数据...', date); + + const start = new Date(date); + start.setHours(0, 0, 0, 0); + const end = new Date(date); + end.setHours(23, 59, 59, 999); + + const options = { + startDate: start.toISOString(), + endDate: end.toISOString() + } as any; + + return new Promise((resolve) => { + AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { + if (err) { + console.error('获取HRV数据失败:', err); + return resolve(null); + } + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn('HRV数据为空或格式错误'); + return resolve(null); + } + console.log('HRV数据:', res); + + // 获取最新的HRV值 + const latestHrv = res[res.length - 1]; + if (latestHrv && latestHrv.value) { + resolve(latestHrv.value); + } else { + resolve(null); + } + }); + }); +} + +// 新增:获取今日HRV数据 +export async function fetchTodayHRV(): Promise { + return fetchHRVForDate(new Date()); } \ No newline at end of file