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({ name: '', gender: '', birthDate: '', weight: undefined, height: undefined, avatarUri: null, activityLevel: undefined, }); const [weightInput, setWeightInput] = useState(''); const [heightInput, setHeightInput] = useState(''); // 出生日期选择器 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, }; 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 ( {/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */} router.back()} withSafeTop={false} transparent={true} variant="elevated" /> {/* 头像(带相机蒙层,点击从相册选择) */} {uploading && ( )} {/* 用户信息卡片列表 */} {/* 姓名 */} { setTempValue(profile.name || ''); setEditingField('name'); }} /> {/* 性别 */} { setEditingField('gender'); }} /> {/* 身高 */} { setTempValue(profile.height ? String(Math.round(profile.height)) : '170'); setEditingField('height'); }} /> {/* 体重 */} { setTempValue(profile.weight ? String(round(profile.weight, 1)) : '55'); setEditingField('weight'); }} /> {/* 活动水平 */} { switch (profile.activityLevel) { case 1: return '久坐'; case 2: return '轻度活跃'; case 3: return '中度活跃'; case 4: return '非常活跃'; default: return '久坐'; } })()} onPress={() => { setEditingField('activity'); }} /> {/* 出生日期 */} { 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(); }} /> {/* 编辑弹窗 */} { 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} /> {/* 出生日期选择器弹窗 */} { if (Platform.OS === 'ios') { if (date) setPickerDate(date); } else { if (event.type === 'set' && date) { onConfirmDate(date); } else { closeDatePicker(); } } }} /> {Platform.OS === 'ios' && ( 取消 { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> 确定 )} ); } function ProfileCard({ icon, iconUri, iconColor, title, value, onPress }: { icon?: keyof typeof Ionicons.glyphMap; iconUri?: string; iconColor?: string; title: string; value: string; onPress: () => void; }) { return ( {iconUri ? : } {title} {value} ); } 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 ( 昵称 ); case 'gender': return ( 性别 setSelectedGender('female')} > 女性 setSelectedGender('male')} > 男性 ); case 'height': return ( 身高 {Array.from({ length: 101 }, (_, i) => 120 + i).map(height => ( ))} ); case 'weight': return ( 体重 公斤 (kg) ); case 'activity': return ( 活动水平 {[ { key: 1, label: '久坐', desc: '很少运动' }, { key: 2, label: '轻度活跃', desc: '每周1-3次运动' }, { key: 3, label: '中度活跃', desc: '每周3-5次运动' }, { key: 4, label: '非常活跃', desc: '每周6-7次运动' }, ].map(item => ( setSelectedActivity(item.key)} > {item.label} {item.desc} {selectedActivity === item.key && } ))} ); default: return null; } }; return ( {renderContent()} 取消 { if (field === 'gender') { onSave(field, selectedGender); } else if (field === 'activity') { onSave(field, String(selectedActivity)); } else { onSave(field!, inputValue); } }} style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]} > 保存 ); } 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', }, });