import { DateSelector } from '@/components/DateSelector'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health'; import { getTestHealthData } from '@/utils/mockHealthData'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Animated, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function StepsDetailScreen() { const router = useRouter(); const dispatch = useAppDispatch(); const insets = useSafeAreaInsets(); // 开发调试:设置为true来使用mock数据 const useMockData = __DEV__; // 日期选择相关状态 const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); // 数据加载状态 const [isLoading, setIsLoading] = useState(false); // 获取当前选中日期 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 获取指定日期的健康数据 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 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 loadHealthData = async (targetDate: Date) => { if (useMockData) return; // 如果使用mock数据,不需要加载 try { setIsLoading(true); console.log('加载步数详情数据...', targetDate); const ok = await ensureHealthPermissions(); if (!ok) { console.warn('无法获取健康权限'); return; } const data = await fetchHealthDataForDate(targetDate); console.log('data', data); const dateString = dayjs(targetDate).format('YYYY-MM-DD'); // 使用 Redux 存储健康数据 dispatch(setHealthData({ date: dateString, data: data })); console.log('步数详情数据加载完成'); } catch (error) { console.error('加载步数详情数据失败:', error); } finally { setIsLoading(false); } }; // 日期选择处理 const onSelectDate = (index: number, date: Date) => { setSelectedIndex(index); loadHealthData(date); }; // 页面初始化时加载当前日期数据 useEffect(() => { loadHealthData(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: '不怎么动', minSteps: 0, maxSteps: 3000, color: '#B8C8D6' }, { key: 'light', label: '轻度活跃', minSteps: 3000, maxSteps: 7500, color: '#93C5FD' }, { key: 'moderate', label: '中等活跃', minSteps: 7500, maxSteps: 10000, color: '#FCD34D' }, { key: 'very_active', label: '非常活跃', minSteps: 10000, maxSteps: Infinity, color: '#FB923C' } ], []); // 计算当前活动等级 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 ( {/* 背景渐变 */} {/* 顶部导航栏 */} router.back()} > 步数详情 {/* 日期选择器 */} {/* 统计卡片 */} {isLoading ? ( 加载中... ) : ( {totalSteps.toLocaleString()} 总步数 {averageHourlySteps} 平均每小时 {mostActiveHour ? `${mostActiveHour.hour}:00` : '--'} 最活跃时段 )} {/* 详细柱状图卡片 */} 每小时步数分布 {dayjs(currentSelectedDate).format('YYYY年MM月DD日')} {/* 柱状图容器 */} {/* 平均值刻度线 - 放在chartArea外面,相对于chartContainer定位 */} {averageLinePosition > 0 && ( {/* 创建更多的虚线段来确保完整覆盖 */} {Array.from({ length: 80 }, (_, index) => ( 0 ? 2 : 0, flex: 0 // 防止 flex 拉伸 } ]} /> ))} 平均 {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} )} */} ); })} {/* 底部时间轴标签 */} 0:00 12:00 24:00 {/* 活动等级展示卡片 */} {/* 活动级别文本 */} 你今天的活动量处于 {currentActivityLevel.label} {/* 进度条 */} {/* 步数信息 */} {totalSteps.toLocaleString()} 步 当前 {nextActivityLevel ? `${nextActivityLevel.minSteps.toLocaleString()} 步` : '--'} {nextActivityLevel ? `下一级: ${nextActivityLevel.label}` : '已达最高级'} {/* 活动等级图例 */} {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, }, safeArea: { flex: 1, }, 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', }, });