feat: 更新训练计划和今日训练页面

- 在训练计划中添加了新的类型定义,优化了排课功能
- 修改了今日训练页面的布局,提升用户体验
- 删除了不再使用的排课相关文件,简化代码结构
- 更新了 Redux 状态管理,确保数据处理的准确性和稳定性
This commit is contained in:
richarjiang
2025-08-15 17:16:39 +08:00
parent dacbee197c
commit 6b6c4fdbad
5 changed files with 71 additions and 795 deletions

View File

@@ -51,7 +51,7 @@ export default function SplashScreen() {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: primaryColor,
// backgroundColor: primaryColor,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,

View File

@@ -35,6 +35,9 @@ import { buildClassicalSession } from '@/utils/classicalSession';
// Tab 类型定义
type TabType = 'list' | 'schedule';
// ScheduleItemType 类型定义
type ScheduleItemType = 'exercise' | 'rest' | 'note';
const GOAL_TEXT: Record<string, { title: string; color: string; description: string }> = {
postpartum_recovery: { title: '产后恢复', color: '#9BE370', description: '温和激活,核心重建' },
@@ -371,7 +374,7 @@ export default function TrainingPlanScreen() {
durationSec: item.durationSec,
restSec: item.restSec,
note: item.note,
itemType: item.itemType || 'exercise' as const,
itemType: item.itemType || 'exercise',
};
await dispatch(addExercise({ planId: selectedPlanId, dto })).unwrap();
@@ -485,13 +488,13 @@ export default function TrainingPlanScreen() {
<Text style={styles.scheduleActionBtnText}></Text>
</TouchableOpacity>
<TouchableOpacity
{/* <TouchableOpacity
style={[styles.scheduleSecondaryBtn, { borderColor: goalConfig.color }]}
onPress={() => setGenVisible(true)}
>
<Ionicons name="flash" size={16} color={goalConfig.color} style={{ marginRight: 4 }} />
<Text style={[styles.scheduleSecondaryBtnText, { color: goalConfig.color }]}>一键排课</Text>
</TouchableOpacity>
</TouchableOpacity> */}
</View>
{/* 动作列表 */}
@@ -625,7 +628,11 @@ export default function TrainingPlanScreen() {
{/* 一键排课配置弹窗 */}
{selectedPlan && (
<Modal visible={genVisible} transparent animationType="fade" onRequestClose={() => setGenVisible(false)}>
<Modal
visible={genVisible}
transparent
animationType="fade"
onRequestClose={() => setGenVisible(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setGenVisible(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalSheet} onPress={(e) => e.stopPropagation() as any}>
<Text style={styles.modalTitle}></Text>

View File

@@ -1,737 +0,0 @@
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import { Alert, FlatList, Modal, SafeAreaView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ThemedText } from '@/components/ThemedText';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { palette } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { buildClassicalSession } from '@/utils/classicalSession';
// 训练计划排课项目类型
export interface ScheduleExercise {
key: string;
name: string;
category: string;
sets: number;
reps?: number;
durationSec?: number;
restSec?: number;
note?: string;
itemType?: 'exercise' | 'rest' | 'note';
completed?: boolean;
}
// 训练计划排课数据
export interface PlanSchedule {
planId: string;
exercises: ScheduleExercise[];
note?: string;
lastModified: string;
}
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 PlanScheduleScreen() {
const router = useRouter();
const dispatch = useAppDispatch();
const params = useLocalSearchParams<{ planId?: string; newExercise?: string }>();
const { plans } = useAppSelector((s) => s.trainingPlan);
const planId = params.planId;
const plan = useMemo(() => plans.find(p => p.id === planId), [plans, planId]);
// 排课数据状态
const [exercises, setExercises] = useState<ScheduleExercise[]>([]);
const [scheduleNote, setScheduleNote] = useState('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// 一键排课配置
const [genVisible, setGenVisible] = useState(false);
const [genLevel, setGenLevel] = useState<'beginner' | 'intermediate' | 'advanced'>('beginner');
const [genWithRests, setGenWithRests] = useState(true);
const [genWithNotes, setGenWithNotes] = useState(true);
const [genRest, setGenRest] = useState('30');
const goalConfig = plan ? (GOAL_TEXT[plan.goal] || { title: '训练计划', color: palette.primary, description: '开始你的训练之旅' }) : null;
useEffect(() => {
if (!plan) {
Alert.alert('错误', '找不到指定的训练计划', [
{ text: '确定', onPress: () => router.back() }
]);
return;
}
// TODO: 从存储中加载已有的排课数据
// loadPlanSchedule(planId);
}, [plan, planId]);
// 处理从选择页面传回的新动作
useEffect(() => {
if (params.newExercise) {
try {
const newExercise: ScheduleExercise = JSON.parse(params.newExercise);
setExercises(prev => [...prev, newExercise]);
setHasUnsavedChanges(true);
// 清除路由参数,避免重复添加
router.setParams({ newExercise: undefined } as any);
} catch (error) {
console.error('解析新动作数据失败:', error);
}
}
}, [params.newExercise]);
const handleSave = async () => {
if (!plan) return;
try {
// TODO: 保存排课数据到存储
const scheduleData: PlanSchedule = {
planId: plan.id,
exercises,
note: scheduleNote,
lastModified: new Date().toISOString(),
};
console.log('保存排课数据:', scheduleData);
setHasUnsavedChanges(false);
Alert.alert('保存成功', '训练计划排课已保存');
} catch (error) {
console.error('保存排课失败:', error);
Alert.alert('保存失败', '请稍后重试');
}
};
const handleAddExercise = () => {
router.push(`/training-plan/schedule/select?planId=${planId}` as any);
};
const handleRemoveExercise = (key: string) => {
Alert.alert('确认移除', '确定要移除该动作吗?', [
{ text: '取消', style: 'cancel' },
{
text: '移除',
style: 'destructive',
onPress: () => {
setExercises(prev => prev.filter(ex => ex.key !== key));
setHasUnsavedChanges(true);
},
},
]);
};
const handleToggleCompleted = (key: string) => {
setExercises(prev => prev.map(ex =>
ex.key === key ? { ...ex, completed: !ex.completed } : ex
));
setHasUnsavedChanges(true);
};
const onGenerate = () => {
const restSec = Math.max(10, Math.min(120, parseInt(genRest || '30', 10)));
const { items, note } = buildClassicalSession({
withSectionRests: genWithRests,
restSeconds: restSec,
withNotes: genWithNotes,
level: genLevel
});
// 转换为排课格式
const scheduleItems: ScheduleExercise[] = items.map((item, index) => ({
key: `generated_${Date.now()}_${index}`,
name: item.name,
category: item.category,
sets: item.sets,
reps: item.reps,
durationSec: item.durationSec,
restSec: item.restSec,
note: item.note,
itemType: item.itemType,
completed: false,
}));
setExercises(scheduleItems);
setScheduleNote(note || '');
setHasUnsavedChanges(true);
setGenVisible(false);
Alert.alert('排课已生成', '已为你生成经典普拉提序列,可继续调整。');
};
if (!plan || !goalConfig) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="训练排课" onBack={() => router.back()} />
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}></ThemedText>
</View>
</SafeAreaView>
);
}
return (
<View style={styles.safeArea}>
{/* 动态背景 */}
<DynamicBackground color={goalConfig.color} />
<SafeAreaView style={styles.contentWrapper}>
<HeaderBar
title="训练排课"
onBack={() => router.back()}
withSafeTop={false}
tone='light'
transparent={true}
right={hasUnsavedChanges ? (
<TouchableOpacity onPress={handleSave} style={styles.saveBtn}>
<ThemedText style={styles.saveBtnText}></ThemedText>
</TouchableOpacity>
) : undefined}
/>
<View style={styles.content}>
{/* 计划信息头部 */}
<Animated.View entering={FadeInUp.duration(600)} style={[styles.planHeader, { backgroundColor: `${goalConfig.color}20` }]}>
<View style={[styles.planColorIndicator, { backgroundColor: goalConfig.color }]} />
<View style={styles.planInfo}>
<ThemedText style={styles.planTitle}>{goalConfig.title}</ThemedText>
<ThemedText style={styles.planDescription}>{goalConfig.description}</ThemedText>
</View>
</Animated.View>
{/* 操作按钮区域 */}
<View style={styles.actionRow}>
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: goalConfig.color }]}
onPress={handleAddExercise}
>
<Ionicons name="add" size={16} color="#FFFFFF" style={{ marginRight: 4 }} />
<Text style={styles.primaryBtnText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.secondaryBtn, { borderColor: goalConfig.color }]}
onPress={() => setGenVisible(true)}
>
<Ionicons name="flash" size={16} color={goalConfig.color} style={{ marginRight: 4 }} />
<Text style={[styles.secondaryBtnText, { color: goalConfig.color }]}></Text>
</TouchableOpacity>
</View>
{/* 动作列表 */}
<FlatList
data={exercises}
keyExtractor={(item) => item.key}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
<Animated.View entering={FadeInUp.delay(200).duration(600)} style={styles.emptyContainer}>
<View style={[styles.emptyIcon, { backgroundColor: `${goalConfig.color}20` }]}>
<ThemedText style={styles.emptyIconText}>💪</ThemedText>
</View>
<ThemedText style={styles.emptyText}></ThemedText>
<ThemedText style={styles.emptySubtext}>"添加动作"使"一键排课"</ThemedText>
</Animated.View>
}
renderItem={({ item, index }) => {
const isRest = item.itemType === 'rest';
const isNote = item.itemType === 'note';
if (isRest || isNote) {
return (
<Animated.View
entering={FadeInUp.delay(index * 50).duration(400)}
style={styles.inlineRow}
>
<Ionicons
name={isRest ? 'time-outline' : 'information-circle-outline'}
size={14}
color="#888F92"
/>
<View style={[
styles.inlineBadge,
isRest ? styles.inlineBadgeRest : styles.inlineBadgeNote
]}>
<Text style={[
isNote ? styles.inlineTextItalic : styles.inlineText,
{ color: '#888F92' }
]}>
{isRest ? `间隔休息 ${item.restSec ?? 30}s` : (item.note || '提示')}
</Text>
</View>
<TouchableOpacity
style={styles.inlineRemoveBtn}
onPress={() => handleRemoveExercise(item.key)}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
>
<Ionicons name="close-outline" size={16} color="#888F92" />
</TouchableOpacity>
</Animated.View>
);
}
return (
<Animated.View
entering={FadeInUp.delay(index * 50).duration(400)}
style={styles.exerciseCard}
>
<View style={styles.exerciseContent}>
<View style={styles.exerciseInfo}>
<ThemedText style={styles.exerciseName}>{item.name}</ThemedText>
<ThemedText style={styles.exerciseCategory}>{item.category}</ThemedText>
<ThemedText style={styles.exerciseMeta}>
{item.sets}
{item.reps ? ` · 每组 ${item.reps}` : ''}
{item.durationSec ? ` · 每组 ${item.durationSec}s` : ''}
</ThemedText>
</View>
<View style={styles.exerciseActions}>
<TouchableOpacity
style={styles.completeBtn}
onPress={() => handleToggleCompleted(item.key)}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
>
<Ionicons
name={item.completed ? 'checkmark-circle' : 'checkmark-circle-outline'}
size={24}
color={item.completed ? goalConfig.color : '#888F92'}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.removeBtn}
onPress={() => handleRemoveExercise(item.key)}
>
<Text style={styles.removeBtnText}></Text>
</TouchableOpacity>
</View>
</View>
</Animated.View>
);
}}
/>
</View>
{/* 一键排课配置弹窗 */}
<Modal visible={genVisible} transparent animationType="fade" onRequestClose={() => setGenVisible(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setGenVisible(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalSheet} onPress={(e) => e.stopPropagation() as any}>
<Text style={styles.modalTitle}></Text>
<Text style={styles.modalLabel}></Text>
<View style={styles.segmentedRow}>
{(['beginner', 'intermediate', 'advanced'] as const).map((lv) => (
<TouchableOpacity
key={lv}
style={[
styles.segment,
genLevel === lv && { backgroundColor: goalConfig.color }
]}
onPress={() => setGenLevel(lv)}
>
<Text style={[
styles.segmentText,
genLevel === lv && { color: '#FFFFFF' }
]}>
{lv === 'beginner' ? '入门' : lv === 'intermediate' ? '进阶' : '高级'}
</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}></Text>
<Switch value={genWithRests} onValueChange={setGenWithRests} />
</View>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}></Text>
<Switch value={genWithNotes} onValueChange={setGenWithNotes} />
</View>
<View style={styles.inputRow}>
<Text style={styles.inputLabel}></Text>
<TextInput
value={genRest}
onChangeText={setGenRest}
keyboardType="number-pad"
style={styles.input}
/>
</View>
<TouchableOpacity
style={[styles.generateBtn, { backgroundColor: goalConfig.color }]}
onPress={onGenerate}
>
<Text style={styles.generateBtnText}></Text>
</TouchableOpacity>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</SafeAreaView>
</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,
},
// 操作按钮
actionRow: {
flexDirection: 'row',
gap: 12,
marginBottom: 20,
},
primaryBtn: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
primaryBtnText: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '700',
},
secondaryBtn: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
borderRadius: 12,
borderWidth: 1.5,
backgroundColor: '#FFFFFF',
},
secondaryBtnText: {
fontSize: 14,
fontWeight: '700',
},
// 保存按钮
saveBtn: {
backgroundColor: palette.primary,
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
shadowColor: palette.primary,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 4,
},
saveBtnText: {
color: palette.ink,
fontWeight: '800',
fontSize: 14,
},
// 列表
listContent: {
paddingBottom: 40,
},
// 空状态
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyIcon: {
width: 80,
height: 80,
borderRadius: 40,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
emptyIconText: {
fontSize: 32,
},
emptyText: {
fontSize: 18,
color: '#192126',
fontWeight: '600',
marginBottom: 4,
},
emptySubtext: {
fontSize: 14,
color: '#5E6468',
textAlign: 'center',
lineHeight: 20,
},
// 动作卡片
exerciseCard: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginBottom: 12,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 12,
shadowOffset: { width: 0, height: 6 },
elevation: 3,
},
exerciseContent: {
flexDirection: 'row',
alignItems: 'center',
},
exerciseInfo: {
flex: 1,
},
exerciseName: {
fontSize: 16,
fontWeight: '800',
color: '#192126',
marginBottom: 4,
},
exerciseCategory: {
fontSize: 12,
color: '#888F92',
marginBottom: 4,
},
exerciseMeta: {
fontSize: 12,
color: '#5E6468',
},
exerciseActions: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
completeBtn: {
padding: 4,
},
removeBtn: {
backgroundColor: '#F3F4F6',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 8,
},
removeBtnText: {
color: '#384046',
fontWeight: '700',
fontSize: 12,
},
// 内联项目(休息、提示)
inlineRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
inlineBadge: {
marginLeft: 6,
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 999,
paddingVertical: 6,
paddingHorizontal: 10,
flex: 1,
},
inlineBadgeRest: {
backgroundColor: '#F8FAFC',
},
inlineBadgeNote: {
backgroundColor: '#F9FAFB',
},
inlineText: {
fontSize: 12,
fontWeight: '700',
},
inlineTextItalic: {
fontSize: 12,
fontStyle: 'italic',
},
inlineRemoveBtn: {
marginLeft: 6,
padding: 4,
borderRadius: 999,
},
// 错误状态
errorContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
errorText: {
fontSize: 16,
color: '#ED4747',
fontWeight: '600',
},
// 弹窗样式
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: 16,
fontWeight: '800',
marginBottom: 16,
color: '#192126',
},
modalLabel: {
fontSize: 12,
color: '#888F92',
marginBottom: 8,
fontWeight: '600',
},
segmentedRow: {
flexDirection: 'row',
gap: 8,
marginBottom: 16,
},
segment: {
flex: 1,
borderRadius: 999,
borderWidth: 1,
borderColor: '#E5E7EB',
paddingVertical: 8,
alignItems: 'center',
},
segmentText: {
fontWeight: '700',
color: '#384046',
},
switchRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 12,
},
switchLabel: {
fontWeight: '700',
color: '#384046',
},
inputRow: {
marginBottom: 20,
},
inputLabel: {
fontSize: 12,
color: '#888F92',
marginBottom: 8,
fontWeight: '600',
},
input: {
height: 40,
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 10,
paddingHorizontal: 12,
color: '#384046',
},
generateBtn: {
paddingVertical: 12,
borderRadius: 12,
alignItems: 'center',
},
generateBtnText: {
color: '#FFFFFF',
fontWeight: '800',
fontSize: 14,
},
});

