- 在训练计划中添加了新的类型定义,优化了排课功能 - 修改了今日训练页面的布局,提升用户体验 - 删除了不再使用的排课相关文件,简化代码结构 - 更新了 Redux 状态管理,确保数据处理的准确性和稳定性
1412 lines
40 KiB
TypeScript
1412 lines
40 KiB
TypeScript
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 {
|
||
addExercise,
|
||
clearExercises,
|
||
clearError as clearScheduleError,
|
||
deleteExercise,
|
||
loadExercises,
|
||
toggleCompletion
|
||
} from '@/store/scheduleExerciseSlice';
|
||
import { activatePlan, clearError, deletePlan, loadPlans, type TrainingPlan } 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, currentId, loading, error } = useAppSelector((s) => s.trainingPlan);
|
||
const { exercises, error: scheduleError } = useAppSelector((s) => s.scheduleExercise);
|
||
|
||
// 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 || currentId || 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]);
|
||
|
||
useEffect(() => {
|
||
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.id === currentId) || 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.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.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: 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,
|
||
},
|
||
|
||
// 统计显示
|
||
statsContainer: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 4,
|
||
backgroundColor: 'rgba(187,242,70,0.2)',
|
||
borderRadius: 16,
|
||
},
|
||
statsText: {
|
||
fontSize: 12,
|
||
fontWeight: '800',
|
||
color: palette.ink,
|
||
},
|
||
});
|