feat: 更新用户资料编辑功能及相关组件

- 在 EditProfileScreen 中新增活动水平字段,支持用户设置和保存活动水平
- 更新个人信息卡片,增加活动水平的展示和编辑功能
- 在 ProfileCard 组件中优化样式,提升用户体验
- 更新 package.json 和 package-lock.json,新增 @react-native-picker/picker 依赖
- 在多个组件中引入 expo-image,优化图片加载和展示效果
This commit is contained in:
richarjiang
2025-08-27 09:59:44 +08:00
parent 5e3203f1ce
commit a6dbe7c723
7 changed files with 518 additions and 177 deletions

View File

@@ -8,8 +8,9 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto
import { Ionicons } from '@expo/vector-icons';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
import { Image } from 'expo-image';
import React, { useEffect, useMemo, useState } from 'react';
import { Alert, Image, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg';
@@ -110,8 +111,11 @@ export default function PersonalScreen() {
<View style={styles.userInfoContainer}>
<View style={styles.avatarContainer}>
<Image
source={{ uri: userProfile.avatar || DEFAULT_AVATAR_URL }}
source={userProfile.avatar || DEFAULT_AVATAR_URL}
style={styles.avatar}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
</View>
<View style={styles.userDetails}>

View File

@@ -8,14 +8,15 @@ import { fetchMyProfile } from '@/store/userSlice';
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import DateTimePicker from '@react-native-community/datetimepicker';
import { Picker } from '@react-native-picker/picker';
import { useFocusEffect } from '@react-navigation/native';
import { Image } from 'expo-image';
import * as ImagePicker from 'expo-image-picker';
import { router } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Image,
KeyboardAvoidingView,
Modal,
Platform,
@@ -30,8 +31,6 @@ import {
View,
} from 'react-native';
type WeightUnit = 'kg' | 'lb';
type HeightUnit = 'cm' | 'ft';
interface UserProfile {
name?: string;
@@ -42,6 +41,7 @@ interface UserProfile {
height?: number; // cm
avatarUri?: string | null;
avatarBase64?: string | null; // 兼容旧逻辑(不再上报)
activityLevel?: number; // 活动水平 1-4
}
const STORAGE_KEY = '@user_profile';
@@ -68,6 +68,7 @@ export default function EditProfileScreen() {
weight: undefined,
height: undefined,
avatarUri: null,
activityLevel: undefined,
});
const [weightInput, setWeightInput] = useState<string>('');
@@ -76,6 +77,8 @@ export default function EditProfileScreen() {
// 出生日期选择器
const [datePickerVisible, setDatePickerVisible] = useState(false);
const [pickerDate, setPickerDate] = useState<Date>(new Date());
const [editingField, setEditingField] = useState<string | null>(null);
const [tempValue, setTempValue] = useState<string>('');
// 输入框字符串
@@ -93,6 +96,7 @@ export default function EditProfileScreen() {
weight: undefined,
height: undefined,
avatarUri: null,
activityLevel: undefined,
};
if (fromOnboarding) {
try {
@@ -152,34 +156,20 @@ export default function EditProfileScreen() {
: prev.avatarUri,
weight: accountProfile?.weight ?? prev.weight ?? undefined,
height: accountProfile?.height ?? prev.height ?? undefined,
activityLevel: accountProfile?.activityLevel ?? prev.activityLevel ?? undefined,
}));
}, [accountProfile]);
const textColor = colors.text;
const placeholderColor = colors.icon;
const handleSave = async () => {
const handleSaveWithProfile = async (profileData: UserProfile) => {
try {
if (!userId) {
Alert.alert('未登录', '请先登录后再尝试保存');
return;
}
const next: UserProfile = { ...profile };
// 将当前输入同步为公制(固定 kg/cm
const w = parseFloat(weightInput);
if (!isNaN(w)) {
next.weight = w;
} else {
next.weight = undefined;
}
const h = parseFloat(heightInput);
if (!isNaN(h)) {
next.height = h;
} else {
next.height = undefined;
}
const next: UserProfile = { ...profileData };
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
@@ -194,6 +184,7 @@ export default function EditProfileScreen() {
weight: next.weight || undefined,
height: next.height || undefined,
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined,
activityLevel: next.activityLevel || undefined,
});
// 拉取最新用户信息,刷新全局状态
await dispatch(fetchMyProfile() as any);
@@ -201,16 +192,11 @@ export default function EditProfileScreen() {
// 接口失败不阻断本地保存
console.warn('更新用户信息失败', e?.message || e);
}
Alert.alert('已保存', '个人资料已更新。');
router.back();
} catch (e) {
Alert.alert('保存失败', '请稍后重试');
}
};
// 不再需要单位切换
const { upload, uploading } = useCosUpload();
// 出生日期选择器交互
@@ -221,14 +207,19 @@ export default function EditProfileScreen() {
setDatePickerVisible(true);
};
const closeDatePicker = () => setDatePickerVisible(false);
const onConfirmDate = (date: Date) => {
const onConfirmDate = async (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;
const updatedProfile = { ...profile, birthDate: finalDate.toISOString() };
setProfile((p) => ({ ...p, birthDate: finalDate.toISOString() }));
closeDatePicker();
// 保存到后端
await handleSaveWithProfile(updatedProfile);
};
const pickAvatarFromLibrary = async () => {
@@ -270,21 +261,21 @@ export default function EditProfileScreen() {
return (
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
<StatusBar barStyle={'dark-content'} />
{/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */}
<HeaderBar
title="编辑资料"
onBack={() => router.back()}
withSafeTop={false}
<HeaderBar
title="编辑资料"
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
variant="elevated"
/>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
{/* 头像(带相机蒙层,点击从相册选择) */}
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 32 }}>
<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} />
@@ -300,87 +291,132 @@ export default function EditProfileScreen() {
</TouchableOpacity>
</View>
{/* 姓名 */}
<FieldLabel text="姓名" />
<View style={[styles.inputWrapper, { borderColor: '#E0E0E0' }]}>
<TextInput
style={[styles.textInput, { color: textColor }]}
placeholder="填写姓名(可选)"
placeholderTextColor={placeholderColor}
value={profile.name}
onChangeText={(t) => setProfile((p) => ({ ...p, name: t }))}
{/* 用户信息卡片列表 */}
<View style={styles.cardContainer}>
{/* 姓名 */}
<ProfileCard
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-name.png'
title="昵称"
value={profile.name || '今晚要吃肉'}
onPress={() => {
setTempValue(profile.name || '');
setEditingField('name');
}}
/>
{/* 性别 */}
<ProfileCard
icon="body"
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-sex.png"
iconColor="#FF6B9D"
title="性别"
value={profile.gender === 'male' ? '男' : profile.gender === 'female' ? '女' : '未设置'}
onPress={() => {
setEditingField('gender');
}}
/>
{/* 身高 */}
<ProfileCard
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-height.png"
title="身高"
value={profile.height ? `${Math.round(profile.height)}厘米` : '170厘米'}
onPress={() => {
setTempValue(profile.height ? String(Math.round(profile.height)) : '170');
setEditingField('height');
}}
/>
{/* 体重 */}
<ProfileCard
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-weight.png"
title="体重"
value={profile.weight ? `${round(profile.weight, 1)}公斤` : '55公斤'}
onPress={() => {
setTempValue(profile.weight ? String(round(profile.weight, 1)) : '55');
setEditingField('weight');
}}
/>
{/* 活动水平 */}
<ProfileCard
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-activity.png'
title="活动水平"
value={(() => {
switch (profile.activityLevel) {
case 1: return '久坐';
case 2: return '轻度活跃';
case 3: return '中度活跃';
case 4: return '非常活跃';
default: return '久坐';
}
})()}
onPress={() => {
setEditingField('activity');
}}
/>
{/* 出生日期 */}
<ProfileCard
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-birth.png'
title="出生日期"
value={profile.birthDate ? (() => {
try {
const d = new Date(profile.birthDate);
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}`;
} catch {
return '1995年1月1日';
}
})() : '1995年1月1日'}
onPress={() => {
openDatePicker();
}}
/>
{/* 校验勾无需强制,仅装饰 */}
{!!profile.name && <Text style={{ color: '#C4C4C4' }}></Text>}
</View>
{/* 体重kg */}
<FieldLabel text="体重" />
<View style={styles.row}>
<View style={[styles.inputWrapper, styles.flex1, { borderColor: '#E0E0E0' }]}>
<TextInput
style={[styles.textInput, { color: textColor }]}
placeholder={'输入体重'}
placeholderTextColor={placeholderColor}
keyboardType="numeric"
value={weightInput}
onChangeText={setWeightInput}
/>
<Text style={{ color: '#5E6468', marginLeft: 6 }}>kg</Text>
</View>
</View>
{/* 编辑弹窗 */}
<EditModal
visible={!!editingField}
field={editingField}
value={tempValue}
profile={profile}
onClose={() => {
setEditingField(null);
setTempValue('');
}}
onSave={async (field, value) => {
// 先更新本地状态
let updatedProfile = { ...profile };
if (field === 'name') {
updatedProfile.name = value;
setProfile(p => ({ ...p, name: value }));
} else if (field === 'gender') {
updatedProfile.gender = value as 'male' | 'female';
setProfile(p => ({ ...p, gender: value as 'male' | 'female' }));
} else if (field === 'height') {
updatedProfile.height = parseFloat(value) || undefined;
setProfile(p => ({ ...p, height: parseFloat(value) || undefined }));
setHeightInput(value);
} else if (field === 'weight') {
updatedProfile.weight = parseFloat(value) || undefined;
setProfile(p => ({ ...p, weight: parseFloat(value) || undefined }));
setWeightInput(value);
} else if (field === 'activity') {
const activityLevel = parseInt(value) as number;
updatedProfile.activityLevel = activityLevel;
setProfile(p => ({ ...p, activityLevel: activityLevel }));
}
{/* 身高cm */}
<FieldLabel text="身高" />
<View style={styles.row}>
<View style={[styles.inputWrapper, styles.flex1, { borderColor: '#E0E0E0' }]}>
<TextInput
style={[styles.textInput, { color: textColor }]}
placeholder={'输入身高'}
placeholderTextColor={placeholderColor}
keyboardType="numeric"
value={heightInput}
onChangeText={setHeightInput}
/>
<Text style={{ color: '#5E6468', marginLeft: 6 }}>cm</Text>
</View>
</View>
setEditingField(null);
setTempValue('');
{/* 性别 */}
<FieldLabel text="性别" />
<View style={[styles.selector, { borderColor: '#E0E0E0' }]}>
<TouchableOpacity
style={[styles.selectorItem, profile.gender === 'female' && { backgroundColor: colors.primary + '40' }]}
onPress={() => setProfile((p) => ({ ...p, gender: 'female' }))}
>
<Text style={styles.selectorEmoji}></Text>
<Text style={styles.selectorText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.selectorItem, profile.gender === 'male' && { backgroundColor: colors.primary + '40' }]}
onPress={() => setProfile((p) => ({ ...p, gender: 'male' }))}
>
<Text style={styles.selectorEmoji}></Text>
<Text style={styles.selectorText}></Text>
</TouchableOpacity>
</View>
{/* 出生日期 */}
<FieldLabel text="出生日期" />
<TouchableOpacity onPress={openDatePicker} activeOpacity={0.8} style={[styles.inputWrapper, { borderColor: '#E0E0E0' }]}>
<Text style={[styles.textInput, { color: profile.birthDate ? textColor : placeholderColor }]}>
{profile.birthDate
? (() => {
try {
const d = new Date(profile.birthDate);
return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }).format(d);
} catch {
return profile.birthDate;
}
})()
: '选择出生日期(可选)'}
</Text>
</TouchableOpacity>
// 使用更新后的数据保存
await handleSaveWithProfile(updatedProfile);
}}
colors={colors}
textColor={textColor}
placeholderColor={placeholderColor}
/>
{/* 出生日期选择器弹窗 */}
<Modal
@@ -415,32 +451,201 @@ export default function EditProfileScreen() {
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
<Text style={styles.modalBtnText}></Text>
</Pressable>
<Pressable onPress={() => { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}>
<Pressable onPress={() => {
onConfirmDate(pickerDate);
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></Text>
</Pressable>
</View>
)}
</View>
</Modal>
{/* 保存按钮 */}
<TouchableOpacity onPress={handleSave} activeOpacity={0.9} style={[styles.saveBtn, { backgroundColor: colors.primary }]}>
<Text style={{ color: colors.onPrimary, fontSize: 18, fontWeight: '700' }}></Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
function FieldLabel({ text }: { text: string }) {
function ProfileCard({ icon, iconUri, iconColor, title, value, onPress }: {
icon?: keyof typeof Ionicons.glyphMap;
iconUri?: string;
iconColor?: string;
title: string;
value: string;
onPress: () => void;
}) {
return (
<Text style={{ fontSize: 14, color: '#5E6468', marginBottom: 8, marginTop: 10 }}>{text}</Text>
<TouchableOpacity onPress={onPress} style={styles.profileCard} activeOpacity={0.8}>
<View style={styles.profileCardLeft}>
<View style={[styles.iconContainer]}>
{iconUri ? <Image
source={{ uri: iconUri }}
style={{ width: 20, height: 20 }}
cachePolicy="memory-disk" /> : <Ionicons name={icon} size={20} color={iconColor} />}
</View>
<Text style={styles.profileCardTitle}>{title}</Text>
</View>
<View style={styles.profileCardRight}>
<Text style={styles.profileCardValue}>{value}</Text>
<Ionicons name="chevron-forward" size={16} color="#C7C7CC" />
</View>
</TouchableOpacity>
);
}
// 单位切换组件已移除(固定 kg/cm
function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor }: {
visible: boolean;
field: string | null;
value: string;
profile: UserProfile;
onClose: () => void;
onSave: (field: string, value: string) => void;
colors: any;
textColor: string;
placeholderColor: string;
}) {
const [inputValue, setInputValue] = useState(value);
const [selectedGender, setSelectedGender] = useState(profile.gender || 'female');
const [selectedActivity, setSelectedActivity] = useState(profile.activityLevel || 1);
useEffect(() => {
setInputValue(value);
if (field === 'activity') {
setSelectedActivity(profile.activityLevel || 1);
}
}, [value, field, profile.activityLevel]);
const renderContent = () => {
switch (field) {
case 'name':
return (
<View>
<Text style={styles.modalTitle}></Text>
<TextInput
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
placeholder="输入昵称"
placeholderTextColor={placeholderColor}
value={inputValue}
onChangeText={setInputValue}
autoFocus
/>
</View>
);
case 'gender':
return (
<View>
<Text style={styles.modalTitle}></Text>
<View style={styles.genderSelector}>
<TouchableOpacity
style={[styles.genderOption, selectedGender === 'female' && { backgroundColor: colors.primary + '20' }]}
onPress={() => setSelectedGender('female')}
>
<Text style={[styles.genderEmoji, selectedGender === 'female' && { color: colors.primary }]}></Text>
<Text style={[styles.genderText, selectedGender === 'female' && { color: colors.primary }]}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.genderOption, selectedGender === 'male' && { backgroundColor: colors.primary + '20' }]}
onPress={() => setSelectedGender('male')}
>
<Text style={[styles.genderEmoji, selectedGender === 'male' && { color: colors.primary }]}></Text>
<Text style={[styles.genderText, selectedGender === 'male' && { color: colors.primary }]}></Text>
</TouchableOpacity>
</View>
</View>
);
case 'height':
return (
<View>
<Text style={styles.modalTitle}></Text>
<View style={styles.pickerContainer}>
<Picker
selectedValue={inputValue}
onValueChange={setInputValue}
style={styles.picker}
>
{Array.from({ length: 101 }, (_, i) => 120 + i).map(height => (
<Picker.Item key={height} label={`${height}厘米`} value={String(height)} />
))}
</Picker>
</View>
</View>
);
case 'weight':
return (
<View>
<Text style={styles.modalTitle}></Text>
<TextInput
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
placeholder="输入体重"
placeholderTextColor={placeholderColor}
value={inputValue}
onChangeText={setInputValue}
keyboardType="numeric"
autoFocus
/>
<Text style={styles.unitText}> (kg)</Text>
</View>
);
case 'activity':
return (
<View>
<Text style={styles.modalTitle}></Text>
<View style={styles.activitySelector}>
{[
{ key: 1, label: '久坐', desc: '很少运动' },
{ key: 2, label: '轻度活跃', desc: '每周1-3次运动' },
{ key: 3, label: '中度活跃', desc: '每周3-5次运动' },
{ key: 4, label: '非常活跃', desc: '每周6-7次运动' },
].map(item => (
<TouchableOpacity
key={item.key}
style={[styles.activityOption, selectedActivity === item.key && { backgroundColor: colors.primary + '20' }]}
onPress={() => setSelectedActivity(item.key)}
>
<View style={styles.activityContent}>
<Text style={[styles.activityLabel, selectedActivity === item.key && { color: colors.primary }]}>{item.label}</Text>
<Text style={styles.activityDesc}>{item.desc}</Text>
</View>
{selectedActivity === item.key && <Ionicons name="checkmark" size={20} color={colors.primary} />}
</TouchableOpacity>
))}
</View>
</View>
);
default:
return null;
}
};
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<Pressable style={styles.modalBackdrop} onPress={onClose} />
<View style={styles.editModalSheet}>
<View style={styles.modalHandle} />
{renderContent()}
<View style={styles.modalButtons}>
<TouchableOpacity onPress={onClose} style={styles.modalCancelBtn}>
<Text style={styles.modalCancelText}></Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
if (field === 'gender') {
onSave(field, selectedGender);
} else if (field === 'activity') {
onSave(field, String(selectedActivity));
} else {
onSave(field!, inputValue);
}
}}
style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]}
>
<Text style={[styles.modalSaveText, { color: colors.onPrimary }]}></Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
function round(n: number, d = 0) { const p = Math.pow(10, d); return Math.round(n * p) / p; }
@@ -481,71 +686,51 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 60,
},
inputWrapper: {
height: 52,
backgroundColor: '#fff',
borderRadius: 12,
borderWidth: 1,
paddingHorizontal: 16,
alignItems: 'center',
flexDirection: 'row',
},
textInput: {
flex: 1,
fontSize: 16,
},
row: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
flex1: { flex: 1 },
segmented: {
flexDirection: 'row',
borderRadius: 12,
padding: 4,
backgroundColor: '#EFEFEF',
},
segmentBtn: {
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 10,
backgroundColor: 'transparent',
minWidth: 64,
alignItems: 'center',
},
selector: {
flexDirection: 'row',
gap: 12,
cardContainer: {
backgroundColor: '#FFFFFF',
borderWidth: 1,
borderRadius: 12,
padding: 8,
borderRadius: 16,
overflow: 'hidden',
marginBottom: 20,
},
selectorItem: {
flex: 1,
height: 48,
borderRadius: 10,
profileCard: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
selectorEmoji: { fontSize: 16, marginBottom: 2 },
selectorText: { fontSize: 15, fontWeight: '600' },
saveBtn: {
marginTop: 24,
height: 56,
profileCardLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
marginRight: 12,
},
profileCardTitle: {
fontSize: 16,
color: '#333333',
fontWeight: '500',
},
profileCardRight: {
flexDirection: 'row',
alignItems: 'center',
},
profileCardValue: {
fontSize: 16,
color: '#666666',
marginRight: 8,
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.35)',
backgroundColor: 'rgba(0,0,0,0.4)',
},
modalSheet: {
position: 'absolute',
@@ -580,6 +765,129 @@ const styles = StyleSheet.create({
color: '#0F172A',
fontWeight: '700',
},
editModalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 20,
paddingBottom: 40,
paddingTop: 20,
},
modalHandle: {
width: 36,
height: 4,
backgroundColor: '#E0E0E0',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 20,
fontWeight: '600',
color: '#333333',
marginBottom: 20,
textAlign: 'center',
},
modalInput: {
height: 50,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 16,
fontSize: 16,
marginBottom: 20,
},
unitText: {
fontSize: 14,
color: '#666666',
textAlign: 'center',
marginBottom: 20,
},
genderSelector: {
flexDirection: 'row',
gap: 16,
marginBottom: 20,
},
genderOption: {
flex: 1,
height: 80,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F8F8F8',
},
genderEmoji: {
fontSize: 24,
marginBottom: 4,
},
genderText: {
fontSize: 16,
fontWeight: '500',
},
pickerContainer: {
height: 200,
marginBottom: 20,
},
picker: {
height: 200,
},
activitySelector: {
marginBottom: 20,
},
activityOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
borderRadius: 12,
marginBottom: 8,
backgroundColor: '#F8F8F8',
},
activityContent: {
flex: 1,
},
activityLabel: {
fontSize: 16,
fontWeight: '500',
color: '#333333',
},
activityDesc: {
fontSize: 14,
color: '#666666',
marginTop: 2,
},
modalButtons: {
flexDirection: 'row',
gap: 12,
},
modalCancelBtn: {
flex: 1,
height: 50,
backgroundColor: '#F0F0F0',
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
modalCancelText: {
fontSize: 16,
fontWeight: '600',
color: '#666666',
},
modalSaveBtn: {
flex: 1,
height: 50,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
modalSaveText: {
fontSize: 16,
fontWeight: '600',
},
});