View File

@@ -403,7 +403,7 @@ export default function TodayWorkoutScreen() {
if (!currentSession) {
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.safeArea}>
<HeaderBar title="今日训练" onBack={() => router.back()} />
<View style={styles.emptyContainer}>
<Ionicons name="calendar-outline" size={64} color="#9CA3AF" />
@@ -416,7 +416,7 @@ export default function TodayWorkoutScreen() {
<Text style={styles.createPlanBtnText}></Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
);
}
@@ -447,58 +447,61 @@ export default function TodayWorkoutScreen() {
<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>
<TouchableOpacity onPress={() => router.push(`/training-plan`)}>
<View style={[styles.planHeader, { backgroundColor: `${goalConfig.color}20` }]}>
<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}
style={styles.deleteBtn}
onPress={handleDeleteSession}
disabled={loading}
>
<Ionicons name="play" size={20} color="#FFFFFF" />
<Ionicons name="trash-outline" size={18} color="#EF4444" />
</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}%
<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>
)}
</View>
)}
</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' && (
@@ -924,7 +927,8 @@ const styles = StyleSheet.create({
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'flex-start',
paddingTop: 40,
padding: 20,
},
emptyTitle: {

View File

@@ -227,8 +227,10 @@ const workoutSlice = createSlice({
})
.addCase(loadTodayWorkout.fulfilled, (state, action) => {
state.loading = false;
state.currentSession = action.payload;
state.exercises = action.payload.exercises || [];
if (action.payload) {
state.currentSession = action.payload;
state.exercises = action.payload.exercises || [];
}
})
.addCase(loadTodayWorkout.rejected, (state, action) => {
state.loading = false;
@@ -372,14 +374,14 @@ const workoutSlice = createSlice({
.addCase(deleteWorkoutSession.fulfilled, (state, action) => {
state.loading = false;
const deletedSessionId = action.payload;
// 如果删除的是当前会话,清空当前会话数据
if (state.currentSession?.id === deletedSessionId) {
state.currentSession = null;
state.exercises = [];
state.stats = null;
}
// 从会话列表中移除已删除的会话
state.sessions = state.sessions.filter(session => session.id !== deletedSessionId);
})
@@ -391,5 +393,5 @@ const workoutSlice = createSlice({
});
export const { clearWorkoutError, clearCurrentWorkout, updateExerciseLocally } = workoutSlice.actions;
export { addWorkoutExercise, deleteWorkoutSession };
export default workoutSlice.reducer;