Files
digital-pilates/app/training-plan.tsx
richarjiang 5a4d86ff7d feat: 更新应用配置和引入新依赖
- 修改 app.json,禁用平板支持以优化用户体验
- 在 package.json 和 package-lock.json 中新增 react-native-toast-message 依赖,支持消息提示功能
- 在多个组件中集成 Toast 组件,提升用户交互反馈
- 更新训练计划相关逻辑,优化状态管理和数据处理
- 调整样式以适应新功能的展示和交互
2025-08-16 09:42:33 +08:00

1426 lines
40 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 MaskedView from '@react-native-masked-view/masked-view';
import { LinearGradient } from 'expo-linear-gradient';
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Alert, FlatList, Modal, Pressable, SafeAreaView, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
import Animated, {
FadeInUp,
FadeOut,
interpolate,
Layout,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSpring,
withTiming
} from 'react-native-reanimated';
import { ThemedText } from '@/components/ThemedText';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors, palette } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { TrainingPlan } from '@/services/trainingPlanApi';
import {
addExercise,
clearExercises,
clearError as clearScheduleError,
deleteExercise,
loadExercises,
toggleCompletion
} from '@/store/scheduleExerciseSlice';
import { activatePlan, clearError, deletePlan, loadPlans } from '@/store/trainingPlanSlice';
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: '温和激活,核心重建' },
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() {
const rotate = useSharedValue(0);
const scale = useSharedValue(1);
React.useEffect(() => {
rotate.value = withRepeat(withTiming(360, { duration: 20000 }), -1);
scale.value = withRepeat(withTiming(1.2, { duration: 8000 }), -1, true);
}, []);
const backgroundStyle = useAnimatedStyle(() => ({
transform: [
{ rotate: `${rotate.value}deg` },
{ scale: scale.value }
],
}));
return (
<View style={StyleSheet.absoluteFillObject}>
<LinearGradient
colors={['#F9FBF2', '#FFFFFF', '#F5F9F0']}
style={StyleSheet.absoluteFillObject}
/>
<Animated.View style={[styles.backgroundOrb, backgroundStyle]} />
<Animated.View style={[styles.backgroundOrb2, backgroundStyle]} />
</View>
);
}
// 渐变文字
function GradientText({ children }: { children: string }) {
return (
<MaskedView maskElement={<Text style={styles.gradientTitle}>{children}</Text>}>
<LinearGradient
colors={["#FF3D3D", "#FF8C1A"]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Text style={[styles.gradientTitle, { opacity: 0 }]}>{children}</Text>
</LinearGradient>
</MaskedView>
);
}
// 新视觉训练计划卡片
function PlanCard({ plan, onPress, onDelete, onActivate, onSchedule, isActive, index }: { plan: TrainingPlan; onPress: () => void; onDelete: () => void; onActivate: () => void; onSchedule: () => void; isActive?: boolean; index: number }) {
const scale = useSharedValue(1);
const glow = useSharedValue(0);
React.useEffect(() => {
glow.value = withRepeat(withTiming(1, { duration: 2000 + index * 100 }), -1, true);
}, [index]);
const goalConfig = GOAL_TEXT[plan.goal] || { title: '训练计划', color: palette.primary, description: '开始你的训练之旅' };
const cardStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const glowStyle = useAnimatedStyle(() => {
const opacity = isActive ? interpolate(glow.value, [0, 1], [0.25, 0.55]) : interpolate(glow.value, [0, 1], [0.1, 0.3]);
return {
shadowOpacity: opacity,
shadowColor: '#000',
shadowRadius: isActive ? 28 : 18,
elevation: isActive ? 18 : 10,
borderColor: isActive ? `${goalConfig.color}55` : '#1B262B',
};
});
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return `${date.getMonth() + 1}${date.getDate()}`;
};
const getFrequencyText = () => {
if (plan.mode === 'daysOfWeek') {
return `每周${plan.daysOfWeek.length}`;
}
return `每周${plan.sessionsPerWeek}`;
};
const displayTitle = plan.name?.trim() ? plan.name : goalConfig.title;
const frequencyCount = plan.mode === 'daysOfWeek' ? plan.daysOfWeek.length : plan.sessionsPerWeek;
const sinceCreatedDays = Math.max(0, Math.floor((Date.now() - new Date(plan.createdAt).getTime()) / (24 * 3600 * 1000)));
const startDeltaDays = Math.floor((Date.now() - new Date(plan.startDate).getTime()) / (24 * 3600 * 1000));
return (
<Animated.View
entering={FadeInUp.delay(index * 100).duration(400)}
exiting={FadeOut}
layout={Layout.springify()}
style={[styles.planCard, cardStyle, glowStyle]}
>
<Pressable
onPress={onPress}
onLongPress={() => {
Alert.alert('操作', '选择要执行的操作', [
{ text: '排课', onPress: onSchedule },
{ text: isActive ? '已激活' : '激活', onPress: onActivate },
{ text: '删除', style: 'destructive', onPress: onDelete },
{ text: '取消', style: 'cancel' },
]);
}}
onPressIn={() => { scale.value = withSpring(0.98); }}
onPressOut={() => { scale.value = withSpring(1); }}
style={styles.darkCard}
>
<LinearGradient
colors={["rgba(255,149,0,0.10)", "rgba(255,94,58,0.06)"]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.cardTintGradient}
pointerEvents="none"
/>
<GradientText>{displayTitle}</GradientText>
<Text style={styles.darkSubtitle} numberOfLines={2}>
{`${goalConfig.description} · 开始于 ${formatDate(plan.startDate)} · ${getFrequencyText()}${plan.preferredTimeOfDay ? ` · ${plan.preferredTimeOfDay === 'morning' ? '晨练' : plan.preferredTimeOfDay === 'noon' ? '午间' : '晚间'}` : ''}`}
</Text>
<View style={styles.metricsRow}>
<Pressable style={styles.metricItem} onPress={onSchedule} hitSlop={8}>
<Ionicons name="calendar-outline" size={22} color="#E6EEF2" />
<Text style={styles.metricText}></Text>
</Pressable>
<Pressable style={[styles.metricItem, isActive && styles.metricActive]} onPress={onActivate} hitSlop={8}>
<Ionicons name={isActive ? 'checkmark-done-circle-outline' : 'flash-outline'} size={22} color="#E6EEF2" />
<Text style={[styles.metricText, { color: isActive ? '#BBF246' : '#E6EEF2' }]}>{isActive ? '已激活' : '激活'}</Text>
</Pressable>
<Pressable style={styles.metricItem} onPress={() => {
Alert.alert('确认删除', '确定要删除这个训练计划吗?此操作无法撤销。', [
{ text: '取消', style: 'cancel' },
{ text: '删除', style: 'destructive', onPress: onDelete },
]);
}} hitSlop={8}>
<Ionicons name="trash-outline" size={22} color="#E6EEF2" />
<Text style={[styles.metricText, { color: '#ED4747' }]}></Text>
</Pressable>
</View>
</Pressable>
</Animated.View>
);
}
// 底部 Tab 组件
function BottomTabs({ activeTab, onTabChange, selectedPlan }: {
activeTab: TabType;
onTabChange: (tab: TabType) => void;
selectedPlan?: TrainingPlan;
}) {
const theme = useColorScheme() ?? 'light';
const colorTokens = Colors[theme];
return (
<View style={styles.bottomTabContainer}>
<View style={[styles.bottomTabBar, { backgroundColor: colorTokens.tabBarBackground }]}>
<TouchableOpacity
style={[
styles.tabButton,
activeTab === 'list' && { backgroundColor: colorTokens.tabBarActiveBackground }
]}
onPress={() => onTabChange('list')}
>
<Ionicons
name={activeTab === 'list' ? 'list' : 'list-outline'}
size={22}
color={activeTab === 'list' ? colorTokens.tabIconSelected : colorTokens.tabIconDefault}
/>
{activeTab === 'list' && (
<Text style={[styles.tabText, { color: colorTokens.tabIconSelected }]}></Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={[
styles.tabButton,
activeTab === 'schedule' && { backgroundColor: colorTokens.tabBarActiveBackground }
]}
onPress={() => onTabChange('schedule')}
>
<Ionicons
name={activeTab === 'schedule' ? 'calendar' : 'calendar-outline'}
size={22}
color={activeTab === 'schedule' ? colorTokens.tabIconSelected : colorTokens.tabIconDefault}
/>
{activeTab === 'schedule' && (
<Text style={[styles.tabText, { color: colorTokens.tabIconSelected }]}></Text>
)}
</TouchableOpacity>
</View>
</View>
);
}
export default function TrainingPlanScreen() {
const router = useRouter();
const dispatch = useAppDispatch();
const params = useLocalSearchParams<{ planId?: string; tab?: string }>();
const { plans, loading, error } = useAppSelector((s) => s.trainingPlan);
const { exercises, error: scheduleError } = useAppSelector((s) => s.scheduleExercise);
console.log('plans', plans);
// Tab 状态管理 - 支持从URL参数设置初始tab
const initialTab: TabType = params.tab === 'schedule' ? 'schedule' : 'list';
const [activeTab, setActiveTab] = useState<TabType>(initialTab);
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(params.planId || null);
// 一键排课配置
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 selectedPlan = useMemo(() => plans.find(p => p.id === selectedPlanId), [plans, selectedPlanId]);
// 监听选中计划变化,加载对应的排课数据
useEffect(() => {
if (selectedPlanId) {
dispatch(loadExercises(selectedPlanId));
} else {
dispatch(clearExercises());
}
}, [selectedPlanId, dispatch]);
// 每次页面获得焦点时,如果当前有选中的计划,重新加载其排课数据
useFocusEffect(
useCallback(() => {
if (selectedPlanId) {
dispatch(loadExercises(selectedPlanId));
}
}, [selectedPlanId, dispatch])
);
// 每次页面获得焦点时重新加载训练计划数据
useFocusEffect(
useCallback(() => {
dispatch(loadPlans());
}, [dispatch])
);
useEffect(() => {
if (error) {
console.error('训练计划错误:', error);
const timer = setTimeout(() => {
dispatch(clearError());
}, 3000);
return () => clearTimeout(timer);
}
}, [error, dispatch]);
useEffect(() => {
if (scheduleError) {
console.error('排课错误:', scheduleError);
const timer = setTimeout(() => {
dispatch(clearScheduleError());
}, 3000);
return () => clearTimeout(timer);
}
}, [scheduleError, dispatch]);
const handleActivate = async (planId: string) => {
try {
await dispatch(activatePlan(planId));
} catch (error) {
console.error('激活训练计划失败:', error);
}
}
const handlePlanSelect = (plan: TrainingPlan) => {
setSelectedPlanId(plan.id);
setActiveTab('schedule');
}
const handleTabChange = (tab: TabType) => {
if (tab === 'schedule' && !selectedPlanId && plans.length > 0) {
// 如果没有选中计划但要切换到排课页面,自动选择当前激活的计划或第一个计划
const targetPlan = plans.find(p => p.isActive) || plans[0];
setSelectedPlanId(targetPlan.id);
}
setActiveTab(tab);
}
// 排课相关方法
const handleAddExercise = () => {
router.push(`/training-plan/schedule/select?planId=${selectedPlanId}` as any);
};
const handleRemoveExercise = (exerciseId: string) => {
if (!selectedPlanId) return;
Alert.alert('确认移除', '确定要移除该动作吗?', [
{ text: '取消', style: 'cancel' },
{
text: '移除',
style: 'destructive',
onPress: () => {
dispatch(deleteExercise({ planId: selectedPlanId, exerciseId }));
},
},
]);
};
const handleToggleCompleted = (exerciseId: string, currentCompleted: boolean) => {
if (!selectedPlanId) return;
dispatch(toggleCompletion({
planId: selectedPlanId,
exerciseId,
completed: !currentCompleted
}));
};
const onGenerate = async () => {
if (!selectedPlanId) return;
const restSec = Math.max(10, Math.min(120, parseInt(genRest || '30', 10)));
const { items } = buildClassicalSession({
withSectionRests: genWithRests,
restSeconds: restSec,
withNotes: genWithNotes,
level: genLevel
});
setGenVisible(false);
try {
// 按顺序添加每个生成的训练项目
for (const item of items) {
const dto = {
exerciseKey: item.key, // 使用key作为exerciseKey
name: item.name,
sets: item.sets,
reps: item.reps,
durationSec: item.durationSec,
restSec: item.restSec,
note: item.note,
itemType: item.itemType || 'exercise',
};
await dispatch(addExercise({ planId: selectedPlanId, dto })).unwrap();
}
Alert.alert('排课已生成', '已为你生成经典普拉提序列,可继续调整。');
} catch (error) {
console.error('生成排课失败:', error);
Alert.alert('生成失败', '请稍后重试');
}
};
// 渲染训练计划列表
const renderPlansList = () => (
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
<Animated.View entering={FadeInUp.duration(600)} style={styles.headerSection}>
<ThemedText style={styles.title}></ThemedText>
<ThemedText style={styles.subtitle}>使</ThemedText>
</Animated.View>
{(error || scheduleError) && (
<Animated.View entering={FadeInUp.duration(400)} style={styles.errorContainer}>
<ThemedText style={styles.errorText}> {error || scheduleError}</ThemedText>
</Animated.View>
)}
{loading && plans.length === 0 ? (
<Animated.View entering={FadeInUp.delay(200).duration(600)} style={styles.loadingWrap}>
<View style={styles.loadingIcon}>
<ThemedText style={styles.loadingIconText}></ThemedText>
</View>
<ThemedText style={styles.loadingText}>...</ThemedText>
</Animated.View>
) : plans.length === 0 ? (
<Animated.View entering={FadeInUp.delay(200).duration(600)} style={styles.emptyWrap}>
<View style={styles.emptyIcon}>
<ThemedText style={styles.emptyIconText}>📋</ThemedText>
</View>
<ThemedText style={styles.emptyText}></ThemedText>
<ThemedText style={styles.emptySubtext}></ThemedText>
<Pressable onPress={() => router.push('/training-plan/create' as any)} style={styles.primaryBtn}>
<ThemedText style={styles.primaryBtnText}></ThemedText>
</Pressable>
</Animated.View>
) : (
<View style={styles.plansList}>
{plans.map((p, index) => (
<PlanCard
key={p.id}
plan={p}
index={index}
isActive={p.isActive}
onPress={() => handlePlanSelect(p)}
onDelete={() => dispatch(deletePlan(p.id))}
onActivate={() => handleActivate(p.id)}
onSchedule={() => handlePlanSelect(p)}
/>
))}
{loading && (
<Animated.View entering={FadeInUp.duration(400)} style={styles.loadingIndicator}>
<ThemedText style={styles.loadingIndicatorText}>...</ThemedText>
</Animated.View>
)}
</View>
)}
<View style={{ height: 100 }} />
</ScrollView>
);
// 渲染排课页面
const renderSchedulePage = () => {
if (!selectedPlan) {
return (
<View style={styles.noSelectionContainer}>
<View style={styles.noSelectionIcon}>
<ThemedText style={styles.noSelectionIconText}>📅</ThemedText>
</View>
<ThemedText style={styles.noSelectionText}></ThemedText>
<ThemedText style={styles.noSelectionSubtext}></ThemedText>
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: palette.primary }]}
onPress={() => setActiveTab('list')}
>
<ThemedText style={styles.primaryBtnText}></ThemedText>
</TouchableOpacity>
</View>
);
}
const goalConfig = GOAL_TEXT[selectedPlan.goal] || { title: '训练计划', color: palette.primary, description: '开始你的训练之旅' };
return (
<View style={styles.scheduleContent}>
{/* 计划信息头部 */}
<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}>{selectedPlan.name || goalConfig.title}</ThemedText>
<ThemedText style={styles.planDescription}>{goalConfig.description}</ThemedText>
</View>
</Animated.View>
{/* 操作按钮区域 */}
<View style={styles.actionRow}>
<TouchableOpacity
style={[styles.scheduleActionBtn, { backgroundColor: goalConfig.color }]}
onPress={handleAddExercise}
>
<Ionicons name="add" size={16} color="#FFFFFF" style={{ marginRight: 4 }} />
<Text style={styles.scheduleActionBtnText}></Text>
</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> */}
</View>
{/* 动作列表 */}
<FlatList
data={exercises}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.scheduleListContent}
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.id)}
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.exercise?.categoryName || '运动'}</ThemedText>
<ThemedText style={styles.exerciseMeta}>
{item.sets || 1}
{item.reps ? ` · 每组 ${item.reps}` : ''}
{item.durationSec ? ` · 每组 ${item.durationSec}s` : ''}
</ThemedText>
</View>
<View style={styles.exerciseActions}>
<TouchableOpacity
style={styles.completeBtn}
onPress={() => handleToggleCompleted(item.id, item.completed)}
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.id)}
>
<Text style={styles.removeBtnText}></Text>
</TouchableOpacity>
</View>
</View>
</Animated.View>
);
}}
/>
</View>
);
};
return (
<View style={styles.safeArea}>
{/* 动态背景 */}
<DynamicBackground />
<SafeAreaView style={styles.contentWrapper}>
<HeaderBar
title={activeTab === 'list' ? '训练计划' : '锻炼排期'}
onBack={() => router.back()}
withSafeTop={false}
tone='light'
transparent={true}
right={
activeTab === 'list' ? (
<TouchableOpacity onPress={() => router.push('/training-plan/create' as any)} style={styles.headerRightBtn}>
<ThemedText style={styles.headerRightBtnText}>+ </ThemedText>
</TouchableOpacity>
) : undefined
}
/>
<View style={styles.mainContent}>
{activeTab === 'list' ? renderPlansList() : renderSchedulePage()}
</View>
{/* 底部 Tab */}
<BottomTabs
activeTab={activeTab}
onTabChange={handleTabChange}
selectedPlan={selectedPlan}
/>
{/* 一键排课配置弹窗 */}
{selectedPlan && (
<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: GOAL_TEXT[selectedPlan.goal]?.color || palette.primary }
]}
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: GOAL_TEXT[selectedPlan.goal]?.color || palette.primary }]}
onPress={onGenerate}
>
<Text style={styles.generateBtnText}></Text>
</TouchableOpacity>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
)}
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
contentWrapper: {
flex: 1,
},
content: {
paddingHorizontal: 20,
paddingTop: 8,
},
// 动态背景
backgroundOrb: {
position: 'absolute',
width: 300,
height: 300,
borderRadius: 150,
backgroundColor: 'rgba(187,242,70,0.15)',
top: -150,
right: -100,
},
backgroundOrb2: {
position: 'absolute',
width: 400,
height: 400,
borderRadius: 200,
backgroundColor: 'rgba(164,138,237,0.12)',
bottom: -200,
left: -150,
},
// 页面标题区域
headerSection: {
marginBottom: 20,
},
title: {
fontSize: 28,
fontWeight: '800',
color: '#192126',
lineHeight: 34,
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#5E6468',
opacity: 0.8,
},
// 训练计划列表
plansList: {
gap: 10,
},
// 训练计划卡片
planCard: {
borderRadius: 28,
overflow: 'hidden',
shadowOffset: { width: 0, height: 10 },
shadowRadius: 20,
elevation: 8,
borderWidth: 1,
borderColor: '#3A261B',
backgroundColor: '#1F1410',
shadowColor: '#000',
},
cardContent: {
position: 'relative',
},
darkCard: {
backgroundColor: '#1F1410',
padding: 24,
borderRadius: 28,
},
cardTintGradient: {
...StyleSheet.absoluteFillObject,
borderRadius: 28,
},
cardGradient: {
...StyleSheet.absoluteFillObject,
borderRadius: 14,
},
cardGlow: {
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
borderRadius: 18,
backgroundColor: 'transparent',
},
colorIndicator: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 4,
borderTopLeftRadius: 16,
borderBottomLeftRadius: 16,
},
cardMain: {
padding: 16,
paddingLeft: 20,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
titleSection: {
flex: 1,
},
planTitle: {
fontSize: 17,
fontWeight: '800',
color: '#1A1E23',
marginBottom: 2,
},
gradientTitle: {
fontSize: 34,
fontWeight: '800',
lineHeight: 40,
color: '#FFFFFF',
},
planDescription: {
fontSize: 12,
color: '#6A5E58',
opacity: 0.9,
},
darkSubtitle: {
marginTop: 16,
fontSize: 16,
color: '#E0D2C9',
lineHeight: 24,
},
activeBadge: {
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 10,
marginLeft: 8,
},
activeText: {
fontSize: 10,
fontWeight: '800',
color: palette.ink,
},
cardInfo: {
flexDirection: 'row',
gap: 16,
},
infoItem: {
flex: 1,
},
infoLabel: {
fontSize: 10,
color: '#8A7F78',
marginBottom: 1,
fontWeight: '600',
},
infoValue: {
fontSize: 13,
color: '#2F2A26',
fontWeight: '700',
},
metricsRow: {
marginTop: 28,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
metricItem: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
borderRadius: 20,
},
metricActive: {
backgroundColor: 'rgba(255,255,255,0.06)',
},
metricText: {
marginLeft: 8,
color: '#E6EEF2',
fontSize: 16,
fontWeight: '700',
},
// 操作按钮区域
actionButtons: {
flexDirection: 'row',
marginTop: 10,
gap: 6,
},
actionButton: {
flex: 1,
paddingVertical: 6,
paddingHorizontal: 8,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
scheduleButton: {
backgroundColor: 'transparent',
borderWidth: 1,
},
activateButton: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.12,
shadowRadius: 3,
elevation: 3,
},
activeIndicator: {
borderWidth: 1.5,
},
actionButtonText: {
fontSize: 11,
fontWeight: '700',
},
activateButtonText: {
fontSize: 11,
fontWeight: '700',
color: '#FFFFFF',
},
activeIndicatorText: {
fontSize: 11,
fontWeight: '700',
},
deleteButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#ED4747',
},
deleteButtonText: {
fontSize: 11,
fontWeight: '700',
color: '#ED4747',
},
// 按钮样式
primaryBtn: {
marginTop: 20,
backgroundColor: palette.primary,
paddingVertical: 14,
paddingHorizontal: 28,
borderRadius: 24,
alignItems: 'center',
shadowColor: palette.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6,
},
primaryBtnText: {
color: palette.ink,
fontSize: 15,
fontWeight: '800',
},
createBtn: {
backgroundColor: palette.primary,
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 22,
shadowColor: palette.primary,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 4,
minWidth: 44,
minHeight: 44,
alignItems: 'center',
justifyContent: 'center',
},
createBtnText: {
color: palette.ink,
fontWeight: '800',
fontSize: 14,
},
// 空状态
emptyWrap: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyIcon: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: 'rgba(187,242,70,0.1)',
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',
marginBottom: 20,
},
// 加载状态
loadingWrap: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
loadingIcon: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: 'rgba(187,242,70,0.1)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
loadingIconText: {
fontSize: 32,
},
loadingText: {
fontSize: 18,
color: '#192126',
fontWeight: '600',
marginBottom: 4,
},
loadingIndicator: {
alignItems: 'center',
paddingVertical: 20,
},
loadingIndicatorText: {
fontSize: 14,
color: '#5E6468',
fontWeight: '600',
},
// 错误状态
errorContainer: {
backgroundColor: 'rgba(237,71,71,0.1)',
borderRadius: 12,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: 'rgba(237,71,71,0.2)',
},
errorText: {
fontSize: 14,
color: '#ED4747',
fontWeight: '600',
textAlign: 'center',
},
// 底部 Tab 样式(与主页一致)
bottomTabContainer: {
position: 'absolute',
bottom: 20, // TAB_BAR_BOTTOM_OFFSET
left: 0,
right: 0,
paddingHorizontal: 20,
},
bottomTabBar: {
flexDirection: 'row',
height: 68, // TAB_BAR_HEIGHT
borderRadius: 34,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 10,
elevation: 5,
paddingHorizontal: 10,
paddingTop: 0,
paddingBottom: 0,
marginHorizontal: 0,
width: '100%',
alignSelf: 'center',
},
tabButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 6,
marginVertical: 10,
borderRadius: 25,
paddingHorizontal: 16,
paddingVertical: 8,
},
tabText: {
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
},
// 主内容区域
mainContent: {
flex: 1,
paddingBottom: 60, // 为底部 tab 留出空间
},
// 排课页面样式
scheduleContent: {
flex: 1,
paddingHorizontal: 20,
},
planHeader: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 16,
marginBottom: 16,
},
planColorIndicator: {
width: 4,
height: 40,
borderRadius: 2,
marginRight: 12,
},
planInfo: {
flex: 1,
},
// 排课操作按钮
actionRow: {
flexDirection: 'row',
gap: 12,
marginBottom: 20,
},
scheduleActionBtn: {
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,
},
scheduleActionBtnText: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '700',
},
scheduleSecondaryBtn: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
borderRadius: 12,
borderWidth: 1.5,
backgroundColor: '#FFFFFF',
},
scheduleSecondaryBtnText: {
fontSize: 14,
fontWeight: '700',
},
// 排课列表
scheduleListContent: {
paddingBottom: 40,
},
// 动作卡片
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,
},
// 空状态(排课页面)
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
// 未选择计划状态
noSelectionContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
paddingHorizontal: 20,
},
noSelectionIcon: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: 'rgba(187,242,70,0.1)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
noSelectionIconText: {
fontSize: 32,
},
noSelectionText: {
fontSize: 18,
color: '#192126',
fontWeight: '600',
marginBottom: 4,
textAlign: 'center',
},
noSelectionSubtext: {
fontSize: 14,
color: '#5E6468',
textAlign: 'center',
marginBottom: 20,
},
// 弹窗样式
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,
},
// 顶部导航右侧按钮(与 HeaderBar 标准尺寸一致,使用 tab 配色)
headerRightBtn: {
width: 52,
height: 32,
backgroundColor: palette.primary, // 使用 tab 的主色
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
headerRightBtnText: {
color: palette.ink,
fontWeight: '800',
fontSize: 10,
},
// 统计显示
statsContainer: {
paddingHorizontal: 12,
paddingVertical: 4,
backgroundColor: 'rgba(187,242,70,0.2)',
borderRadius: 16,
},
statsText: {
fontSize: 12,
fontWeight: '800',
color: palette.ink,
},
});