import { HeaderBar } from '@/components/ui/HeaderBar'; import { palette } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { loadExerciseLibrary } from '@/store/exerciseLibrarySlice'; import { EXERCISE_LIBRARY, getCategories } from '@/utils/exerciseLibrary'; import { Ionicons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Animated, FlatList, LayoutAnimation, Modal, Platform, SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, UIManager, View } from 'react-native'; import { ThemedText } from '@/components/ThemedText'; import { addExercise } from '@/store/scheduleExerciseSlice'; import { addWorkoutExercise } from '@/store/workoutSlice'; 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 SelectExerciseForScheduleScreen() { const router = useRouter(); const dispatch = useAppDispatch(); const params = useLocalSearchParams<{ planId?: string; sessionId?: string }>(); const { plans } = useAppSelector((s) => s.trainingPlan); const { currentSession } = useAppSelector((s) => s.workout); const planId = params.planId; const sessionId = params.sessionId; const plan = useMemo(() => plans.find(p => p.id === planId), [plans, planId]); const session = useMemo(() => sessionId ? currentSession : null, [sessionId, currentSession]); // 根据是否有sessionId来确定是训练计划模式还是训练会话模式 const isSessionMode = !!sessionId; const targetGoal = plan?.goal || session?.trainingPlan?.goal; const goalConfig = targetGoal ? (GOAL_TEXT[targetGoal] || { title: isSessionMode ? '添加动作' : '训练计划', color: palette.primary, description: isSessionMode ? '选择要添加的动作' : '开始你的训练之旅' }) : null; const [keyword, setKeyword] = useState(''); const [category, setCategory] = useState('全部'); const [selectedKey, setSelectedKey] = useState(null); const [sets, setSets] = useState(3); const [reps, setReps] = useState(undefined); const [showCustomReps, setShowCustomReps] = useState(false); const [customRepsInput, setCustomRepsInput] = useState(''); const [showCategoryPicker, setShowCategoryPicker] = useState(false); const [showRestModal, setShowRestModal] = useState(false); const [showNoteModal, setShowNoteModal] = useState(false); const [restDuration, setRestDuration] = useState(30); const [noteContent, setNoteContent] = useState(''); const { categories: serverCategoryDtos, exercises: serverExercises } = useAppSelector((s) => s.exerciseLibrary); const [adding, setAdding] = useState(false); const controlsOpacity = useRef(new Animated.Value(0)).current; useEffect(() => { if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } }, []); useEffect(() => { dispatch(loadExerciseLibrary()); }, [dispatch]); const categories = useMemo(() => { const base = serverCategoryDtos && serverCategoryDtos.length ? serverCategoryDtos.map((c) => c.name) : getCategories(); const unique = Array.from(new Set(base)); return ['全部', ...unique]; }, [serverCategoryDtos]); const mainCategories = useMemo(() => { const preferred = ['全部', '核心与腹部', '脊柱与后链', '侧链与髋', '平衡与支撑']; const exists = (name: string) => categories.includes(name); const picked = preferred.filter(exists); const rest = categories.filter((c) => !picked.includes(c)); while (picked.length < 5 && rest.length) picked.push(rest.shift() as string); return picked; }, [categories]); const library = useMemo(() => (serverExercises && serverExercises.length ? serverExercises : EXERCISE_LIBRARY), [serverExercises]); const filtered = useMemo(() => { const kw = keyword.trim().toLowerCase(); const base = kw ? library.filter((e) => e.name.toLowerCase().includes(kw) || (e.description || '').toLowerCase().includes(kw)) : library; if (category === '全部') return base; return base.filter((e) => e.category === category); }, [keyword, category, library]); const selected = useMemo(() => library.find((e) => e.key === selectedKey) || null, [selectedKey, library]); useEffect(() => { Animated.timing(controlsOpacity, { toValue: selected ? 1 : 0, duration: selected ? 220 : 160, useNativeDriver: true, }).start(); }, [selected, controlsOpacity]); const handleAdd = async () => { if (!selected || adding) return; console.log('选择动作:', selected); const newExerciseDto = { exerciseKey: selected.key, name: selected.name, plannedSets: sets, plannedReps: reps, itemType: 'exercise' as const, note: `${selected.category}训练`, }; setAdding(true); try { if (isSessionMode && sessionId) { // 训练会话模式:添加到训练会话 await dispatch(addWorkoutExercise({ sessionId, dto: newExerciseDto })).unwrap(); } else if (plan) { // 训练计划模式:添加到训练计划 const planExerciseDto = { exerciseKey: selected.key, name: selected.name, sets: sets, reps: reps, itemType: 'exercise' as const, note: `${selected.category}训练`, }; await dispatch(addExercise({ planId: plan.id, dto: planExerciseDto })).unwrap(); } else { throw new Error('缺少必要的参数'); } // 返回到上一页 router.back(); } catch (error) { console.error('添加动作失败:', error); Alert.alert('添加失败', '添加动作时出现错误,请稍后重试'); } finally { setAdding(false); } }; // 添加休息项目 const handleAddRest = () => { setShowRestModal(true); Haptics.selectionAsync(); }; // 添加备注项目 const handleAddNote = () => { setShowNoteModal(true); Haptics.selectionAsync(); }; // 确认添加休息 const confirmAddRest = async () => { if (adding) return; const restDto = { name: `间隔休息 ${restDuration}s`, restSec: restDuration, itemType: 'rest' as const, }; setAdding(true); try { if (isSessionMode && sessionId) { // 训练会话模式 await dispatch(addWorkoutExercise({ sessionId, dto: restDto })).unwrap(); } else if (plan) { // 训练计划模式 await dispatch(addExercise({ planId: plan.id, dto: restDto })).unwrap(); } else { throw new Error('缺少必要的参数'); } setShowRestModal(false); setRestDuration(30); router.back(); } catch (error) { console.error('添加休息失败:', error); Alert.alert('添加失败', '添加休息时出现错误,请稍后重试'); } finally { setAdding(false); } }; // 确认添加备注 const confirmAddNote = async () => { if (adding || !noteContent.trim()) return; const noteDto = { name: '训练提示', note: noteContent.trim(), itemType: 'note' as const, }; setAdding(true); try { if (isSessionMode && sessionId) { // 训练会话模式 await dispatch(addWorkoutExercise({ sessionId, dto: noteDto })).unwrap(); } else if (plan) { // 训练计划模式 await dispatch(addExercise({ planId: plan.id, dto: noteDto })).unwrap(); } else { throw new Error('缺少必要的参数'); } setShowNoteModal(false); setNoteContent(''); router.back(); } catch (error) { console.error('添加备注失败:', error); Alert.alert('添加失败', '添加备注时出现错误,请稍后重试'); } finally { setAdding(false); } }; const onSelectItem = (key: string) => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); if (selectedKey === key) { setSelectedKey(null); return; } const sel = library.find((e) => e.key === key) as any; setSets(sel?.beginnerSets ?? 3); setReps(sel?.beginnerReps); setShowCustomReps(false); setCustomRepsInput(''); setSelectedKey(key); }; if (!goalConfig || (!plan && !isSessionMode)) { return ( router.back()} /> {isSessionMode ? '找不到指定的训练会话' : '找不到指定的训练计划'} ); } return ( {/* 动态背景 */} router.back()} withSafeTop={false} transparent={true} tone="light" /> {/* 计划信息头部 */} {goalConfig.title} {isSessionMode ? '为当前训练会话添加动作' : '选择动作或添加休息、备注项目'} {/* 快捷添加区域 */} 添加休息 添加备注 {/* 大分类宫格 */} {[...mainCategories, '更多'].map((item) => { const active = category === item; const meta: Record = { 全部: { bg: `${goalConfig.color}22` }, 核心与腹部: { bg: `${goalConfig.color}18` }, 脊柱与后链: { bg: 'rgba(149,204,227,0.20)' }, 侧链与髋: { bg: 'rgba(164,138,237,0.20)' }, 平衡与支撑: { bg: 'rgba(252,196,111,0.22)' }, 进阶控制: { bg: 'rgba(237,71,71,0.18)' }, 柔韧与拉伸: { bg: 'rgba(149,204,227,0.18)' }, 更多: { bg: 'rgba(24,24,27,0.06)' }, }; const scale = new Animated.Value(1); const onPressIn = () => Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, speed: 20, bounciness: 6 }).start(); const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 20, bounciness: 6 }).start(); const handlePress = () => { onPressOut(); if (item === '更多') { setShowCategoryPicker(true); Haptics.selectionAsync(); } else { setCategory(item); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } }; return ( {item} ); })} {/* 分类选择弹层 */} setShowCategoryPicker(false)} > setShowCategoryPicker(false)}> e.stopPropagation() as any} > 选择分类 {categories.filter((c) => c !== '全部').map((c) => { const scale = new Animated.Value(1); const onPressIn = () => Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, speed: 20, bounciness: 6 }).start(); const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 20, bounciness: 6 }).start(); return ( { onPressOut(); setCategory(c); setShowCategoryPicker(false); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }} activeOpacity={0.9} style={[styles.catTile, { backgroundColor: 'rgba(24,24,27,0.06)' }]} > {c} ); })} {/* 搜索框 */} {/* 动作列表 */} item.key} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} renderItem={({ item }) => { const isSelected = item.key === selectedKey; return ( onSelectItem(item.key)} activeOpacity={0.9} > {item.name} {item.category} {((item as any).targetMuscleGroups || (item as any).equipmentName) && ( {[(item as any).targetMuscleGroups, (item as any).equipmentName].filter(Boolean).join(' · ')} )} {(((item as any).beginnerSets || (item as any).beginnerReps)) && ( 建议 {(item as any).beginnerSets ?? '-'} 组 × {(item as any).beginnerReps ?? '-'} 次 )} {item.description} {isSelected && } {isSelected && ( 组数 setSets(Math.max(1, sets - 1))} > - {sets} setSets(Math.min(20, sets + 1))} > + 每组次数 {[6, 8, 10, 12, 15, 20, 25, 30].map((v) => { const active = reps === v; return ( { setReps(v); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }} > {v} ); })} { setShowCustomReps((s) => !s); Haptics.selectionAsync(); }} > 自定义 {showCustomReps && ( { const n = Math.max(1, Math.min(100, parseInt(customRepsInput || '0', 10))); if (!Number.isNaN(n)) { setReps(n); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } }} > 确定 )} {adding ? '添加中...' : (isSessionMode ? '添加到训练会话' : '添加到训练计划')} )} ); }} /> {/* 休息时间配置模态框 */} setShowRestModal(false)}> setShowRestModal(false)}> e.stopPropagation() as any}> 设置休息时间 {[15, 30, 45, 60, 90, 120].map((v) => { const active = restDuration === v; return ( { setRestDuration(v); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }} > {v}s ); })} 自定义时间 { const num = parseInt(text) || 30; setRestDuration(Math.max(10, Math.min(300, num))); }} keyboardType="number-pad" style={styles.customRestInput} /> {adding ? '添加中...' : '确认添加'} {/* 备注配置模态框 */} setShowNoteModal(false)}> setShowNoteModal(false)}> e.stopPropagation() as any}> 添加训练提示 {noteContent.length}/100 {noteContent.length > 0 && ( setNoteContent('')} style={styles.noteClearBtn} > )} {adding ? '添加中...' : '确认添加'} ); } 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, }, // 快捷添加区域 quickAddSection: { flexDirection: 'row', gap: 12, marginBottom: 16, }, quickAddBtn: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, borderWidth: 1, gap: 8, }, quickAddText: { fontSize: 14, fontWeight: '700', }, // 分类网格 catGrid: { paddingTop: 10, flexDirection: 'row', flexWrap: 'wrap', marginBottom: 16, }, catTileWrapper: { width: '33.33%', padding: 6, }, catTile: { borderRadius: 14, paddingVertical: 16, paddingHorizontal: 8, alignItems: 'center', justifyContent: 'center', }, catText: { fontSize: 13, fontWeight: '700', color: '#384046', }, // 搜索框 searchRow: { marginBottom: 16, }, searchInput: { backgroundColor: '#FFFFFF', borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10, color: '#384046', borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 8, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, // 列表 listContent: { paddingBottom: 40, }, // 动作卡片 itemCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginBottom: 12, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3, }, itemTitle: { fontSize: 16, fontWeight: '800', color: '#192126', marginBottom: 4, }, itemMeta: { fontSize: 12, color: '#888F92', marginBottom: 4, }, itemDesc: { fontSize: 12, color: '#5E6468', lineHeight: 16, }, // 展开的控制区域 expandedBox: { marginTop: 12, }, controlsRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 12, flexWrap: 'wrap', marginBottom: 16, }, counterBox: { backgroundColor: '#F8F9FA', borderRadius: 8, padding: 12, minWidth: 120, }, counterLabel: { fontSize: 10, color: '#888F92', marginBottom: 8, fontWeight: '600', }, counterRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, counterBtn: { backgroundColor: '#E5E7EB', width: 28, height: 28, borderRadius: 6, alignItems: 'center', justifyContent: 'center', }, counterBtnText: { fontWeight: '800', color: '#384046', }, counterValue: { minWidth: 40, textAlign: 'center', fontWeight: '700', color: '#384046', }, repsChipsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 6, }, repChip: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 999, backgroundColor: '#F3F4F6', borderWidth: 1, borderColor: '#E5E7EB', }, repChipText: { color: '#384046', fontWeight: '700', fontSize: 12, }, repChipGhost: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 999, borderWidth: 1, backgroundColor: 'transparent', borderColor: '#E5E7EB', }, repChipGhostText: { fontWeight: '700', color: '#384046', fontSize: 12, }, customRepsRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 8, }, customRepsInput: { flex: 1, height: 40, borderWidth: 1, borderColor: '#E5E7EB', borderRadius: 10, paddingHorizontal: 12, color: '#384046', }, customRepsBtn: { paddingHorizontal: 12, paddingVertical: 10, borderRadius: 10, }, customRepsBtnText: { fontWeight: '800', color: '#FFFFFF', fontSize: 12, }, // 休息时间配置 restTimeRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 16, }, restChip: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 16, borderWidth: 1, borderColor: '#E5E7EB', backgroundColor: '#FFFFFF', }, restChipText: { fontSize: 12, fontWeight: '700', color: '#384046', }, // 模态框自定义休息时间 customRestSection: { marginBottom: 20, }, sectionLabel: { fontSize: 14, fontWeight: '700', color: '#384046', marginBottom: 10, }, customRestRow: { flexDirection: 'row', alignItems: 'center', gap: 10, }, customRestInput: { flex: 1, height: 40, borderWidth: 1, borderColor: '#E5E7EB', borderRadius: 10, paddingHorizontal: 12, color: '#384046', fontSize: 16, textAlign: 'center', }, customRestUnit: { fontSize: 14, color: '#384046', fontWeight: '600', }, // 备注模态框 noteModalInput: { minHeight: 100, maxHeight: 150, borderWidth: 1, borderColor: '#E5E7EB', borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10, color: '#384046', fontSize: 14, textAlignVertical: 'top', backgroundColor: '#FFFFFF', marginBottom: 10, }, noteModalInfo: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, }, noteCounter: { fontSize: 11, color: '#888F92', }, noteClearBtn: { padding: 4, }, // 确认按钮 confirmBtn: { paddingVertical: 12, borderRadius: 12, alignItems: 'center', marginTop: 10, }, confirmBtnText: { color: '#FFFFFF', fontWeight: '800', fontSize: 14, }, addBtn: { marginTop: 20, paddingVertical: 12, borderRadius: 12, alignItems: 'center', }, addBtnText: { color: '#FFFFFF', fontWeight: '800', fontSize: 14, }, // 错误状态 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', }, catGridModal: { flexDirection: 'row', flexWrap: 'wrap', }, });