Files
digital-pilates/app/training-plan.tsx
richarjiang f95401c1ce feat: 添加 BMI 计算和训练计划排课功能
- 新增 BMI 计算工具,支持用户输入体重和身高计算 BMI 值,并根据结果提供分类和建议
- 在训练计划中集成排课功能,允许用户选择和安排训练动作
- 更新个人信息页面,添加出生日期字段,支持用户完善个人资料
- 优化训练计划卡片样式,提升用户体验
- 更新相关依赖,确保项目兼容性和功能完整性
2025-08-15 10:45:37 +08:00

1466 lines
42 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 { useLocalSearchParams, useRouter } from 'expo-router';
import React, { 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 { activatePlan, clearError, deletePlan, loadPlans, type TrainingPlan } from '@/store/trainingPlanSlice';
import { buildClassicalSession } from '@/utils/classicalSession';
// 训练计划排课项目类型
export interface ScheduleExercise {
key: string;
name: string;
category: string;
sets: number;
reps?: number;
durationSec?: number;
restSec?: number;
note?: string;
itemType?: 'exercise' | 'rest' | 'note';
completed?: boolean;
}
// 训练计划排课数据
export interface PlanSchedule {
planId: string;
exercises: ScheduleExercise[];
note?: string;
lastModified: string;
}
// Tab 类型定义
type TabType = 'list' | 'schedule';
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; newExercise?: string }>();
const { plans, currentId, loading, error } = useAppSelector((s) => s.trainingPlan);
// Tab 状态管理
const [activeTab, setActiveTab] = useState<TabType>('list');
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(params.planId || currentId || null);
// 排课相关状态
const [exercises, setExercises] = useState<ScheduleExercise[]>([]);
const [scheduleNote, setScheduleNote] = useState('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// 一键排课配置
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]);
// 模拟加载排课数据的函数
const loadScheduleData = async (planId: string): Promise<PlanSchedule | null> => {
// 模拟 API 调用延迟
await new Promise(resolve => setTimeout(resolve, 300));
// 模拟数据 - 在实际应用中,这里应该从后端或本地存储获取数据
const mockData: Record<string, PlanSchedule> = {
// 示例数据结构,实际应用中应从服务器或本地存储获取
// 'plan1': {
// planId: 'plan1',
// exercises: [...],
// note: '示例备注',
// lastModified: new Date().toISOString()
// }
};
return mockData[planId] || null;
};
// 监听 selectedPlan 变化,加载对应的排课数据
useEffect(() => {
const loadSchedule = async () => {
if (selectedPlan) {
try {
const scheduleData = await loadScheduleData(selectedPlan.id);
if (scheduleData) {
setExercises(scheduleData.exercises);
setScheduleNote(scheduleData.note || '');
} else {
// 如果没有保存的排课数据,重置为默认空状态
setExercises([]);
setScheduleNote('');
}
} catch (error) {
console.error('加载排课数据失败:', error);
// 出错时重置为默认空状态
setExercises([]);
setScheduleNote('');
}
} else {
// 没有选中计划时,重置为默认空状态
setExercises([]);
setScheduleNote('');
}
};
loadSchedule();
}, [selectedPlan]);
useEffect(() => {
dispatch(loadPlans());
}, [dispatch]);
useEffect(() => {
if (error) {
console.error('训练计划错误:', error);
const timer = setTimeout(() => {
dispatch(clearError());
}, 3000);
return () => clearTimeout(timer);
}
}, [error, dispatch]);
// 处理从选择页面传回的新动作
useEffect(() => {
if (params.newExercise) {
try {
const newExercise: ScheduleExercise = JSON.parse(params.newExercise);
setExercises(prev => [...prev, newExercise]);
setHasUnsavedChanges(true);
router.setParams({ newExercise: undefined } as any);
} catch (error) {
console.error('解析新动作数据失败:', error);
}
}
}, [params.newExercise]);
const handleActivate = async (planId: string) => {
try {
await dispatch(activatePlan(planId));
} catch (error) {
console.error('激活训练计划失败:', error);
}
}
const handlePlanSelect = (plan: TrainingPlan) => {
setSelectedPlanId(plan.id);
setActiveTab('schedule');
// TODO: 加载该计划的排课数据
}
const handleTabChange = (tab: TabType) => {
if (tab === 'schedule' && !selectedPlanId && plans.length > 0) {
// 如果没有选中计划但要切换到排课页面,自动选择当前激活的计划或第一个计划
const targetPlan = plans.find(p => p.id === currentId) || plans[0];
setSelectedPlanId(targetPlan.id);
}
setActiveTab(tab);
}
// 排课相关方法
const handleSave = async () => {
if (!selectedPlan) return;
try {
const scheduleData: PlanSchedule = {
planId: selectedPlan.id,
exercises,
note: scheduleNote,
lastModified: new Date().toISOString(),
};
console.log('保存排课数据:', scheduleData);
setHasUnsavedChanges(false);
Alert.alert('保存成功', '训练计划排课已保存');
} catch (error) {
console.error('保存排课失败:', error);
Alert.alert('保存失败', '请稍后重试');
}
};
const handleAddExercise = () => {
router.push(`/training-plan/schedule/select?planId=${selectedPlanId}` as any);
};
const handleRemoveExercise = (key: string) => {
Alert.alert('确认移除', '确定要移除该动作吗?', [
{ text: '取消', style: 'cancel' },
{
text: '移除',
style: 'destructive',
onPress: () => {
setExercises(prev => prev.filter(ex => ex.key !== key));
setHasUnsavedChanges(true);
},
},
]);
};
const handleToggleCompleted = (key: string) => {
setExercises(prev => prev.map(ex =>
ex.key === key ? { ...ex, completed: !ex.completed } : ex
));
setHasUnsavedChanges(true);
};
const onGenerate = () => {
const restSec = Math.max(10, Math.min(120, parseInt(genRest || '30', 10)));
const { items, note } = buildClassicalSession({
withSectionRests: genWithRests,
restSeconds: restSec,
withNotes: genWithNotes,
level: genLevel
});
const scheduleItems: ScheduleExercise[] = items.map((item, index) => ({
key: `generated_${Date.now()}_${index}`,
name: item.name,
category: item.category,
sets: item.sets,
reps: item.reps,
durationSec: item.durationSec,
restSec: item.restSec,
note: item.note,
itemType: item.itemType,
completed: false,
}));
setExercises(scheduleItems);
setScheduleNote(note || '');
setHasUnsavedChanges(true);
setGenVisible(false);
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 && (
<Animated.View entering={FadeInUp.duration(400)} style={styles.errorContainer}>
<ThemedText style={styles.errorText}> {error}</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.id === currentId}
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.key}
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.key)}
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.category}</ThemedText>
<ThemedText style={styles.exerciseMeta}>
{item.sets}
{item.reps ? ` · 每组 ${item.reps}` : ''}
{item.durationSec ? ` · 每组 ${item.durationSec}s` : ''}
</ThemedText>
</View>
<View style={styles.exerciseActions}>
<TouchableOpacity
style={styles.completeBtn}
onPress={() => handleToggleCompleted(item.key)}
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.key)}
>
<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>
) : hasUnsavedChanges ? (
<TouchableOpacity onPress={handleSave} 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: 100, // 为底部 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,
},
});