Files
digital-pilates/app/profile/edit.tsx
richarjiang a6dbe7c723 feat: 更新用户资料编辑功能及相关组件
- 在 EditProfileScreen 中新增活动水平字段,支持用户设置和保存活动水平
- 更新个人信息卡片,增加活动水平的展示和编辑功能
- 在 ProfileCard 组件中优化样式,提升用户体验
- 更新 package.json 和 package-lock.json,新增 @react-native-picker/picker 依赖
- 在多个组件中引入 expo-image,优化图片加载和展示效果
2025-08-27 09:59:44 +08:00

894 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useCosUpload } from '@/hooks/useCosUpload';
import { updateUser as updateUserApi } from '@/services/users';
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,
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
interface UserProfile {
name?: string;
gender?: 'male' | 'female' | '';
birthDate?: string; // 出生日期
// 以公制为基准存储
weight?: number; // kg
height?: number; // cm
avatarUri?: string | null;
avatarBase64?: string | null; // 兼容旧逻辑(不再上报)
activityLevel?: number; // 活动水平 1-4
}
const STORAGE_KEY = '@user_profile';
export default function EditProfileScreen() {
const colorScheme = useColorScheme();
const colors = Colors[colorScheme ?? 'light'];
const dispatch = useAppDispatch();
const accountProfile = useAppSelector((s) => (s as any)?.user?.profile as any);
const userId: string | undefined = useMemo(() => {
return (
accountProfile?.userId ||
accountProfile?.id ||
accountProfile?._id ||
accountProfile?.uid ||
undefined
) as string | undefined;
}, [accountProfile]);
const [profile, setProfile] = useState<UserProfile>({
name: '',
gender: '',
birthDate: '',
weight: undefined,
height: undefined,
avatarUri: null,
activityLevel: undefined,
});
const [weightInput, setWeightInput] = useState<string>('');
const [heightInput, setHeightInput] = useState<string>('');
// 出生日期选择器
const [datePickerVisible, setDatePickerVisible] = useState(false);
const [pickerDate, setPickerDate] = useState<Date>(new Date());
const [editingField, setEditingField] = useState<string | null>(null);
const [tempValue, setTempValue] = useState<string>('');
// 输入框字符串
// 从本地存储加载(身高/体重等本地字段)
const loadLocalProfile = async () => {
try {
const [p, fromOnboarding] = await Promise.all([
AsyncStorage.getItem(STORAGE_KEY),
AsyncStorage.getItem('@user_personal_info'),
]);
let next: UserProfile = {
name: '',
gender: '',
birthDate: '',
weight: undefined,
height: undefined,
avatarUri: null,
activityLevel: undefined,
};
if (fromOnboarding) {
try {
const o = JSON.parse(fromOnboarding);
if (o?.weight) next.weight = parseFloat(o.weight) || undefined;
if (o?.height) next.height = parseFloat(o.height) || undefined;
if (o?.birthDate) next.birthDate = o.birthDate;
if (o?.gender) next.gender = o.gender;
} catch { }
}
if (p) {
try {
const parsed: UserProfile = JSON.parse(p);
next = { ...next, ...parsed };
} catch { }
}
console.log('loadLocalProfile', next);
setProfile((prev) => ({ ...next, avatarUri: prev.avatarUri ?? next.avatarUri ?? null }));
setWeightInput(next.weight != null ? String(round(next.weight, 1)) : '');
setHeightInput(next.height != null ? String(Math.round(next.height)) : '');
} catch (e) {
console.warn('读取资料失败', e);
}
};
useEffect(() => {
loadLocalProfile();
}, []);
// 页面聚焦时拉取最新用户信息,并刷新本地 UI
useFocusEffect(
React.useCallback(() => {
let cancelled = false;
(async () => {
try {
await dispatch(fetchMyProfile() as any);
if (!cancelled) {
// 拉取完成后,再次从本地存储同步身高/体重等字段
await loadLocalProfile();
}
} catch { }
})();
return () => { cancelled = true; };
}, [dispatch])
);
// 当全局 profile 更新时,用后端字段覆盖页面 UI 的对应字段(不影响本地身高/体重)
useEffect(() => {
if (!accountProfile) return;
setProfile((prev) => ({
...prev,
name: accountProfile?.name ?? prev.name ?? '',
gender: (accountProfile?.gender === 'male' || accountProfile?.gender === 'female') ? accountProfile.gender : (prev.gender ?? ''),
avatarUri: accountProfile?.avatar && typeof accountProfile.avatar === 'string'
? (accountProfile.avatar.startsWith('http') || accountProfile.avatar.startsWith('data:') ? accountProfile.avatar : prev.avatarUri)
: 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 handleSaveWithProfile = async (profileData: UserProfile) => {
try {
if (!userId) {
Alert.alert('未登录', '请先登录后再尝试保存');
return;
}
const next: UserProfile = { ...profileData };
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
// 同步到后端(仅更新后端需要的字段)
try {
await updateUserApi({
userId,
name: next.name || undefined,
gender: (next.gender === 'male' || next.gender === 'female') ? next.gender : undefined,
// 头像采用已上传的 URL若有
avatar: next.avatarUri || undefined,
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);
} catch (e: any) {
// 接口失败不阻断本地保存
console.warn('更新用户信息失败', e?.message || e);
}
} catch (e) {
Alert.alert('保存失败', '请稍后重试');
}
};
const { upload, uploading } = useCosUpload();
// 出生日期选择器交互
const openDatePicker = () => {
const base = profile.birthDate ? new Date(profile.birthDate) : new Date();
base.setHours(0, 0, 0, 0);
setPickerDate(base);
setDatePickerVisible(true);
};
const closeDatePicker = () => setDatePickerVisible(false);
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 () => {
try {
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
if (!libGranted) {
Alert.alert('权限不足', '需要相册权限以选择头像');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
quality: 0.9,
aspect: [1, 1],
mediaTypes: ['images'],
base64: false,
});
if (!result.canceled) {
const asset = result.assets?.[0];
if (!asset?.uri) return;
// 直接上传到 COS成功后写入 URL
try {
const { url } = await upload(
{ 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);
Alert.alert('上传失败', '头像上传失败,请重试');
}
}
} catch (e) {
Alert.alert('发生错误', '选择头像失败,请重试');
}
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
<StatusBar barStyle={'dark-content'} />
{/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */}
<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: 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} />
<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>
{/* 用户信息卡片列表 */}
<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();
}}
/>
</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 }));
}
setEditingField(null);
setTempValue('');
// 使用更新后的数据保存
await handleSaveWithProfile(updatedProfile);
}}
colors={colors}
textColor={textColor}
placeholderColor={placeholderColor}
/>
{/* 出生日期选择器弹窗 */}
<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(1900, 0, 1)}
maximumDate={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]}>
<Text style={styles.modalBtnText}></Text>
</Pressable>
<Pressable onPress={() => {
onConfirmDate(pickerDate);
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></Text>
</Pressable>
</View>
)}
</View>
</Modal>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
function ProfileCard({ icon, iconUri, iconColor, title, value, onPress }: {
icon?: keyof typeof Ionicons.glyphMap;
iconUri?: string;
iconColor?: string;
title: string;
value: string;
onPress: () => void;
}) {
return (
<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>
);
}
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; }
const styles = StyleSheet.create({
container: { flex: 1 },
avatarCircle: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: '#E8D4F0',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
},
avatarImage: {
width: 120,
height: 120,
borderRadius: 60,
},
avatarOverlay: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
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,
},
cardContainer: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
overflow: 'hidden',
marginBottom: 20,
},
profileCard: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
profileCardLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
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.4)',
},
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: '#7a5af8',
},
modalBtnText: {
color: '#334155',
fontWeight: '700',
},
modalBtnTextPrimary: {
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',
},
});