import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useCallback, useMemo, useState } from 'react'; import { ActivityIndicator, SectionList, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { addHealthPermissionListener, checkHealthPermissionStatus, ensureHealthPermissions, fetchWorkoutsForDateRange, getHealthPermissionStatus, getWorkoutTypeDisplayName, HealthPermissionStatus, removeHealthPermissionListener, WorkoutActivityType, WorkoutData, } from '@/utils/health'; type WorkoutSection = { title: string; data: WorkoutData[]; }; const ICON_MAP: Partial> = { // 球类运动 [WorkoutActivityType.AmericanFootball]: 'football', [WorkoutActivityType.Archery]: 'target', [WorkoutActivityType.AustralianFootball]: 'football', [WorkoutActivityType.Badminton]: 'tennis', [WorkoutActivityType.Baseball]: 'baseball', [WorkoutActivityType.Basketball]: 'basketball', [WorkoutActivityType.Bowling]: 'bowling', [WorkoutActivityType.Boxing]: 'boxing-glove', [WorkoutActivityType.Cricket]: 'cricket', [WorkoutActivityType.Fencing]: 'sword', [WorkoutActivityType.Golf]: 'golf', [WorkoutActivityType.Handball]: 'basketball', [WorkoutActivityType.Hockey]: 'hockey-sticks', [WorkoutActivityType.Lacrosse]: 'tennis', [WorkoutActivityType.Racquetball]: 'tennis', [WorkoutActivityType.Soccer]: 'soccer', [WorkoutActivityType.Softball]: 'baseball', [WorkoutActivityType.Squash]: 'tennis', [WorkoutActivityType.TableTennis]: 'table-tennis', [WorkoutActivityType.Tennis]: 'tennis', [WorkoutActivityType.Volleyball]: 'volleyball', [WorkoutActivityType.WaterPolo]: 'swim', [WorkoutActivityType.Pickleball]: 'tennis', // 水上运动 [WorkoutActivityType.Swimming]: 'swim', [WorkoutActivityType.Sailing]: 'sail-boat', [WorkoutActivityType.SurfingSports]: 'waves', [WorkoutActivityType.WaterFitness]: 'swim', [WorkoutActivityType.WaterSports]: 'swim', [WorkoutActivityType.UnderwaterDiving]: 'swim', // 跑步和步行 [WorkoutActivityType.Running]: 'run', [WorkoutActivityType.Walking]: 'walk', [WorkoutActivityType.Hiking]: 'hiking', [WorkoutActivityType.StairClimbing]: 'stairs', [WorkoutActivityType.Stairs]: 'stairs', // 骑行 [WorkoutActivityType.Cycling]: 'bike', [WorkoutActivityType.HandCycling]: 'bike', // 滑雪和滑冰 [WorkoutActivityType.CrossCountrySkiing]: 'ski', [WorkoutActivityType.DownhillSkiing]: 'ski', [WorkoutActivityType.Snowboarding]: 'snowboard', [WorkoutActivityType.SkatingSports]: 'skateboarding', [WorkoutActivityType.SnowSports]: 'ski', // 力量训练 [WorkoutActivityType.FunctionalStrengthTraining]: 'weight-lifter', [WorkoutActivityType.TraditionalStrengthTraining]: 'dumbbell', [WorkoutActivityType.CrossTraining]: 'arm-flex', [WorkoutActivityType.CoreTraining]: 'arm-flex', // 有氧运动 [WorkoutActivityType.Elliptical]: 'bike', [WorkoutActivityType.Rowing]: 'rowing', [WorkoutActivityType.MixedCardio]: 'heart-pulse', [WorkoutActivityType.MixedMetabolicCardioTraining]: 'heart-pulse', [WorkoutActivityType.HighIntensityIntervalTraining]: 'run-fast', [WorkoutActivityType.JumpRope]: 'skip-forward', [WorkoutActivityType.StepTraining]: 'stairs', // 舞蹈和身心训练 [WorkoutActivityType.Dance]: 'music', [WorkoutActivityType.DanceInspiredTraining]: 'music', [WorkoutActivityType.CardioDance]: 'music', [WorkoutActivityType.SocialDance]: 'music', [WorkoutActivityType.Yoga]: 'meditation', [WorkoutActivityType.MindAndBody]: 'meditation', [WorkoutActivityType.TaiChi]: 'meditation', [WorkoutActivityType.Pilates]: 'meditation', [WorkoutActivityType.Barre]: 'meditation', [WorkoutActivityType.Flexibility]: 'meditation', [WorkoutActivityType.Cooldown]: 'meditation', [WorkoutActivityType.PreparationAndRecovery]: 'meditation', // 户外运动 [WorkoutActivityType.Climbing]: 'hiking', [WorkoutActivityType.EquestrianSports]: 'horse', [WorkoutActivityType.Fishing]: 'target', [WorkoutActivityType.Hunting]: 'target', [WorkoutActivityType.PaddleSports]: 'rowing', // 综合运动 [WorkoutActivityType.SwimBikeRun]: 'run-fast', [WorkoutActivityType.Transition]: 'swap-horizontal-variant', [WorkoutActivityType.Play]: 'gamepad-variant', [WorkoutActivityType.FitnessGaming]: 'gamepad-variant', [WorkoutActivityType.DiscSports]: 'target', // 其他 [WorkoutActivityType.Other]: 'arm-flex', [WorkoutActivityType.MartialArts]: 'karate', [WorkoutActivityType.Kickboxing]: 'boxing-glove', [WorkoutActivityType.Gymnastics]: 'human', [WorkoutActivityType.TrackAndField]: 'run-fast', [WorkoutActivityType.WheelchairWalkPace]: 'wheelchair', [WorkoutActivityType.WheelchairRunPace]: 'wheelchair', [WorkoutActivityType.Curling]: 'target', }; function getIntensityBadge(totalCalories?: number, durationInSeconds?: number) { if (!totalCalories || !durationInSeconds) { return { label: '低强度', color: '#7C85A3', background: '#E4E7F2' }; } const minutes = Math.max(durationInSeconds / 60, 1); const caloriesPerMinute = totalCalories / minutes; if (caloriesPerMinute >= 9) { return { label: '高强度', color: '#F85959', background: '#FFE6E6' }; } if (caloriesPerMinute >= 5) { return { label: '中强度', color: '#0EAF71', background: '#E4F6EF' }; } return { label: '低强度', color: '#5966FF', background: '#E7EBFF' }; } function groupWorkouts(workouts: WorkoutData[]): WorkoutSection[] { const grouped = workouts.reduce>((acc, workout) => { const dateKey = dayjs(workout.startDate || workout.endDate).format('YYYY-MM-DD'); if (!acc[dateKey]) { acc[dateKey] = []; } acc[dateKey].push(workout); return acc; }, {}); return Object.keys(grouped) .sort((a, b) => dayjs(b).valueOf() - dayjs(a).valueOf()) .map((dateKey) => ({ title: dayjs(dateKey).format('M月D日'), data: grouped[dateKey] .sort((a, b) => dayjs(b.startDate || b.endDate).valueOf() - dayjs(a.startDate || a.endDate).valueOf()), })); } export default function WorkoutHistoryScreen() { const [sections, setSections] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const loadHistory = useCallback(async () => { setIsLoading(true); setError(null); try { let permissionStatus = getHealthPermissionStatus(); if (permissionStatus !== HealthPermissionStatus.Authorized) { permissionStatus = await checkHealthPermissionStatus(true); } let hasPermission = permissionStatus === HealthPermissionStatus.Authorized; if (!hasPermission) { hasPermission = await ensureHealthPermissions(); } if (!hasPermission) { setSections([]); setError('尚未授予健康数据权限'); return; } const end = dayjs(); const start = end.subtract(1, 'month'); const workouts = await fetchWorkoutsForDateRange(start.toDate(), end.toDate(), 200); const filteredWorkouts = workouts.filter((workout) => workout.duration && workout.duration > 0); setSections(groupWorkouts(filteredWorkouts)); } catch (err) { console.error('加载锻炼历史失败:', err); setError('加载锻炼记录失败,请稍后再试'); setSections([]); } finally { setIsLoading(false); } }, []); useFocusEffect( useCallback(() => { loadHistory(); }, [loadHistory]) ); React.useEffect(() => { const handlePermissionGranted = () => { loadHistory(); }; addHealthPermissionListener('permissionGranted', handlePermissionGranted); return () => { removeHealthPermissionListener('permissionGranted', handlePermissionGranted); }; }, [loadHistory]); const headerComponent = useMemo(() => ( 历史 最近一个月的锻炼记录 ), []); const emptyComponent = useMemo(() => ( 暂无锻炼记录 完成一次锻炼后即可在此查看详细历史 ), []); const renderItem = useCallback(({ item }: { item: WorkoutData }) => { const calories = Math.round(item.totalEnergyBurned || 0); const minutes = Math.max(Math.round((item.duration || 0) / 60), 1); const intensity = getIntensityBadge(item.totalEnergyBurned, item.duration || 0); const iconName = ICON_MAP[item.workoutActivityType as WorkoutActivityType] || 'arm-flex'; const time = dayjs(item.startDate || item.endDate).format('HH:mm'); const activityLabel = getWorkoutTypeDisplayName(item.workoutActivityType); return ( { }}> {calories}千卡 · {minutes}分钟 {intensity.label} {activityLabel},{time} {/* */} ); }, []); const renderSectionHeader = useCallback(({ section }: { section: WorkoutSection }) => ( {section.title} ), []); return ( {isLoading ? ( 正在加载锻炼记录... ) : ( item.id} renderItem={renderItem} renderSectionHeader={renderSectionHeader} ListHeaderComponent={headerComponent} ListEmptyComponent={error ? ( {error} 重试 ) : emptyComponent} contentContainerStyle={styles.listContent} stickySectionHeadersEnabled={false} showsVerticalScrollIndicator={false} /> )} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: 'transparent', }, sectionList: { flex: 1, }, headerContainer: { paddingHorizontal: 20, paddingTop: 12, paddingBottom: 16, }, headerTitle: { fontSize: 26, fontWeight: '700', color: '#1F2355', marginBottom: 6, }, headerSubtitle: { fontSize: 14, color: '#677086', }, listContent: { paddingBottom: 40, }, sectionHeader: { fontSize: 14, color: '#8087A2', fontWeight: '600', marginTop: 18, paddingHorizontal: 20, }, historyCard: { marginTop: 12, marginHorizontal: 16, paddingVertical: 18, paddingHorizontal: 18, backgroundColor: '#FFFFFF', borderRadius: 26, flexDirection: 'row', alignItems: 'center', shadowColor: '#5460E54D', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.1, shadowRadius: 16, elevation: 6, }, cardIconWrapper: { width: 46, height: 46, borderRadius: 23, backgroundColor: '#EEF0FF', alignItems: 'center', justifyContent: 'center', marginRight: 14, }, cardContent: { flex: 1, }, cardTitleRow: { flexDirection: 'row', alignItems: 'center', }, cardTitle: { fontSize: 16, fontWeight: '700', color: '#1F2355', flexShrink: 1, }, intensityBadge: { marginLeft: 8, paddingHorizontal: 8, paddingVertical: 3, borderRadius: 10, }, intensityText: { fontSize: 12, fontWeight: '600', }, cardSubtitle: { marginTop: 8, fontSize: 13, color: '#6B7693', }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, }, loadingText: { fontSize: 14, color: '#596182', }, emptyContainer: { marginTop: 60, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, gap: 12, }, emptyText: { fontSize: 16, fontWeight: '600', color: '#596182', }, emptySubText: { fontSize: 13, color: '#8F96AF', textAlign: 'center', lineHeight: 18, }, retryButton: { marginTop: 8, paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: '#5C55FF', }, retryText: { color: '#FFFFFF', fontSize: 13, fontWeight: '600', }, });