feat: 更新应用图标和启动画面

- 将应用图标更改为 logo.jpeg,更新相关配置文件
- 删除旧的图标文件,确保资源整洁
- 更新启动画面使用新的 logo 图片,提升视觉一致性
- 在训练计划相关功能中集成新的 API 接口,支持训练计划的创建和管理
- 优化 Redux 状态管理,支持训练计划的加载和删除功能
- 更新样式以适应新图标和功能的展示
This commit is contained in:
richarjiang
2025-08-14 19:28:38 +08:00
parent 5d09cc05dc
commit 56d4c7fd7f
18 changed files with 1411 additions and 536 deletions

View File

@@ -165,6 +165,7 @@ export default function AICoachChatScreen() {
setKeyboardOffset(height);
} catch { setKeyboardOffset(0); }
});
hideSub = Keyboard.addListener('keyboardWillHide', () => setKeyboardOffset(0));
} else {
showSub = Keyboard.addListener('keyboardDidShow', (e: any) => {
try { setKeyboardOffset(Math.max(0, e.endCoordinates?.height ?? 0)); } catch { setKeyboardOffset(0); }

View File

@@ -4,6 +4,7 @@ import * as ImagePicker from 'expo-image-picker';
import { useRouter } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Image,
Linking,
@@ -12,13 +13,14 @@ import {
StyleSheet,
Text,
TouchableOpacity,
View,
View
} from 'react-native';
import ImageViewing from 'react-native-image-viewing';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useCosUpload } from '@/hooks/useCosUpload';
type PoseView = 'front' | 'side' | 'back';
@@ -59,6 +61,9 @@ export default function AIPostureAssessmentScreen() {
[uploadState]
);
const { upload, uploading } = useCosUpload();
const [uploadingKey, setUploadingKey] = useState<PoseView | null>(null);
const [cameraPerm, setCameraPerm] = useState<ImagePicker.PermissionStatus | null>(null);
const [libraryPerm, setLibraryPerm] = useState<ImagePicker.PermissionStatus | null>(null);
const [libraryAccess, setLibraryAccess] = useState<'all' | 'limited' | 'none' | null>(null);
@@ -129,7 +134,25 @@ export default function AIPostureAssessmentScreen() {
aspect: [3, 4],
});
if (!result.canceled) {
setUploadState((s) => ({ ...s, [key]: result.assets[0]?.uri ?? null }));
// 设置正在上传状态
setUploadingKey(key);
try {
// 上传到 COS
const { url } = await upload(
{ uri: result.assets[0]?.uri ?? '', name: `posture-${key}.jpg`, type: 'image/jpeg' },
{ prefix: 'posture-assessment/' }
);
// 上传成功,更新状态
setUploadState((s) => ({ ...s, [key]: url }));
} catch (uploadError) {
console.warn('上传图片失败', uploadError);
Alert.alert('上传失败', '图片上传失败,请重试');
// 上传失败,清除状态
setUploadState((s) => ({ ...s, [key]: null }));
} finally {
// 清除上传状态
setUploadingKey(null);
}
}
} else {
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
@@ -158,7 +181,25 @@ export default function AIPostureAssessmentScreen() {
aspect: [3, 4],
});
if (!result.canceled) {
setUploadState((s) => ({ ...s, [key]: result.assets[0]?.uri ?? null }));
// 设置正在上传状态
setUploadingKey(key);
try {
// 上传到 COS
const { url } = await upload(
{ uri: result.assets[0]?.uri ?? '', name: `posture-${key}.jpg`, type: 'image/jpeg' },
{ prefix: 'posture-assessment/' }
);
// 上传成功,更新状态
setUploadState((s) => ({ ...s, [key]: url }));
} catch (uploadError) {
console.warn('上传图片失败', uploadError);
Alert.alert('上传失败', '图片上传失败,请重试');
// 上传失败,清除状态
setUploadState((s) => ({ ...s, [key]: null }));
} finally {
// 清除上传状态
setUploadingKey(null);
}
}
}
} catch (e) {
@@ -219,6 +260,7 @@ export default function AIPostureAssessmentScreen() {
onPickCamera={() => requestPermissionAndPick('camera', 'front')}
onPickLibrary={() => requestPermissionAndPick('library', 'front')}
samples={SAMPLES.front}
uploading={uploading && uploadingKey === 'front'}
/>
<UploadTile
@@ -227,6 +269,7 @@ export default function AIPostureAssessmentScreen() {
onPickCamera={() => requestPermissionAndPick('camera', 'side')}
onPickLibrary={() => requestPermissionAndPick('library', 'side')}
samples={SAMPLES.side}
uploading={uploading && uploadingKey === 'side'}
/>
<UploadTile
@@ -235,6 +278,7 @@ export default function AIPostureAssessmentScreen() {
onPickCamera={() => requestPermissionAndPick('camera', 'back')}
onPickLibrary={() => requestPermissionAndPick('library', 'back')}
samples={SAMPLES.back}
uploading={uploading && uploadingKey === 'back'}
/>
</ScrollView>
@@ -264,12 +308,14 @@ function UploadTile({
onPickCamera,
onPickLibrary,
samples,
uploading,
}: {
label: string;
value?: string | null;
onPickCamera: () => void;
onPickLibrary: () => void;
samples: Sample[];
uploading?: boolean;
}) {
const [viewerVisible, setViewerVisible] = React.useState(false);
const [viewerIndex, setViewerIndex] = React.useState(0);
@@ -291,8 +337,14 @@ function UploadTile({
onLongPress={onPickLibrary}
onPress={onPickCamera}
style={styles.uploader}
disabled={uploading}
>
{value ? (
{uploading ? (
<View style={[styles.placeholder, { backgroundColor: '#f5f5f5' }]}>
<ActivityIndicator size="large" color="#BBF246" />
<Text style={styles.placeholderTitle}>...</Text>
</View>
) : value ? (
<Image source={{ uri: value }} style={styles.preview} />
) : (
<View style={styles.placeholder}>

View File

@@ -12,6 +12,7 @@ import * as ImagePicker from 'expo-image-picker';
import { router } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Image,
KeyboardAvoidingView,
@@ -23,7 +24,7 @@ import {
Text,
TextInput,
TouchableOpacity,
View,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -230,6 +231,7 @@ export default function EditProfileScreen() {
{ uri: asset.uri, name: asset.fileName || 'avatar.jpg', type: asset.mimeType || 'image/jpeg' },
{ prefix: 'avatars/', userId }
);
setProfile((p) => ({ ...p, avatarUri: url, avatarBase64: null }));
} catch (e) {
console.warn('上传头像失败', e);
@@ -250,12 +252,17 @@ export default function EditProfileScreen() {
{/* 头像(带相机蒙层,点击从相册选择) */}
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary}>
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary} disabled={uploading}>
<View style={styles.avatarCircle}>
<Image source={{ uri: profile.avatarUri || 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg' }} style={styles.avatarImage} />
<View style={styles.avatarOverlay}>
<Ionicons name="camera" size={22} color="#192126" />
</View>
{uploading && (
<View style={styles.avatarLoadingOverlay}>
<ActivityIndicator size="large" color="#FFFFFF" />
</View>
)}
</View>
</TouchableOpacity>
</View>
@@ -356,12 +363,7 @@ function FieldLabel({ text }: { text: string }) {
// 单位切换组件已移除(固定 kg/cm
// 工具函数
// 转换函数不再使用,保留 round
function kgToLb(kg: number) { return kg * 2.2046226218; }
function lbToKg(lb: number) { return lb / 2.2046226218; }
function cmToFt(cm: number) { return cm / 30.48; }
function ftToCm(ft: number) { return ft * 30.48; }
function round(n: number, d = 0) { const p = Math.pow(10, d); return Math.round(n * p) / p; }
const styles = StyleSheet.create({
@@ -390,6 +392,17 @@ const styles = StyleSheet.create({
justifyContent: 'center',
backgroundColor: 'rgba(255,255,255,0.25)',
},
avatarLoadingOverlay: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 60,
},
inputWrapper: {
height: 52,
backgroundColor: '#fff',

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,602 @@
import DateTimePicker from '@react-native-community/datetimepicker';
import { useRouter } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import { Modal, Platform, Pressable, SafeAreaView, ScrollView, StyleSheet, TextInput, View } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { palette } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import {
clearError,
loadPlans,
saveDraftAsPlan,
setGoal,
setMode,
setName,
setPreferredTime,
setSessionsPerWeek,
setStartDate,
setStartDateNextMonday,
setStartWeight,
toggleDayOfWeek,
type PlanGoal
} from '@/store/trainingPlanSlice';
const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'];
const GOALS: { key: PlanGoal; title: string; desc: string }[] = [
{ key: 'postpartum_recovery', title: '产后恢复', desc: '温和激活,核心重建' },
{ key: 'posture_correction', title: '体态矫正', desc: '打开胸肩,改善圆肩驼背' },
{ key: 'fat_loss', title: '减脂塑形', desc: '全身燃脂,线条雕刻' },
{ key: 'core_strength', title: '核心力量', desc: '核心稳定,提升运动表现' },
{ key: 'flexibility', title: '柔韧灵活', desc: '拉伸延展,释放紧张' },
{ key: 'rehab', title: '康复保健', desc: '循序渐进,科学修复' },
{ key: 'stress_relief', title: '释压放松', desc: '舒缓身心,改善睡眠' },
];
export default function TrainingPlanCreateScreen() {
const router = useRouter();
const dispatch = useAppDispatch();
const { draft, loading, error } = useAppSelector((s) => s.trainingPlan);
const [weightInput, setWeightInput] = useState<string>('');
const [datePickerVisible, setDatePickerVisible] = useState(false);
const [pickerDate, setPickerDate] = useState<Date>(new Date());
useEffect(() => {
dispatch(loadPlans());
}, [dispatch]);
useEffect(() => {
if (draft.startWeightKg && !weightInput) setWeightInput(String(draft.startWeightKg));
}, [draft.startWeightKg]);
const selectedCount = draft.mode === 'daysOfWeek' ? draft.daysOfWeek.length : draft.sessionsPerWeek;
const canSave = useMemo(() => {
if (!draft.goal) return false;
if (draft.mode === 'daysOfWeek' && draft.daysOfWeek.length === 0) return false;
if (draft.mode === 'sessionsPerWeek' && draft.sessionsPerWeek <= 0) return false;
return true;
}, [draft]);
const formattedStartDate = useMemo(() => {
const d = new Date(draft.startDate);
try {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short',
}).format(d);
} catch {
return d.toLocaleDateString('zh-CN');
}
}, [draft.startDate]);
const handleSave = async () => {
try {
await dispatch(saveDraftAsPlan()).unwrap();
router.back();
} catch (error) {
// 错误已经在Redux中处理这里可以显示额外的用户反馈
console.error('保存训练计划失败:', error);
}
};
useEffect(() => {
if (error) {
// 3秒后自动清除错误
const timer = setTimeout(() => {
dispatch(clearError());
}, 3000);
return () => clearTimeout(timer);
}
}, [error, dispatch]);
const openDatePicker = () => {
const base = draft.startDate ? new Date(draft.startDate) : new Date();
base.setHours(0, 0, 0, 0);
setPickerDate(base);
setDatePickerVisible(true);
};
const closeDatePicker = () => setDatePickerVisible(false);
const onConfirmDate = (date: Date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const picked = new Date(date);
picked.setHours(0, 0, 0, 0);
const finalDate = picked < today ? today : picked;
dispatch(setStartDate(finalDate.toISOString()));
closeDatePicker();
};
return (
<SafeAreaView style={styles.safeArea}>
<ThemedView style={styles.container}>
<HeaderBar title="新建训练计划" onBack={() => router.back()} withSafeTop={false} transparent />
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.content}>
<ThemedText style={styles.title}></ThemedText>
<ThemedText style={styles.subtitle}></ThemedText>
{error && (
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}> {error}</ThemedText>
</View>
)}
<View style={styles.card}>
<ThemedText style={styles.cardTitle}></ThemedText>
<TextInput
placeholder="为你的训练计划起个名字(可选)"
value={draft.name || ''}
onChangeText={(text) => dispatch(setName(text))}
style={styles.nameInput}
maxLength={50}
/>
</View>
<View style={styles.card}>
<ThemedText style={styles.cardTitle}></ThemedText>
<View style={styles.segment}>
<Pressable
onPress={() => dispatch(setMode('daysOfWeek'))}
style={[styles.segmentItem, draft.mode === 'daysOfWeek' && styles.segmentItemActive]}
>
<ThemedText style={[styles.segmentText, draft.mode === 'daysOfWeek' && styles.segmentTextActive]}></ThemedText>
</Pressable>
<Pressable
onPress={() => dispatch(setMode('sessionsPerWeek'))}
style={[styles.segmentItem, draft.mode === 'sessionsPerWeek' && styles.segmentItemActive]}
>
<ThemedText style={[styles.segmentText, draft.mode === 'sessionsPerWeek' && styles.segmentTextActive]}></ThemedText>
</Pressable>
</View>
{draft.mode === 'daysOfWeek' ? (
<View style={styles.weekRow}>
{WEEK_DAYS.map((d, i) => {
const active = draft.daysOfWeek.includes(i);
return (
<Pressable key={i} onPress={() => dispatch(toggleDayOfWeek(i))} style={[styles.dayChip, active && styles.dayChipActive]}>
<ThemedText style={[styles.dayChipText, active && styles.dayChipTextActive]}>{d}</ThemedText>
</Pressable>
);
})}
</View>
) : (
<View style={styles.sliderRow}>
<ThemedText style={styles.sliderLabel}></ThemedText>
<View style={styles.counter}>
<Pressable onPress={() => dispatch(setSessionsPerWeek(Math.max(1, draft.sessionsPerWeek - 1)))} style={styles.counterBtn}>
<ThemedText style={styles.counterBtnText}>-</ThemedText>
</Pressable>
<ThemedText style={styles.counterValue}>{draft.sessionsPerWeek}</ThemedText>
<Pressable onPress={() => dispatch(setSessionsPerWeek(Math.min(7, draft.sessionsPerWeek + 1)))} style={styles.counterBtn}>
<ThemedText style={styles.counterBtnText}>+</ThemedText>
</Pressable>
</View>
<ThemedText style={styles.sliderSuffix}></ThemedText>
</View>
)}
<ThemedText style={styles.helper}>{selectedCount} /</ThemedText>
</View>
<View style={styles.card}>
<ThemedText style={styles.cardTitle}></ThemedText>
<View style={styles.goalGrid}>
{GOALS.map((g) => {
const active = draft.goal === g.key;
return (
<Pressable key={g.key} onPress={() => dispatch(setGoal(g.key))} style={[styles.goalItem, active && styles.goalItemActive]}>
<ThemedText style={[styles.goalTitle, active && styles.goalTitleActive]}>{g.title}</ThemedText>
<ThemedText style={styles.goalDesc}>{g.desc}</ThemedText>
</Pressable>
);
})}
</View>
</View>
<View style={styles.card}>
<ThemedText style={styles.cardTitle}></ThemedText>
<View style={styles.rowBetween}>
<ThemedText style={styles.label}></ThemedText>
<View style={styles.rowRight}>
<Pressable onPress={openDatePicker} style={styles.linkBtn}>
<ThemedText style={styles.linkText}></ThemedText>
</Pressable>
<Pressable onPress={() => dispatch(setStartDateNextMonday())} style={[styles.linkBtn, { marginLeft: 8 }]}>
<ThemedText style={styles.linkText}></ThemedText>
</Pressable>
</View>
</View>
<ThemedText style={styles.dateHint}>{formattedStartDate}</ThemedText>
<View style={styles.rowBetween}>
<ThemedText style={styles.label}> (kg)</ThemedText>
<TextInput
keyboardType="numeric"
placeholder="可选"
value={weightInput}
onChangeText={(t) => {
setWeightInput(t);
const v = Number(t);
dispatch(setStartWeight(Number.isFinite(v) ? v : undefined));
}}
style={styles.input}
/>
</View>
<View style={[styles.rowBetween, { marginTop: 12 }]}>
<ThemedText style={styles.label}></ThemedText>
<View style={styles.segmentSmall}>
{(['morning', 'noon', 'evening', ''] as const).map((k) => (
<Pressable key={k || 'none'} onPress={() => dispatch(setPreferredTime(k))} style={[styles.segmentItemSmall, draft.preferredTimeOfDay === k && styles.segmentItemActiveSmall]}>
<ThemedText style={[styles.segmentTextSmall, draft.preferredTimeOfDay === k && styles.segmentTextActiveSmall]}>{k === 'morning' ? '晨练' : k === 'noon' ? '午间' : k === 'evening' ? '晚间' : '不限'}</ThemedText>
</Pressable>
))}
</View>
</View>
</View>
<Pressable disabled={!canSave || loading} onPress={handleSave} style={[styles.primaryBtn, (!canSave || loading) && styles.primaryBtnDisabled]}>
<ThemedText style={styles.primaryBtnText}>
{loading ? '创建中...' : canSave ? '生成计划' : '请先选择目标/频率'}
</ThemedText>
</Pressable>
<View style={{ height: 32 }} />
</ScrollView>
</ThemedView>
<Modal
visible={datePickerVisible}
transparent
animationType="fade"
onRequestClose={closeDatePicker}
>
<Pressable style={styles.modalBackdrop} onPress={closeDatePicker} />
<View style={styles.modalSheet}>
<DateTimePicker
value={pickerDate}
mode="date"
display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
minimumDate={new Date()}
{...(Platform.OS === 'ios' ? { locale: 'zh-CN' } : {})}
onChange={(event, date) => {
if (Platform.OS === 'ios') {
if (date) setPickerDate(date);
} else {
if (event.type === 'set' && date) {
onConfirmDate(date);
} else {
closeDatePicker();
}
}
}}
/>
{Platform.OS === 'ios' && (
<View style={styles.modalActions}>
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
<ThemedText style={styles.modalBtnText}></ThemedText>
</Pressable>
<Pressable onPress={() => { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}>
<ThemedText style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></ThemedText>
</Pressable>
</View>
)}
</View>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#F7F8FA',
},
container: {
flex: 1,
backgroundColor: '#F7F8FA',
},
content: {
paddingHorizontal: 20,
paddingTop: 16,
},
title: {
fontSize: 28,
fontWeight: '800',
color: '#1A1A1A',
lineHeight: 36,
},
subtitle: {
fontSize: 14,
color: '#5E6468',
marginTop: 6,
marginBottom: 16,
lineHeight: 20,
},
card: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginTop: 14,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 12,
shadowOffset: { width: 0, height: 6 },
elevation: 3,
},
cardTitle: {
fontSize: 18,
fontWeight: '700',
color: '#0F172A',
marginBottom: 12,
},
segment: {
flexDirection: 'row',
backgroundColor: '#F1F5F9',
padding: 4,
borderRadius: 999,
},
segmentItem: {
flex: 1,
borderRadius: 999,
paddingVertical: 10,
alignItems: 'center',
},
segmentItemActive: {
backgroundColor: palette.primary,
},
segmentText: {
fontSize: 14,
color: '#475569',
fontWeight: '600',
},
segmentTextActive: {
color: palette.ink,
},
weekRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 14,
},
dayChip: {
width: 44,
height: 44,
borderRadius: 12,
backgroundColor: '#F1F5F9',
alignItems: 'center',
justifyContent: 'center',
},
dayChipActive: {
backgroundColor: '#E0F8A2',
borderWidth: 2,
borderColor: palette.primary,
},
dayChipText: {
fontSize: 16,
color: '#334155',
fontWeight: '700',
},
dayChipTextActive: {
color: '#0F172A',
},
sliderRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 16,
},
sliderLabel: {
fontSize: 16,
color: '#334155',
fontWeight: '700',
},
counter: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 12,
},
counterBtn: {
width: 36,
height: 36,
borderRadius: 999,
backgroundColor: '#F1F5F9',
alignItems: 'center',
justifyContent: 'center',
},
counterBtnText: {
fontSize: 18,
fontWeight: '800',
color: '#0F172A',
},
counterValue: {
width: 44,
textAlign: 'center',
fontSize: 18,
fontWeight: '800',
color: '#0F172A',
},
sliderSuffix: {
marginLeft: 8,
color: '#475569',
},
helper: {
marginTop: 10,
color: '#5E6468',
},
goalGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
goalItem: {
width: '48%',
backgroundColor: '#F8FAFC',
borderRadius: 14,
padding: 12,
marginBottom: 12,
},
goalItemActive: {
backgroundColor: '#E0F8A2',
borderColor: palette.primary,
borderWidth: 2,
},
goalTitle: {
fontSize: 16,
fontWeight: '800',
color: '#0F172A',
},
goalTitleActive: {
color: '#0F172A',
},
goalDesc: {
marginTop: 6,
fontSize: 12,
color: '#5E6468',
lineHeight: 16,
},
rowBetween: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 6,
},
rowRight: {
flexDirection: 'row',
alignItems: 'center',
},
label: {
fontSize: 14,
color: '#0F172A',
fontWeight: '700',
},
linkBtn: {
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: '#F1F5F9',
},
linkText: {
color: '#334155',
fontWeight: '700',
},
dateHint: {
marginTop: 6,
color: '#5E6468',
},
input: {
marginLeft: 12,
backgroundColor: '#F1F5F9',
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 8,
minWidth: 88,
textAlign: 'right',
color: '#0F172A',
},
segmentSmall: {
flexDirection: 'row',
backgroundColor: '#F1F5F9',
padding: 3,
borderRadius: 999,
},
segmentItemSmall: {
borderRadius: 999,
paddingVertical: 6,
paddingHorizontal: 10,
marginHorizontal: 3,
},
segmentItemActiveSmall: {
backgroundColor: palette.primary,
},
segmentTextSmall: {
fontSize: 12,
color: '#475569',
fontWeight: '700',
},
segmentTextActiveSmall: {
color: palette.ink,
},
primaryBtn: {
marginTop: 18,
backgroundColor: palette.primary,
paddingVertical: 14,
borderRadius: 14,
alignItems: 'center',
},
primaryBtnDisabled: {
opacity: 0.5,
},
primaryBtnText: {
color: palette.ink,
fontSize: 16,
fontWeight: '800',
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.35)',
},
modalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
padding: 16,
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
},
modalActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 8,
gap: 12,
},
modalBtn: {
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 10,
backgroundColor: '#F1F5F9',
},
modalBtnPrimary: {
backgroundColor: palette.primary,
},
modalBtnText: {
color: '#334155',
fontWeight: '700',
},
modalBtnTextPrimary: {
color: palette.ink,
},
// 计划名称输入框
nameInput: {
backgroundColor: '#F1F5F9',
paddingHorizontal: 12,
paddingVertical: 12,
borderRadius: 8,
fontSize: 16,
color: '#0F172A',
marginTop: 8,
},
// 错误状态
errorContainer: {
backgroundColor: 'rgba(237,71,71,0.1)',
borderRadius: 12,
padding: 16,
marginTop: 16,
borderWidth: 1,
borderColor: 'rgba(237,71,71,0.2)',
},
errorText: {
fontSize: 14,
color: '#ED4747',
fontWeight: '600',
textAlign: 'center',
},
});