- 将应用名称修改为“每日普拉提”,提升品牌识别度 - 新增隐私同意弹窗,确保用户在使用应用前同意隐私政策 - 更新 Redux 状态管理,添加隐私同意状态的处理 - 优化用户信息页面,确保体重和身高的格式化显示 - 更新今日训练页面标题为“快速训练”,提升用户体验 - 添加开发工具函数,便于测试隐私同意功能
1071 lines
30 KiB
TypeScript
1071 lines
30 KiB
TypeScript
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 (
|
||
<View 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>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
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}>
|
||
{/* 训练计划信息头部 */}
|
||
<TouchableOpacity onPress={() => router.push(`/training-plan`)}>
|
||
<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>
|
||
</TouchableOpacity>
|
||
|
||
{/* 训练完成提示 */}
|
||
{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: 'flex-start',
|
||
paddingTop: 40,
|
||
padding: 20,
|
||
},
|
||
emptyTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginTop: 16,
|
||
marginBottom: 8,
|
||
},
|
||
emptyText: {
|
||
fontSize: 14,
|
||
color: '#6B7280',
|
||
textAlign: 'center',
|
||
marginBottom: 24,
|
||
},
|
||
createPlanBtn: {
|
||
backgroundColor: '#22C55E',
|
||
paddingVertical: 12,
|
||
paddingHorizontal: 24,
|
||
borderRadius: 8,
|
||
},
|
||
createPlanBtnText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
},
|
||
|
||
// 加载状态
|
||
loadingContainer: {
|
||
flex: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
loadingText: {
|
||
fontSize: 16,
|
||
color: '#6B7280',
|
||
},
|
||
|
||
// 模态框
|
||
modalOverlay: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||
alignItems: 'center',
|
||
justifyContent: 'flex-end',
|
||
},
|
||
modalSheet: {
|
||
width: '100%',
|
||
backgroundColor: '#FFFFFF',
|
||
borderTopLeftRadius: 16,
|
||
borderTopRightRadius: 16,
|
||
paddingHorizontal: 16,
|
||
paddingTop: 14,
|
||
paddingBottom: 24,
|
||
},
|
||
modalTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
marginBottom: 8,
|
||
color: '#192126',
|
||
textAlign: 'center',
|
||
},
|
||
modalSubtitle: {
|
||
fontSize: 14,
|
||
color: '#6B7280',
|
||
textAlign: 'center',
|
||
marginBottom: 24,
|
||
},
|
||
inputRow: {
|
||
flexDirection: 'row',
|
||
gap: 16,
|
||
marginBottom: 24,
|
||
},
|
||
inputBox: {
|
||
flex: 1,
|
||
},
|
||
inputLabel: {
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
color: '#192126',
|
||
marginBottom: 12,
|
||
},
|
||
counterRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
backgroundColor: '#F3F4F6',
|
||
borderRadius: 8,
|
||
padding: 4,
|
||
},
|
||
counterBtn: {
|
||
backgroundColor: '#FFFFFF',
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 6,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.05,
|
||
shadowRadius: 2,
|
||
shadowOffset: { width: 0, height: 1 },
|
||
elevation: 1,
|
||
},
|
||
counterBtnText: {
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
fontSize: 16,
|
||
},
|
||
counterValue: {
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
fontSize: 16,
|
||
minWidth: 40,
|
||
textAlign: 'center',
|
||
},
|
||
confirmBtn: {
|
||
paddingVertical: 16,
|
||
borderRadius: 12,
|
||
alignItems: 'center',
|
||
},
|
||
confirmBtnText: {
|
||
color: '#FFFFFF',
|
||
fontWeight: '800',
|
||
fontSize: 16,
|
||
},
|
||
|
||
// 添加动作按钮
|
||
addExerciseBtn: {
|
||
width: 28,
|
||
height: 28,
|
||
borderRadius: 14,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 4,
|
||
shadowOffset: { width: 0, height: 2 },
|
||
elevation: 2,
|
||
},
|
||
});
|