Files
digital-pilates/app/workout/create-session.tsx
2025-08-16 14:15:11 +08:00

517 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
},
});