/** * 编辑资料页 EditProfileScreen - Twitter/X 风格 * 威友 - 编辑用户资料 * 与 Twitter/X 个人资料编辑页样式一致 */ import React, { useState, useMemo } from 'react'; import { View, ScrollView, StyleSheet, TouchableOpacity, TextInput, Alert, KeyboardAvoidingView, Platform, Image, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { spacing, fontSizes, borderRadius, useAppColors, type AppColors, } from '../../theme'; import { useAuthStore } from '../../stores'; import { Avatar, Text } from '../../components/common'; import { authService, uploadService } from '../../services'; import { useResponsive, useResponsiveSpacing } from '../../hooks'; function createEditProfileStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, }, keyboardView: { flex: 1, }, scrollContent: { paddingBottom: spacing.xl, }, // ===== 封面区域 - Twitter 风格全宽 ===== coverContainer: { position: 'relative', backgroundColor: colors.background.default, }, coverTouchable: { width: '100%', height: '100%', }, coverImage: { width: '100%', height: '100%', }, coverFallback: { width: '100%', height: '100%', backgroundColor: colors.primary.main, justifyContent: 'center', alignItems: 'center', }, coverOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.4)', justifyContent: 'center', alignItems: 'center', }, // ===== 信息区 ===== infoSection: { paddingHorizontal: spacing.lg, paddingBottom: spacing.md, backgroundColor: colors.background.paper, }, avatarActionRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: spacing.sm, }, avatarWrapper: { // 负边距在 inline style 中计算 }, avatarContainer: { borderRadius: borderRadius.full, backgroundColor: colors.background.paper, padding: 3, justifyContent: 'center', alignItems: 'center', position: 'relative', }, avatarOverlay: { position: 'absolute', top: 3, left: 3, right: 3, bottom: 3, borderRadius: borderRadius.full, backgroundColor: 'rgba(0, 0, 0, 0.4)', justifyContent: 'center', alignItems: 'center', }, editAvatarButton: { position: 'absolute', bottom: 4, right: 4, width: 28, height: 28, borderRadius: 14, backgroundColor: colors.primary.main, justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: colors.background.paper, }, // 名字预览 nameSection: { marginBottom: spacing.sm, }, nickname: { fontSize: fontSizes['2xl'], fontWeight: '800', color: colors.text.primary, marginBottom: 2, }, username: { fontSize: fontSizes.md, color: colors.text.hint, }, // 编辑提示 editHint: { flexDirection: 'row', alignItems: 'center', marginTop: spacing.xs, gap: spacing.xs, }, editHintText: { fontSize: fontSizes.xs, color: colors.text.hint, }, // ===== 表单区域 - Twitter 风格全宽卡片 ===== formSection: { backgroundColor: colors.background.paper, marginTop: spacing.md, borderTopWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: colors.divider, }, formField: { paddingHorizontal: spacing.lg, paddingVertical: spacing.md, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.divider, }, formFieldLast: { borderBottomWidth: 0, }, fieldLabel: { fontSize: fontSizes.sm, fontWeight: '500', color: colors.text.secondary, marginBottom: spacing.xs, }, fieldInput: { fontSize: fontSizes.md, color: colors.text.primary, paddingVertical: spacing.xs, }, textArea: { height: 80, textAlignVertical: 'top', }, disabledInput: { color: colors.text.disabled, }, charCount: { textAlign: 'right', marginTop: spacing.xs, }, // 保存按钮 buttonContainer: { marginTop: spacing.lg, paddingHorizontal: spacing.lg, }, saveButton: { height: 48, backgroundColor: colors.text.primary, borderRadius: borderRadius.full, alignItems: 'center', justifyContent: 'center', }, saveButtonDisabled: { opacity: 0.5, }, saveButtonText: { color: colors.background.paper, fontSize: fontSizes.md, fontWeight: '700', }, }); } type EditProfileSheet = ReturnType; interface FormFieldProps { label: string; value: string; onChangeText: (text: string) => void; placeholder: string; maxLength?: number; multiline?: boolean; numberOfLines?: number; autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url'; editable?: boolean; sheet: EditProfileSheet; colors: AppColors; isLast?: boolean; } const FormField: React.FC = ({ label, value, onChangeText, placeholder, maxLength, multiline = false, numberOfLines = 1, autoCapitalize = 'sentences', keyboardType = 'default', editable = true, sheet, colors, isLast = false, }) => { return ( {label} {maxLength && ( {value.length}/{maxLength} )} ); }; export const EditProfileScreen: React.FC = () => { const colors = useAppColors(); const styles = useMemo(() => createEditProfileStyles(colors), [colors]); const navigation = useNavigation(); const { currentUser, updateUser } = useAuthStore(); const { isWideScreen, isMobile, width } = useResponsive(); const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 }); const insets = useSafeAreaInsets(); const [nickname, setNickname] = useState(currentUser?.nickname || ''); const [bio, setBio] = useState(currentUser?.bio || ''); const [location, setLocation] = useState(currentUser?.location || ''); const [website, setWebsite] = useState(currentUser?.website || ''); const [phone, setPhone] = useState(currentUser?.phone || ''); const [email, setEmail] = useState(currentUser?.email || ''); const [avatar, setAvatar] = useState(currentUser?.avatar || ''); const [coverUrl, setCoverUrl] = useState(currentUser?.cover_url || ''); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [uploadingCover, setUploadingCover] = useState(false); const [saving, setSaving] = useState(false); const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg; const coverHeight = isWideScreen ? Math.min((width * 10) / 16, 320) : (width * 10) / 16; const avatarSize = isWideScreen ? 120 : 88; const handlePickCover = async () => { const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!permissionResult.granted) { Alert.alert('权限不足', '需要访问相册权限来选择头图'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: 'images', allowsEditing: true, aspect: [16, 9], quality: 0.9, }); if (!result.canceled && result.assets[0]) { const selectedImage = result.assets[0]; setCoverUrl(selectedImage.uri); try { setUploadingCover(true); const uploadResult = await uploadService.uploadCover({ uri: selectedImage.uri, name: selectedImage.fileName || 'cover.jpg', type: selectedImage.mimeType || 'image/jpeg', }); if (uploadResult) { updateUser({ cover_url: uploadResult.url }); Alert.alert('成功', '头图已更新'); } else { Alert.alert('错误', '头图上传失败,请重试'); } } catch (error) { console.error('上传头图失败:', error); Alert.alert('错误', '头图上传失败,请重试'); } finally { setUploadingCover(false); } } }; const handlePickAvatar = async () => { const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!permissionResult.granted) { Alert.alert('权限不足', '需要访问相册权限来选择头像'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: 'images', allowsEditing: true, aspect: [1, 1], quality: 0.8, }); if (!result.canceled && result.assets[0]) { const selectedImage = result.assets[0]; setAvatar(selectedImage.uri); try { setUploadingAvatar(true); const uploadResult = await uploadService.uploadAvatar({ uri: selectedImage.uri, name: selectedImage.fileName || 'avatar.jpg', type: selectedImage.mimeType || 'image/jpeg', }); if (uploadResult) { updateUser({ avatar: uploadResult.url }); Alert.alert('成功', '头像已更新'); } else { Alert.alert('错误', '头像上传失败,请重试'); } } catch (error) { console.error('上传头像失败:', error); Alert.alert('错误', '头像上传失败,请重试'); } finally { setUploadingAvatar(false); } } }; const handleSave = async () => { if (!nickname.trim()) { Alert.alert('错误', '昵称不能为空'); return; } if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { Alert.alert('错误', '请输入正确的邮箱地址'); return; } if (phone && !/^1[3-9]\d{9}$/.test(phone)) { Alert.alert('错误', '请输入正确的手机号'); return; } setSaving(true); try { const updatedUser = await authService.updateUser({ nickname: nickname.trim(), bio: bio.trim() || undefined, location: location.trim() || undefined, website: website.trim() || undefined, phone: phone.trim() || undefined, email: email.trim() || undefined, }); if (updatedUser) { updateUser({ nickname: nickname.trim(), bio: bio.trim() || undefined, location: location.trim() || undefined, website: website.trim() || undefined, phone: phone.trim() || undefined, email: email.trim() || undefined, }); Alert.alert('成功', '资料已更新', [ { text: '确定', onPress: () => navigation.goBack() } ]); } else { Alert.alert('错误', '保存失败,请重试'); } } catch (error) { console.error('保存资料失败:', error); Alert.alert('错误', '保存失败,请重试'); } finally { setSaving(false); } }; const renderFormContent = () => ( <> {/* ===== 封面区域 - Twitter 风格全宽 ===== */} {coverUrl ? ( ) : ( )} {uploadingCover ? ( ) : ( )} {/* ===== 用户信息区 - Twitter 风格左对齐 ===== */} {uploadingAvatar && ( )} {nickname || currentUser?.nickname || ''} @{currentUser?.username || ''} 点击头像或头图可更换照片 {/* ===== 表单区域 - Twitter 风格全宽分组 ===== */} {/* 联系方式区域 */} {/* 保存按钮 */} {saving ? '保存中...' : '保存修改'} ); return ( {renderFormContent()} ); }; export default EditProfileScreen;