import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, InteractionManager, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { ChallengeType } from '@/services/challengesApi'; import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice'; import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health'; import { logger } from '@/utils/logger'; import dayjs from 'dayjs'; import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; import { useTranslation } from 'react-i18next'; import { AnimatedNumber } from './AnimatedNumber'; // 使用原生View来替代SVG,避免导入问题 // import Svg, { Rect } from 'react-native-svg'; interface StepsCardProps { curDate: Date; stepGoal?: number; style?: ViewStyle; } const StepsCard: React.FC = ({ curDate, style, }) => { const { t } = useTranslation(); const router = useRouter(); const dispatch = useAppDispatch(); const challenges = useAppSelector(selectChallengeList); const [stepCount, setStepCount] = useState(0); const [hourlySteps, setHourSteps] = useState([]); // 过滤出已参加的步数挑战 const joinedStepsChallenges = useMemo( () => challenges.filter((challenge) => challenge.type === ChallengeType.STEP && challenge.isJoined && challenge.status === 'ongoing'), [challenges] ); // 跟踪上次上报的记录,避免重复上报 const lastReportedRef = useRef<{ date: string; value: number } | null>(null); const getStepData = useCallback(async (date: Date) => { try { logger.info('Getting step data...'); // 先获取步数,立即更新UI const [steps, hourly] = await Promise.all([ fetchStepCount(date), fetchHourlyStepSamples(date) ]); setStepCount(steps); setHourSteps(hourly); } catch (error) { logger.error('Failed to get step data:', error); } }, []); useEffect(() => { if (curDate) { getStepData(curDate); } }, [curDate]); // 步数挑战进度上报逻辑 useEffect(() => { if (!curDate || !stepCount || !joinedStepsChallenges.length) { return; } // 如果当前日期不是今天,不上报 if (!dayjs(curDate).isSame(dayjs(), 'day')) { return; } const dateKey = dayjs(curDate).format('YYYY-MM-DD'); const lastReport = lastReportedRef.current; if (lastReport && lastReport.date === dateKey && lastReport.value === stepCount) { return; } const reportProgress = async () => { const stepsChallenge = joinedStepsChallenges.find((c) => c.type === ChallengeType.STEP); if (!stepsChallenge) { return; } try { await dispatch(reportChallengeProgress({ id: stepsChallenge.id, value: stepCount })).unwrap(); } catch (error) { logger.warn('StepsCard: Challenge progress report failed', { error, challengeId: stepsChallenge.id }); } lastReportedRef.current = { date: dateKey, value: stepCount }; }; reportProgress(); }, [dispatch, joinedStepsChallenges, curDate, stepCount]); // 优化:减少动画值数量,只为有数据的小时创建动画 const animatedValues = useRef>(new Map()).current; // 优化:简化柱状图数据计算,减少计算量 const chartData = useMemo(() => { if (!hourlySteps || hourlySteps.length === 0) { return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 })); } // 优化:只计算有数据的小时的最大步数 const activeSteps = hourlySteps.filter(data => data.steps > 0); if (activeSteps.length === 0) { return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 })); } const maxSteps = Math.max(...activeSteps.map(data => data.steps)); const maxHeight = 20; return hourlySteps.map(data => ({ ...data, height: data.steps > 0 ? (data.steps / maxSteps) * maxHeight : 0 })); }, [hourlySteps]); // 获取当前小时 const currentHour = new Date().getHours(); // 优化:延迟执行动画,减少UI阻塞 useEffect(() => { const hasData = chartData && chartData.length > 0 && chartData.some(data => data.steps > 0); if (hasData) { // 使用 InteractionManager 确保动画不会阻塞用户交互 InteractionManager.runAfterInteractions(() => { // 只为有数据的小时创建和执行动画 chartData.forEach((data, index) => { if (data.steps > 0) { // 懒创建动画值 if (!animatedValues.has(index)) { animatedValues.set(index, new Animated.Value(0)); } const animValue = animatedValues.get(index)!; animValue.setValue(0); // 使用更高性能的timing动画替代spring Animated.timing(animValue, { toValue: 1, duration: 300, useNativeDriver: false, }).start(); } }); }); } }, [chartData, animatedValues]); const CardContent = () => ( <> {/* 标题和步数显示 */} {t('statistics.components.steps.title')} {/* 柱状图 */} {chartData.map((data, index) => { // 判断是否是当前小时或者有活动的小时 const isActive = data.steps > 0; const isCurrent = index <= currentHour; // 优化:只为有数据的柱体创建动画插值 const animValue = animatedValues.get(index); let animatedScale: Animated.AnimatedInterpolation | undefined; let animatedOpacity: Animated.AnimatedInterpolation | undefined; if (animValue && isActive) { animatedScale = animValue.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }); animatedOpacity = animValue.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }); } return ( {/* 背景柱体 - 始终显示,使用相似色系的淡色 */} {/* 数据柱体 - 只有当有数据时才显示并执行动画 */} {isActive && ( )} ); })} {/* 步数和目标显示 */} stepCount !== null ? `${Math.round(v)}` : '--'} resetToken={stepCount} /> ); return ( { // 传递当前日期参数到详情页 const dateParam = dayjs(curDate).format('YYYY-MM-DD'); router.push(`/steps/detail?date=${dateParam}`); }} activeOpacity={0.8} > ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'space-between', borderRadius: 20, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.08, shadowRadius: 20, elevation: 8, }, header: { flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', }, titleIcon: { width: 16, height: 16, marginRight: 6, resizeMode: 'contain', }, title: { fontSize: 14, color: '#192126', fontWeight: '600', fontFamily: 'AliBold', }, footprintIcons: { flexDirection: 'row', alignItems: 'center', gap: 6, }, chartContainer: { flex: 1, justifyContent: 'center', marginTop: 6 }, chartWrapper: { width: '100%', alignItems: 'center', }, chartArea: { flexDirection: 'row', alignItems: 'flex-end', height: 20, width: '100%', maxWidth: 240, justifyContent: 'space-between', paddingHorizontal: 4, }, barContainer: { width: 4, height: 20, alignItems: 'center', justifyContent: 'flex-end', position: 'relative', }, chartBar: { width: 4, borderRadius: 1, position: 'absolute', bottom: 0, }, statsContainer: { alignItems: 'flex-start', marginTop: 6 }, stepCount: { fontSize: 18, fontWeight: '600', color: '#192126', fontFamily: 'AliBold', }, }); export default StepsCard;