Files
digital-pilates/app/workout/today.tsx
richarjiang 849447c5da feat: 引入路由常量并更新相关页面导航
- 新增 ROUTES 常量文件,集中管理应用路由
- 更新多个页面的导航逻辑,使用 ROUTES 常量替代硬编码路径
- 修改教练页面和今日训练页面的路由,提升代码可维护性
- 优化标签页和登录页面的导航,确保一致性和易用性
2025-08-18 10:05:22 +08:00

1238 lines
34 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import { ROUTES } from '@/constants/Routes';
// ==================== 工具函数 ====================
// 计算两个时间之间的耗时(秒)
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<string, { title: string; color: string; description: string }> = {
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 (
<View style={StyleSheet.absoluteFillObject}>
<LinearGradient
colors={['#F9FBF2', '#FFFFFF', '#F5F9F0']}
style={StyleSheet.absoluteFillObject}
/>
<View style={[styles.backgroundOrb, { backgroundColor: `${color}15` }]} />
<View style={[styles.backgroundOrb2, { backgroundColor: `${color}10` }]} />
</View>
);
}
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(ROUTES.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 (
<Animated.View
entering={FadeInUp.delay(index * 50)}
style={[styles.restCard, { borderLeftColor: goalConfig.color }]}
>
<View style={styles.restIconContainer}>
<Ionicons name="time-outline" size={24} color={goalConfig.color} />
</View>
<View style={styles.restContent}>
<Text style={styles.restTitle}>{item.name}</Text>
<Text style={styles.restDuration}>{item.restSec}</Text>
</View>
</Animated.View>
);
}
if (item.itemType === 'note') {
return (
<Animated.View
entering={FadeInUp.delay(index * 50)}
style={[styles.noteCard, { borderLeftColor: goalConfig.color }]}
>
<View style={styles.noteIconContainer}>
<Ionicons name="document-text-outline" size={24} color={goalConfig.color} />
</View>
<View style={styles.noteContent}>
<Text style={styles.noteTitle}>{item.name}</Text>
{item.note && <Text style={styles.noteText}>{item.note}</Text>}
</View>
</Animated.View>
);
}
return (
<Animated.View
entering={FadeInUp.delay(index * 50)}
style={[
styles.exerciseCard,
item.status === 'completed' && { backgroundColor: '#F0FDF4' },
item.status === 'in_progress' && { backgroundColor: '#FFFBEB' },
]}
>
<View style={styles.exerciseHeader}>
<View style={styles.exerciseInfo}>
<Text style={styles.exerciseName}>{item.name}</Text>
{item.exercise && (
<Text style={styles.exerciseCategory}>{item.exercise.categoryName}</Text>
)}
</View>
<View style={[styles.statusBadge, { backgroundColor: statusConfig.backgroundColor }]}>
<Text style={[styles.statusText, { color: statusConfig.color }]}>
{statusConfig.text}
</Text>
</View>
</View>
<View style={styles.exerciseDetails}>
{item.plannedSets && (
<Text style={styles.exerciseParams}>
{item.plannedSets} × {item.plannedReps || '-'}
</Text>
)}
{item.plannedDurationSec && (
<Text style={styles.exerciseParams}>
{item.plannedDurationSec}
</Text>
)}
</View>
{item.status === 'completed' && (
<View style={styles.completedInfo}>
<View style={styles.completedRow}>
<Text style={styles.completedText}>
: {item.completedSets || '-'} × {item.completedReps || '-'}
</Text>
{(() => {
const durationInfo = getExerciseDuration(item);
return durationInfo ? (
<View style={[styles.durationBadge, { backgroundColor: `${goalConfig.color}15` }]}>
<Ionicons name="time-outline" size={12} color={goalConfig.color} />
<Text style={[styles.durationText, { color: goalConfig.color }]}>
{durationInfo.formatted}
</Text>
</View>
) : null;
})()}
</View>
</View>
)}
<View style={styles.exerciseActions}>
{item.status === 'pending' && currentSession?.status === 'in_progress' && (
<TouchableOpacity
style={[styles.actionBtn, styles.startBtn, { backgroundColor: goalConfig.color }]}
onPress={() => handleStartExercise(item)}
disabled={isLoading}
>
<Text style={styles.startBtnText}>
{isLoading ? '开始中...' : '开始'}
</Text>
</TouchableOpacity>
)}
{item.status === 'in_progress' && (
<>
<TouchableOpacity
style={[styles.actionBtn, styles.completeBtn, { backgroundColor: goalConfig.color }]}
onPress={() => handleShowCompleteModal(item)}
disabled={isLoading}
>
<Text style={styles.completeBtnText}>
{isLoading ? '完成中...' : '完成'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionBtn, styles.skipBtn]}
onPress={() => handleSkipExercise(item)}
disabled={isLoading}
>
<Text style={styles.skipBtnText}></Text>
</TouchableOpacity>
</>
)}
{item.status === 'pending' && (
<TouchableOpacity
style={[styles.actionBtn, styles.skipBtn]}
onPress={() => handleSkipExercise(item)}
disabled={isLoading}
>
<Text style={styles.skipBtnText}></Text>
</TouchableOpacity>
)}
</View>
</Animated.View>
);
};
// 渲染训练会话卡片
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 (
<Animated.View
entering={FadeInUp.delay(index * 100)}
style={[styles.sessionCard, { borderLeftColor: goalConfig.color }]}
>
<TouchableOpacity
style={styles.sessionCardContent}
onPress={() => {
router.push(`${ROUTES.WORKOUT_SESSION}/${item.id}`);
}}
activeOpacity={0.9}
>
<View style={styles.sessionHeader}>
<View style={styles.sessionInfo}>
<Text style={styles.sessionName}>{item.name}</Text>
{item.trainingPlan && (
<Text style={styles.sessionPlan}>{item.trainingPlan.name}</Text>
)}
<Text style={styles.sessionDate}>
{dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</Text>
<Text style={styles.sessionDate}>
{dayjs(item.scheduledDate ).format('YYYY-MM-DD HH:mm:ss')}
</Text>
</View>
<View style={styles.sessionStats}>
{item.status === 'completed' ? (
<View style={[styles.statusBadge, { backgroundColor: '#22C55E15' }]}>
<Text style={[styles.statusText, { color: '#22C55E' }]}></Text>
</View>
) : item.status === 'in_progress' ? (
<View style={[styles.statusBadge, { backgroundColor: '#F59E0B15' }]}>
<Text style={[styles.statusText, { color: '#F59E0B' }]}></Text>
</View>
) : (
<View style={[styles.statusBadge, { backgroundColor: '#6B728015' }]}>
<Text style={[styles.statusText, { color: '#6B7280' }]}></Text>
</View>
)}
</View>
</View>
{exerciseCount > 0 && (
<View style={styles.sessionProgress}>
<Text style={styles.progressText}>
{completedCount}/{exerciseCount}
</Text>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{
width: `${exerciseCount > 0 ? (completedCount / exerciseCount) * 100 : 0}%`,
backgroundColor: goalConfig.color
}
]}
/>
</View>
</View>
)}
</TouchableOpacity>
</Animated.View>
);
};
if (loading && sessions.length === 0) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="训练记录" onBack={() => router.back()} />
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>...</Text>
</View>
</SafeAreaView>
);
}
if (sessions.length === 0 && !loading) {
return (
<View style={styles.safeArea}>
<HeaderBar title="训练记录" onBack={() => router.back()} />
<View style={styles.emptyContainer}>
<Ionicons name="fitness-outline" size={64} color="#9CA3AF" />
<Text style={styles.emptyTitle}></Text>
<Text style={styles.emptyText}></Text>
<TouchableOpacity
style={styles.createSessionBtn}
onPress={handleCreateSession}
>
<Text style={styles.createSessionBtnText}></Text>
</TouchableOpacity>
</View>
</View>
);
}
return (
<View style={styles.safeArea}>
{/* 动态背景 */}
<DynamicBackground color={palette.primary} />
<SafeAreaView style={styles.contentWrapper}>
<HeaderBar
title="训练记录"
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
tone="light"
right={
<TouchableOpacity
style={[styles.addSessionBtn, { backgroundColor: palette.primary }]}
onPress={handleCreateSession}
disabled={loading}
>
<Ionicons name="add" size={20} color={palette.ink} />
</TouchableOpacity>
}
/>
<View style={styles.content}>
{/* 会话列表 */}
<FlatList
data={sessions}
keyExtractor={(item) => item.id}
renderItem={renderSessionItem}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshing={refreshing}
onRefresh={handleRefresh}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.1}
ListFooterComponent={
loadingMore ? (
<View style={styles.loadMoreContainer}>
<Text style={styles.loadMoreText}>...</Text>
</View>
) : sessionsPagination && sessionsPagination.page >= sessionsPagination.totalPages ? (
<View style={styles.loadMoreContainer}>
<Text style={styles.loadMoreText}></Text>
</View>
) : null
}
/>
</View>
</SafeAreaView>
{/* 完成动作模态框 */}
<Modal
visible={completionModal.visible}
transparent
animationType="fade"
onRequestClose={() => setCompletionModal({ visible: false, exercise: null, sets: 0, reps: 0 })}
>
<TouchableOpacity
activeOpacity={1}
style={styles.modalOverlay}
onPress={() => setCompletionModal({ visible: false, exercise: null, sets: 0, reps: 0 })}
>
<TouchableOpacity
activeOpacity={1}
style={styles.modalSheet}
onPress={(e) => e.stopPropagation()}
>
<Text style={styles.modalTitle}></Text>
<Text style={styles.modalSubtitle}>{completionModal.exercise?.name}</Text>
<View style={styles.inputRow}>
<View style={styles.inputBox}>
<Text style={styles.inputLabel}></Text>
<View style={styles.counterRow}>
<TouchableOpacity
style={styles.counterBtn}
onPress={() => setCompletionModal(prev => ({
...prev,
sets: Math.max(0, prev.sets - 1)
}))}
>
<Text style={styles.counterBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.counterValue}>{completionModal.sets}</Text>
<TouchableOpacity
style={styles.counterBtn}
onPress={() => setCompletionModal(prev => ({
...prev,
sets: Math.min(20, prev.sets + 1)
}))}
>
<Text style={styles.counterBtnText}>+</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.inputBox}>
<Text style={styles.inputLabel}></Text>
<View style={styles.counterRow}>
<TouchableOpacity
style={styles.counterBtn}
onPress={() => setCompletionModal(prev => ({
...prev,
reps: Math.max(0, prev.reps - 1)
}))}
>
<Text style={styles.counterBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.counterValue}>{completionModal.reps}</Text>
<TouchableOpacity
style={styles.counterBtn}
onPress={() => setCompletionModal(prev => ({
...prev,
reps: Math.min(50, prev.reps + 1)
}))}
>
<Text style={styles.counterBtnText}>+</Text>
</TouchableOpacity>
</View>
</View>
</View>
<TouchableOpacity
style={[styles.confirmBtn, { backgroundColor: goalConfig.color }]}
onPress={handleCompleteExercise}
>
<Text style={styles.confirmBtnText}></Text>
</TouchableOpacity>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</View>
);
}
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',
},
});