import { ROUTES } from '@/constants/Routes'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { ChallengeType } from '@/services/challengesApi'; import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice'; import { ActivityRingsData, fetchActivityRingsForDate } from '@/utils/health'; import { logger } from '@/utils/logger'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { CircularRing } from './CircularRing'; type FitnessRingsCardProps = { style?: any; selectedDate?: Date; // 动画重置令牌 resetToken?: unknown; }; /** * 健身圆环卡片组件,模仿 Apple Watch 的健身圆环 */ export function FitnessRingsCard({ style, selectedDate, resetToken, }: FitnessRingsCardProps) { const dispatch = useAppDispatch(); const challenges = useAppSelector(selectChallengeList); const [activityData, setActivityData] = useState(null); const [loading, setLoading] = useState(false); const loadingRef = useRef(false); const lastReportedRef = useRef<{ date: string } | null>(null); const joinedExerciseChallenges = useMemo( () => challenges.filter((challenge) => challenge.type === ChallengeType.EXERCISE && challenge.isJoined && challenge.status === 'ongoing'), [challenges] ); // 获取健身圆环数据 - 在页面聚焦、日期变化、从后台切换到前台时触发 useFocusEffect( useCallback(() => { const loadActivityData = async () => { if (!selectedDate) return; // 防止重复请求 if (loadingRef.current) return; try { loadingRef.current = true; setLoading(true); const data = await fetchActivityRingsForDate(selectedDate); setActivityData(data); } catch (error) { console.error('FitnessRingsCard: 获取健身圆环数据失败:', error); setActivityData(null); } finally { setLoading(false); loadingRef.current = false; } }; loadActivityData(); }, [selectedDate]) ); useEffect(() => { if (!selectedDate || !activityData || !joinedExerciseChallenges.length) { return; } if (!dayjs(selectedDate).isSame(dayjs(), 'day')) { return; } const { activeEnergyBurned, activeEnergyBurnedGoal, appleExerciseTime, appleExerciseTimeGoal, appleStandHours, appleStandHoursGoal, } = activityData; if ( activeEnergyBurnedGoal <= 0 || appleExerciseTimeGoal <= 0 || appleStandHoursGoal <= 0 ) { return; } const allRingsClosed = activeEnergyBurned >= activeEnergyBurnedGoal && appleExerciseTime >= appleExerciseTimeGoal && appleStandHours >= appleStandHoursGoal; if (!allRingsClosed) { return; } const dateKey = dayjs(selectedDate).format('YYYY-MM-DD'); if (lastReportedRef.current?.date === dateKey) { return; } const exerciseChallenge = joinedExerciseChallenges[0]; if (!exerciseChallenge) { return; } const reportProgressAsync = async () => { try { await dispatch(reportChallengeProgress({ id: exerciseChallenge.id, value: 1 })).unwrap(); lastReportedRef.current = { date: dateKey }; } catch (error) { logger.warn('FitnessRingsCard: 挑战进度上报失败', { error, challengeId: exerciseChallenge.id }); } }; reportProgressAsync(); }, [activityData, dispatch, joinedExerciseChallenges, selectedDate]); // 使用获取到的数据或默认值 const activeCalories = activityData?.activeEnergyBurned ?? 0; const activeCaloriesGoal = activityData?.activeEnergyBurnedGoal ?? 350; const exerciseMinutes = activityData?.appleExerciseTime ?? 0; const exerciseMinutesGoal = activityData?.appleExerciseTimeGoal ?? 30; const standHours = activityData?.appleStandHours ?? 0; const standHoursGoal = activityData?.appleStandHoursGoal ?? 12; // 计算进度百分比 const caloriesProgress = Math.min(1, Math.max(0, activeCalories / activeCaloriesGoal)); const exerciseProgress = Math.min(1, Math.max(0, exerciseMinutes / exerciseMinutesGoal)); const standProgress = Math.min(1, Math.max(0, standHours / standHoursGoal)); const handlePress = () => { router.push(ROUTES.FITNESS_RINGS_DETAIL); }; return ( {/* 左侧圆环 */} {/* 外圈 - 活动卡路里 (红色) */} {/* 中圈 - 锻炼分钟 (橙色) */} {/* 内圈 - 站立小时 (蓝色) */} {/* 右侧数据显示 */} {loading ? ( -- ) : ( <> {Math.round(activeCalories)} /{activeCaloriesGoal} )} 千卡 {loading ? ( -- ) : ( <> {Math.round(exerciseMinutes)} /{exerciseMinutesGoal} )} 分钟 {loading ? ( -- ) : ( <> {Math.round(standHours)} /{standHoursGoal} )} 小时 ); } const styles = StyleSheet.create({ container: { borderRadius: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3, }, contentContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, ringsContainer: { alignItems: 'center', justifyContent: 'center', marginRight: 12, }, ringWrapper: { position: 'relative', width: 36, height: 36, alignItems: 'center', justifyContent: 'center', }, ringPosition: { position: 'absolute', alignItems: 'center', justifyContent: 'center', }, dataContainer: { flex: 1, gap: 3, }, dataRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, dataText: { fontSize: 12, fontWeight: '700', flex: 1, }, dataValue: { color: '#192126', }, dataGoal: { color: '#9AA3AE', }, dataUnit: { fontSize: 10, color: '#9AA3AE', fontWeight: '500', minWidth: 25, textAlign: 'right', }, });