feat: 更新训练计划和今日训练页面
- 在训练计划中添加了新的类型定义,优化了排课功能 - 修改了今日训练页面的布局,提升用户体验 - 删除了不再使用的排课相关文件,简化代码结构 - 更新了 Redux 状态管理,确保数据处理的准确性和稳定性
This commit is contained in:
@@ -51,7 +51,7 @@ export default function SplashScreen() {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
borderRadius: 40,
|
borderRadius: 40,
|
||||||
backgroundColor: primaryColor,
|
// backgroundColor: primaryColor,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ import { buildClassicalSession } from '@/utils/classicalSession';
|
|||||||
// Tab 类型定义
|
// Tab 类型定义
|
||||||
type TabType = 'list' | 'schedule';
|
type TabType = 'list' | 'schedule';
|
||||||
|
|
||||||
|
// ScheduleItemType 类型定义
|
||||||
|
type ScheduleItemType = 'exercise' | 'rest' | 'note';
|
||||||
|
|
||||||
|
|
||||||
const GOAL_TEXT: Record<string, { title: string; color: string; description: string }> = {
|
const GOAL_TEXT: Record<string, { title: string; color: string; description: string }> = {
|
||||||
postpartum_recovery: { title: '产后恢复', color: '#9BE370', description: '温和激活,核心重建' },
|
postpartum_recovery: { title: '产后恢复', color: '#9BE370', description: '温和激活,核心重建' },
|
||||||
@@ -371,7 +374,7 @@ export default function TrainingPlanScreen() {
|
|||||||
durationSec: item.durationSec,
|
durationSec: item.durationSec,
|
||||||
restSec: item.restSec,
|
restSec: item.restSec,
|
||||||
note: item.note,
|
note: item.note,
|
||||||
itemType: item.itemType || 'exercise' as const,
|
itemType: item.itemType || 'exercise',
|
||||||
};
|
};
|
||||||
|
|
||||||
await dispatch(addExercise({ planId: selectedPlanId, dto })).unwrap();
|
await dispatch(addExercise({ planId: selectedPlanId, dto })).unwrap();
|
||||||
@@ -485,13 +488,13 @@ export default function TrainingPlanScreen() {
|
|||||||
<Text style={styles.scheduleActionBtnText}>添加动作</Text>
|
<Text style={styles.scheduleActionBtnText}>添加动作</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
{/* <TouchableOpacity
|
||||||
style={[styles.scheduleSecondaryBtn, { borderColor: goalConfig.color }]}
|
style={[styles.scheduleSecondaryBtn, { borderColor: goalConfig.color }]}
|
||||||
onPress={() => setGenVisible(true)}
|
onPress={() => setGenVisible(true)}
|
||||||
>
|
>
|
||||||
<Ionicons name="flash" size={16} color={goalConfig.color} style={{ marginRight: 4 }} />
|
<Ionicons name="flash" size={16} color={goalConfig.color} style={{ marginRight: 4 }} />
|
||||||
<Text style={[styles.scheduleSecondaryBtnText, { color: goalConfig.color }]}>一键排课</Text>
|
<Text style={[styles.scheduleSecondaryBtnText, { color: goalConfig.color }]}>一键排课</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity> */}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 动作列表 */}
|
{/* 动作列表 */}
|
||||||
@@ -625,7 +628,11 @@ export default function TrainingPlanScreen() {
|
|||||||
|
|
||||||
{/* 一键排课配置弹窗 */}
|
{/* 一键排课配置弹窗 */}
|
||||||
{selectedPlan && (
|
{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.modalOverlay} onPress={() => setGenVisible(false)}>
|
||||||
<TouchableOpacity activeOpacity={1} style={styles.modalSheet} onPress={(e) => e.stopPropagation() as any}>
|
<TouchableOpacity activeOpacity={1} style={styles.modalSheet} onPress={(e) => e.stopPropagation() as any}>
|
||||||
<Text style={styles.modalTitle}>一键排课配置</Text>
|
<Text style={styles.modalTitle}>一键排课配置</Text>
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -403,7 +403,7 @@ export default function TodayWorkoutScreen() {
|
|||||||
|
|
||||||
if (!currentSession) {
|
if (!currentSession) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<View style={styles.safeArea}>
|
||||||
<HeaderBar title="今日训练" onBack={() => router.back()} />
|
<HeaderBar title="今日训练" onBack={() => router.back()} />
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<Ionicons name="calendar-outline" size={64} color="#9CA3AF" />
|
<Ionicons name="calendar-outline" size={64} color="#9CA3AF" />
|
||||||
@@ -416,7 +416,7 @@ export default function TodayWorkoutScreen() {
|
|||||||
<Text style={styles.createPlanBtnText}>去创建训练计划</Text>
|
<Text style={styles.createPlanBtnText}>去创建训练计划</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,58 +447,61 @@ export default function TodayWorkoutScreen() {
|
|||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* 训练计划信息头部 */}
|
{/* 训练计划信息头部 */}
|
||||||
<View style={[styles.planHeader, { backgroundColor: `${goalConfig.color}20` }]}>
|
<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
|
<TouchableOpacity
|
||||||
style={[styles.planStartBtn, { backgroundColor: goalConfig.color }]}
|
style={styles.deleteBtn}
|
||||||
onPress={handleStartWorkout}
|
onPress={handleDeleteSession}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Ionicons name="play" size={20} color="#FFFFFF" />
|
<Ionicons name="trash-outline" size={18} color="#EF4444" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
|
||||||
<View style={styles.circularProgressContainer}>
|
<View style={[styles.planColorIndicator, { backgroundColor: goalConfig.color }]} />
|
||||||
<CircularRing
|
<View style={styles.planInfo}>
|
||||||
size={60}
|
<ThemedText style={styles.planTitle}>{goalConfig.title}</ThemedText>
|
||||||
strokeWidth={6}
|
<ThemedText style={styles.planDescription}>
|
||||||
trackColor={`${goalConfig.color}20`}
|
{currentSession.trainingPlan?.name || '今日训练'}
|
||||||
progressColor={goalConfig.color}
|
</ThemedText>
|
||||||
progress={completionPercentage / 100}
|
{/* 进度统计文字 */}
|
||||||
showCenterText={false}
|
{currentSession.status !== 'planned' && (
|
||||||
durationMs={800}
|
<Text style={styles.planProgressStats}>
|
||||||
/>
|
{workoutStats.completed}/{workoutStats.total} 个动作已完成
|
||||||
<View style={styles.circularProgressText}>
|
|
||||||
<Text style={[styles.circularProgressPercentage, { color: goalConfig.color }]}>
|
|
||||||
{completionPercentage}%
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
)}
|
||||||
</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' && (
|
{currentSession.status === 'completed' && (
|
||||||
@@ -924,7 +927,8 @@ const styles = StyleSheet.create({
|
|||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'flex-start',
|
||||||
|
paddingTop: 40,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
emptyTitle: {
|
emptyTitle: {
|
||||||
|
|||||||
@@ -227,8 +227,10 @@ const workoutSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(loadTodayWorkout.fulfilled, (state, action) => {
|
.addCase(loadTodayWorkout.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.currentSession = action.payload;
|
if (action.payload) {
|
||||||
state.exercises = action.payload.exercises || [];
|
state.currentSession = action.payload;
|
||||||
|
state.exercises = action.payload.exercises || [];
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(loadTodayWorkout.rejected, (state, action) => {
|
.addCase(loadTodayWorkout.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
@@ -372,14 +374,14 @@ const workoutSlice = createSlice({
|
|||||||
.addCase(deleteWorkoutSession.fulfilled, (state, action) => {
|
.addCase(deleteWorkoutSession.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
const deletedSessionId = action.payload;
|
const deletedSessionId = action.payload;
|
||||||
|
|
||||||
// 如果删除的是当前会话,清空当前会话数据
|
// 如果删除的是当前会话,清空当前会话数据
|
||||||
if (state.currentSession?.id === deletedSessionId) {
|
if (state.currentSession?.id === deletedSessionId) {
|
||||||
state.currentSession = null;
|
state.currentSession = null;
|
||||||
state.exercises = [];
|
state.exercises = [];
|
||||||
state.stats = null;
|
state.stats = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从会话列表中移除已删除的会话
|
// 从会话列表中移除已删除的会话
|
||||||
state.sessions = state.sessions.filter(session => session.id !== deletedSessionId);
|
state.sessions = state.sessions.filter(session => session.id !== deletedSessionId);
|
||||||
})
|
})
|
||||||
@@ -391,5 +393,5 @@ const workoutSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const { clearWorkoutError, clearCurrentWorkout, updateExerciseLocally } = workoutSlice.actions;
|
export const { clearWorkoutError, clearCurrentWorkout, updateExerciseLocally } = workoutSlice.actions;
|
||||||
export { addWorkoutExercise, deleteWorkoutSession };
|
|
||||||
export default workoutSlice.reducer;
|
export default workoutSlice.reducer;
|
||||||
|
|||||||
Reference in New Issue
Block a user