Files
digital-pilates/app/workout/today.tsx
richarjiang 3312250f2d feat: 添加教练功能和更新用户界面
- 新增教练页面,用户可以与教练进行互动和咨询
- 更新首页,切换到教练 tab 并传递名称参数
- 优化个人信息页面,添加注销帐号和退出登录功能
- 更新隐私政策和用户协议的链接,确保用户在使用前同意相关条款
- 修改今日训练页面标题为“开始训练”,提升用户体验
- 删除不再使用的进度条组件,简化代码结构
2025-08-15 21:38:19 +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,
},
});