Files
digital-pilates/app/(tabs)/statistics.tsx
richarjiang 62690ee3fc refactor(sleep): 重构睡眠数据获取逻辑,移除冗余代码并优化组件结构
- 从 healthSlice 和 health.ts 中移除 sleepDuration 字段及相关获取逻辑
- 将 SleepCard 改为按需异步获取睡眠数据,支持传入指定日期
- 睡眠详情页改为通过路由参数接收日期,支持查看历史记录
- 移除 statistics 页面对 sleepDuration 的直接依赖,统一由 SleepCard 管理
- 删除未使用的 SleepStageChart 组件,简化页面结构
2025-09-11 09:08:51 +08:00

1005 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard';
import SleepCard from '@/components/statistic/SleepCard';
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 { backgroundTaskManager } from '@/services/backgroundTaskManager';
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
import { fetchTodayWaterStats } from '@/store/waterSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
import { getTestHealthData } from '@/utils/mockHealthData';
import { calculateNutritionGoals } from '@/utils/nutrition';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { debounce } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
AppState,
Image,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
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 (
<View
style={[
style,
{
marginBottom: 8,
},
]}
>
{children}
</View>
);
};
export default function ExploreScreen() {
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
const userProfile = useAppSelector((s) => s.user.profile);
// å¼€å<E282AC>调试:设置为trueæ<65>¥ä½¿ç”¨mockæ•°æ<C2B0>®
// 在真机æµè¯•时,å<C592>¯ä»¥æšæ—¶è®¾ç½®ä¸ºtrueæ<65>¥éªŒè¯<C3A8>组件显示逻è¾
const useMockData = __DEV__ || false; // 改为trueæ<65>¥å<C2A5>¯ç”¨mockæ•°æ<C2B0>®è°ƒè¯•
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]);
// 获å<C2B7>当å‰<C3A5>选中日期 - 使用 useMemo 缓存é<CB9C>¿å…<C3A5>é‡<C3A9>å¤<C3A5>计算
const currentSelectedDate = useMemo(() => {
const days = getMonthDaysZh();
return days[selectedIndex]?.date?.toDate() ?? new Date();
}, [selectedIndex]);
const currentSelectedDateString = useMemo(() => {
return dayjs(currentSelectedDate).format('YYYY-MM-DD');
}, [currentSelectedDate]);
// 从 Redux 获å<C2B7>指定日期的å<E2809E>¥åº·æ•°æ<C2B0>®
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
// 解构å<E2809E>¥åº·æ•°æ<C2B0>®ï¼ˆæ”¯æŒ<C3A6>mockæ•°æ<C2B0>®ï¼‰
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 hrvValue = useMockData ? (mockData?.hrv ?? null) : (healthData?.hrv ?? null);
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
// 调试HRVæ•°æ<C2B0>®
console.log('=== HRVæ•°æ<C2B0>®è°ƒè¯• ===');
console.log('useMockData:', useMockData);
console.log('mockData?.hrv:', mockData?.hrv);
console.log('healthData?.hrv:', healthData?.hrv);
console.log('final hrvValue:', hrvValue);
console.log('healthData:', healthData);
console.log('==================');
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<Date>(new Date());
// 用于触å<C2A6>动画é‡<C3A9>置的 tokenï¼ˆå½“æ—¥æœŸæˆæ•°æ<C2B0>®å<C2AE>˜åŒæ—¶æ´æ°ï¼‰
const [animToken, setAnimToken] = useState(0);
// 从 Redux 获å<C2B7>è<E28093>¥å…»æ•°æ<C2B0>®
const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(currentSelectedDateString));
// 计算用户的è<E2809E>¥å…»ç®æ 
const nutritionGoals = useMemo(() => {
return calculateNutritionGoals({
weight: userProfile.weight,
height: userProfile.height,
birthDate: userProfile?.birthDate ? new Date(userProfile?.birthDate) : undefined,
gender: userProfile?.gender || undefined,
});
}, [userProfile]);
// 心情ç¸å…³çжæ€<C3A6>
const dispatch = useAppDispatch();
const [isMoodLoading, setIsMoodLoading] = useState(false);
// 记录最近一次请求的"日期键",é<C592>¿å…<C3A5>旧请æ±è¦†çæ°ç»“æžœ
const latestRequestKeyRef = useRef<string | null>(null);
// 请æ±çжæ€<C3A6>管ç<C2A1>†ï¼Œé˜²æ­¢é‡<C3A9>å¤<C3A5>请æ±
const loadingRef = useRef({
health: false,
nutrition: false,
mood: false
});
// æ•°æ<C2B0>®ç¼“存时间戳,é<C592>¿å…<C3A5>短时间内é‡<C3A9>å¤<C3A5>æ‰å<E280B0>
const dataTimestampRef = useRef<{ [key: string]: number }>({});
const getDateKey = (d: Date) => `${dayjs(d).year()}-${dayjs(d).month() + 1}-${dayjs(d).date()}`;
// 检查数æ<C2B0>®æ˜¯å<C2AF>¦éœ€è¦<C3A8>刷æ°ï¼ˆˆ†éŸå†…ä¸<C3A4>é‡<C3A9>å¤<C3A5>æ‰å<E280B0>,对è<C2B9>¥å…»æ•°æ<C2B0>®æ´ä¸¥æ ¼ï¼‰
const shouldRefreshData = (dateKey: string, dataType: string) => {
const cacheKey = `${dateKey}-${dataType}`;
const lastUpdate = dataTimestampRef.current[cacheKey];
const now = Date.now();
// è<>¥å…»æ•°æ<C2B0>®ä½¿ç”¨æ´çŸ­çš„ç¼“å­˜æ—¶é—´ï¼Œå…¶ä»æ•°æ<C2B0>®ä½¿ç”¨5分éŸ
const cacheTime = dataType === 'nutrition' ? 2 * 60 * 1000 : 5 * 60 * 1000;
return !lastUpdate || (now - lastUpdate) > cacheTime;
};
// æ´æ°æ•°æ<C2B0>®æ—¶é—´æˆ³
const updateDataTimestamp = (dateKey: string, dataType: string) => {
const cacheKey = `${dateKey}-${dataType}`;
dataTimestampRef.current[cacheKey] = Date.now();
};
// 从 Redux 获å<C2B7>当å‰<C3A5>日期的心情记录
const currentMoodCheckin = useAppSelector(selectLatestMoodRecordByDate(
currentSelectedDateString
));
// 加载心情数æ<C2B0>®
const loadMoodData = async (targetDate?: Date, forceRefresh = false) => {
if (!isLoggedIn) return;
// 确定è¦<C3A8>查询的日期
let derivedDate: Date;
if (targetDate) {
derivedDate = targetDate;
} else {
derivedDate = currentSelectedDate;
}
const requestKey = getDateKey(derivedDate);
// 检查是å<C2AF>¦æ­£åœ¨åŠ è½½æˆä¸<C3A4>需è¦<C3A8>刷æ°
if (loadingRef.current.mood) {
console.log('心情数æ<C2B0>®æ­£åœ¨åŠ è½½ä¸­ï¼Œè·³è¿‡é‡<C3A9>å¤<C3A5>请æ±');
return;
}
if (!forceRefresh && !shouldRefreshData(requestKey, 'mood')) {
console.log('心情数æ<C2B0>®ç¼“存未过期,跳过请æ±');
return;
}
try {
loadingRef.current.mood = true;
setIsMoodLoading(true);
const dateString = dayjs(derivedDate).format('YYYY-MM-DD');
await dispatch(fetchDailyMoodCheckins(dateString));
// 更新缓存时间戳
updateDataTimestamp(requestKey, 'mood');
} catch (error) {
console.error('加载心情数æ<C2B0>®å¤±è´¥:', error);
} finally {
loadingRef.current.mood = false;
setIsMoodLoading(false);
}
};
const loadHealthData = async (targetDate?: Date, forceRefresh = false) => {
// 确定è¦<C3A8>查询的日期
let derivedDate: Date;
if (targetDate) {
derivedDate = targetDate;
} else {
derivedDate = currentSelectedDate;
}
const requestKey = getDateKey(derivedDate);
// 检查是å<C2AF>¦æ­£åœ¨åŠ è½½æˆä¸<C3A4>需è¦<C3A8>刷æ°
if (loadingRef.current.health) {
console.log(<>¥åº·æ•°æ<C2B0>®æ­£åœ¨åŠ è½½ä¸­ï¼Œè·³è¿‡é‡<C3A9>å¤<C3A5>请æ±');
return;
}
if (!forceRefresh && !shouldRefreshData(requestKey, 'health')) {
console.log(<>¥åº·æ•°æ<C2B0>®ç¼“存未过期,跳过请æ±');
return;
}
try {
loadingRef.current.health = true;
console.log('=== å¼€å§HealthKitåˆ<C3A5>å§åŒæµ<C3A6>ç¨ ===');
const ok = await ensureHealthPermissions();
if (!ok) {
const errorMsg = '无法获å<C2B7>å<E28093>¥åº·æ<C2B7>ƒé™<C3A9>,请确ä¿<C3A4>在真实iOS设备上è¿<C3A8>行并授æ<CB86>ƒåº”用访问å<C2AE>¥åº·æ•°æ<C2B0>®';
console.warn(errorMsg);
return;
}
latestRequestKeyRef.current = requestKey;
console.log(<>ƒé™<C3A9>获å<C2B7>æˆ<C3A6>功,开å§èŽ·å<C2B7>å<E28093>¥åº·æ•°æ<C2B0>®...', derivedDate);
const data = await fetchHealthDataForDate(derivedDate);
console.log('设置UI状æ€<C3A6>:', data);
console.log('HRVæ•°æ<C2B0>®è¯¦ç»†ä¿¡æ<C2A1>¯:', data.hrv, typeof data.hrv);
// 仅当该请æ±ä»<C3A4>æ˜¯æœ€æ°æ—¶ï¼Œæ‰<C3A6>应用结果
if (latestRequestKeyRef.current === requestKey) {
const dateString = dayjs(derivedDate).format('YYYY-MM-DD');
// 使用 Redux å­˜å¨å<C2A8>¥åº·æ•°æ<C2B0>®
dispatch(setHealthData({
date: dateString,
data: {
steps: data.steps,
activeCalories: data.activeEnergyBurned,
basalEnergyBurned: data.basalEnergyBurned,
hrv: data.hrv,
oxygenSaturation: data.oxygenSaturation,
heartRate: data.heartRate,
activeEnergyBurned: data.activeEnergyBurned,
activeCaloriesGoal: data.activeCaloriesGoal,
exerciseMinutes: data.exerciseMinutes,
exerciseMinutesGoal: data.exerciseMinutesGoal,
standHours: data.standHours,
standHoursGoal: data.standHoursGoal,
hourlySteps: data.hourlySteps,
}
}));
// æ´æ°HRVæ•°æ<C2B0>®æ—¶é—´
setHrvUpdateTime(new Date());
setAnimToken((t) => t + 1);
// 更新缓存时间戳
updateDataTimestamp(requestKey, 'health');
} else {
console.log('忽略过期å<C5B8>¥åº·æ•°æ<C2B0>®è¯·æ±ç»“果,key=', requestKey, '最æ°key=', latestRequestKeyRef.current);
}
console.log('=== HealthKitæ•°æ<C2B0>®èŽ·å<C2B7>完æˆ<C3A6> ===');
} catch (error) {
console.error('HealthKitæµ<C3A6>ç¨å‡ºçްå¼å¸¸:', error);
} finally {
loadingRef.current.health = false;
}
};
// 加载è<C2BD>¥å…»æ•°æ<C2B0>®
const loadNutritionData = async (targetDate?: Date, forceRefresh = false) => {
if (!isLoggedIn) return;
// 确定è¦<C3A8>查询的日期
let derivedDate: Date;
if (targetDate) {
derivedDate = targetDate;
} else {
derivedDate = currentSelectedDate;
}
const requestKey = getDateKey(derivedDate);
// 检查是å<C2AF>¦æ­£åœ¨åŠ è½½æˆä¸<C3A4>需è¦<C3A8>刷æ°
if (loadingRef.current.nutrition) {
console.log(<>¥å…»æ•°æ<C2B0>®æ­£åœ¨åŠ è½½ä¸­ï¼Œè·³è¿‡é‡<C3A9>å¤<C3A5>请æ±');
return;
}
if (!forceRefresh && !shouldRefreshData(requestKey, 'nutrition')) {
console.log(<>¥å…»æ•°æ<C2B0>®ç¼“存未过期,跳过请æ±');
return;
}
try {
loadingRef.current.nutrition = true;
console.log('加载è<C2BD>¥å…»æ•°æ<C2B0>®...', derivedDate);
await dispatch(fetchDailyNutritionData(derivedDate));
console.log(<>¥å…»æ•°æ<C2B0>®åŠ è½½å®Œæˆ<C3A6>');
// 更新缓存时间戳
updateDataTimestamp(requestKey, 'nutrition');
} catch (error) {
console.error(<>¥å…»æ•°æ<C2B0>®åŠ è½½å¤±è´¥:', error);
} finally {
loadingRef.current.nutrition = false;
}
};
// 实际执行数æ<C2B0>®åŠ è½½çš„æ¹æ³•
const executeLoadAllData = React.useCallback((targetDate?: Date, forceRefresh = false) => {
const dateToUse = targetDate || currentSelectedDate;
if (dateToUse) {
console.log('执行数æ<C2B0>®åŠ è½½ï¼Œæ—¥æœŸ:', dateToUse, '强制刷新:', forceRefresh);
loadHealthData(dateToUse, forceRefresh);
if (isLoggedIn) {
loadNutritionData(dateToUse, forceRefresh);
loadMoodData(dateToUse, forceRefresh);
// 加载å<C3A5>æ°´æ•°æ<C2B0>®ï¼ˆå<CB86>ªåŠ è½½ä»Šæ—¥æ•°æ<C2B0>®ç”¨äºŽå<C5BD>Žå<C5BD>°æ£€æŸ¥ï¼‰
const isToday = dayjs(dateToUse).isSame(dayjs(), 'day');
if (isToday) {
dispatch(fetchTodayWaterStats());
}
}
}
}, [isLoggedIn, dispatch]);
// 使用 lodash debounce 防æŠçš„加载所有数æ<C2B0>®æ¹æ³•
const debouncedLoadAllData = React.useMemo(
() => debounce(executeLoadAllData, 500), // 500ms 防抖延迟
[executeLoadAllData]
);
// 对外暴露的 loadAllData 方法
const loadAllData = React.useCallback((targetDate?: Date, forceRefresh = false) => {
if (forceRefresh) {
// 妿žœæ˜¯å¼ºåˆ¶åˆ·æ°ï¼Œç«å<E280B9>³æ‰§è¡Œï¼Œä¸<C3A4>使用防æŠ
executeLoadAllData(targetDate, forceRefresh);
} else {
// 普通调用使用防抖
debouncedLoadAllData(targetDate, forceRefresh);
}
}, [executeLoadAllData, debouncedLoadAllData]);
// 页é<C2B5>¢è<C2A2>šç„¦æ—¶çš„æ•°æ<C2B0>®åŠ è½½é€»è¾
// useFocusEffect(
// React.useCallback(() => {
// // 页é<C2B5>¢è<C2A2>šç„¦æ—¶åŠ è½½æ•°æ<C2B0>®ï¼Œä½¿ç”¨ç¼“存机制é<C2B6>¿å…<C3A5>é¢ç¹<C3A7>请æ±
// console.log('页é<C2B5>¢è<C2A2>šç„¦ï¼Œæ£€æŸ¥æ˜¯å<C2AF>¦éœ€è¦<C3A8>åˆ·æ°æ•°æ<C2B0>®...');
// loadAllData(currentSelectedDate);
// }, [loadAllData, currentSelectedDate])
// );
// AppState çå<E28098>¬ï¼šåº”用从å<C5BD>Žå<C5BD>°è¿”åžå‰<C3A5>å<EFBFBD>°æ—¶çš„处ç<E2809E>
useEffect(() => {
const handleAppStateChange = (nextAppState: string) => {
if (nextAppState === 'active') {
// 判æ­å½“å‰<C3A5>选中的日期是å<C2AF>¦æ˜¯æœ€æ°çš„(今天)
const todayIndex = getTodayIndexInMonth();
const isTodaySelected = selectedIndex === todayIndex;
if (!isTodaySelected) {
// 妿žœå½“å‰<C3A5>ä¸<C3A4>是选中今天,则切æ<E280A1>¢åˆ°ä»Šå¤©ï¼ˆè¿™ä¸ªæ´æ°ä¼šè§¦å<C2A6>æ•°æ<C2B0>®åŠ è½½ï¼‰
console.log('应用åžåˆ°å‰<C3A5>å<EFBFBD>°ï¼Œåˆ‡æ<E280A1>¢åˆ°ä»Šå¤©å¹¶åŠ è½½æ•°æ<C2B0>®');
setSelectedIndex(todayIndex);
// 注æ„<C3A6>:这里ä¸<C3A4>ç´æŽ¥è°ƒç”¨loadAllData,å ä¸ºsetSelectedIndex会触å<C2A6>useEffecté‡<C3A9>æ°è®¡ç®—currentSelectedDate
// ç„¶å<C2B6>ŽonSelectDate会被调用,从而触å<C2A6>æ•°æ<C2B0>®åŠ è½½
} else {
// 妿žœå·²ç»<C3A7>æ˜¯ä»Šå¤©ï¼Œåˆ™ç´æŽ¥è°ƒç”¨åŠ è½½æ•°æ<C2B0>®çš„æ¹æ³•
console.log('应用åžåˆ°å‰<C3A5>å<EFBFBD>°ï¼Œå½“å‰<C3A5>å·²æ˜¯ä»Šå¤©ï¼Œç´æŽ¥åŠ è½½æ•°æ<C2B0>®');
loadAllData(currentSelectedDate);
}
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => {
subscription?.remove();
};
}, [loadAllData, currentSelectedDate, selectedIndex]);
// 日期ç¹å‡»æ—¶ï¼ŒåŠ è½½å¯¹åº”æ—¥æœŸæ•°æ<C2B0>®
const onSelectDate = React.useCallback((index: number, date: Date) => {
setSelectedIndex(index);
console.log('日期切æ<E280A1>¢ï¼ŒåŠ è½½æ•°æ<C2B0>®...', date);
// 日期切æ<E280A1>¢æ—¶ä¸<C3A4>强制刷æ°ï¼Œä¾<C3A4>èµç¼“存机制å‡<C3A5>å°ä¸<C3A4>å¿…è¦<C3A8>的请æ±
// loadAllData 内部已ç»<C3A7>实现了防æŠï¼Œæ— éœ€é¢<C3A9>å¤é˜²æŠå¤„ç<E2809E>
loadAllData(date, false);
}, [loadAllData]);
return (
<View style={styles.container}>
{/* 背景æ¸<C3A6>å<EFBFBD>˜ */}
<LinearGradient
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={{
paddingTop: insets.top,
paddingBottom: bottomPadding,
paddingHorizontal: 20
}}
showsVerticalScrollIndicator={false}
>
{/* 顶部信æ<C2A1>¯æ <C3A6> */}
<View style={styles.headerContainer}>
<View style={styles.headerContent}>
{/* 左边logo */}
<Image
source={require('@/assets/images/Sealife.jpeg')}
style={styles.logoImage}
resizeMode="cover"
/>
{/* å<>³è¾¹æ‡å­—区域 */}
<View style={styles.headerTextContainer}>
<Text style={styles.headerTitle}>Out Live</Text>
</View>
{/* å¼€å<E282AC>çŽ¯å¢ƒè°ƒè¯•æŒ‰é® */}
{__DEV__ && (
<View style={styles.debugButtonsContainer}>
<TouchableOpacity
style={styles.debugButton}
onPress={async () => {
console.log('🔧 æ‰åŠ¨è§¦å<C2A6>å<E28098>Žå<C5BD>°ä»»åŠ¡æµè¯•...');
await backgroundTaskManager.triggerTaskForTesting();
}}
>
<Text style={styles.debugButtonText}>ðŸ§</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.debugButton, styles.hrvTestButton]}
onPress={async () => {
console.log('🫀 æµè¯•HRVæ•°æ<C2B0>®èŽ·å<C2B7>...');
await testHRVDataFetch();
}}
>
<Text style={styles.debugButtonText}>ðŸ«</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
{/* 日期选择器 */}
<DateSelector
selectedIndex={selectedIndex}
onDateSelect={onSelectDate}
showMonthTitle={false}
disableFutureDates={true}
/>
{/* è<>¥å…»æ„å…¥é·è¾¾å¾å<C2BE>¡ç‰‡ */}
<NutritionRadarCard
nutritionSummary={nutritionSummary}
nutritionGoals={nutritionGoals}
burnedCalories={(basalMetabolism || 0) + (activeCalories || 0)}
basalMetabolism={basalMetabolism || 0}
activeCalories={activeCalories || 0}
resetToken={animToken}
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
console.log('选æ©é¤<C3A9>次:', mealType);
// 这里å<C592>¯ä»¥å¯¼èˆªåˆ°è<C2B0>¥å…»è®°å½•页é<C2B5>¢
pushIfAuthedElseLogin('/nutrition/records');
}}
/>
<WeightHistoryCard />
{/* 真正ç€å¸ƒæµ<C3A6>布局 */}
<View style={styles.masonryContainer}>
{/* 左列 */}
<View style={styles.masonryColumn}>
{/* 心情å<E280A6>¡ç‰‡ */}
<FloatingCard style={styles.masonryCard} delay={1500}>
<MoodCard
moodCheckin={currentMoodCheckin}
onPress={() => pushIfAuthedElseLogin('/mood/calendar')}
isLoading={isMoodLoading}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard}>
<StepsCard
stepCount={stepCount}
stepGoal={stepGoal}
hourlySteps={hourlySteps}
style={styles.stepsCardOverride}
onPress={() => pushIfAuthedElseLogin('/steps/detail')}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard} delay={0}>
<StressMeter
value={hrvValue}
updateTime={hrvUpdateTime}
hrvValue={hrvValue}
/>
</FloatingCard>
{/* 心率å<E280A1>¡ç‰‡ */}
{/* <FloatingCard style={styles.masonryCard} delay={2000}>
<HeartRateCard
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
heartRate={heartRate}
/>
</FloatingCard> */}
<FloatingCard style={styles.masonryCard}>
<SleepCard
selectedDate={currentSelectedDate}
onPress={() => pushIfAuthedElseLogin(`/sleep-detail?date=${dayjs(currentSelectedDate).format('YYYY-MM-DD')}`)}
/>
</FloatingCard>
</View>
{/* å<>³åˆ— */}
<View style={styles.masonryColumn}>
<FloatingCard style={styles.masonryCard} delay={250}>
<FitnessRingsCard
activeCalories={fitnessRingsData.activeCalories}
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
exerciseMinutes={fitnessRingsData.exerciseMinutes}
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
standHours={fitnessRingsData.standHours}
standHoursGoal={fitnessRingsData.standHoursGoal}
resetToken={animToken}
/>
</FloatingCard>
{/* 饮水记录å<E280A2>¡ç‰‡ */}
<FloatingCard style={styles.masonryCard} delay={500}>
<WaterIntakeCard
selectedDate={currentSelectedDateString}
style={styles.waterCardOverride}
/>
</FloatingCard>
{/* 基础代谢å<C2A2>¡ç‰‡ */}
<FloatingCard style={styles.masonryCard} delay={1250}>
<BasalMetabolismCard
value={basalMetabolism}
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
/>
</FloatingCard>
{/* 血氧饱åŒåº¦å<C2A6>¡ç‰‡ */}
<FloatingCard style={styles.masonryCard} delay={1750}>
<OxygenSaturationCard
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
oxygenSaturation={oxygenSaturation}
/>
</FloatingCard>
</View>
</View>
</ScrollView>
</View>
);
}
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,
},
scrollView: {
flex: 1,
},
headerContainer: {
marginBottom: 10,
},
headerContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
logoImage: {
width: 36,
height: 36,
borderRadius: 20,
},
headerTextContainer: {
flex: 1,
marginLeft: 12,
},
headerTitle: {
fontSize: 16,
fontWeight: '500',
color: '#192126',
},
debugButtonsContainer: {
flexDirection: 'row',
gap: 8,
},
debugButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#FF6B6B',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
hrvTestButton: {
backgroundColor: '#8B5CF6',
},
debugButtonText: {
fontSize: 12,
},
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: 16,
marginTop: 6,
},
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',
marginTop: 6
},
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,
},
weightCard: {
backgroundColor: '#F0F9FF',
},
weightValue: {
fontSize: 22,
color: '#0369A1',
fontWeight: '800',
marginTop: 8,
},
addWeightButton: {
position: 'absolute',
right: 0,
top: 0,
padding: 4,
},
});