- 新增 BMI 计算工具,支持用户输入体重和身高计算 BMI 值,并根据结果提供分类和建议 - 在训练计划中集成排课功能,允许用户选择和安排训练动作 - 更新个人信息页面,添加出生日期字段,支持用户完善个人资料 - 优化训练计划卡片样式,提升用户体验 - 更新相关依赖,确保项目兼容性和功能完整性
724 lines
24 KiB
TypeScript
724 lines
24 KiB
TypeScript
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { palette } from '@/constants/Colors';
|
||
import { useAppSelector } from '@/hooks/redux';
|
||
import { fetchExerciseConfig, normalizeToLibraryItems } from '@/services/exercises';
|
||
import { EXERCISE_LIBRARY, getCategories } from '@/utils/exerciseLibrary';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||
import * as Haptics from 'expo-haptics';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import { Animated, FlatList, LayoutAnimation, Modal, Platform, SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, UIManager, View } from 'react-native';
|
||
|
||
import { ThemedText } from '@/components/ThemedText';
|
||
import type { ScheduleExercise } from './index';
|
||
|
||
const GOAL_TEXT: Record<string, { title: string; color: string; description: string }> = {
|
||
postpartum_recovery: { title: '产后恢复', color: '#9BE370', description: '温和激活,核心重建' },
|
||
fat_loss: { title: '减脂塑形', color: '#FFB86B', description: '全身燃脂,线条雕刻' },
|
||
posture_correction: { title: '体态矫正', color: '#95CCE3', description: '打开胸肩,改善圆肩驼背' },
|
||
core_strength: { title: '核心力量', color: '#A48AED', description: '核心稳定,提升运动表现' },
|
||
flexibility: { title: '柔韧灵活', color: '#B0F2A7', description: '拉伸延展,释放紧张' },
|
||
rehab: { title: '康复保健', color: '#FF8E9E', description: '循序渐进,科学修复' },
|
||
stress_relief: { title: '释压放松', color: '#9BD1FF', description: '舒缓身心,改善睡眠' },
|
||
};
|
||
|
||
// 动态背景组件
|
||
function DynamicBackground({ color }: { color: string }) {
|
||
return (
|
||
<View style={StyleSheet.absoluteFillObject}>
|
||
<LinearGradient
|
||
colors={['#F9FBF2', '#FFFFFF', '#F5F9F0']}
|
||
style={StyleSheet.absoluteFillObject}
|
||
/>
|
||
<View style={[styles.backgroundOrb, { backgroundColor: `${color}15` }]} />
|
||
<View style={[styles.backgroundOrb2, { backgroundColor: `${color}10` }]} />
|
||
</View>
|
||
);
|
||
}
|
||
|
||
export default function SelectExerciseForScheduleScreen() {
|
||
const router = useRouter();
|
||
const params = useLocalSearchParams<{ planId?: string }>();
|
||
const { plans } = useAppSelector((s) => s.trainingPlan);
|
||
|
||
const planId = params.planId;
|
||
const plan = useMemo(() => plans.find(p => p.id === planId), [plans, planId]);
|
||
const goalConfig = plan ? (GOAL_TEXT[plan.goal] || { title: '训练计划', color: palette.primary, description: '开始你的训练之旅' }) : null;
|
||
|
||
const [keyword, setKeyword] = useState('');
|
||
const [category, setCategory] = useState<string>('全部');
|
||
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||
const [sets, setSets] = useState(3);
|
||
const [reps, setReps] = useState<number | undefined>(undefined);
|
||
const [showCustomReps, setShowCustomReps] = useState(false);
|
||
const [customRepsInput, setCustomRepsInput] = useState('');
|
||
const [showCategoryPicker, setShowCategoryPicker] = useState(false);
|
||
const [serverLibrary, setServerLibrary] = useState<{ key: string; name: string; description: string; category: string }[] | null>(null);
|
||
const [serverCategories, setServerCategories] = useState<string[] | null>(null);
|
||
|
||
const controlsOpacity = useRef(new Animated.Value(0)).current;
|
||
|
||
useEffect(() => {
|
||
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
let aborted = false;
|
||
const CACHE_KEY = '@exercise_config_v1';
|
||
(async () => {
|
||
try {
|
||
const cached = await AsyncStorage.getItem(CACHE_KEY);
|
||
if (cached && !aborted) {
|
||
const parsed = JSON.parse(cached);
|
||
const items = normalizeToLibraryItems(parsed);
|
||
if (items.length) {
|
||
setServerLibrary(items);
|
||
const cats = Array.from(new Set(items.map((i) => i.category)));
|
||
setServerCategories(cats);
|
||
}
|
||
}
|
||
} catch { }
|
||
try {
|
||
const resp = await fetchExerciseConfig();
|
||
console.log('fetchExerciseConfig', resp);
|
||
if (aborted) return;
|
||
const items = normalizeToLibraryItems(resp);
|
||
setServerLibrary(items);
|
||
const cats = Array.from(new Set(items.map((i) => i.category)));
|
||
setServerCategories(cats);
|
||
try { await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(resp)); } catch { }
|
||
} catch (err) { }
|
||
})();
|
||
return () => { aborted = true; };
|
||
}, []);
|
||
|
||
const categories = useMemo(() => {
|
||
const base = serverCategories ?? getCategories();
|
||
return ['全部', ...base];
|
||
}, [serverCategories]);
|
||
|
||
const mainCategories = useMemo(() => {
|
||
const preferred = ['全部', '核心与腹部', '脊柱与后链', '侧链与髋', '平衡与支撑'];
|
||
const exists = (name: string) => categories.includes(name);
|
||
const picked = preferred.filter(exists);
|
||
const rest = categories.filter((c) => !picked.includes(c));
|
||
while (picked.length < 5 && rest.length) picked.push(rest.shift() as string);
|
||
return picked;
|
||
}, [categories]);
|
||
|
||
const library = useMemo(() => serverLibrary ?? EXERCISE_LIBRARY, [serverLibrary]);
|
||
|
||
const filtered = useMemo(() => {
|
||
const kw = keyword.trim().toLowerCase();
|
||
const base = kw
|
||
? library.filter((e) => e.name.toLowerCase().includes(kw) || (e.description || '').toLowerCase().includes(kw))
|
||
: library;
|
||
if (category === '全部') return base;
|
||
return base.filter((e) => e.category === category);
|
||
}, [keyword, category, library]);
|
||
|
||
const selected = useMemo(() => library.find((e) => e.key === selectedKey) || null, [selectedKey, library]);
|
||
|
||
useEffect(() => {
|
||
Animated.timing(controlsOpacity, {
|
||
toValue: selected ? 1 : 0,
|
||
duration: selected ? 220 : 160,
|
||
useNativeDriver: true,
|
||
}).start();
|
||
}, [selected, controlsOpacity]);
|
||
|
||
const handleAdd = () => {
|
||
if (!selected || !plan) return;
|
||
|
||
const exerciseData: ScheduleExercise = {
|
||
key: `${selected.key}_${Date.now()}`,
|
||
name: selected.name,
|
||
category: selected.category,
|
||
sets: Math.max(1, sets),
|
||
reps: reps && reps > 0 ? reps : undefined,
|
||
itemType: 'exercise',
|
||
completed: false,
|
||
};
|
||
|
||
console.log('添加动作到排课:', exerciseData);
|
||
|
||
// 通过路由参数传递数据回到排课页面
|
||
router.push({
|
||
pathname: '/training-plan/schedule',
|
||
params: {
|
||
planId: planId,
|
||
newExercise: JSON.stringify(exerciseData)
|
||
}
|
||
} as any);
|
||
};
|
||
|
||
const onSelectItem = (key: string) => {
|
||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||
if (selectedKey === key) {
|
||
setSelectedKey(null);
|
||
return;
|
||
}
|
||
setSets(3);
|
||
setReps(undefined);
|
||
setShowCustomReps(false);
|
||
setCustomRepsInput('');
|
||
setSelectedKey(key);
|
||
};
|
||
|
||
if (!plan || !goalConfig) {
|
||
return (
|
||
<SafeAreaView style={styles.safeArea}>
|
||
<HeaderBar title="选择动作" onBack={() => router.back()} />
|
||
<View style={styles.errorContainer}>
|
||
<ThemedText style={styles.errorText}>找不到指定的训练计划</ThemedText>
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<View style={styles.safeArea}>
|
||
{/* 动态背景 */}
|
||
<DynamicBackground color={goalConfig.color} />
|
||
|
||
<SafeAreaView style={styles.contentWrapper}>
|
||
<HeaderBar
|
||
title="选择动作"
|
||
onBack={() => router.back()}
|
||
withSafeTop={false}
|
||
transparent={true}
|
||
tone="light"
|
||
/>
|
||
|
||
<View style={styles.content}>
|
||
{/* 计划信息头部 */}
|
||
<View style={[styles.planHeader, { backgroundColor: `${goalConfig.color}20` }]}>
|
||
<View style={[styles.planColorIndicator, { backgroundColor: goalConfig.color }]} />
|
||
<View style={styles.planInfo}>
|
||
<ThemedText style={styles.planTitle}>{goalConfig.title}</ThemedText>
|
||
<ThemedText style={styles.planDescription}>从动作库里选择一个动作,设置组数与每组次数</ThemedText>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 大分类宫格 */}
|
||
<View style={styles.catGrid}>
|
||
{[...mainCategories, '更多'].map((item) => {
|
||
const active = category === item;
|
||
const meta: Record<string, { bg: string }> = {
|
||
全部: { bg: `${goalConfig.color}22` },
|
||
核心与腹部: { bg: `${goalConfig.color}18` },
|
||
脊柱与后链: { bg: 'rgba(149,204,227,0.20)' },
|
||
侧链与髋: { bg: 'rgba(164,138,237,0.20)' },
|
||
平衡与支撑: { bg: 'rgba(252,196,111,0.22)' },
|
||
进阶控制: { bg: 'rgba(237,71,71,0.18)' },
|
||
柔韧与拉伸: { bg: 'rgba(149,204,227,0.18)' },
|
||
更多: { bg: 'rgba(24,24,27,0.06)' },
|
||
};
|
||
const scale = new Animated.Value(1);
|
||
const onPressIn = () => Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, speed: 20, bounciness: 6 }).start();
|
||
const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 20, bounciness: 6 }).start();
|
||
const handlePress = () => {
|
||
onPressOut();
|
||
if (item === '更多') {
|
||
setShowCategoryPicker(true);
|
||
Haptics.selectionAsync();
|
||
} else {
|
||
setCategory(item);
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||
}
|
||
};
|
||
return (
|
||
<Animated.View key={item} style={[styles.catTileWrapper, { transform: [{ scale }] }]}>
|
||
<TouchableOpacity
|
||
activeOpacity={0.9}
|
||
onPressIn={onPressIn}
|
||
onPressOut={handlePress}
|
||
style={[
|
||
styles.catTile,
|
||
{ backgroundColor: meta[item]?.bg ?? 'rgba(24,24,27,0.06)' },
|
||
active && { borderWidth: 2, borderColor: goalConfig.color }
|
||
]}
|
||
>
|
||
<Text style={[
|
||
styles.catText,
|
||
{ color: active ? goalConfig.color : '#384046' },
|
||
active && { fontWeight: '800' }
|
||
]}>
|
||
{item}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
</Animated.View>
|
||
);
|
||
})}
|
||
</View>
|
||
|
||
{/* 分类选择弹层 */}
|
||
<Modal
|
||
visible={showCategoryPicker}
|
||
animationType="fade"
|
||
transparent
|
||
onRequestClose={() => setShowCategoryPicker(false)}
|
||
>
|
||
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setShowCategoryPicker(false)}>
|
||
<TouchableOpacity activeOpacity={1} style={styles.modalSheet}
|
||
onPress={(e) => e.stopPropagation() as any}
|
||
>
|
||
<Text style={styles.modalTitle}>选择分类</Text>
|
||
<View style={styles.catGridModal}>
|
||
{categories.filter((c) => c !== '全部').map((c) => {
|
||
const scale = new Animated.Value(1);
|
||
const onPressIn = () => Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, speed: 20, bounciness: 6 }).start();
|
||
const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 20, bounciness: 6 }).start();
|
||
return (
|
||
<Animated.View key={c} style={[styles.catTileWrapper, { transform: [{ scale }] }]}>
|
||
<TouchableOpacity
|
||
onPressIn={onPressIn}
|
||
onPressOut={() => {
|
||
onPressOut();
|
||
setCategory(c);
|
||
setShowCategoryPicker(false);
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||
}}
|
||
activeOpacity={0.9}
|
||
style={[styles.catTile, { backgroundColor: 'rgba(24,24,27,0.06)' }]}
|
||
>
|
||
<Text style={styles.catText}>{c}</Text>
|
||
</TouchableOpacity>
|
||
</Animated.View>
|
||
);
|
||
})}
|
||
</View>
|
||
</TouchableOpacity>
|
||
</TouchableOpacity>
|
||
</Modal>
|
||
|
||
{/* 搜索框 */}
|
||
<View style={styles.searchRow}>
|
||
<TextInput
|
||
value={keyword}
|
||
onChangeText={setKeyword}
|
||
placeholder="搜索动作名称/要点"
|
||
placeholderTextColor="#888F92"
|
||
style={[styles.searchInput, { borderColor: `${goalConfig.color}30` }]}
|
||
/>
|
||
</View>
|
||
|
||
{/* 动作列表 */}
|
||
<FlatList
|
||
data={filtered}
|
||
keyExtractor={(item) => item.key}
|
||
contentContainerStyle={styles.listContent}
|
||
showsVerticalScrollIndicator={false}
|
||
renderItem={({ item }) => {
|
||
const isSelected = item.key === selectedKey;
|
||
return (
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.itemCard,
|
||
isSelected && { borderWidth: 2, borderColor: goalConfig.color },
|
||
]}
|
||
onPress={() => onSelectItem(item.key)}
|
||
activeOpacity={0.9}
|
||
>
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={styles.itemTitle}>{item.name}</Text>
|
||
<Text style={styles.itemMeta}>{item.category}</Text>
|
||
<Text style={styles.itemDesc}>{item.description}</Text>
|
||
</View>
|
||
{isSelected && <Ionicons name="chevron-down" size={20} color={goalConfig.color} />}
|
||
{isSelected && (
|
||
<Animated.View style={[styles.expandedBox, { opacity: controlsOpacity }]}>
|
||
<View style={styles.controlsRow}>
|
||
<View style={styles.counterBox}>
|
||
<Text style={styles.counterLabel}>组数</Text>
|
||
<View style={styles.counterRow}>
|
||
<TouchableOpacity
|
||
style={styles.counterBtn}
|
||
onPress={() => setSets(Math.max(1, sets - 1))}
|
||
>
|
||
<Text style={styles.counterBtnText}>-</Text>
|
||
</TouchableOpacity>
|
||
<Text style={styles.counterValue}>{sets}</Text>
|
||
<TouchableOpacity
|
||
style={styles.counterBtn}
|
||
onPress={() => setSets(Math.min(20, sets + 1))}
|
||
>
|
||
<Text style={styles.counterBtnText}>+</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.counterBox}>
|
||
<Text style={styles.counterLabel}>每组次数</Text>
|
||
<View style={styles.repsChipsRow}>
|
||
{[6, 8, 10, 12, 15, 20, 25, 30].map((v) => {
|
||
const active = reps === v;
|
||
return (
|
||
<TouchableOpacity
|
||
key={v}
|
||
style={[
|
||
styles.repChip,
|
||
active && { backgroundColor: goalConfig.color, borderColor: goalConfig.color }
|
||
]}
|
||
onPress={() => {
|
||
setReps(v);
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||
}}
|
||
>
|
||
<Text style={[styles.repChipText, active && { color: '#FFFFFF' }]}>{v}</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
<TouchableOpacity
|
||
style={styles.repChipGhost}
|
||
onPress={() => {
|
||
setShowCustomReps((s) => !s);
|
||
Haptics.selectionAsync();
|
||
}}
|
||
>
|
||
<Text style={styles.repChipGhostText}>自定义</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
{showCustomReps && (
|
||
<View style={styles.customRepsRow}>
|
||
<TextInput
|
||
keyboardType="number-pad"
|
||
value={customRepsInput}
|
||
onChangeText={setCustomRepsInput}
|
||
placeholder="输入次数 (1-100)"
|
||
placeholderTextColor="#888F92"
|
||
style={styles.customRepsInput}
|
||
/>
|
||
<TouchableOpacity
|
||
style={[styles.customRepsBtn, { backgroundColor: goalConfig.color }]}
|
||
onPress={() => {
|
||
const n = Math.max(1, Math.min(100, parseInt(customRepsInput || '0', 10)));
|
||
if (!Number.isNaN(n)) {
|
||
setReps(n);
|
||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||
}
|
||
}}
|
||
>
|
||
<Text style={styles.customRepsBtnText}>确定</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.addBtn,
|
||
{ backgroundColor: goalConfig.color },
|
||
(!reps || reps <= 0) && { opacity: 0.5 }
|
||
]}
|
||
disabled={!reps || reps <= 0}
|
||
onPress={handleAdd}
|
||
>
|
||
<Text style={styles.addBtnText}>添加到训练计划</Text>
|
||
</TouchableOpacity>
|
||
</Animated.View>
|
||
)}
|
||
</TouchableOpacity>
|
||
);
|
||
}}
|
||
/>
|
||
</View>
|
||
</SafeAreaView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
safeArea: {
|
||
flex: 1,
|
||
},
|
||
contentWrapper: {
|
||
flex: 1,
|
||
},
|
||
content: {
|
||
flex: 1,
|
||
paddingHorizontal: 20,
|
||
},
|
||
|
||
// 动态背景
|
||
backgroundOrb: {
|
||
position: 'absolute',
|
||
width: 300,
|
||
height: 300,
|
||
borderRadius: 150,
|
||
top: -150,
|
||
right: -100,
|
||
},
|
||
backgroundOrb2: {
|
||
position: 'absolute',
|
||
width: 400,
|
||
height: 400,
|
||
borderRadius: 200,
|
||
bottom: -200,
|
||
left: -150,
|
||
},
|
||
|
||
// 计划信息头部
|
||
planHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
padding: 16,
|
||
borderRadius: 16,
|
||
marginBottom: 16,
|
||
},
|
||
planColorIndicator: {
|
||
width: 4,
|
||
height: 40,
|
||
borderRadius: 2,
|
||
marginRight: 12,
|
||
},
|
||
planInfo: {
|
||
flex: 1,
|
||
},
|
||
planTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
planDescription: {
|
||
fontSize: 13,
|
||
color: '#5E6468',
|
||
opacity: 0.8,
|
||
},
|
||
|
||
// 分类网格
|
||
catGrid: {
|
||
paddingTop: 10,
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
marginBottom: 16,
|
||
},
|
||
catTileWrapper: {
|
||
width: '33.33%',
|
||
padding: 6,
|
||
},
|
||
catTile: {
|
||
borderRadius: 14,
|
||
paddingVertical: 16,
|
||
paddingHorizontal: 8,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
catText: {
|
||
fontSize: 13,
|
||
fontWeight: '700',
|
||
color: '#384046',
|
||
},
|
||
|
||
// 搜索框
|
||
searchRow: {
|
||
marginBottom: 16,
|
||
},
|
||
searchInput: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 12,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 10,
|
||
color: '#384046',
|
||
borderWidth: 1,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 8,
|
||
shadowOffset: { width: 0, height: 2 },
|
||
elevation: 2,
|
||
},
|
||
|
||
// 列表
|
||
listContent: {
|
||
paddingBottom: 40,
|
||
},
|
||
|
||
// 动作卡片
|
||
itemCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 16,
|
||
marginBottom: 12,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 12,
|
||
shadowOffset: { width: 0, height: 6 },
|
||
elevation: 3,
|
||
},
|
||
itemTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
itemMeta: {
|
||
fontSize: 12,
|
||
color: '#888F92',
|
||
marginBottom: 4,
|
||
},
|
||
itemDesc: {
|
||
fontSize: 12,
|
||
color: '#5E6468',
|
||
lineHeight: 16,
|
||
},
|
||
|
||
// 展开的控制区域
|
||
expandedBox: {
|
||
marginTop: 12,
|
||
},
|
||
controlsRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-start',
|
||
gap: 12,
|
||
flexWrap: 'wrap',
|
||
marginBottom: 16,
|
||
},
|
||
counterBox: {
|
||
backgroundColor: '#F8F9FA',
|
||
borderRadius: 8,
|
||
padding: 12,
|
||
minWidth: 120,
|
||
},
|
||
counterLabel: {
|
||
fontSize: 10,
|
||
color: '#888F92',
|
||
marginBottom: 8,
|
||
fontWeight: '600',
|
||
},
|
||
counterRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
},
|
||
counterBtn: {
|
||
backgroundColor: '#E5E7EB',
|
||
width: 28,
|
||
height: 28,
|
||
borderRadius: 6,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
counterBtnText: {
|
||
fontWeight: '800',
|
||
color: '#384046',
|
||
},
|
||
counterValue: {
|
||
minWidth: 40,
|
||
textAlign: 'center',
|
||
fontWeight: '700',
|
||
color: '#384046',
|
||
},
|
||
repsChipsRow: {
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
gap: 8,
|
||
marginTop: 6,
|
||
},
|
||
repChip: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
borderRadius: 999,
|
||
backgroundColor: '#F3F4F6',
|
||
borderWidth: 1,
|
||
borderColor: '#E5E7EB',
|
||
},
|
||
repChipText: {
|
||
color: '#384046',
|
||
fontWeight: '700',
|
||
fontSize: 12,
|
||
},
|
||
repChipGhost: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
borderRadius: 999,
|
||
borderWidth: 1,
|
||
backgroundColor: 'transparent',
|
||
borderColor: '#E5E7EB',
|
||
},
|
||
repChipGhostText: {
|
||
fontWeight: '700',
|
||
color: '#384046',
|
||
fontSize: 12,
|
||
},
|
||
customRepsRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
marginTop: 8,
|
||
},
|
||
customRepsInput: {
|
||
flex: 1,
|
||
height: 40,
|
||
borderWidth: 1,
|
||
borderColor: '#E5E7EB',
|
||
borderRadius: 10,
|
||
paddingHorizontal: 12,
|
||
color: '#384046',
|
||
},
|
||
customRepsBtn: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 10,
|
||
borderRadius: 10,
|
||
},
|
||
customRepsBtnText: {
|
||
fontWeight: '800',
|
||
color: '#FFFFFF',
|
||
fontSize: 12,
|
||
},
|
||
addBtn: {
|
||
paddingVertical: 12,
|
||
borderRadius: 12,
|
||
alignItems: 'center',
|
||
},
|
||
addBtnText: {
|
||
color: '#FFFFFF',
|
||
fontWeight: '800',
|
||
fontSize: 14,
|
||
},
|
||
|
||
// 错误状态
|
||
errorContainer: {
|
||
flex: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: 20,
|
||
},
|
||
errorText: {
|
||
fontSize: 16,
|
||
color: '#ED4747',
|
||
fontWeight: '600',
|
||
},
|
||
|
||
// 弹窗样式
|
||
modalOverlay: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||
alignItems: 'center',
|
||
justifyContent: 'flex-end',
|
||
},
|
||
modalSheet: {
|
||
width: '100%',
|
||
backgroundColor: '#FFFFFF',
|
||
borderTopLeftRadius: 16,
|
||
borderTopRightRadius: 16,
|
||
paddingHorizontal: 16,
|
||
paddingTop: 14,
|
||
paddingBottom: 24,
|
||
},
|
||
modalTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '800',
|
||
marginBottom: 16,
|
||
color: '#192126',
|
||
},
|
||
catGridModal: {
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
},
|
||
}); |