From 6b6c4fdbaded3034283ed3ce4c114af1b4ae06b0 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 15 Aug 2025 17:16:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=AE=AD=E7=BB=83?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E5=92=8C=E4=BB=8A=E6=97=A5=E8=AE=AD=E7=BB=83?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在训练计划中添加了新的类型定义,优化了排课功能 - 修改了今日训练页面的布局,提升用户体验 - 删除了不再使用的排课相关文件,简化代码结构 - 更新了 Redux 状态管理,确保数据处理的准确性和稳定性 --- app/index.tsx | 2 +- app/training-plan.tsx | 15 +- app/training-plan/schedule/index.tsx | 737 --------------------------- app/workout/today.tsx | 100 ++-- store/workoutSlice.ts | 12 +- 5 files changed, 71 insertions(+), 795 deletions(-) delete mode 100644 app/training-plan/schedule/index.tsx diff --git a/app/index.tsx b/app/index.tsx index c20c79e..5f7cb80 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -51,7 +51,7 @@ export default function SplashScreen() { width: 80, height: 80, borderRadius: 40, - backgroundColor: primaryColor, + // backgroundColor: primaryColor, justifyContent: 'center', alignItems: 'center', marginBottom: 20, diff --git a/app/training-plan.tsx b/app/training-plan.tsx index 6fffb81..08c9917 100644 --- a/app/training-plan.tsx +++ b/app/training-plan.tsx @@ -35,6 +35,9 @@ import { buildClassicalSession } from '@/utils/classicalSession'; // Tab 类型定义 type TabType = 'list' | 'schedule'; +// ScheduleItemType 类型定义 +type ScheduleItemType = 'exercise' | 'rest' | 'note'; + const GOAL_TEXT: Record = { 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() { 添加动作 - setGenVisible(true)} > 一键排课 - + */} {/* 动作列表 */} @@ -625,7 +628,11 @@ export default function TrainingPlanScreen() { {/* 一键排课配置弹窗 */} {selectedPlan && ( - setGenVisible(false)}> + setGenVisible(false)}> setGenVisible(false)}> e.stopPropagation() as any}> 一键排课配置 diff --git a/app/training-plan/schedule/index.tsx b/app/training-plan/schedule/index.tsx deleted file mode 100644 index c83cdb4..0000000 --- a/app/training-plan/schedule/index.tsx +++ /dev/null @@ -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 = { - 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 ( - - - - - - ); -} - -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([]); - 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 ( - - router.back()} /> - - 找不到指定的训练计划 - - - ); - } - - return ( - - {/* 动态背景 */} - - - - router.back()} - withSafeTop={false} - tone='light' - transparent={true} - right={hasUnsavedChanges ? ( - - 保存 - - ) : undefined} - /> - - - {/* 计划信息头部 */} - - - - {goalConfig.title} - {goalConfig.description} - - - - {/* 操作按钮区域 */} - - - - 添加动作 - - - setGenVisible(true)} - > - - 一键排课 - - - - {/* 动作列表 */} - item.key} - contentContainerStyle={styles.listContent} - showsVerticalScrollIndicator={false} - ListEmptyComponent={ - - - 💪 - - 还没有添加任何动作 - 点击"添加动作"开始排课,或使用"一键排课"快速生成 - - } - renderItem={({ item, index }) => { - const isRest = item.itemType === 'rest'; - const isNote = item.itemType === 'note'; - - if (isRest || isNote) { - return ( - - - - - {isRest ? `间隔休息 ${item.restSec ?? 30}s` : (item.note || '提示')} - - - handleRemoveExercise(item.key)} - hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }} - > - - - - ); - } - - return ( - - - - {item.name} - {item.category} - - 组数 {item.sets} - {item.reps ? ` · 每组 ${item.reps} 次` : ''} - {item.durationSec ? ` · 每组 ${item.durationSec}s` : ''} - - - - - handleToggleCompleted(item.key)} - hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }} - > - - - - handleRemoveExercise(item.key)} - > - 移除 - - - - - ); - }} - /> - - - {/* 一键排课配置弹窗 */} - setGenVisible(false)}> - setGenVisible(false)}> - e.stopPropagation() as any}> - 一键排课配置 - - 强度水平 - - {(['beginner', 'intermediate', 'advanced'] as const).map((lv) => ( - setGenLevel(lv)} - > - - {lv === 'beginner' ? '入门' : lv === 'intermediate' ? '进阶' : '高级'} - - - ))} - - - - 段间休息 - - - - - 插入操作提示 - - - - - 休息秒数 - - - - - 生成训练计划 - - - - - - - ); -} - -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, - }, -}); \ No newline at end of file diff --git a/app/workout/today.tsx b/app/workout/today.tsx index 56fc9ad..066f9f0 100644 --- a/app/workout/today.tsx +++ b/app/workout/today.tsx @@ -403,7 +403,7 @@ export default function TodayWorkoutScreen() { if (!currentSession) { return ( - + router.back()} /> @@ -416,7 +416,7 @@ export default function TodayWorkoutScreen() { 去创建训练计划 - + ); } @@ -447,58 +447,61 @@ export default function TodayWorkoutScreen() { {/* 训练计划信息头部 */} - - {/* 删除按钮 - 右上角 */} - - - + router.push(`/training-plan`)}> + - - - {goalConfig.title} - - {currentSession.trainingPlan?.name || '今日训练'} - - {/* 进度统计文字 */} - {currentSession.status !== 'planned' && ( - - {workoutStats.completed}/{workoutStats.total} 个动作已完成 - - )} - - - {/* 右侧区域:圆环进度或开始按钮 */} - {currentSession.status === 'planned' ? ( + {/* 删除按钮 - 右上角 */} - + - ) : ( - - - - - {completionPercentage}% + + + + {goalConfig.title} + + {currentSession.trainingPlan?.name || '今日训练'} + + {/* 进度统计文字 */} + {currentSession.status !== 'planned' && ( + + {workoutStats.completed}/{workoutStats.total} 个动作已完成 - + )} - )} - + + {/* 右侧区域:圆环进度或开始按钮 */} + {currentSession.status === 'planned' ? ( + + + + ) : ( + + + + + {completionPercentage}% + + + + )} + + {/* 训练完成提示 */} {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: { diff --git a/store/workoutSlice.ts b/store/workoutSlice.ts index 58653cb..e75a107 100644 --- a/store/workoutSlice.ts +++ b/store/workoutSlice.ts @@ -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;