Files
digital-pilates/app/training-plan/schedule/select.tsx
2025-08-16 14:15:11 +08:00

1116 lines
36 KiB
TypeScript
Raw 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 { 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<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 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, setSession] = useState<any>(null);
const [sessionLoading, setSessionLoading] = useState(false);
// 根据是否有sessionId来确定是训练计划模式还是训练会话模式
const isSessionMode = !!sessionId;
// 加载会话详情(如果是会话模式)
useEffect(() => {
if (sessionId && !session) {
const loadSession = async () => {
try {
setSessionLoading(true);
// 首先尝试使用 currentSession如果 sessionId 匹配)
if (currentSession?.id === sessionId) {
setSession(currentSession);
} else {
// 否则从 API 获取会话详情
const { workoutsApi } = await import('@/services/workoutsApi');
const sessionDetail = await workoutsApi.getSessionDetail(sessionId);
setSession(sessionDetail);
}
} catch (error) {
console.error('加载会话详情失败:', error);
} finally {
setSessionLoading(false);
}
};
loadSession();
}
}, [sessionId, currentSession, session]);
const targetGoal = plan?.goal || session?.trainingPlan?.goal;
const goalConfig = targetGoal
? (GOAL_TEXT[targetGoal] || { title: isSessionMode ? '添加动作' : '训练计划', color: palette.primary, description: isSessionMode ? '选择要添加的动作' : '开始你的训练之旅' })
: { title: isSessionMode ? '添加动作' : '训练计划', color: palette.primary, description: isSessionMode ? '选择要添加的动作' : '开始你的训练之旅' };
const [keyword, setKeyword] = useState('');
const [category, setCategory] = useState<string>('全部');
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [sets, setSets] = useState(3);
const [reps, setReps] = useState<number | undefined>(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 && planId) {
// 训练计划模式:添加到训练计划
const planExerciseDto = {
exerciseKey: selected.key,
name: selected.name,
sets: sets,
reps: reps,
itemType: 'exercise' as const,
note: `${selected.category}训练`,
};
await dispatch(addExercise({ planId: planId, 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 && planId) {
// 训练计划模式
await dispatch(addExercise({ planId: planId, 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 && planId) {
// 训练计划模式
await dispatch(addExercise({ planId: planId, 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 (sessionLoading) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="选择动作" onBack={() => router.back()} />
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}>...</ThemedText>
</View>
</SafeAreaView>
);
}
// 错误状态检查
if (isSessionMode && !session) {
return (
<SafeAreaView style={styles.safeArea}>
<HeaderBar title="选择动作" onBack={() => router.back()} />
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}></ThemedText>
</View>
</SafeAreaView>
);
}
if (!isSessionMode && !plan) {
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={isSessionMode ? "添加动作" : "选择动作"}
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
tone="light"
/>
<View style={styles.content}>
{/* 计划信息头部 */}
<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}>
{isSessionMode ? '为当前训练会话添加动作' : '选择动作或添加休息、备注项目'}
</ThemedText>
</View>
</View>
{/* 快捷添加区域 */}
<View style={styles.quickAddSection}>
<TouchableOpacity
style={[styles.quickAddBtn, { backgroundColor: `${goalConfig.color}15`, borderColor: goalConfig.color }]}
onPress={handleAddRest}
>
<Ionicons name="time-outline" size={20} color={goalConfig.color} />
<Text style={[styles.quickAddText, { color: goalConfig.color }]}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.quickAddBtn, { backgroundColor: `${goalConfig.color}15`, borderColor: goalConfig.color }]}
onPress={handleAddNote}
>
<Ionicons name="document-text-outline" size={20} color={goalConfig.color} />
<Text style={[styles.quickAddText, { color: goalConfig.color }]}></Text>
</TouchableOpacity>
</View>
{/* 大分类宫格 */}
<View style={styles.catGrid}>
{[...mainCategories, '更多'].map((item) => {
const active = category === item;
const meta: Record<string, { bg: string }> = {
: { 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 (
<Animated.View key={item} style={[styles.catTileWrapper, { transform: [{ scale }] }]}>
<TouchableOpacity
activeOpacity={0.9}
onPressIn={onPressIn}
onPressOut={handlePress}
style={[
styles.catTile,
{ backgroundColor: meta[item]?.bg ?? 'rgba(24,24,27,0.06)' },
active && { borderWidth: 2, borderColor: goalConfig.color }
]}
>
<Text style={[
styles.catText,
{ color: active ? goalConfig.color : '#384046' },
active && { fontWeight: '800' }
]}>
{item}
</Text>
</TouchableOpacity>
</Animated.View>
);
})}
</View>
{/* 分类选择弹层 */}
<Modal
visible={showCategoryPicker}
animationType="fade"
transparent
onRequestClose={() => setShowCategoryPicker(false)}
>
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setShowCategoryPicker(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalSheet}
onPress={(e) => e.stopPropagation() as any}
>
<Text style={styles.modalTitle}></Text>
<View style={styles.catGridModal}>
{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 (
<Animated.View key={c} style={[styles.catTileWrapper, { transform: [{ scale }] }]}>
<TouchableOpacity
onPressIn={onPressIn}
onPressOut={() => {
onPressOut();
setCategory(c);
setShowCategoryPicker(false);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
activeOpacity={0.9}
style={[styles.catTile, { backgroundColor: 'rgba(24,24,27,0.06)' }]}
>
<Text style={styles.catText}>{c}</Text>
</TouchableOpacity>
</Animated.View>
);
})}
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
{/* 搜索框 */}
<View style={styles.searchRow}>
<TextInput
value={keyword}
onChangeText={setKeyword}
placeholder="搜索动作名称/要点"
placeholderTextColor="#888F92"
style={[styles.searchInput, { borderColor: `${goalConfig.color}30` }]}
/>
</View>
{/* 动作列表 */}
<FlatList
data={filtered}
keyExtractor={(item) => item.key}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => {
const isSelected = item.key === selectedKey;
return (
<TouchableOpacity
style={[
styles.itemCard,
isSelected && { borderWidth: 2, borderColor: goalConfig.color },
]}
onPress={() => onSelectItem(item.key)}
activeOpacity={0.9}
>
<View style={{ flex: 1 }}>
<Text style={styles.itemTitle}>{item.name}</Text>
<Text style={styles.itemMeta}>{item.category}</Text>
{((item as any).targetMuscleGroups || (item as any).equipmentName) && (
<Text style={styles.itemMeta}>
{[(item as any).targetMuscleGroups, (item as any).equipmentName].filter(Boolean).join(' · ')}
</Text>
)}
{(((item as any).beginnerSets || (item as any).beginnerReps)) && (
<Text style={styles.itemMeta}>
{(item as any).beginnerSets ?? '-'} × {(item as any).beginnerReps ?? '-'}
</Text>
)}
<Text style={styles.itemDesc}>{item.description}</Text>
</View>
{isSelected && <Ionicons name="chevron-down" size={20} color={goalConfig.color} />}
{isSelected && (
<Animated.View style={[styles.expandedBox, { opacity: controlsOpacity }]}>
<View style={styles.controlsRow}>
<View style={styles.counterBox}>
<Text style={styles.counterLabel}></Text>
<View style={styles.counterRow}>
<TouchableOpacity
style={styles.counterBtn}
onPress={() => setSets(Math.max(1, sets - 1))}
>
<Text style={styles.counterBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.counterValue}>{sets}</Text>
<TouchableOpacity
style={styles.counterBtn}
onPress={() => setSets(Math.min(20, sets + 1))}
>
<Text style={styles.counterBtnText}>+</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.counterBox}>
<Text style={styles.counterLabel}></Text>
<View style={styles.repsChipsRow}>
{[6, 8, 10, 12, 15, 20, 25, 30].map((v) => {
const active = reps === v;
return (
<TouchableOpacity
key={v}
style={[
styles.repChip,
active && { backgroundColor: goalConfig.color, borderColor: goalConfig.color }
]}
onPress={() => {
setReps(v);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Text style={[styles.repChipText, active && { color: '#FFFFFF' }]}>{v}</Text>
</TouchableOpacity>
);
})}
<TouchableOpacity
style={styles.repChipGhost}
onPress={() => {
setShowCustomReps((s) => !s);
Haptics.selectionAsync();
}}
>
<Text style={styles.repChipGhostText}></Text>
</TouchableOpacity>
</View>
{showCustomReps && (
<View style={styles.customRepsRow}>
<TextInput
keyboardType="number-pad"
value={customRepsInput}
onChangeText={setCustomRepsInput}
placeholder="输入次数 (1-100)"
placeholderTextColor="#888F92"
style={styles.customRepsInput}
/>
<TouchableOpacity
style={[styles.customRepsBtn, { backgroundColor: goalConfig.color }]}
onPress={() => {
const n = Math.max(1, Math.min(100, parseInt(customRepsInput || '0', 10)));
if (!Number.isNaN(n)) {
setReps(n);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
}}
>
<Text style={styles.customRepsBtnText}></Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
<TouchableOpacity
style={[
styles.addBtn,
{ backgroundColor: goalConfig.color },
((!reps || reps <= 0) || adding) && { opacity: 0.5 }
]}
disabled={!reps || reps <= 0 || adding}
onPress={handleAdd}
>
<Text style={styles.addBtnText}>
{adding ? '添加中...' : (isSessionMode ? '添加到训练会话' : '添加到训练计划')}
</Text>
</TouchableOpacity>
</Animated.View>
)}
</TouchableOpacity>
);
}}
/>
</View>
</SafeAreaView>
{/* 休息时间配置模态框 */}
<Modal visible={showRestModal} transparent animationType="fade" onRequestClose={() => setShowRestModal(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setShowRestModal(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalSheet} onPress={(e) => e.stopPropagation() as any}>
<Text style={styles.modalTitle}></Text>
<View style={styles.restTimeRow}>
{[15, 30, 45, 60, 90, 120].map((v) => {
const active = restDuration === v;
return (
<TouchableOpacity
key={v}
style={[
styles.restChip,
active && { backgroundColor: goalConfig.color, borderColor: goalConfig.color }
]}
onPress={() => {
setRestDuration(v);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Text style={[styles.restChipText, active && { color: '#FFFFFF' }]}>{v}s</Text>
</TouchableOpacity>
);
})}
</View>
<View style={styles.customRestSection}>
<Text style={styles.sectionLabel}></Text>
<View style={styles.customRestRow}>
<TextInput
value={restDuration.toString()}
onChangeText={(text) => {
const num = parseInt(text) || 30;
setRestDuration(Math.max(10, Math.min(300, num)));
}}
keyboardType="number-pad"
style={styles.customRestInput}
/>
<Text style={styles.customRestUnit}></Text>
</View>
</View>
<TouchableOpacity
style={[styles.confirmBtn, { backgroundColor: goalConfig.color }]}
onPress={confirmAddRest}
disabled={adding}
>
<Text style={styles.confirmBtnText}>{adding ? '添加中...' : '确认添加'}</Text>
</TouchableOpacity>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
{/* 备注配置模态框 */}
<Modal visible={showNoteModal} transparent animationType="fade" onRequestClose={() => setShowNoteModal(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setShowNoteModal(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalSheet} onPress={(e) => e.stopPropagation() as any}>
<Text style={styles.modalTitle}></Text>
<TextInput
value={noteContent}
onChangeText={setNoteContent}
placeholder="输入训练提醒或注意事项..."
placeholderTextColor="#888F92"
style={styles.noteModalInput}
multiline
maxLength={100}
autoFocus
/>
<View style={styles.noteModalInfo}>
<Text style={styles.noteCounter}>{noteContent.length}/100</Text>
{noteContent.length > 0 && (
<TouchableOpacity
onPress={() => setNoteContent('')}
style={styles.noteClearBtn}
>
<Ionicons name="close-circle" size={20} color="#888F92" />
</TouchableOpacity>
)}
</View>
<TouchableOpacity
style={[
styles.confirmBtn,
{ backgroundColor: goalConfig.color },
(!noteContent.trim() || adding) && { opacity: 0.5 }
]}
onPress={confirmAddNote}
disabled={!noteContent.trim() || adding}
>
<Text style={styles.confirmBtnText}>{adding ? '添加中...' : '确认添加'}</Text>
</TouchableOpacity>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</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,
},
// 快捷添加区域
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',
},
});