Files
digital-pilates/app/profile/edit.tsx
richarjiang 84abfa2506 feat(medication): 重构AI分析为结构化展示并支持喝水提醒个性化配置
- 将药品AI分析从Markdown流式输出重构为结构化数据展示(V2)
- 新增适合人群、不适合人群、主要成分、副作用等分类卡片展示
- 优化AI分析UI布局,采用卡片式设计提升可读性
- 新增药品跳过功能,支持用户标记本次用药为已跳过
- 修复喝水提醒逻辑,支持用户开关控制和自定义时间段配置
- 优化个人资料编辑页面键盘适配,避免输入框被遮挡
- 统一API响应码处理,兼容200和0两种成功状态码
- 更新版本号至1.0.28

BREAKING CHANGE: 药品AI分析接口从流式Markdown输出改为结构化JSON格式,旧版本分析结果将不再显示
2025-11-20 10:10:53 +08:00

996 lines
34 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 { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { syncServerToHealthKit } from '@/services/healthKitSync';
import { fetchMyProfile, updateUserProfile } from '@/store/userSlice';
import { fetchMaximumHeartRate } from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
import { Ionicons } from '@expo/vector-icons';
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,
Keyboard,
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface UserProfile {
name?: string;
gender?: 'male' | 'female' | '';
birthDate?: string; // 出生日期
// 以公制为基准存储
weight?: number; // kg
height?: number; // cm
avatarUri?: string | null;
activityLevel?: number; // 活动水平 1-4
maxHeartRate?: number; // 最大心率
}
const STORAGE_KEY = '@user_profile';
export default function EditProfileScreen() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop()
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,
maxHeartRate: undefined,
});
// 出生日期选择器
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 [keyboardHeight, setKeyboardHeight] = useState(0);
// 从本地存储加载(身高/体重等本地字段)
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,
maxHeartRate: 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 }));
} catch (e) {
console.warn('读取资料失败', e);
}
};
useEffect(() => {
loadLocalProfile();
}, []);
// 键盘事件监听器 - 只在名称和体重输入框显示时监听
useEffect(() => {
// 只有在编辑名称或体重字段时才需要监听键盘(这两个字段使用 TextInput
const needsKeyboardHandling = editingField === 'name' || editingField === 'weight';
if (!needsKeyboardHandling) {
setKeyboardHeight(0);
return;
}
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const handleShow = (event: any) => {
const height = event?.endCoordinates?.height ?? 0;
setKeyboardHeight(height);
};
const handleHide = () => setKeyboardHeight(0);
const showSub = Keyboard.addListener(showEvent, handleShow);
const hideSub = Keyboard.addListener(hideEvent, handleHide);
return () => {
showSub.remove();
hideSub.remove();
};
}, [editingField]);
// 获取最大心率数据
useEffect(() => {
const loadMaximumHeartRate = async () => {
try {
const today = new Date();
const startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); // 过去7天
const maxHeartRate = await fetchMaximumHeartRate({
startDate: startDate.toISOString(),
endDate: today.toISOString(),
});
if (maxHeartRate !== null) {
setProfile(prev => ({ ...prev, maxHeartRate }));
}
} catch (error) {
console.warn('获取最大心率失败', error);
}
};
loadMaximumHeartRate();
}, []);
// 页面聚焦时拉取最新用户信息,并刷新本地 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,
// maxHeartRate 不从后端获取,保持本地状态
maxHeartRate: prev.maxHeartRate,
}));
}, [accountProfile]);
const textColor = colors.text;
const placeholderColor = colors.icon;
const handleSaveWithProfile = async (profileData: UserProfile) => {
try {
if (!userId) {
Alert.alert(t('editProfile.alerts.notLoggedIn.title'), t('editProfile.alerts.notLoggedIn.message'));
return;
}
const next: UserProfile = { ...profileData };
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
// 同步到后端(仅更新后端需要的字段)
try {
await dispatch(updateUserProfile({
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);
// 同步身高、体重到 HealthKit
console.log('开始同步个人健康数据到 HealthKit...');
const syncSuccess = await syncServerToHealthKit({
height: next.height,
weight: next.weight,
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined
});
if (syncSuccess) {
console.log('个人健康数据已同步到 HealthKit');
}
} catch (e: any) {
// 接口失败不阻断本地保存
console.warn('更新用户信息失败', e?.message || e);
}
} catch (e) {
Alert.alert(t('editProfile.alerts.saveFailed.title'), t('editProfile.alerts.saveFailed.message'));
}
};
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(t('editProfile.alerts.avatarPermissions.title'), t('editProfile.alerts.avatarPermissions.message'));
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 }
);
console.log('url', url);
setProfile((p) => ({ ...p, avatarUri: url }));
// 保存更新后的 profile
await handleSaveWithProfile({ ...profile, avatarUri: url });
Alert.alert(t('editProfile.alerts.avatarSuccess.title'), t('editProfile.alerts.avatarSuccess.message'));
} catch (e) {
console.warn('上传头像失败', e);
Alert.alert(t('editProfile.alerts.avatarUploadFailed.title'), t('editProfile.alerts.avatarUploadFailed.message'));
}
}
} catch (e) {
Alert.alert(t('editProfile.alerts.avatarError.title'), t('editProfile.alerts.avatarError.message'));
}
};
return (
<View style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
<HeaderBar
title={t('editProfile.title')}
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
variant="elevated"
/>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ paddingBottom: 40, paddingTop: safeAreaTop }} 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/seal-avatar/2.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={t('editProfile.fields.name')}
value={profile.name || t('editProfile.defaultValues.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={t('editProfile.fields.gender')}
value={profile.gender === 'male' ? t('editProfile.gender.male') : profile.gender === 'female' ? t('editProfile.gender.female') : t('editProfile.gender.notSet')}
onPress={() => {
setEditingField('gender');
}}
/>
{/* 身高 */}
<ProfileCard
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-height.png"
title={t('editProfile.fields.height')}
value={profile.height ? `${Math.round(profile.height)}${t('editProfile.height.unit')}` : t('editProfile.height.placeholder')}
onPress={() => {
setTempValue(profile.height ? String(Math.round(profile.height)) : String(t('editProfile.defaultValues.height')));
setEditingField('height');
}}
/>
{/* 体重 */}
<ProfileCard
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-weight.png"
title={t('editProfile.fields.weight')}
value={profile.weight ? `${round(profile.weight, 1)}${t('editProfile.weight.unit')}` : t('editProfile.weight.placeholder')}
onPress={() => {
setTempValue(profile.weight ? String(round(profile.weight, 1)) : String(t('editProfile.defaultValues.weight')));
setEditingField('weight');
}}
/>
{/* 活动水平 */}
<ProfileCard
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-activity.png'
title={t('editProfile.fields.activityLevel')}
value={(() => {
switch (profile.activityLevel) {
case 1: return t('editProfile.activityLevels.1');
case 2: return t('editProfile.activityLevels.2');
case 3: return t('editProfile.activityLevels.3');
case 4: return t('editProfile.activityLevels.4');
default: return t('editProfile.activityLevels.1');
}
})()}
onPress={() => {
setEditingField('activity');
}}
/>
{/* 出生日期 */}
<ProfileCard
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-birth.png'
title={t('editProfile.fields.birthDate')}
value={profile.birthDate ? (() => {
try {
const d = new Date(profile.birthDate);
if (t('editProfile.birthDate.format').includes('{{year}}年')) {
return t('editProfile.birthDate.format', { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() });
} else {
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return t('editProfile.birthDate.format', { year: d.getFullYear(), month: monthNames[d.getMonth()], day: d.getDate() });
}
} catch {
return t('editProfile.birthDate.placeholder');
}
})() : t('editProfile.birthDate.placeholder')}
onPress={() => {
openDatePicker();
}}
/>
{/* 最大心率 */}
<ProfileCard
icon="heart"
iconColor="#FF6B9D"
title={t('editProfile.fields.maxHeartRate')}
value={profile.maxHeartRate ? `${Math.round(profile.maxHeartRate)}${t('editProfile.maxHeartRate.unit')}` : t('editProfile.maxHeartRate.notAvailable')}
onPress={() => {
// 最大心率不可编辑,只显示
Alert.alert(t('editProfile.maxHeartRate.alert.title'), t('editProfile.maxHeartRate.alert.message'));
}}
disabled={true}
hideArrow={true}
/>
</View>
{/* 编辑弹窗 */}
<EditModal
visible={!!editingField}
field={editingField}
value={tempValue}
profile={profile}
keyboardHeight={keyboardHeight}
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 }));
} else if (field === 'weight') {
updatedProfile.weight = parseFloat(value) || undefined;
setProfile(p => ({ ...p, weight: parseFloat(value) || undefined }));
} 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}
t={t}
/>
{/* 出生日期选择器弹窗 */}
<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}>{t('editProfile.modals.cancel')}</Text>
</Pressable>
<Pressable onPress={() => {
onConfirmDate(pickerDate);
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>{t('editProfile.modals.confirm')}</Text>
</Pressable>
</View>
)}
</View>
</Modal>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
function ProfileCard({ icon, iconUri, iconColor, title, value, onPress, disabled, hideArrow }: {
icon?: keyof typeof Ionicons.glyphMap;
iconUri?: string;
iconColor?: string;
title: string;
value: string;
onPress: () => void;
disabled?: boolean;
hideArrow?: boolean;
}) {
const Container = disabled ? View : TouchableOpacity;
return (
<Container onPress={disabled ? undefined : onPress} style={styles.profileCard} {...(disabled ? {} : { 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>
{!hideArrow && <Ionicons name="chevron-forward" size={16} color="#C7C7CC" />}
</View>
</Container>
);
}
function EditModal({ visible, field, value, profile, keyboardHeight, onClose, onSave, colors, textColor, placeholderColor, t }: {
visible: boolean;
field: string | null;
value: string;
profile: UserProfile;
keyboardHeight: number;
onClose: () => void;
onSave: (field: string, value: string) => void;
colors: any;
textColor: string;
placeholderColor: string;
t: (key: string) => string;
}) {
const insets = useSafeAreaInsets();
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}>{t('editProfile.fields.name')}</Text>
<TextInput
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
placeholder={t('editProfile.modals.input.namePlaceholder')}
placeholderTextColor={placeholderColor}
value={inputValue}
onChangeText={setInputValue}
autoFocus
/>
</View>
);
case 'gender':
return (
<View>
<Text style={styles.modalTitle}>{t('editProfile.fields.gender')}</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 }]}>{t('editProfile.modals.female')}</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 }]}>{t('editProfile.modals.male')}</Text>
</TouchableOpacity>
</View>
</View>
);
case 'height':
return (
<View>
<Text style={styles.modalTitle}>{t('editProfile.fields.height')}</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}${t('editProfile.height.unit')}`} value={String(height)} />
))}
</Picker>
</View>
</View>
);
case 'weight':
return (
<View>
<Text style={styles.modalTitle}>{t('editProfile.fields.weight')}</Text>
<TextInput
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
placeholder={t('editProfile.modals.input.weightPlaceholder')}
placeholderTextColor={placeholderColor}
value={inputValue}
onChangeText={setInputValue}
keyboardType="numeric"
autoFocus
/>
<Text style={styles.unitText}>{t('editProfile.modals.input.weightUnit')}</Text>
</View>
);
case 'activity':
return (
<View>
<Text style={styles.modalTitle}>{t('editProfile.fields.activityLevel')}</Text>
<View style={styles.activitySelector}>
{[
{ key: 1, label: t('editProfile.activityLevels.1'), desc: t('editProfile.activityLevels.descriptions.1') },
{ key: 2, label: t('editProfile.activityLevels.2'), desc: t('editProfile.activityLevels.descriptions.2') },
{ key: 3, label: t('editProfile.activityLevels.3'), desc: t('editProfile.activityLevels.descriptions.3') },
{ key: 4, label: t('editProfile.activityLevels.4'), desc: t('editProfile.activityLevels.descriptions.4') },
].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,
{ paddingBottom: Math.max(keyboardHeight, insets.bottom) + 12 }
]}>
<View style={styles.modalHandle} />
{renderContent()}
<View style={styles.modalButtons}>
<TouchableOpacity onPress={onClose} style={styles.modalCancelBtn}>
<Text style={styles.modalCancelText}>{t('editProfile.modals.cancel')}</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 }]}>{t('editProfile.modals.save')}</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',
},
});