import { AnimatedNumber } from '@/components/AnimatedNumber';
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 { ProgressBar } from '@/components/ProgressBar';
import { StressMeter } from '@/components/StressMeter';
import { WeightHistoryCard } from '@/components/WeightHistoryCard';
import HeartRateCard from '@/components/statistic/HeartRateCard';
import OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard';
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 { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords';
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
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 {
Animated,
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;
}) => {
const floatAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
const startAnimation = () => {
Animated.loop(
Animated.sequence([
Animated.timing(floatAnim, {
toValue: 1,
duration: 3000,
delay: delay,
useNativeDriver: true,
}),
Animated.timing(floatAnim, {
toValue: 0,
duration: 3000,
useNativeDriver: true,
}),
])
).start();
};
startAnimation();
}, [floatAnim, delay]);
const translateY = floatAnim.interpolate({
inputRange: [0, 1],
outputRange: [-2, -6],
});
return (
{children}
);
};
export default function ExploreScreen() {
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
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));
// 解构健康数据
const stepCount = healthData?.steps ?? null;
const activeCalories = healthData?.activeEnergyBurned ?? null;
const basalMetabolism = healthData?.basalEnergyBurned ?? null;
const sleepDuration = healthData?.sleepDuration ?? null;
const hrvValue = healthData?.hrv ?? 0;
const oxygenSaturation = healthData?.oxygenSaturation ?? null;
const heartRate = healthData?.heartRate ?? null;
const fitnessRingsData = 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);
// 营养数据状态
const [nutritionSummary, setNutritionSummary] = useState(null);
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);
const data = await getDietRecords({
startDate: dayjs(derivedDate).startOf('day').toISOString(),
endDate: dayjs(derivedDate).endOf('day').toISOString(),
});
if (data.records.length > 0) {
const summary = calculateNutritionSummary(data.records);
setNutritionSummary(summary);
} else {
setNutritionSummary(null);
}
console.log('营养数据加载完成:', data);
} catch (error) {
console.error('营养数据加载失败:', error);
setNutritionSummary(null);
}
};
useFocusEffect(
React.useCallback(() => {
// 聚焦时按当前选中的日期加载,避免与用户手动选择的日期不一致
const currentDate = currentSelectedDate;
if (currentDate) {
loadHealthData(currentDate);
if (isLoggedIn) {
loadNutritionData(currentDate);
loadMoodData(currentDate);
}
}
}, [selectedIndex])
);
useEffect(() => {
// 注册任务
registerTask({
id: 'health-data-task',
name: 'health-data-task',
handler: async () => {
try {
await loadHealthData();
} catch (error) {
console.error('健康数据任务执行失败:', error);
}
},
});
}, []);
// 日期点击时,加载对应日期数据
const onSelectDate = (index: number, date: Date) => {
setSelectedIndex(index);
loadHealthData(date);
if (isLoggedIn) {
loadNutritionData(date);
loadMoodData(date);
}
};
return (
{/* 背景渐变 */}
{/* 装饰性圆圈 */}
{/* 体重历史记录卡片 */}
健康数据
{/* 日期选择器 */}
{/* 营养摄入雷达图卡片 */}
{
console.log('选择餐次:', mealType);
// 这里可以导航到营养记录页面
pushIfAuthedElseLogin('/nutrition/records');
}}
/>
{/* 真正瀑布流布局 */}
{/* 左列 */}
消耗卡路里
{activeCalories != null ? (
`${Math.round(v)} 千卡`}
/>
) : (
——
)}
步数
{stepCount != null ? (
`${Math.round(v)}/${stepGoal}`}
/>
) : (
——/{stepGoal}
)}
{/* 心情卡片 */}
pushIfAuthedElseLogin('/mood/calendar')}
isLoading={isMoodLoading}
/>
{/* 右列 */}
睡眠
{sleepDuration != null ? (
{Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟
) : (
——
)}
{/* 基础代谢卡片 */}
{/* 血氧饱和度卡片 */}
{/* 心率卡片 */}
);
}
const primary = Colors.light.primary;
const lightColors = Colors.light;
const darkColors = Colors.dark;
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,
fontWeight: '800',
marginTop: 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,
fontWeight: '800',
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,
},
basalMetabolismCardOverride: {
margin: -16, // 抵消 masonryCard 的 padding
borderRadius: 16,
},
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,
},
});