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 { 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, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, 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; 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({ name: '', gender: '', birthDate: '', weight: undefined, height: undefined, avatarUri: null, activityLevel: undefined, maxHeartRate: undefined, }); // 出生日期选择器 const [datePickerVisible, setDatePickerVisible] = useState(false); const [pickerDate, setPickerDate] = useState(new Date()); const [editingField, setEditingField] = useState(null); const [tempValue, setTempValue] = useState(''); // 输入框字符串 // 从本地存储加载(身高/体重等本地字段) 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(() => { 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); } 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 ( router.back()} withSafeTop={false} transparent={true} variant="elevated" /> {/* 头像(带相机蒙层,点击从相册选择) */} {uploading && ( )} {/* 用户信息卡片列表 */} {/* 姓名 */} { setTempValue(profile.name || ''); setEditingField('name'); }} /> {/* 性别 */} { setEditingField('gender'); }} /> {/* 身高 */} { setTempValue(profile.height ? String(Math.round(profile.height)) : String(t('editProfile.defaultValues.height'))); setEditingField('height'); }} /> {/* 体重 */} { setTempValue(profile.weight ? String(round(profile.weight, 1)) : String(t('editProfile.defaultValues.weight'))); setEditingField('weight'); }} /> {/* 活动水平 */} { 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'); }} /> {/* 出生日期 */} { 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(); }} /> {/* 最大心率 */} { // 最大心率不可编辑,只显示 Alert.alert(t('editProfile.maxHeartRate.alert.title'), t('editProfile.maxHeartRate.alert.message')); }} disabled={true} hideArrow={true} /> {/* 编辑弹窗 */} { 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} /> {/* 出生日期选择器弹窗 */} { if (Platform.OS === 'ios') { if (date) setPickerDate(date); } else { if (event.type === 'set' && date) { onConfirmDate(date); } else { closeDatePicker(); } } }} /> {Platform.OS === 'ios' && ( {t('editProfile.modals.cancel')} { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> {t('editProfile.modals.confirm')} )} ); } 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 ( {iconUri ? : } {title} {value} {!hideArrow && } ); } function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor, t }: { visible: boolean; field: string | null; value: string; profile: UserProfile; onClose: () => void; onSave: (field: string, value: string) => void; colors: any; textColor: string; placeholderColor: string; t: (key: string) => 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 ( {t('editProfile.fields.name')} ); case 'gender': return ( {t('editProfile.fields.gender')} setSelectedGender('female')} > {t('editProfile.modals.female')} setSelectedGender('male')} > {t('editProfile.modals.male')} ); case 'height': return ( {t('editProfile.fields.height')} {Array.from({ length: 101 }, (_, i) => 120 + i).map(height => ( ))} ); case 'weight': return ( {t('editProfile.fields.weight')} {t('editProfile.modals.input.weightUnit')} ); case 'activity': return ( {t('editProfile.fields.activityLevel')} {[ { 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 => ( setSelectedActivity(item.key)} > {item.label} {item.desc} {selectedActivity === item.key && } ))} ); default: return null; } }; return ( {renderContent()} {t('editProfile.modals.cancel')} { if (field === 'gender') { onSave(field, selectedGender); } else if (field === 'activity') { onSave(field, String(selectedActivity)); } else { onSave(field!, inputValue); } }} style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]} > {t('editProfile.modals.save')} ); } 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', }, });