import { BasalMetabolismCard } from '@/components/BasalMetabolismCard'; import { DateSelector } from '@/components/DateSelector'; import { FitnessRingsCard } from '@/components/FitnessRingsCard'; import { MoodCard } from '@/components/MoodCard'; import { NutritionRadarCard } from '@/components/NutritionRadarCard'; import HeartRateCard from '@/components/statistic/HeartRateCard'; import OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard'; import StepsCard from '@/components/StepsCard'; import { StressMeter } from '@/components/StressMeter'; import WaterIntakeCard from '@/components/WaterIntakeCard'; import { WeightHistoryCard } from '@/components/weight/WeightHistoryCard'; import { Colors } from '@/constants/Colors'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useBackgroundTasks } from '@/hooks/useBackgroundTasks'; import { notificationService } from '@/services/notifications'; import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice'; import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice'; import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { ensureHealthPermissions, fetchHealthDataForDate, fetchRecentHRV } from '@/utils/health'; import { getTestHealthData } from '@/utils/mockHealthData'; import { calculateNutritionGoals } from '@/utils/nutrition'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { AppState, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; // 浮动动画组件 const FloatingCard = ({ children, delay = 0, style }: { children: React.ReactNode; delay?: number; style?: any; }) => { return ( {children} ); }; export default function ExploreScreen() { const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000; const userProfile = useAppSelector((s) => s.user.profile); // 开发调试:设置为true来使用mock数据 const useMockData = __DEV__; // 改为true来启用mock数据调试 const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard(); // 使用 dayjs:当月日期与默认选中"今天" const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); const tabBarHeight = useBottomTabBarHeight(); const insets = useSafeAreaInsets(); const bottomPadding = useMemo(() => { return getTabBarBottomPadding(tabBarHeight) + (insets?.bottom ?? 0); }, [tabBarHeight, insets?.bottom]); // 获取当前选中日期 const getCurrentSelectedDate = () => { const days = getMonthDaysZh(); return days[selectedIndex]?.date?.toDate() ?? new Date(); }; // 获取当前选中日期 const currentSelectedDate = getCurrentSelectedDate(); const currentSelectedDateString = dayjs(currentSelectedDate).format('YYYY-MM-DD'); // 从 Redux 获取指定日期的健康数据 const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString)); // 解构健康数据(支持mock数据) const mockData = useMockData ? getTestHealthData('mock') : null; const stepCount: number | null = useMockData ? (mockData?.steps ?? null) : (healthData?.steps ?? null); const hourlySteps = useMockData ? (mockData?.hourlySteps ?? []) : (healthData?.hourlySteps ?? []); const activeCalories = useMockData ? (mockData?.activeEnergyBurned ?? null) : (healthData?.activeEnergyBurned ?? null); const basalMetabolism: number | null = useMockData ? (mockData?.basalEnergyBurned ?? null) : (healthData?.basalEnergyBurned ?? null); const sleepDuration = useMockData ? (mockData?.sleepDuration ?? null) : (healthData?.sleepDuration ?? null); const hrvValue = useMockData ? (mockData?.hrv ?? 0) : (healthData?.hrv ?? 0); const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null); const heartRate = useMockData ? (mockData?.heartRate ?? null) : (healthData?.heartRate ?? null); const fitnessRingsData = useMockData ? { activeCalories: mockData?.activeCalories ?? 0, activeCaloriesGoal: mockData?.activeCaloriesGoal ?? 350, exerciseMinutes: mockData?.exerciseMinutes ?? 0, exerciseMinutesGoal: mockData?.exerciseMinutesGoal ?? 30, standHours: mockData?.standHours ?? 0, standHoursGoal: mockData?.standHoursGoal ?? 12, } : (healthData ? { activeCalories: healthData.activeEnergyBurned, activeCaloriesGoal: healthData.activeCaloriesGoal, exerciseMinutes: healthData.exerciseMinutes, exerciseMinutesGoal: healthData.exerciseMinutesGoal, standHours: healthData.standHours, standHoursGoal: healthData.standHoursGoal, } : { activeCalories: 0, activeCaloriesGoal: 350, exerciseMinutes: 0, exerciseMinutesGoal: 30, standHours: 0, standHoursGoal: 12, }); // HRV更新时间 const [hrvUpdateTime, setHrvUpdateTime] = useState(new Date()); // 用于触发动画重置的 token(当日期或数据变化时更新) const [animToken, setAnimToken] = useState(0); // 从 Redux 获取营养数据 const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(currentSelectedDateString)); // 计算用户的营养目标 const nutritionGoals = useMemo(() => { return calculateNutritionGoals({ weight: userProfile.weight, height: userProfile.height, birthDate: userProfile?.birthDate ? new Date(userProfile?.birthDate) : undefined, gender: userProfile?.gender || undefined, }); }, [userProfile]); const { registerTask } = useBackgroundTasks(); // 心情相关状态 const dispatch = useAppDispatch(); const [isMoodLoading, setIsMoodLoading] = useState(false); // 记录最近一次请求的"日期键",避免旧请求覆盖新结果 const latestRequestKeyRef = useRef(null); const getDateKey = (d: Date) => `${dayjs(d).year()}-${dayjs(d).month() + 1}-${dayjs(d).date()}`; // 从 Redux 获取当前日期的心情记录 const currentMoodCheckin = useAppSelector(selectLatestMoodRecordByDate( currentSelectedDateString )); // 加载心情数据 const loadMoodData = async (targetDate?: Date) => { if (!isLoggedIn) return; try { setIsMoodLoading(true); // 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期 let derivedDate: Date; if (targetDate) { derivedDate = targetDate; } else { derivedDate = getCurrentSelectedDate(); } const dateString = dayjs(derivedDate).format('YYYY-MM-DD'); await dispatch(fetchDailyMoodCheckins(dateString)); } catch (error) { console.error('加载心情数据失败:', error); } finally { setIsMoodLoading(false); } }; const loadHealthData = async (targetDate?: Date) => { try { console.log('=== 开始HealthKit初始化流程 ==='); const ok = await ensureHealthPermissions(); if (!ok) { const errorMsg = '无法获取健康权限,请确保在真实iOS设备上运行并授权应用访问健康数据'; console.warn(errorMsg); return; } // 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期 let derivedDate: Date; if (targetDate) { derivedDate = targetDate; } else { derivedDate = getCurrentSelectedDate(); } const requestKey = getDateKey(derivedDate); latestRequestKeyRef.current = requestKey; console.log('权限获取成功,开始获取健康数据...', derivedDate); const data = await fetchHealthDataForDate(derivedDate); console.log('设置UI状态:', data); // 仅当该请求仍是最新时,才应用结果 if (latestRequestKeyRef.current === requestKey) { const dateString = dayjs(derivedDate).format('YYYY-MM-DD'); // 使用 Redux 存储健康数据 dispatch(setHealthData({ date: dateString, data: data })); // 更新HRV数据时间 setHrvUpdateTime(new Date()); setAnimToken((t) => t + 1); } else { console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current); } console.log('=== HealthKit数据获取完成 ==='); } catch (error) { console.error('HealthKit流程出现异常:', error); } }; // 加载营养数据 const loadNutritionData = async (targetDate?: Date) => { try { // 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期 let derivedDate: Date; if (targetDate) { derivedDate = targetDate; } else { derivedDate = getCurrentSelectedDate(); } console.log('加载营养数据...', derivedDate); await dispatch(fetchDailyNutritionData(derivedDate)); console.log('营养数据加载完成'); } catch (error) { console.error('营养数据加载失败:', error); } }; // 加载所有数据的统一方法 const loadAllData = React.useCallback((targetDate?: Date) => { const dateToUse = targetDate || getCurrentSelectedDate(); if (dateToUse) { loadHealthData(dateToUse); if (isLoggedIn) { loadNutritionData(dateToUse); loadMoodData(dateToUse); } } }, [isLoggedIn]); useFocusEffect( React.useCallback(() => { // 每次聚焦时都拉取当前选中日期的最新数据 loadAllData(); }, [loadAllData]) ); // AppState 监听:应用从后台返回前台时刷新数据 useEffect(() => { const handleAppStateChange = (nextAppState: string) => { if (nextAppState === 'active') { // 应用从后台返回前台,刷新当前选中日期的数据 console.log('应用从后台返回前台,刷新统计数据...'); loadAllData(); } }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => { subscription?.remove(); }; }, [loadAllData]); useEffect(() => { // 注册任务 registerTask({ id: 'health-data-task', name: 'health-data-task', handler: async () => { try { await loadHealthData(); checkStressLevelAndNotify() } catch (error) { console.error('健康数据任务执行失败:', error); } }, }); }, []); // 检查压力水平并发送通知 const checkStressLevelAndNotify = React.useCallback(async () => { try { console.log('开始检查压力水平...'); // 确保有健康权限 const hasPermission = await ensureHealthPermissions(); if (!hasPermission) { console.log('没有健康权限,跳过压力检查'); return; } // 获取最近2小时内的实时HRV数据 const recentHRV = await fetchRecentHRV(2); console.log('获取到的最近2小时HRV值:', recentHRV); if (recentHRV === null || recentHRV === undefined) { console.log('没有最近的HRV数据,跳过压力检查'); return; } // 判断压力水平(HRV值低于60表示压力过大) if (recentHRV < 60) { console.log(`检测到压力过大,HRV值: ${recentHRV},准备发送鼓励通知`); // 检查是否在过去2小时内已经发送过压力提醒,避免重复打扰 const lastNotificationKey = '@last_stress_notification'; const lastNotificationTime = await AsyncStorage.getItem(lastNotificationKey); const now = new Date().getTime(); const twoHoursAgo = now - (2 * 60 * 60 * 1000); // 2小时前 if (lastNotificationTime && parseInt(lastNotificationTime) > twoHoursAgo) { console.log('2小时内已发送过压力提醒,跳过本次通知'); return; } // 随机选择一条鼓励性消息 const encouragingMessages = [ '放松一下吧 🌸\n检测到您的压力指数较高,不妨暂停一下,做几个深呼吸,或者来一段轻松的普拉提练习。您的健康最重要!', '该休息一下了 🧘‍♀️\n您的身体在提醒您需要放松。试试冥想、散步或听听舒缓的音乐,让心情平静下来。', '压力山大?我们来帮您 💆‍♀️\n高压力对健康不利,建议您做一些放松运动,比如瑜伽或普拉提,释放身心压力。', '关爱自己,从现在开始 💝\n检测到您可能承受较大压力,记得给自己一些时间,做喜欢的事情,保持身心健康。', '深呼吸,一切都会好的 🌈\n压力只是暂时的,试试腹式呼吸或简单的伸展运动,让身体和心灵都得到放松。' ]; const randomMessage = encouragingMessages[Math.floor(Math.random() * encouragingMessages.length)]; const [title, body] = randomMessage.split('\n'); // 发送鼓励性推送通知 await notificationService.sendImmediateNotification({ title: title, body: body, data: { type: 'stress_alert', hrvValue: recentHRV, timestamp: new Date().toISOString(), url: '/mood/calendar' // 点击通知跳转到心情页面 }, sound: true, priority: 'high' }); // 记录通知发送时间 await AsyncStorage.setItem(lastNotificationKey, now.toString()); console.log('压力提醒通知已发送'); } else { console.log(`压力水平正常,HRV值: ${recentHRV}`); } } catch (error) { console.error('检查压力水平失败:', error); } }, []); // 日期点击时,加载对应日期数据 const onSelectDate = (index: number, date: Date) => { setSelectedIndex(index); loadAllData(date); }; return ( {/* 背景渐变 */} {/* 装饰性圆圈 */} {/* 日期选择器 */} {/* 营养摄入雷达图卡片 */} { console.log('选择餐次:', mealType); // 这里可以导航到营养记录页面 pushIfAuthedElseLogin('/nutrition/records'); }} /> {/* 真正瀑布流布局 */} {/* 左列 */} {/* 心情卡片 */} pushIfAuthedElseLogin('/mood/calendar')} isLoading={isMoodLoading} /> {/* 饮水记录卡片 */} {/* 心率卡片 */} {/* 右列 */} 睡眠 {sleepDuration != null ? ( {Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟 ) : ( —— )} {/* 基础代谢卡片 */} {/* 血氧饱和度卡片 */} ); } const primary = Colors.light.primary; const styles = StyleSheet.create({ container: { flex: 1, }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, decorativeCircle1: { position: 'absolute', top: 40, right: 20, width: 60, height: 60, borderRadius: 30, backgroundColor: '#0EA5E9', opacity: 0.1, }, decorativeCircle2: { position: 'absolute', bottom: -15, left: -15, width: 40, height: 40, borderRadius: 20, backgroundColor: '#0EA5E9', opacity: 0.05, }, safeArea: { flex: 1, }, scrollView: { flex: 1, paddingHorizontal: 20, }, sectionTitle: { fontSize: 24, fontWeight: '800', color: '#192126', marginTop: 24, marginBottom: 14, }, metricsRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16, }, card: { backgroundColor: '#0F1418', borderRadius: 22, padding: 18, marginBottom: 16, }, metricsLeft: { flex: 1, backgroundColor: '#EEE9FF', borderRadius: 22, padding: 18, marginRight: 12, }, metricsRight: { width: 160, gap: 12, }, metricsRightCard: { backgroundColor: '#FFFFFF', borderRadius: 22, padding: 16, }, caloriesValue: { color: '#192126', fontSize: 18, lineHeight: 18, fontWeight: '600', textAlignVertical: 'bottom' }, caloriesUnit: { color: '#515558ff', fontSize: 12, marginLeft: 4, lineHeight: 18, }, trainingContent: { marginTop: 8, width: 120, height: 120, borderRadius: 60, alignItems: 'center', justifyContent: 'center', alignSelf: 'center', }, trainingRingTrack: { position: 'absolute', width: '100%', height: '100%', borderRadius: 60, borderWidth: 12, borderColor: '#E2D9FD', }, trainingRingProgress: { position: 'absolute', width: '100%', height: '100%', borderRadius: 60, borderWidth: 12, borderColor: 'transparent', borderTopColor: '#8B74F3', borderRightColor: '#8B74F3', transform: [{ rotateZ: '45deg' }], }, trainingPercent: { fontSize: 18, fontWeight: '800', color: '#8B74F3', }, cyclingHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, cyclingIconBadge: { width: 30, height: 30, borderRadius: 6, backgroundColor: primary, alignItems: 'center', justifyContent: 'center', marginRight: 8, }, cyclingTitle: { color: '#FFFFFF', fontSize: 20, fontWeight: '800', }, mapArea: { backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 14, height: 180, padding: 8, flexDirection: 'row', flexWrap: 'wrap', overflow: 'hidden', }, mapTile: { width: '25%', height: '25%', borderWidth: 1, borderColor: 'rgba(255,255,255,0.12)', }, routeLine: { position: 'absolute', height: 6, backgroundColor: primary, borderRadius: 3, }, cardHeaderRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, iconSquare: { width: 24, height: 24, borderRadius: 8, backgroundColor: '#FFFFFF', alignItems: 'center', justifyContent: 'center', marginRight: 10, }, cardTitle: { fontSize: 14, color: '#192126', }, heartCard: { backgroundColor: '#FFE5E5', }, waveContainer: { flexDirection: 'row', alignItems: 'flex-end', height: 70, gap: 6, marginBottom: 8, }, waveBar: { width: 6, borderRadius: 3, backgroundColor: '#E54D4D', }, heartValue: { alignSelf: 'flex-end', color: '#5B5B5B', fontWeight: '600', }, stepsValue: { fontSize: 14, color: '#7A6A42', fontWeight: '700', marginBottom: 8, }, errorContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFE5E5', borderRadius: 12, padding: 12, marginBottom: 16, }, errorText: { fontSize: 14, color: '#E54D4D', fontWeight: '600', marginLeft: 8, flex: 1, }, retryButton: { padding: 4, marginLeft: 8, }, viewMoreContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: 16, }, viewMoreText: { fontSize: 14, color: '#192126', }, viewMoreIcon: { fontSize: 16, color: '#192126', marginLeft: 4, }, stressCardRow: { flexDirection: 'row', justifyContent: 'flex-start', marginBottom: 16, }, healthCardsRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16, }, masonryContainer: { marginBottom: 16, flexDirection: 'row', gap: 12, marginTop: 16, }, masonryColumn: { flex: 1, }, masonryCard: { width: '100%', backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.12, shadowRadius: 12, elevation: 6, minHeight: 100, justifyContent: 'center', }, basalMetabolismCardOverride: { margin: -16, // 抵消 masonryCard 的 padding borderRadius: 16, }, stepsCardOverride: { margin: -16, // 抵消 masonryCard 的 padding borderRadius: 16, height: '100%', // 填充整个masonryCard }, waterCardOverride: { margin: -16, // 抵消 masonryCard 的 padding borderRadius: 16, height: '100%', // 填充整个masonryCard }, compactStepsCard: { minHeight: 100, }, stepsContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 8, }, sleepValue: { fontSize: 16, color: '#1E40AF', fontWeight: '700', marginTop: 8, }, weightCard: { backgroundColor: '#F0F9FF', }, weightValue: { fontSize: 22, color: '#0369A1', fontWeight: '800', marginTop: 8, }, addWeightButton: { position: 'absolute', right: 0, top: 0, padding: 4, }, });