/** * 编辑资料页 EditProfileScreen(响应式适配) * 胡萝卜BBS - 编辑用户资料 * 与用户资料页样式完全一致 * 表单在宽屏下居中显示 */ 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 { LinearGradient } from 'expo-linear-gradient'; import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors, } from '../../theme'; import { useAuthStore } from '../../stores'; import { Avatar, Button, Text } from '../../components/common'; import { authService, uploadService } from '../../services'; import { useResponsive, useResponsiveSpacing } from '../../hooks'; // 内容最大宽度 const CONTENT_MAX_WIDTH = 720; function createEditProfileStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, }, keyboardView: { flex: 1, }, scrollContent: { padding: spacing.lg, }, // ===== 预览区域 - 与 UserProfileHeader 完全一致 ===== previewContainer: { marginBottom: spacing.lg, }, coverContainer: { position: 'relative', overflow: 'hidden', borderRadius: borderRadius.lg, }, coverTouchable: { width: '100%', height: '100%', }, coverImage: { width: '100%', height: '100%', }, gradient: { width: '100%', height: '100%', }, coverUploadingOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', }, coverEditOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.3)', justifyContent: 'center', alignItems: 'center', }, coverEditText: { color: colors.text.inverse, fontSize: fontSizes.sm, marginTop: spacing.xs, fontWeight: '500', }, waveDecoration: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 40, }, wave: { width: '100%', height: '100%', backgroundColor: colors.background.paper, borderTopLeftRadius: 30, borderTopRightRadius: 30, }, // 用户信息卡片 profileCard: { backgroundColor: colors.background.paper, marginHorizontal: spacing.md, marginTop: -50, borderRadius: borderRadius.xl, padding: spacing.lg, ...shadows.md, }, avatarWrapper: { alignItems: 'center', marginTop: -60, marginBottom: spacing.md, }, avatarContainer: { position: 'relative', padding: 4, backgroundColor: colors.background.paper, borderRadius: 50, }, avatarUploadingOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 60, justifyContent: 'center', alignItems: 'center', }, editAvatarButton: { position: 'absolute', bottom: 0, right: 0, width: 28, height: 28, borderRadius: 14, backgroundColor: colors.primary.main, justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: colors.background.paper, }, userInfo: { alignItems: 'center', marginBottom: spacing.md, }, nickname: { marginBottom: spacing.xs, fontWeight: '700', }, username: { marginBottom: spacing.sm, }, bio: { textAlign: 'center', marginTop: spacing.sm, lineHeight: 20, }, bioPlaceholder: { textAlign: 'center', marginTop: spacing.sm, fontStyle: 'italic', }, metaInfo: { flexDirection: 'row', justifyContent: 'center', flexWrap: 'wrap', marginBottom: spacing.md, gap: spacing.sm, }, metaTag: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.default, paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, borderRadius: borderRadius.md, }, metaTagText: { marginLeft: spacing.xs, fontSize: fontSizes.xs, }, editHint: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: colors.background.default, borderRadius: borderRadius.md, paddingVertical: spacing.sm, gap: spacing.xs, }, editHintText: { fontSize: fontSizes.xs, }, // ===== 表单区域 ===== formCard: { backgroundColor: colors.background.paper, borderRadius: borderRadius.lg, padding: spacing.lg, marginBottom: spacing.lg, shadowColor: colors.text.primary, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8, elevation: 2, maxWidth: CONTENT_MAX_WIDTH, alignSelf: 'center', width: '100%', }, sectionTitle: { marginBottom: spacing.lg, }, formField: { flexDirection: 'row', alignItems: 'flex-start', }, fieldIconContainer: { width: 40, height: 40, borderRadius: borderRadius.md, backgroundColor: colors.primary.light + '20', justifyContent: 'center', alignItems: 'center', marginRight: spacing.md, marginTop: spacing.xs, }, fieldContent: { flex: 1, }, fieldLabel: { marginBottom: spacing.xs, fontWeight: '500', }, fieldInput: { fontSize: fontSizes.md, color: colors.text.primary, paddingVertical: spacing.sm, borderBottomWidth: 1, borderBottomColor: colors.divider, }, textArea: { height: 80, textAlignVertical: 'top', }, disabledInput: { color: colors.text.disabled, }, divider: { height: 1, backgroundColor: colors.divider, marginVertical: spacing.md, marginLeft: 56, }, // 保存按钮 buttonContainer: { marginTop: spacing.sm, }, saveButton: { backgroundColor: colors.primary.main, borderRadius: borderRadius.lg, paddingVertical: spacing.md, shadowColor: colors.primary.main, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 4, }, saveButtonDisabled: { opacity: 0.6, }, saveButtonContent: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', }, buttonIcon: { marginRight: spacing.sm, }, saveButtonText: { color: colors.text.inverse, }, }); } type EditProfileSheet = ReturnType; // 表单输入项组件 interface FormFieldProps { icon: string; 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; } const FormField: React.FC = ({ icon, label, value, onChangeText, placeholder, maxLength, multiline = false, numberOfLines = 1, autoCapitalize = 'sentences', keyboardType = 'default', editable = true, sheet, colors, }) => { return ( {label} ); }; 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: 8, sm: 12, md: 16, 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); // 底部间距,避免被 TabBar 遮挡 const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg; // 根据屏幕宽度计算封面高度 const coverHeight = isWideScreen ? Math.min((width * 9) / 16, 300) : (width * 9) / 16; // 选择头图 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 = () => ( <> {/* ===== 用户资料预览区域 - 与 UserProfileHeader 完全一致 ===== */} {/* 封面背景 */} {coverUrl ? ( ) : ( )} {/* 头图编辑蒙版 */} {uploadingCover ? ( ) : ( 更换头图 )} {/* 装饰性波浪 */} {/* 用户信息卡片 */} {/* 悬浮头像 */} {uploadingAvatar && ( )} {/* 用户名和简介 */} {nickname || currentUser?.nickname} @{currentUser?.username} {bio ? ( {bio} ) : ( 这个人很懒,还没有写简介~ )} {/* 个人信息标签 */} {location ? ( {location} ) : ( 未设置地区 )} {website ? ( {website.replace(/^https?:\/\//, '')} ) : ( 未设置网站 )} 加入于 {new Date(currentUser?.created_at || Date.now()).getFullYear()}年 {/* 编辑提示 */} 点击头像或头图可更换照片 {/* ===== 表单区域 ===== */} 个人资料 {/* 联系方式区域 */} 联系方式 {/* 保存按钮 */} {saving ? ( ) : ( )} {saving ? '保存中...' : '保存修改'} ); return ( {renderFormContent()} ); }; export default EditProfileScreen;