Files
digital-pilates/app/workout/session/[id].tsx
2025-08-16 14:15:11 +08:00

1116 lines
32 KiB
TypeScript
Raw 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 { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useCallback, 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, WorkoutSession } from '@/services/workoutsApi';
import { workoutsApi } from '@/services/workoutsApi';
import {
clearWorkoutError,
completeWorkoutExercise,
deleteWorkoutSession,
skipWorkoutExercise,
startWorkoutExercise,
startWorkoutSession
} from '@/store/workoutSlice';
import { useFocusEffect } from '@react-navigation/native';
// ==================== 工具函数 ====================
// 计算两个时间之间的耗时(秒)
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<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 WorkoutSessionDetailScreen() {
const router = useRouter();
const dispatch = useAppDispatch();
const params = useLocalSearchParams<{ id: string }>();
const { exerciseLoading, error } = useAppSelector((s) => s.workout);
const [session, setSession] = useState<WorkoutSession | null>(null);
const [exercises, setExercises] = useState<WorkoutExercise[]>([]);
const [loading, setLoading] = useState(true);
// 本地状态
const [completionModal, setCompletionModal] = useState<{
visible: boolean;
exercise: WorkoutExercise | null;
sets: number;
reps: number;
}>({
visible: false,
exercise: null,
sets: 0,
reps: 0,
});
const sessionId = params.id;
// 加载会话详情 - 每次页面展示时都拉取最新数据
useFocusEffect(
useCallback(() => {
if (!sessionId) return;
const loadSessionDetail = async () => {
try {
setLoading(true);
const [sessionDetail, sessionExercises] = await Promise.all([
workoutsApi.getSessionDetail(sessionId),
workoutsApi.getSessionExercises(sessionId)
]);
setSession(sessionDetail);
setExercises(sessionExercises);
} catch (error) {
console.error('加载会话详情失败:', error);
Alert.alert('加载失败', '无法加载训练会话详情');
} finally {
setLoading(false);
}
};
loadSessionDetail();
}, [sessionId])
);
const goalConfig = session?.trainingPlan
? (GOAL_TEXT[session.trainingPlan.goal] || { title: '训练会话', color: palette.primary, description: '开始你的训练之旅' })
: { title: session?.name, color: palette.primary, description: '开始你的训练之旅' };
// 错误处理
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 (!session) return;
Alert.alert(
'开始训练',
'准备好开始训练了吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '开始',
onPress: async () => {
try {
await dispatch(startWorkoutSession({ sessionId: session.id })).unwrap();
// 重新加载会话详情
const updatedSession = await workoutsApi.getSessionDetail(session.id);
setSession(updatedSession);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch (error) {
console.error('开始训练失败:', error);
}
}
}
]
);
};
// 开始动作
const handleStartExercise = async (exercise: WorkoutExercise) => {
if (!session || exercise.status !== 'pending') return;
try {
const updatedExercise = await dispatch(startWorkoutExercise({
sessionId: session.id,
exerciseId: exercise.id
})).unwrap();
// 更新本地exercises列表
setExercises(prev => prev.map(ex => ex.id === exercise.id ? updatedExercise : ex));
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
} catch (error) {
console.error('开始动作失败:', error);
}
};
// 显示完成动作模态框
const handleShowCompleteModal = (exercise: WorkoutExercise) => {
setCompletionModal({
visible: true,
exercise,
sets: exercise.completedSets || exercise.plannedSets || 0,
reps: exercise.completedReps || exercise.plannedReps || 0,
});
};
// 完成动作
const handleCompleteExercise = async () => {
const { exercise, sets, reps } = completionModal;
if (!session || !exercise) return;
try {
const result = await dispatch(completeWorkoutExercise({
sessionId: session.id,
exerciseId: exercise.id,
dto: {
completedSets: sets,
completedReps: reps,
}
})).unwrap();
// 更新本地exercises列表和session
setExercises(prev => prev.map(ex => ex.id === exercise.id ? result.exercise : ex));
setSession(result.updatedSession);
setCompletionModal({ visible: false, exercise: null, sets: 0, reps: 0 });
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch (error) {
console.error('完成动作失败:', error);
}
};
// 跳过动作
const handleSkipExercise = (exercise: WorkoutExercise) => {
if (!session) return;
Alert.alert(
'跳过动作',
`确定要跳过"${exercise.name}"吗?`,
[
{ text: '取消', style: 'cancel' },
{
text: '跳过',
style: 'destructive',
onPress: async () => {
try {
const result = await dispatch(skipWorkoutExercise({
sessionId: session.id,
exerciseId: exercise.id
})).unwrap();
// 更新本地exercises列表和session
setExercises(prev => prev.map(ex => ex.id === exercise.id ? result.exercise : ex));
setSession(result.updatedSession);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch (error) {
console.error('跳过动作失败:', error);
}
}
}
]
);
};
// 删除训练会话
const handleDeleteSession = () => {
if (!session) return;
Alert.alert(
'删除训练会话',
'确定要删除这个训练会话吗?删除后无法恢复。',
[
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: async () => {
try {
await dispatch(deleteWorkoutSession(session.id)).unwrap();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
router.back();
} catch (error) {
console.error('删除会话失败:', error);
}
}
}
]
);
};
// 获取动作状态文本和颜色
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' && session?.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>
);
};
if (loading) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="训练详情" onBack={() => router.back()} />
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>...</Text>
</View>
</SafeAreaView>
);
}
if (!session) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="训练详情" onBack={() => router.back()} />
<View style={styles.emptyContainer}>
<Ionicons name="alert-circle-outline" size={64} color="#9CA3AF" />
<Text style={styles.emptyTitle}></Text>
<Text style={styles.emptyText}></Text>
</View>
</SafeAreaView>
);
}
return (
<View style={styles.safeArea}>
{/* 动态背景 */}
<DynamicBackground color={goalConfig.color} />
<SafeAreaView style={styles.contentWrapper}>
<HeaderBar
title="训练详情"
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
tone="light"
right={
session?.status === 'planned' ? (
<TouchableOpacity
style={[styles.addExerciseBtn, { backgroundColor: palette.primary }]}
onPress={() => router.push(`/training-plan/schedule/select?sessionId=${session.id}`)}
disabled={loading}
>
<Ionicons name="add" size={16} color={palette.ink} />
</TouchableOpacity>
) : null
}
/>
<View style={styles.content}>
{/* 训练计划信息头部 */}
<View style={[styles.planHeader, { backgroundColor: `${goalConfig.color}20` }]}>
{/* 删除按钮 - 右上角 */}
<TouchableOpacity
style={styles.deleteBtn}
onPress={handleDeleteSession}
disabled={loading}
>
<Ionicons name="trash-outline" size={18} color="#EF4444" />
</TouchableOpacity>
<View style={[styles.planColorIndicator, { backgroundColor: goalConfig.color }]} />
<View style={styles.planInfo}>
<ThemedText style={styles.planTitle}>{goalConfig.title}</ThemedText>
<ThemedText style={styles.planDescription}>
{session.name}
</ThemedText>
{/* 进度统计文字 */}
{session.status !== 'planned' && (
<Text style={styles.planProgressStats}>
{workoutStats.completed}/{workoutStats.total}
</Text>
)}
</View>
{/* 右侧区域:圆环进度或开始按钮 */}
{session.status === 'planned' ? (
<TouchableOpacity
style={[styles.planStartBtn, { backgroundColor: goalConfig.color }]}
onPress={handleStartWorkout}
disabled={loading}
>
<Ionicons name="play" size={20} color="#FFFFFF" />
</TouchableOpacity>
) : (
<View style={styles.circularProgressContainer}>
<CircularRing
size={60}
strokeWidth={6}
trackColor={`${goalConfig.color}20`}
progressColor={goalConfig.color}
progress={completionPercentage / 100}
showCenterText={false}
durationMs={800}
/>
<View style={styles.circularProgressText}>
<Text style={[styles.circularProgressPercentage, { color: goalConfig.color }]}>
{completionPercentage}%
</Text>
</View>
</View>
)}
</View>
{/* 训练完成提示 */}
{session.status === 'completed' && (
<View style={styles.completedBanner}>
<Ionicons name="checkmark-circle" size={24} color="#22C55E" />
<Text style={styles.completedBannerText}></Text>
</View>
)}
{/* 动作列表 */}
<FlatList
data={exercises}
keyExtractor={(item) => item.id}
renderItem={renderExerciseItem}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
</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,
padding: 12,
marginBottom: 12,
},
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,
},
// 加载状态
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,
},
});