import { DateSelector } from '@/components/DateSelector'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health'; import { logger } from '@/utils/logger'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams } from 'expo-router'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Animated, ScrollView, StyleSheet, Text, View } from 'react-native'; export default function StepsDetailScreen() { const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() // 获取路由参数 const { date } = useLocalSearchParams<{ date?: string }>(); // 根据传入的日期参数计算初始选中索引 const getInitialSelectedIndex = () => { if (date) { const targetDate = dayjs(date); const days = getMonthDaysZh(); const foundIndex = days.findIndex(day => day.date && dayjs(day.date.toDate()).isSame(targetDate, 'day') ); return foundIndex >= 0 ? foundIndex : getTodayIndexInMonth(); } return getTodayIndexInMonth(); }; // 日期选择相关状态 const [selectedIndex, setSelectedIndex] = useState(getInitialSelectedIndex()); // 步数数据状态 const [stepCount, setStepCount] = useState(0); const [hourlySteps, setHourSteps] = useState([]); const [isLoading, setIsLoading] = useState(false); // 获取当前选中日期 const currentSelectedDate = useMemo(() => { const days = getMonthDaysZh(); return days[selectedIndex]?.date?.toDate() ?? new Date(); }, [selectedIndex]); // 获取步数数据的函数,参考 StepsCard 的实现 const getStepData = async (date: Date) => { try { setIsLoading(true); logger.info('获取步数详情数据...'); const [steps, hourly] = await Promise.all([ fetchStepCount(date), fetchHourlyStepSamples(date) ]); setStepCount(steps); setHourSteps(hourly); } catch (error) { logger.error('获取步数详情数据失败:', error); } finally { setIsLoading(false); } }; // 为每个柱体创建独立的动画值 const animatedValues = useRef( Array.from({ length: 24 }, () => new Animated.Value(0)) ).current; // 计算柱状图数据 const chartData = useMemo(() => { if (!hourlySteps || hourlySteps.length === 0) { return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 })); } // 找到最大步数用于计算高度比例 const maxSteps = Math.max(...hourlySteps.map(data => data.steps), 1); const maxHeight = 120; // 详情页面使用更大的高度 return hourlySteps.map(data => ({ ...data, height: maxSteps > 0 ? (data.steps / maxSteps) * maxHeight : 0 })); }, [hourlySteps]); // 计算平均值刻度线位置 const averageLinePosition = useMemo(() => { if (!hourlySteps || hourlySteps.length === 0 || !chartData || chartData.length === 0) return 0; const activeHours = hourlySteps.filter(h => h.steps > 0); if (activeHours.length === 0) return 0; const avgSteps = activeHours.reduce((sum, h) => sum + h.steps, 0) / activeHours.length; const maxSteps = Math.max(...hourlySteps.map(data => data.steps), 1); const maxHeight = 120; return maxSteps > 0 ? (avgSteps / maxSteps) * maxHeight : 0; }, [hourlySteps, chartData]); // 获取当前小时 const currentHour = new Date().getHours(); // 触发柱体动画 useEffect(() => { if (chartData && chartData.length > 0) { // 重置所有动画值 animatedValues.forEach(animValue => animValue.setValue(0)); // 延迟启动动画,创建波浪效果 chartData.forEach((data, index) => { if (data.steps > 0) { setTimeout(() => { Animated.spring(animatedValues[index], { toValue: 1, tension: 120, friction: 8, useNativeDriver: false, }).start(); }, index * 50); // 每个柱体延迟50ms } }); } }, [chartData, animatedValues]); // 日期选择处理 const onSelectDate = (index: number, date: Date) => { setSelectedIndex(index); getStepData(date); }; // 当路由参数变化时更新选中索引 useEffect(() => { const newIndex = getInitialSelectedIndex(); setSelectedIndex(newIndex); }, [date]); // 当选中日期变化时获取数据 useEffect(() => { if (currentSelectedDate) { getStepData(currentSelectedDate); } }, [currentSelectedDate]); // 计算总步数和平均步数 const totalSteps = stepCount || 0; const averageHourlySteps = useMemo(() => { if (!hourlySteps || hourlySteps.length === 0) return 0; const activeHours = hourlySteps.filter(h => h.steps > 0); if (activeHours.length === 0) return 0; return Math.round(activeHours.reduce((sum, h) => sum + h.steps, 0) / activeHours.length); }, [hourlySteps]); // 找出最活跃的时间段 const mostActiveHour = useMemo(() => { if (!hourlySteps || hourlySteps.length === 0) return null; const maxStepsData = hourlySteps.reduce((max, current) => current.steps > max.steps ? current : max ); return maxStepsData.steps > 0 ? maxStepsData : null; }, [hourlySteps]); // 活动等级配置 const activityLevels = useMemo(() => [ { key: 'inactive', label: t('stepsDetail.activityLevel.levels.inactive'), minSteps: 0, maxSteps: 3000, color: '#B8C8D6' }, { key: 'light', label: t('stepsDetail.activityLevel.levels.light'), minSteps: 3000, maxSteps: 7500, color: '#93C5FD' }, { key: 'moderate', label: t('stepsDetail.activityLevel.levels.moderate'), minSteps: 7500, maxSteps: 10000, color: '#FCD34D' }, { key: 'very_active', label: t('stepsDetail.activityLevel.levels.very_active'), minSteps: 10000, maxSteps: Infinity, color: '#FB923C' } ], [t]); // 计算当前活动等级 const currentActivityLevel = useMemo(() => { return activityLevels.find(level => totalSteps >= level.minSteps && totalSteps < level.maxSteps ) || activityLevels[0]; }, [totalSteps, activityLevels]); // 计算下一等级 const nextActivityLevel = useMemo(() => { const currentIndex = activityLevels.indexOf(currentActivityLevel); return currentIndex < activityLevels.length - 1 ? activityLevels[currentIndex + 1] : null; }, [currentActivityLevel, activityLevels]); // 计算进度百分比 const progressPercentage = useMemo(() => { if (!nextActivityLevel) return 100; // 已达到最高级 const rangeSize = nextActivityLevel.minSteps - currentActivityLevel.minSteps; const currentProgress = totalSteps - currentActivityLevel.minSteps; return Math.min(Math.max((currentProgress / rangeSize) * 100, 0), 100); }, [totalSteps, currentActivityLevel, nextActivityLevel]); // 倒序显示的活动等级(用于图例) const reversedActivityLevels = useMemo(() => [...activityLevels].reverse(), [activityLevels]); return ( {/* 背景渐变 */} {/* 日期选择器 */} {/* 统计卡片 */} {isLoading ? ( {t('stepsDetail.loading')} ) : ( {totalSteps.toLocaleString()} {t('stepsDetail.stats.totalSteps')} {averageHourlySteps} {t('stepsDetail.stats.averagePerHour')} {mostActiveHour ? `${mostActiveHour.hour}:00` : '--'} {t('stepsDetail.stats.mostActiveTime')} )} {/* 详细柱状图卡片 */} {t('stepsDetail.chart.title')} {dayjs(currentSelectedDate).format('YYYY年MM月DD日')} {/* 柱状图容器 */} {/* 平均值刻度线 - 放在chartArea外面,相对于chartContainer定位 */} {averageLinePosition > 0 && ( {/* 创建更多的虚线段来确保完整覆盖 */} {Array.from({ length: 80 }, (_, index) => ( 0 ? 2 : 0, flex: 0 // 防止 flex 拉伸 } ]} /> ))} {t('stepsDetail.chart.averageLabel', { steps: averageHourlySteps })} )} {/* 柱状图区域 */} {chartData.map((data, index) => { const isActive = data.steps > 0; const isCurrent = index <= currentHour; const isKeyTime = index === 0 || index === 12 || index === 23; // 动画变换 const animatedHeight = animatedValues[index].interpolate({ inputRange: [0, 1], outputRange: [0, data.height], }); const animatedOpacity = animatedValues[index].interpolate({ inputRange: [0, 1], outputRange: [0, 1], }); return ( {/* 背景柱体 */} {/* 数据柱体 */} {isActive && ( )} {/* 步数标签(仅在有数据且是关键时间点时显示) */} {/* {isActive && isKeyTime && ( {data.steps} )} */} ); })} {/* 底部时间轴标签 */} {t('stepsDetail.timeLabels.midnight')} {t('stepsDetail.timeLabels.noon')} {t('stepsDetail.timeLabels.nextDay')} {/* 活动等级展示卡片 */} {/* 活动级别文本 */} {t('stepsDetail.activityLevel.currentActivity')} {currentActivityLevel.label} {/* 进度条 */} {/* 步数信息 */} {totalSteps.toLocaleString()} 步 {t('stepsDetail.activityLevel.progress.current')} {nextActivityLevel ? `${nextActivityLevel.minSteps.toLocaleString()} 步` : '--'} {nextActivityLevel ? t('stepsDetail.activityLevel.progress.nextLevel', { level: nextActivityLevel.label }) : t('stepsDetail.activityLevel.progress.highestLevel')} {/* 活动等级图例 */} {reversedActivityLevels.map((level) => ( 🏃 {level.label} {level.maxSteps === Infinity ? `> ${level.minSteps.toLocaleString()}` : `${level.minSteps.toLocaleString()} - ${level.maxSteps.toLocaleString()}`} ))} ); } const styles = StyleSheet.create({ container: { flex: 1, }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 16, backgroundColor: 'transparent', }, backButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.8)', alignItems: 'center', justifyContent: 'center', }, headerTitle: { fontSize: 18, fontWeight: '600', color: '#192126', }, headerRight: { width: 40, }, scrollView: { flex: 1, paddingHorizontal: 20, }, statsCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginVertical: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 6, }, statsRow: { flexDirection: 'row', justifyContent: 'space-around', }, statItem: { alignItems: 'center', }, statValue: { fontSize: 24, fontWeight: '700', color: '#192126', marginBottom: 4, }, statLabel: { fontSize: 12, color: '#64748B', }, loadingContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 20, }, loadingText: { fontSize: 16, color: '#64748B', }, chartCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 6, }, chartHeader: { marginBottom: 20, }, chartTitle: { fontSize: 18, fontWeight: '600', color: '#192126', marginBottom: 4, }, chartSubtitle: { fontSize: 14, color: '#64748B', }, chartContainer: { position: 'relative', }, timeLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, paddingHorizontal: 8, }, timeLabel: { fontSize: 12, color: '#64748B', fontWeight: '500', }, chartArea: { flexDirection: 'row', alignItems: 'flex-end', height: 120, justifyContent: 'space-between', paddingHorizontal: 4, }, barContainer: { width: 8, height: 120, alignItems: 'center', justifyContent: 'flex-end', position: 'relative', }, backgroundBar: { width: 8, height: 120, borderRadius: 2, position: 'absolute', bottom: 0, }, dataBar: { width: 8, borderRadius: 2, position: 'absolute', bottom: 0, }, stepLabel: { position: 'absolute', top: -20, alignItems: 'center', }, stepLabelText: { fontSize: 10, color: '#64748B', fontWeight: '500', }, averageLine: { position: 'absolute', left: 4, // 匹配 chartArea 的 paddingHorizontal right: 4, // 匹配 chartArea 的 paddingHorizontal flexDirection: 'row', alignItems: 'center', zIndex: 1, }, averageLineDashContainer: { flex: 1, flexDirection: 'row', alignItems: 'center', marginRight: 8, overflow: 'hidden', // 防止虚线段溢出容器 }, dashSegment: { width: 3, height: 1.5, backgroundColor: '#FFA726', opacity: 0.8, }, averageLineLabel: { fontSize: 10, color: '#FFA726', fontWeight: '600', backgroundColor: '#FFFFFF', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, borderWidth: 0.5, borderColor: '#FFA726', }, activityLevelCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 24, marginVertical: 16, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 6, }, activityIconContainer: { marginBottom: 16, }, activityIcon: { width: 80, height: 80, borderRadius: 40, backgroundColor: '#E0F2FE', alignItems: 'center', justifyContent: 'center', borderWidth: 2, borderColor: '#93C5FD', borderStyle: 'dashed', }, meditationIcon: { width: 50, height: 50, borderRadius: 25, backgroundColor: '#93C5FD', alignItems: 'center', justifyContent: 'center', }, meditationEmoji: { fontSize: 24, }, activityMainText: { fontSize: 16, color: '#64748B', marginBottom: 4, }, activityLevelText: { fontSize: 24, fontWeight: '700', color: '#192126', marginBottom: 20, }, progressBarContainer: { width: '100%', marginBottom: 24, }, progressBarBackground: { width: '100%', height: 8, backgroundColor: '#F0F9FF', borderRadius: 4, overflow: 'hidden', }, progressBarFill: { height: '100%', borderRadius: 4, }, stepsInfoContainer: { flexDirection: 'row', justifyContent: 'space-between', width: '100%', marginBottom: 32, }, currentStepsInfo: { alignItems: 'flex-start', }, nextStepsInfo: { alignItems: 'flex-end', }, stepsValue: { fontSize: 20, fontWeight: '700', color: '#192126', marginBottom: 4, }, stepsLabel: { fontSize: 14, color: '#64748B', }, activityLegendContainer: { width: '100%', }, legendItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, paddingHorizontal: 16, backgroundColor: '#F8FAFC', marginBottom: 8, borderRadius: 12, }, legendIcon: { width: 32, height: 32, borderRadius: 8, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, legendIconText: { fontSize: 16, }, legendLabel: { flex: 1, fontSize: 14, fontWeight: '600', color: '#192126', }, legendRange: { fontSize: 14, color: '#64748B', fontWeight: '500', }, });