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 { useGlobalDialog } from '@/components/ui/DialogProvider'; 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, createWorkoutSession, deleteWorkoutSession, loadWorkoutSessions, skipWorkoutExercise, startWorkoutExercise, startWorkoutSession } from '@/store/workoutSlice'; import dayjs from 'dayjs'; // ==================== 工具函数 ==================== // 计算两个时间之间的耗时(秒) const calculateDuration = (startTime: string, endTime: string): number => { const start = dayjs(startTime); const end = dayjs(endTime); return Math.floor((end.valueOf() - start.valueOf()) / 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, sessions, sessionsPagination, loading, exerciseLoading, error } = useAppSelector((s) => s.workout); const { showConfirm, showActionSheet } = useGlobalDialog(); const [refreshing, setRefreshing] = useState(false); const [loadingMore, setLoadingMore] = useState(false); // 本地状态 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(loadWorkoutSessions({ page: 1, limit: 10 })); }, [dispatch]); // 刷新数据 const handleRefresh = async () => { setRefreshing(true); try { await dispatch(loadWorkoutSessions({ page: 1, limit: 10 })).unwrap(); } catch (error) { console.error('刷新失败:', error); } finally { setRefreshing(false); } }; // 加载更多数据 const handleLoadMore = async () => { if (loadingMore || loading || !sessionsPagination) return; const currentPage = sessionsPagination.page; const totalPages = sessionsPagination.totalPages; if (currentPage >= totalPages) return; // 已经是最后一页 setLoadingMore(true); try { await dispatch(loadWorkoutSessions({ page: currentPage + 1, limit: 10, append: true // 追加数据而不是替换 })).unwrap(); } catch (error) { console.error('加载更多失败:', error); } finally { setLoadingMore(false); } }; // 错误处理 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; showConfirm( { title: '开始训练', message: '准备好开始今日的训练了吗?', icon: 'fitness-outline', }, () => { 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; showConfirm( { title: '跳过动作', message: `确定要跳过"${exercise.name}"吗?`, icon: 'play-skip-forward-outline', destructive: true, }, () => { dispatch(skipWorkoutExercise({ sessionId: currentSession.id, exerciseId: exercise.id })); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } ); }; // 创建新的训练会话 const handleCreateSession = () => { showActionSheet( { title: '新建训练', subtitle: '选择创建方式开始你的训练', }, [ { id: 'custom', title: '自定义训练', subtitle: '创建一个空的训练,可以自由添加动作', icon: 'create-outline', iconColor: '#3B82F6', onPress: () => { // 创建空的自定义会话 const sessionName = `训练 ${dayjs().format('YYYY年MM月DD日')}`; dispatch(createWorkoutSession({ name: sessionName, scheduledDate: dayjs().format('YYYY-MM-DD') })); } }, { id: 'from-plan', title: '从训练计划导入', subtitle: '基于现有训练计划创建训练', icon: 'library-outline', iconColor: '#10B981', onPress: () => { // 跳转到创建页面选择训练计划 router.push('/workout/create-session'); } } ] ); }; // 删除训练会话 const handleDeleteSession = () => { if (!currentSession) return; showConfirm( { title: '删除训练会话', message: '确定要删除这个训练会话吗?删除后无法恢复。', icon: 'trash-outline', destructive: true, }, () => { dispatch(deleteWorkoutSession(currentSession.id)); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); // 删除成功后清空当前会话 // 不需要返回上一页,因为现在显示的是会话列表 } ); }; // 获取动作状态文本和颜色 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} > 跳过 )} ); }; // 渲染训练会话卡片 const renderSessionItem = ({ item, index }: { item: any; index: number }) => { const goalConfig = item.trainingPlan?.goal ? (GOAL_TEXT[item.trainingPlan.goal] || { title: '训练会话', color: palette.primary, description: '开始你的训练之旅' }) : { title: '训练会话', color: palette.primary, description: '开始你的训练之旅' }; const exerciseCount = item.exercises?.length || 0; const completedCount = item.exercises?.filter((ex: any) => ex.status === 'completed').length || 0; return ( { router.push(`/workout/session/${item.id}`); }} activeOpacity={0.9} > {item.name} {item.trainingPlan && ( {item.trainingPlan.name} )} 创建时间 {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')} 计划时间 {dayjs(item.scheduledDate ).format('YYYY-MM-DD HH:mm:ss')} {item.status === 'completed' ? ( 已完成 ) : item.status === 'in_progress' ? ( 进行中 ) : ( 待开始 )} {exerciseCount > 0 && ( {completedCount}/{exerciseCount} 个动作已完成 0 ? (completedCount / exerciseCount) * 100 : 0}%`, backgroundColor: goalConfig.color } ]} /> )} ); }; if (loading && sessions.length === 0) { return ( router.back()} /> 加载中... ); } if (sessions.length === 0 && !loading) { return ( router.back()} /> 暂无训练记录 开始你的第一次训练吧 新建训练会话 ); } return ( {/* 动态背景 */} router.back()} withSafeTop={false} transparent={true} tone="light" right={ } /> {/* 会话列表 */} item.id} renderItem={renderSessionItem} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} refreshing={refreshing} onRefresh={handleRefresh} onEndReached={handleLoadMore} onEndReachedThreshold={0.1} ListFooterComponent={ loadingMore ? ( 加载更多... ) : sessionsPagination && sessionsPagination.page >= sessionsPagination.totalPages ? ( 已加载全部数据 ) : null } /> {/* 完成动作模态框 */} 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, }, createSessionBtn: { backgroundColor: '#22C55E', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, }, createSessionBtnText: { 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, }, // 添加会话按钮 addSessionBtn: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, // 会话卡片 sessionCard: { backgroundColor: '#FFFFFF', borderRadius: 16, marginBottom: 12, borderLeftWidth: 4, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3, }, sessionCardContent: { padding: 16, }, sessionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12, }, sessionInfo: { flex: 1, }, sessionName: { fontSize: 16, fontWeight: '800', color: '#192126', marginBottom: 4, }, sessionPlan: { fontSize: 12, color: '#888F92', marginBottom: 2, }, sessionDate: { fontSize: 11, color: '#6B7280', }, sessionStats: { alignItems: 'flex-end', }, sessionProgress: { marginTop: 8, }, progressText: { fontSize: 12, color: '#6B7280', marginTop: 6 }, progressBar: { height: 4, backgroundColor: '#F3F4F6', borderRadius: 2, overflow: 'hidden', }, progressFill: { height: '100%', borderRadius: 2, }, // 加载更多 loadMoreContainer: { paddingVertical: 16, alignItems: 'center', }, loadMoreText: { fontSize: 12, color: '#6B7280', }, });