import { Ionicons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import React, { useEffect, useMemo, useState } from 'react'; import { Alert, FlatList, Modal, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Animated, { FadeInUp } from 'react-native-reanimated'; import { CircularRing } from '@/components/CircularRing'; import { ThemedText } from '@/components/ThemedText'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { palette } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import type { WorkoutExercise } from '@/services/workoutsApi'; import { clearWorkoutError, completeWorkoutExercise, deleteWorkoutSession, loadTodayWorkout, skipWorkoutExercise, startWorkoutExercise, startWorkoutSession } from '@/store/workoutSlice'; // ==================== 工具函数 ==================== // 计算两个时间之间的耗时(秒) const calculateDuration = (startTime: string, endTime: string): number => { const start = new Date(startTime); const end = new Date(endTime); return Math.floor((end.getTime() - start.getTime()) / 1000); }; // 格式化耗时显示(分钟:秒) const formatDuration = (seconds: number): string => { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}分${remainingSeconds.toString().padStart(2, '0')}秒`; }; // 获取动作的耗时信息 const getExerciseDuration = (exercise: WorkoutExercise): { duration: number; formatted: string } | null => { if (exercise.status === 'completed' && exercise.startedAt && exercise.completedAt) { const duration = calculateDuration(exercise.startedAt, exercise.completedAt); return { duration, formatted: formatDuration(duration) }; } return null; }; const GOAL_TEXT: Record = { postpartum_recovery: { title: '产后恢复', color: '#9BE370', description: '温和激活,核心重建' }, fat_loss: { title: '减脂塑形', color: '#FFB86B', description: '全身燃脂,线条雕刻' }, posture_correction: { title: '体态矫正', color: '#95CCE3', description: '打开胸肩,改善圆肩驼背' }, core_strength: { title: '核心力量', color: '#A48AED', description: '核心稳定,提升运动表现' }, flexibility: { title: '柔韧灵活', color: '#B0F2A7', description: '拉伸延展,释放紧张' }, rehab: { title: '康复保健', color: '#FF8E9E', description: '循序渐进,科学修复' }, stress_relief: { title: '释压放松', color: '#9BD1FF', description: '舒缓身心,改善睡眠' }, }; // 动态背景组件 function DynamicBackground({ color }: { color: string }) { return ( ); } export default function TodayWorkoutScreen() { const router = useRouter(); const dispatch = useAppDispatch(); const { currentSession, exercises, loading, exerciseLoading, error } = useAppSelector((s) => s.workout); // 本地状态 const [completionModal, setCompletionModal] = useState<{ visible: boolean; exercise: WorkoutExercise | null; sets: number; reps: number; }>({ visible: false, exercise: null, sets: 0, reps: 0, }); const goalConfig = currentSession?.trainingPlan ? (GOAL_TEXT[currentSession.trainingPlan.goal] || { title: '今日训练', color: palette.primary, description: '开始你的训练之旅' }) : { title: '今日训练', color: palette.primary, description: '开始你的训练之旅' }; // 加载今日训练数据 useEffect(() => { dispatch(loadTodayWorkout()); }, [dispatch]); // 错误处理 useEffect(() => { if (error) { Alert.alert('错误', error, [ { text: '确定', onPress: () => dispatch(clearWorkoutError()) } ]); } }, [error, dispatch]); // 训练状态统计 const workoutStats = useMemo(() => { const exerciseItems = exercises.filter(ex => ex.itemType === 'exercise'); return { total: exerciseItems.length, completed: exerciseItems.filter(ex => ex.status === 'completed').length, inProgress: exerciseItems.filter(ex => ex.status === 'in_progress').length, pending: exerciseItems.filter(ex => ex.status === 'pending').length, skipped: exerciseItems.filter(ex => ex.status === 'skipped').length, }; }, [exercises]); const completionPercentage = workoutStats.total > 0 ? Math.round((workoutStats.completed / workoutStats.total) * 100) : 0; // 开始训练会话 const handleStartWorkout = () => { if (!currentSession) return; Alert.alert( '开始训练', '准备好开始今日的训练了吗?', [ { text: '取消', style: 'cancel' }, { text: '开始', onPress: () => { dispatch(startWorkoutSession({ sessionId: currentSession.id })); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } } ] ); }; // 开始动作 const handleStartExercise = (exercise: WorkoutExercise) => { if (!currentSession || exercise.status !== 'pending') return; dispatch(startWorkoutExercise({ sessionId: currentSession.id, exerciseId: exercise.id })); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); }; // 显示完成动作模态框 const handleShowCompleteModal = (exercise: WorkoutExercise) => { setCompletionModal({ visible: true, exercise, sets: exercise.completedSets || exercise.plannedSets || 0, reps: exercise.completedReps || exercise.plannedReps || 0, }); }; // 完成动作 const handleCompleteExercise = () => { const { exercise, sets, reps } = completionModal; if (!currentSession || !exercise) return; dispatch(completeWorkoutExercise({ sessionId: currentSession.id, exerciseId: exercise.id, dto: { completedSets: sets, completedReps: reps, } })); setCompletionModal({ visible: false, exercise: null, sets: 0, reps: 0 }); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }; // 跳过动作 const handleSkipExercise = (exercise: WorkoutExercise) => { if (!currentSession) return; Alert.alert( '跳过动作', `确定要跳过"${exercise.name}"吗?`, [ { text: '取消', style: 'cancel' }, { text: '跳过', style: 'destructive', onPress: () => { dispatch(skipWorkoutExercise({ sessionId: currentSession.id, exerciseId: exercise.id })); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } } ] ); }; // 删除训练会话 const handleDeleteSession = () => { if (!currentSession) return; Alert.alert( '删除训练会话', '确定要删除这个训练会话吗?删除后无法恢复。', [ { text: '取消', style: 'cancel' }, { text: '删除', style: 'destructive', onPress: () => { dispatch(deleteWorkoutSession(currentSession.id)); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); // 删除成功后返回上一页 router.back(); } } ] ); }; // 获取动作状态文本和颜色 const getExerciseStatusConfig = (exercise: WorkoutExercise) => { switch (exercise.status) { case 'completed': return { text: '已完成', color: '#22C55E', backgroundColor: '#22C55E15' }; case 'in_progress': return { text: '进行中', color: '#F59E0B', backgroundColor: '#F59E0B15' }; case 'skipped': return { text: '已跳过', color: '#6B7280', backgroundColor: '#6B728015' }; default: return { text: '待开始', color: '#6B7280', backgroundColor: '#6B728015' }; } }; // 渲染动作卡片 const renderExerciseItem = ({ item, index }: { item: WorkoutExercise; index: number }) => { const statusConfig = getExerciseStatusConfig(item); const isLoading = exerciseLoading === item.id; if (item.itemType === 'rest') { return ( {item.name} {item.restSec}秒 ); } if (item.itemType === 'note') { return ( {item.name} {item.note && {item.note}} ); } return ( {item.name} {item.exercise && ( {item.exercise.categoryName} )} {statusConfig.text} {item.plannedSets && ( {item.plannedSets} 组 × {item.plannedReps || '-'} 次 )} {item.plannedDurationSec && ( 持续 {item.plannedDurationSec} 秒 )} {item.status === 'completed' && ( 实际完成: {item.completedSets || '-'} 组 × {item.completedReps || '-'} 次 {(() => { const durationInfo = getExerciseDuration(item); return durationInfo ? ( {durationInfo.formatted} ) : null; })()} )} {item.status === 'pending' && currentSession?.status === 'in_progress' && ( handleStartExercise(item)} disabled={isLoading} > {isLoading ? '开始中...' : '开始'} )} {item.status === 'in_progress' && ( <> handleShowCompleteModal(item)} disabled={isLoading} > {isLoading ? '完成中...' : '完成'} handleSkipExercise(item)} disabled={isLoading} > 跳过 )} {item.status === 'pending' && ( handleSkipExercise(item)} disabled={isLoading} > 跳过 )} ); }; if (loading && !currentSession) { return ( router.back()} /> 加载中... ); } if (!currentSession) { return ( router.back()} /> 暂无今日训练 请先激活一个训练计划 router.push('/training-plan' as any)} > 去创建训练计划 ); } return ( {/* 动态背景 */} router.back()} withSafeTop={false} transparent={true} tone="light" right={ currentSession?.status === 'in_progress' ? ( router.push(`/training-plan/schedule/select?sessionId=${currentSession.id}` as any)} disabled={loading} > ) : null } /> {/* 训练计划信息头部 */} router.push(`/training-plan`)}> {/* 删除按钮 - 右上角 */} {goalConfig.title} {currentSession.trainingPlan?.name || '今日训练'} {/* 进度统计文字 */} {currentSession.status !== 'planned' && ( {workoutStats.completed}/{workoutStats.total} 个动作已完成 )} {/* 右侧区域:圆环进度或开始按钮 */} {currentSession.status === 'planned' ? ( ) : ( {completionPercentage}% )} {/* 训练完成提示 */} {currentSession.status === 'completed' && ( 训练已完成! )} {/* 动作列表 */} item.id} renderItem={renderExerciseItem} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} /> {/* 完成动作模态框 */} setCompletionModal({ visible: false, exercise: null, sets: 0, reps: 0 })} > setCompletionModal({ visible: false, exercise: null, sets: 0, reps: 0 })} > e.stopPropagation()} > 完成动作 {completionModal.exercise?.name} 完成组数 setCompletionModal(prev => ({ ...prev, sets: Math.max(0, prev.sets - 1) }))} > - {completionModal.sets} setCompletionModal(prev => ({ ...prev, sets: Math.min(20, prev.sets + 1) }))} > + 每组次数 setCompletionModal(prev => ({ ...prev, reps: Math.max(0, prev.reps - 1) }))} > - {completionModal.reps} setCompletionModal(prev => ({ ...prev, reps: Math.min(50, prev.reps + 1) }))} > + 确认完成 ); } const styles = StyleSheet.create({ safeArea: { flex: 1, }, contentWrapper: { flex: 1, }, content: { flex: 1, paddingHorizontal: 20, }, // 动态背景 backgroundOrb: { position: 'absolute', width: 300, height: 300, borderRadius: 150, top: -150, right: -100, }, backgroundOrb2: { position: 'absolute', width: 400, height: 400, borderRadius: 200, bottom: -200, left: -150, }, // 计划信息头部 planHeader: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, marginBottom: 16, }, planColorIndicator: { width: 4, height: 40, borderRadius: 2, marginRight: 12, }, planInfo: { flex: 1, }, planTitle: { fontSize: 18, fontWeight: '800', color: '#192126', marginBottom: 4, }, planDescription: { fontSize: 13, color: '#5E6468', opacity: 0.8, marginBottom: 4, }, planProgressStats: { fontSize: 12, color: '#6B7280', marginTop: 4, }, // 圆环进度容器 circularProgressContainer: { alignItems: 'center', justifyContent: 'center', marginRight: 32, position: 'relative', }, circularProgressText: { position: 'absolute', alignItems: 'center', justifyContent: 'center', width: 60, height: 60, }, circularProgressPercentage: { fontSize: 14, fontWeight: '800', textAlign: 'center', }, // 删除按钮 deleteBtn: { position: 'absolute', top: 8, right: 8, width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(255, 255, 255, 0.9)', zIndex: 10, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, // 开始训练按钮 planStartBtn: { width: 44, height: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center', marginRight: 32, }, // 完成提示 completedBanner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: '#F0FDF4', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, marginBottom: 16, gap: 8, }, completedBannerText: { color: '#22C55E', fontSize: 16, fontWeight: '700', }, // 列表 listContent: { paddingBottom: 40, }, // 动作卡片 exerciseCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginBottom: 12, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3, }, exerciseHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8, }, exerciseInfo: { flex: 1, }, exerciseName: { fontSize: 16, fontWeight: '800', color: '#192126', marginBottom: 4, }, exerciseCategory: { fontSize: 12, color: '#888F92', }, statusBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, }, statusText: { fontSize: 12, fontWeight: '600', }, exerciseDetails: { marginBottom: 12, }, exerciseParams: { fontSize: 14, color: '#5E6468', marginBottom: 2, }, completedInfo: { backgroundColor: '#F0FDF4', borderRadius: 8, }, completedRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, completedText: { fontSize: 12, color: '#22C55E', fontWeight: '600', flex: 1, }, durationBadge: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 6, paddingVertical: 3, borderRadius: 6, gap: 3, }, durationText: { fontSize: 11, fontWeight: '600', }, exerciseActions: { flexDirection: 'row', gap: 8, }, actionBtn: { flex: 1, paddingVertical: 10, paddingHorizontal: 16, borderRadius: 8, alignItems: 'center', }, startBtn: { backgroundColor: '#22C55E', }, startBtnText: { color: '#FFFFFF', fontSize: 14, fontWeight: '700', }, completeBtn: { backgroundColor: '#22C55E', }, completeBtnText: { color: '#FFFFFF', fontSize: 14, fontWeight: '700', }, skipBtn: { backgroundColor: '#F3F4F6', borderWidth: 1, borderColor: '#E5E7EB', }, skipBtnText: { color: '#6B7280', fontSize: 14, fontWeight: '600', }, // 休息卡片 restCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginBottom: 12, borderLeftWidth: 4, flexDirection: 'row', alignItems: 'center', shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 8, shadowOffset: { width: 0, height: 4 }, elevation: 2, }, restIconContainer: { marginRight: 12, }, restContent: { flex: 1, }, restTitle: { fontSize: 16, fontWeight: '700', color: '#192126', marginBottom: 4, }, restDuration: { fontSize: 14, color: '#5E6468', }, // 备注卡片 noteCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginBottom: 12, borderLeftWidth: 4, flexDirection: 'row', alignItems: 'flex-start', shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 8, shadowOffset: { width: 0, height: 4 }, elevation: 2, }, noteIconContainer: { marginRight: 12, marginTop: 2, }, noteContent: { flex: 1, }, noteTitle: { fontSize: 16, fontWeight: '700', color: '#192126', marginBottom: 4, }, noteText: { fontSize: 14, color: '#5E6468', lineHeight: 20, }, // 空状态 emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'flex-start', paddingTop: 40, padding: 20, }, emptyTitle: { fontSize: 18, fontWeight: '700', color: '#192126', marginTop: 16, marginBottom: 8, }, emptyText: { fontSize: 14, color: '#6B7280', textAlign: 'center', marginBottom: 24, }, createPlanBtn: { backgroundColor: '#22C55E', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, }, createPlanBtnText: { color: '#FFFFFF', fontSize: 14, fontWeight: '700', }, // 加载状态 loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, loadingText: { fontSize: 16, color: '#6B7280', }, // 模态框 modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.35)', alignItems: 'center', justifyContent: 'flex-end', }, modalSheet: { width: '100%', backgroundColor: '#FFFFFF', borderTopLeftRadius: 16, borderTopRightRadius: 16, paddingHorizontal: 16, paddingTop: 14, paddingBottom: 24, }, modalTitle: { fontSize: 18, fontWeight: '800', marginBottom: 8, color: '#192126', textAlign: 'center', }, modalSubtitle: { fontSize: 14, color: '#6B7280', textAlign: 'center', marginBottom: 24, }, inputRow: { flexDirection: 'row', gap: 16, marginBottom: 24, }, inputBox: { flex: 1, }, inputLabel: { fontSize: 14, fontWeight: '600', color: '#192126', marginBottom: 12, }, counterRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#F3F4F6', borderRadius: 8, padding: 4, }, counterBtn: { backgroundColor: '#FFFFFF', width: 32, height: 32, borderRadius: 6, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 2, shadowOffset: { width: 0, height: 1 }, elevation: 1, }, counterBtnText: { fontWeight: '800', color: '#192126', fontSize: 16, }, counterValue: { fontWeight: '700', color: '#192126', fontSize: 16, minWidth: 40, textAlign: 'center', }, confirmBtn: { paddingVertical: 16, borderRadius: 12, alignItems: 'center', }, confirmBtnText: { color: '#FFFFFF', fontWeight: '800', fontSize: 16, }, // 添加动作按钮 addExerciseBtn: { width: 28, height: 28, borderRadius: 14, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, });