Files
digital-pilates/app/workout/today.tsx
richarjiang dacbee197c feat: 更新训练计划和打卡功能
- 在训练计划中新增训练项目的添加、更新和删除功能,支持用户灵活管理训练内容
- 优化训练计划排课界面,提升用户体验
- 更新打卡功能,支持按日期加载和展示打卡记录
- 删除不再使用的打卡相关页面,简化代码结构
- 新增今日训练页面,集成今日训练计划和动作展示
- 更新样式以适应新功能的展示和交互
2025-08-15 17:01:33 +08:00

1067 lines
30 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 { 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<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, 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 (
<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>
);
};
if (loading && !currentSession) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="今日训练" onBack={() => router.back()} />
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>...</Text>
</View>
</SafeAreaView>
);
}
if (!currentSession) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="今日训练" onBack={() => router.back()} />
<View style={styles.emptyContainer}>
<Ionicons name="calendar-outline" size={64} color="#9CA3AF" />
<Text style={styles.emptyTitle}></Text>
<Text style={styles.emptyText}></Text>
<TouchableOpacity
style={styles.createPlanBtn}
onPress={() => router.push('/training-plan' as any)}
>
<Text style={styles.createPlanBtnText}></Text>
</TouchableOpacity>
</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={
currentSession?.status === 'in_progress' ? (
<TouchableOpacity
style={[styles.addExerciseBtn, { backgroundColor: palette.primary }]}
onPress={() => router.push(`/training-plan/schedule/select?sessionId=${currentSession.id}` as any)}
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}>
{currentSession.trainingPlan?.name || '今日训练'}
</ThemedText>
{/* 进度统计文字 */}
{currentSession.status !== 'planned' && (
<Text style={styles.planProgressStats}>
{workoutStats.completed}/{workoutStats.total}
</Text>
)}
</View>
{/* 右侧区域:圆环进度或开始按钮 */}
{currentSession.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>
{/* 训练完成提示 */}
{currentSession.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,
},
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: 'center',
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,
},
});