517 lines
15 KiB
TypeScript
517 lines
15 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons';
|
||
import dayjs from 'dayjs';
|
||
import * as Haptics from 'expo-haptics';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import { useRouter } from 'expo-router';
|
||
import React, { useEffect, useState } from 'react';
|
||
import { Alert, FlatList, SafeAreaView, StyleSheet, 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 { loadPlans } from '@/store/trainingPlanSlice';
|
||
import { createWorkoutSession } from '@/store/workoutSlice';
|
||
|
||
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 CreateWorkoutSessionScreen() {
|
||
const router = useRouter();
|
||
const dispatch = useAppDispatch();
|
||
const { plans, loading: plansLoading } = useAppSelector((s) => s.trainingPlan);
|
||
|
||
const [sessionName, setSessionName] = useState('');
|
||
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
||
const [creating, setCreating] = useState(false);
|
||
|
||
useEffect(() => {
|
||
dispatch(loadPlans());
|
||
}, [dispatch]);
|
||
|
||
// 自动生成会话名称
|
||
useEffect(() => {
|
||
if (!sessionName) {
|
||
const today = new Date();
|
||
const dateStr = `${today.getMonth() + 1}月${today.getDate()}日`;
|
||
setSessionName(`${dateStr}训练`);
|
||
}
|
||
}, [sessionName]);
|
||
|
||
const selectedPlan = plans.find(p => p.id === selectedPlanId);
|
||
const goalConfig = selectedPlan?.goal
|
||
? (GOAL_TEXT[selectedPlan.goal] || { title: '训练', color: palette.primary, description: '开始你的训练之旅' })
|
||
: { title: '新建训练', color: palette.primary, description: '选择创建方式' };
|
||
|
||
// 创建自定义会话
|
||
const handleCreateCustomSession = async () => {
|
||
if (creating || !sessionName.trim()) return;
|
||
|
||
setCreating(true);
|
||
try {
|
||
await dispatch(createWorkoutSession({
|
||
name: sessionName.trim(),
|
||
scheduledDate: dayjs().format('YYYY-MM-DD')
|
||
})).unwrap();
|
||
|
||
// 创建成功后跳转到选择动作页面
|
||
router.replace('/training-plan/schedule/select' as any);
|
||
} catch (error) {
|
||
console.error('创建训练会话失败:', error);
|
||
Alert.alert('创建失败', '创建训练会话时出现错误,请稍后重试');
|
||
} finally {
|
||
setCreating(false);
|
||
}
|
||
};
|
||
|
||
// 从训练计划创建会话
|
||
const handleCreateFromPlan = async () => {
|
||
if (creating || !selectedPlan || !sessionName.trim()) return;
|
||
|
||
setCreating(true);
|
||
try {
|
||
await dispatch(createWorkoutSession({
|
||
name: sessionName.trim(),
|
||
trainingPlanId: selectedPlan.id,
|
||
scheduledDate: dayjs().format('YYYY-MM-DD')
|
||
})).unwrap();
|
||
|
||
// 创建成功后返回到训练记录页面
|
||
router.back();
|
||
} catch (error) {
|
||
console.error('创建训练会话失败:', error);
|
||
Alert.alert('创建失败', '创建训练会话时出现错误,请稍后重试');
|
||
} finally {
|
||
setCreating(false);
|
||
}
|
||
};
|
||
|
||
// 渲染训练计划卡片
|
||
const renderPlanItem = ({ item, index }: { item: any; index: number }) => {
|
||
const isSelected = item.id === selectedPlanId;
|
||
const planGoalConfig = GOAL_TEXT[item.goal] || { title: '训练计划', color: palette.primary, description: '开始你的训练之旅' };
|
||
|
||
return (
|
||
<Animated.View
|
||
entering={FadeInUp.delay(index * 100)}
|
||
style={[
|
||
styles.planCard,
|
||
{ borderLeftColor: planGoalConfig.color },
|
||
isSelected && { borderWidth: 2, borderColor: planGoalConfig.color }
|
||
]}
|
||
>
|
||
<TouchableOpacity
|
||
style={styles.planCardContent}
|
||
onPress={() => {
|
||
setSelectedPlanId(isSelected ? null : item.id);
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||
}}
|
||
activeOpacity={0.9}
|
||
>
|
||
<View style={styles.planHeader}>
|
||
<View style={styles.planInfo}>
|
||
<Text style={styles.planName}>{item.name}</Text>
|
||
<Text style={[styles.planGoal, { color: planGoalConfig.color }]}>
|
||
{planGoalConfig.title}
|
||
</Text>
|
||
<Text style={styles.methodDescription}>
|
||
{planGoalConfig.description}
|
||
</Text>
|
||
</View>
|
||
|
||
<View style={styles.planStatus}>
|
||
{isSelected ? (
|
||
<Ionicons name="checkmark-circle" size={24} color={planGoalConfig.color} />
|
||
) : (
|
||
<View style={[styles.radioButton, { borderColor: planGoalConfig.color }]} />
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
{item.exercises && item.exercises.length > 0 && (
|
||
<View style={styles.planStats}>
|
||
<Text style={styles.statsText}>
|
||
{item.exercises.length} 个动作
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</TouchableOpacity>
|
||
</Animated.View>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<View style={styles.safeArea}>
|
||
{/* 动态背景 */}
|
||
<DynamicBackground color={goalConfig.color} />
|
||
|
||
<SafeAreaView style={styles.contentWrapper}>
|
||
<HeaderBar
|
||
title="新建训练"
|
||
onBack={() => router.back()}
|
||
withSafeTop={false}
|
||
transparent={true}
|
||
tone="light"
|
||
/>
|
||
|
||
<View style={styles.content}>
|
||
{/* 会话信息设置 */}
|
||
<View style={[styles.sessionHeader, { backgroundColor: `${goalConfig.color}20` }]}>
|
||
<View style={[styles.sessionColorIndicator, { backgroundColor: goalConfig.color }]} />
|
||
<View style={styles.sessionInfo}>
|
||
<ThemedText style={styles.sessionTitle}>训练会话设置</ThemedText>
|
||
<ThemedText style={styles.sessionDescription}>
|
||
{goalConfig.description}
|
||
</ThemedText>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 会话名称输入 */}
|
||
<View style={styles.inputSection}>
|
||
<Text style={styles.inputLabel}>会话名称</Text>
|
||
<TextInput
|
||
value={sessionName}
|
||
onChangeText={setSessionName}
|
||
placeholder="输入会话名称"
|
||
placeholderTextColor="#888F92"
|
||
style={[styles.textInput, { borderColor: `${goalConfig.color}30` }]}
|
||
maxLength={50}
|
||
/>
|
||
</View>
|
||
|
||
{/* 创建方式选择 */}
|
||
<View style={styles.methodSection}>
|
||
<Text style={styles.sectionTitle}>选择创建方式</Text>
|
||
|
||
{/* 自定义会话 */}
|
||
<TouchableOpacity
|
||
style={[styles.methodCard, { borderColor: `${goalConfig.color}30` }]}
|
||
onPress={handleCreateCustomSession}
|
||
disabled={creating || !sessionName.trim()}
|
||
activeOpacity={0.9}
|
||
>
|
||
<View style={styles.methodIcon}>
|
||
<Ionicons name="create-outline" size={24} color={goalConfig.color} />
|
||
</View>
|
||
<View style={styles.methodInfo}>
|
||
<Text style={styles.methodTitle}>自定义会话</Text>
|
||
<Text style={styles.methodDescription}>
|
||
创建空的训练会话,然后手动添加动作
|
||
</Text>
|
||
</View>
|
||
<Ionicons name="chevron-forward" size={20} color="#9CA3AF" />
|
||
</TouchableOpacity>
|
||
|
||
{/* 从训练计划导入 */}
|
||
<View style={styles.planImportSection}>
|
||
<Text style={styles.methodTitle}>从训练计划导入</Text>
|
||
<Text style={styles.methodDescription}>
|
||
选择一个训练计划,将其动作导入到新会话中
|
||
</Text>
|
||
|
||
{plansLoading ? (
|
||
<View style={styles.loadingContainer}>
|
||
<Text style={styles.loadingText}>加载训练计划中...</Text>
|
||
</View>
|
||
) : plans.length === 0 ? (
|
||
<View style={styles.emptyPlansContainer}>
|
||
<Ionicons name="document-outline" size={32} color="#9CA3AF" />
|
||
<Text style={styles.emptyPlansText}>暂无训练计划</Text>
|
||
<TouchableOpacity
|
||
style={[styles.createPlanBtn, { backgroundColor: goalConfig.color }]}
|
||
onPress={() => router.push('/training-plan/create' as any)}
|
||
>
|
||
<Text style={styles.createPlanBtnText}>创建训练计划</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
) : (
|
||
<>
|
||
<FlatList
|
||
data={plans}
|
||
keyExtractor={(item) => item.id}
|
||
renderItem={renderPlanItem}
|
||
contentContainerStyle={styles.plansList}
|
||
showsVerticalScrollIndicator={false}
|
||
scrollEnabled={false}
|
||
/>
|
||
|
||
{selectedPlan && (
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.confirmBtn,
|
||
{ backgroundColor: goalConfig.color },
|
||
(!sessionName.trim() || creating) && { opacity: 0.5 }
|
||
]}
|
||
onPress={handleCreateFromPlan}
|
||
disabled={!sessionName.trim() || creating}
|
||
>
|
||
<Text style={styles.confirmBtnText}>
|
||
{creating ? '创建中...' : `从 "${selectedPlan.name}" 创建会话`}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</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,
|
||
},
|
||
|
||
// 会话信息头部
|
||
sessionHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
padding: 16,
|
||
borderRadius: 16,
|
||
marginBottom: 20,
|
||
},
|
||
sessionColorIndicator: {
|
||
width: 4,
|
||
height: 40,
|
||
borderRadius: 2,
|
||
marginRight: 12,
|
||
},
|
||
sessionInfo: {
|
||
flex: 1,
|
||
},
|
||
sessionTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
sessionDescription: {
|
||
fontSize: 13,
|
||
color: '#5E6468',
|
||
opacity: 0.8,
|
||
},
|
||
|
||
// 输入区域
|
||
inputSection: {
|
||
marginBottom: 24,
|
||
},
|
||
inputLabel: {
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 8,
|
||
},
|
||
textInput: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 12,
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
fontSize: 16,
|
||
color: '#192126',
|
||
borderWidth: 1,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 8,
|
||
shadowOffset: { width: 0, height: 2 },
|
||
elevation: 2,
|
||
},
|
||
|
||
// 创建方式区域
|
||
methodSection: {
|
||
flex: 1,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
marginBottom: 16,
|
||
},
|
||
|
||
// 方式卡片
|
||
methodCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 16,
|
||
marginBottom: 16,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
borderWidth: 1,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 8,
|
||
shadowOffset: { width: 0, height: 2 },
|
||
elevation: 2,
|
||
},
|
||
methodIcon: {
|
||
marginRight: 12,
|
||
},
|
||
methodInfo: {
|
||
flex: 1,
|
||
},
|
||
methodTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
methodDescription: {
|
||
fontSize: 12,
|
||
color: '#6B7280',
|
||
lineHeight: 16,
|
||
},
|
||
|
||
// 训练计划导入区域
|
||
planImportSection: {
|
||
marginTop: 8,
|
||
},
|
||
|
||
// 训练计划列表
|
||
plansList: {
|
||
marginTop: 16,
|
||
marginBottom: 20,
|
||
},
|
||
planCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
marginBottom: 12,
|
||
borderLeftWidth: 4,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 8,
|
||
shadowOffset: { width: 0, height: 2 },
|
||
elevation: 2,
|
||
},
|
||
planCardContent: {
|
||
padding: 16,
|
||
},
|
||
planHeader: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'flex-start',
|
||
marginBottom: 8,
|
||
},
|
||
planInfo: {
|
||
flex: 1,
|
||
},
|
||
planName: {
|
||
fontSize: 16,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
planGoal: {
|
||
fontSize: 12,
|
||
fontWeight: '700',
|
||
marginBottom: 2,
|
||
},
|
||
planStatus: {
|
||
marginLeft: 12,
|
||
},
|
||
radioButton: {
|
||
width: 24,
|
||
height: 24,
|
||
borderRadius: 12,
|
||
borderWidth: 2,
|
||
},
|
||
planStats: {
|
||
marginTop: 8,
|
||
},
|
||
statsText: {
|
||
fontSize: 12,
|
||
color: '#6B7280',
|
||
},
|
||
|
||
// 空状态
|
||
loadingContainer: {
|
||
alignItems: 'center',
|
||
paddingVertical: 24,
|
||
},
|
||
loadingText: {
|
||
fontSize: 14,
|
||
color: '#6B7280',
|
||
},
|
||
emptyPlansContainer: {
|
||
alignItems: 'center',
|
||
paddingVertical: 32,
|
||
},
|
||
emptyPlansText: {
|
||
fontSize: 14,
|
||
color: '#6B7280',
|
||
marginTop: 8,
|
||
marginBottom: 16,
|
||
},
|
||
createPlanBtn: {
|
||
paddingVertical: 10,
|
||
paddingHorizontal: 20,
|
||
borderRadius: 8,
|
||
},
|
||
createPlanBtnText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
},
|
||
|
||
// 确认按钮
|
||
confirmBtn: {
|
||
paddingVertical: 16,
|
||
borderRadius: 12,
|
||
alignItems: 'center',
|
||
marginTop: 10,
|
||
},
|
||
confirmBtnText: {
|
||
color: '#FFFFFF',
|
||
fontWeight: '800',
|
||
fontSize: 16,
|
||
},
|
||
});
|