Files
digital-pilates/app/workout/today.tsx
richarjiang 97e89b9bf0 feat: 更新隐私同意弹窗和应用名称
- 将应用名称修改为“每日普拉提”,提升品牌识别度
- 新增隐私同意弹窗,确保用户在使用应用前同意隐私政策
- 更新 Redux 状态管理,添加隐私同意状态的处理
- 优化用户信息页面,确保体重和身高的格式化显示
- 更新今日训练页面标题为“快速训练”,提升用户体验
- 添加开发工具函数,便于测试隐私同意功能
2025-08-15 20:44:06 +08:00

1071 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 (
<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,
},
});