feat: 更新训练计划和今日训练页面
- 在训练计划中添加了新的类型定义,优化了排课功能 - 修改了今日训练页面的布局,提升用户体验 - 删除了不再使用的排课相关文件,简化代码结构 - 更新了 Redux 状态管理,确保数据处理的准确性和稳定性
This commit is contained in:
@@ -51,7 +51,7 @@ export default function SplashScreen() {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: primaryColor,
|
||||
// backgroundColor: primaryColor,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
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: {
|
||||
|
||||
Reference in New Issue
Block a user