2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 编辑资料页 EditProfileScreen(响应式适配)
|
|
|
|
|
|
* 胡萝卜BBS - 编辑用户资料
|
|
|
|
|
|
* 与用户资料页样式完全一致
|
|
|
|
|
|
* 表单在宽屏下居中显示
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState } 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 { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
|
|
|
|
|
|
import { useAuthStore } from '../../stores';
|
|
|
|
|
|
import { Avatar, Button, Text, ResponsiveContainer } from '../../components/common';
|
|
|
|
|
|
import { authService, uploadService } from '../../services';
|
|
|
|
|
|
import { useResponsive } from '../../hooks';
|
|
|
|
|
|
|
|
|
|
|
|
// 表单输入项组件
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const FormField: React.FC<FormFieldProps> = ({
|
|
|
|
|
|
icon,
|
|
|
|
|
|
label,
|
|
|
|
|
|
value,
|
|
|
|
|
|
onChangeText,
|
|
|
|
|
|
placeholder,
|
|
|
|
|
|
maxLength,
|
|
|
|
|
|
multiline = false,
|
|
|
|
|
|
numberOfLines = 1,
|
|
|
|
|
|
autoCapitalize = 'sentences',
|
|
|
|
|
|
keyboardType = 'default',
|
|
|
|
|
|
editable = true,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={styles.formField}>
|
|
|
|
|
|
<View style={styles.fieldIconContainer}>
|
|
|
|
|
|
<MaterialCommunityIcons name={icon as any} size={22} color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.fieldContent}>
|
|
|
|
|
|
<Text variant="caption" style={styles.fieldLabel}>{label}</Text>
|
|
|
|
|
|
<TextInput
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.fieldInput,
|
|
|
|
|
|
multiline && styles.textArea,
|
|
|
|
|
|
!editable && styles.disabledInput
|
|
|
|
|
|
]}
|
|
|
|
|
|
value={value}
|
|
|
|
|
|
onChangeText={onChangeText}
|
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
|
placeholderTextColor={colors.text.hint}
|
|
|
|
|
|
maxLength={maxLength}
|
|
|
|
|
|
multiline={multiline}
|
|
|
|
|
|
numberOfLines={numberOfLines}
|
|
|
|
|
|
autoCapitalize={autoCapitalize}
|
|
|
|
|
|
keyboardType={keyboardType}
|
|
|
|
|
|
editable={editable}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const EditProfileScreen: React.FC = () => {
|
|
|
|
|
|
const navigation = useNavigation();
|
|
|
|
|
|
const { currentUser, updateUser } = useAuthStore();
|
|
|
|
|
|
const { isWideScreen, isMobile, width } = useResponsive();
|
|
|
|
|
|
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 bottomSafeDistance = isMobile ? insets.bottom + 92 : spacing.xl;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据屏幕宽度计算封面高度
|
|
|
|
|
|
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(),
|
2026-03-19 10:53:09 +08:00
|
|
|
|
bio: bio.trim() || undefined,
|
|
|
|
|
|
location: location.trim() || undefined,
|
|
|
|
|
|
website: website.trim() || undefined,
|
|
|
|
|
|
phone: phone.trim() || undefined,
|
|
|
|
|
|
email: email.trim() || undefined,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Alert.alert('成功', '资料已更新', [
|
|
|
|
|
|
{ text: '确定', onPress: () => navigation.goBack() }
|
|
|
|
|
|
]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Alert.alert('错误', '保存失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存资料失败:', error);
|
|
|
|
|
|
Alert.alert('错误', '保存失败,请重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染表单内容
|
|
|
|
|
|
const renderFormContent = () => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* ===== 用户资料预览区域 - 与 UserProfileHeader 完全一致 ===== */}
|
|
|
|
|
|
<View style={styles.previewContainer}>
|
|
|
|
|
|
{/* 封面背景 */}
|
|
|
|
|
|
<View style={[styles.coverContainer, { height: coverHeight }]}>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.coverTouchable}
|
|
|
|
|
|
onPress={handlePickCover}
|
|
|
|
|
|
activeOpacity={0.8}
|
|
|
|
|
|
>
|
|
|
|
|
|
{coverUrl ? (
|
|
|
|
|
|
<Image
|
|
|
|
|
|
source={{ uri: coverUrl }}
|
|
|
|
|
|
style={styles.coverImage}
|
|
|
|
|
|
resizeMode="cover"
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<LinearGradient
|
|
|
|
|
|
colors={['#FF8F66', '#FF6B35', '#E5521D']}
|
|
|
|
|
|
start={{ x: 0, y: 0 }}
|
|
|
|
|
|
end={{ x: 1, y: 1 }}
|
|
|
|
|
|
style={styles.gradient}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 头图编辑蒙版 */}
|
|
|
|
|
|
{uploadingCover ? (
|
|
|
|
|
|
<View style={styles.coverUploadingOverlay}>
|
|
|
|
|
|
<MaterialCommunityIcons name="loading" size={32} color={colors.text.inverse} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<View style={styles.coverEditOverlay}>
|
|
|
|
|
|
<MaterialCommunityIcons name="camera" size={28} color={colors.text.inverse} />
|
|
|
|
|
|
<Text style={styles.coverEditText}>更换头图</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 装饰性波浪 */}
|
|
|
|
|
|
<View style={styles.waveDecoration}>
|
|
|
|
|
|
<View style={styles.wave} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 用户信息卡片 */}
|
|
|
|
|
|
<View style={styles.profileCard}>
|
|
|
|
|
|
{/* 悬浮头像 */}
|
|
|
|
|
|
<View style={styles.avatarWrapper}>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.avatarContainer}
|
|
|
|
|
|
onPress={handlePickAvatar}
|
|
|
|
|
|
activeOpacity={0.8}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
source={avatar || null}
|
|
|
|
|
|
size={isWideScreen ? 110 : 90}
|
|
|
|
|
|
name={nickname}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{uploadingAvatar && (
|
|
|
|
|
|
<View style={styles.avatarUploadingOverlay}>
|
|
|
|
|
|
<MaterialCommunityIcons name="loading" size={32} color={colors.text.inverse} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<View style={styles.editAvatarButton}>
|
|
|
|
|
|
<MaterialCommunityIcons name="camera" size={14} color={colors.text.inverse} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 用户名和简介 */}
|
|
|
|
|
|
<View style={styles.userInfo}>
|
|
|
|
|
|
<Text variant="h2" style={styles.nickname}>
|
|
|
|
|
|
{nickname || currentUser?.nickname}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.username}>
|
|
|
|
|
|
@{currentUser?.username}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
|
|
|
|
{bio ? (
|
|
|
|
|
|
<Text variant="body" color={colors.text.secondary} style={styles.bio}>
|
|
|
|
|
|
{bio}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Text variant="body" color={colors.text.hint} style={styles.bioPlaceholder}>
|
|
|
|
|
|
这个人很懒,还没有写简介~
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 个人信息标签 */}
|
|
|
|
|
|
<View style={styles.metaInfo}>
|
|
|
|
|
|
{location ? (
|
|
|
|
|
|
<View style={styles.metaTag}>
|
|
|
|
|
|
<MaterialCommunityIcons name="map-marker-outline" size={12} color={colors.primary.main} />
|
|
|
|
|
|
<Text variant="caption" color={colors.primary.main} style={styles.metaTagText}>
|
|
|
|
|
|
{location}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<View style={styles.metaTag}>
|
|
|
|
|
|
<MaterialCommunityIcons name="map-marker-outline" size={12} color={colors.text.hint} />
|
|
|
|
|
|
<Text variant="caption" color={colors.text.hint} style={styles.metaTagText}>
|
|
|
|
|
|
未设置地区
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{website ? (
|
|
|
|
|
|
<View style={styles.metaTag}>
|
|
|
|
|
|
<MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} />
|
|
|
|
|
|
<Text variant="caption" color={colors.info.main} style={styles.metaTagText}>
|
|
|
|
|
|
{website.replace(/^https?:\/\//, '')}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<View style={styles.metaTag}>
|
|
|
|
|
|
<MaterialCommunityIcons name="link-variant" size={12} color={colors.text.hint} />
|
|
|
|
|
|
<Text variant="caption" color={colors.text.hint} style={styles.metaTagText}>
|
|
|
|
|
|
未设置网站
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<View style={styles.metaTag}>
|
|
|
|
|
|
<MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} />
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}>
|
|
|
|
|
|
加入于 {new Date(currentUser?.created_at || Date.now()).getFullYear()}年
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 编辑提示 */}
|
|
|
|
|
|
<View style={styles.editHint}>
|
|
|
|
|
|
<MaterialCommunityIcons name="information-outline" size={14} color={colors.text.hint} />
|
|
|
|
|
|
<Text variant="caption" color={colors.text.hint} style={styles.editHintText}>
|
|
|
|
|
|
点击头像或头图可更换照片
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ===== 表单区域 ===== */}
|
|
|
|
|
|
<View style={styles.formCard}>
|
|
|
|
|
|
<Text variant="body" style={[styles.sectionTitle, { fontWeight: '600' }]}>
|
|
|
|
|
|
个人资料
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
icon="account-outline"
|
|
|
|
|
|
label="昵称"
|
|
|
|
|
|
value={nickname}
|
|
|
|
|
|
onChangeText={setNickname}
|
|
|
|
|
|
placeholder="请输入昵称"
|
|
|
|
|
|
maxLength={20}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.divider} />
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
icon="information-outline"
|
|
|
|
|
|
label="个人简介"
|
|
|
|
|
|
value={bio}
|
|
|
|
|
|
onChangeText={setBio}
|
|
|
|
|
|
placeholder="介绍一下自己,让大家更了解你"
|
|
|
|
|
|
maxLength={100}
|
|
|
|
|
|
multiline
|
|
|
|
|
|
numberOfLines={3}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.divider} />
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
icon="map-marker-outline"
|
|
|
|
|
|
label="地区"
|
|
|
|
|
|
value={location}
|
|
|
|
|
|
onChangeText={setLocation}
|
|
|
|
|
|
placeholder="你所在的城市"
|
|
|
|
|
|
maxLength={30}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.divider} />
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
icon="link-variant"
|
|
|
|
|
|
label="个人网站"
|
|
|
|
|
|
value={website}
|
|
|
|
|
|
onChangeText={setWebsite}
|
|
|
|
|
|
placeholder="https://example.com"
|
|
|
|
|
|
maxLength={100}
|
|
|
|
|
|
autoCapitalize="none"
|
|
|
|
|
|
keyboardType="url"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 联系方式区域 */}
|
|
|
|
|
|
<View style={styles.formCard}>
|
|
|
|
|
|
<Text variant="body" style={[styles.sectionTitle, { fontWeight: '600' }]}>
|
|
|
|
|
|
联系方式
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
icon="phone-outline"
|
|
|
|
|
|
label="手机号"
|
|
|
|
|
|
value={phone}
|
|
|
|
|
|
onChangeText={setPhone}
|
|
|
|
|
|
placeholder="请输入手机号"
|
|
|
|
|
|
maxLength={11}
|
|
|
|
|
|
keyboardType="phone-pad"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.divider} />
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
icon="email-outline"
|
|
|
|
|
|
label="邮箱"
|
|
|
|
|
|
value={email}
|
|
|
|
|
|
onChangeText={setEmail}
|
|
|
|
|
|
placeholder="请输入邮箱地址"
|
|
|
|
|
|
maxLength={100}
|
|
|
|
|
|
autoCapitalize="none"
|
|
|
|
|
|
keyboardType="email-address"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 保存按钮 */}
|
|
|
|
|
|
<View style={[styles.buttonContainer, { marginBottom: bottomSafeDistance }]}>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.saveButton,
|
|
|
|
|
|
(saving || uploadingAvatar || uploadingCover) && styles.saveButtonDisabled
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={handleSave}
|
|
|
|
|
|
disabled={saving || uploadingAvatar || uploadingCover}
|
|
|
|
|
|
activeOpacity={0.8}
|
|
|
|
|
|
>
|
|
|
|
|
|
<View style={styles.saveButtonContent}>
|
|
|
|
|
|
{saving ? (
|
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name="loading"
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
color={colors.text.inverse}
|
|
|
|
|
|
style={styles.buttonIcon}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name="check"
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
color={colors.text.inverse}
|
|
|
|
|
|
style={styles.buttonIcon}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Text variant="body" style={[styles.saveButtonText, { fontWeight: '600' }]}>
|
|
|
|
|
|
{saving ? '保存中...' : '保存修改'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
|
|
|
|
<KeyboardAvoidingView
|
|
|
|
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
|
|
|
|
style={styles.keyboardView}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isWideScreen ? (
|
|
|
|
|
|
<ResponsiveContainer maxWidth={800}>
|
|
|
|
|
|
<ScrollView
|
|
|
|
|
|
contentContainerStyle={styles.scrollContent}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderFormContent()}
|
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ScrollView
|
|
|
|
|
|
contentContainerStyle={styles.scrollContent}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderFormContent()}
|
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</KeyboardAvoidingView>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const styles = 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: {
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
height: '60%',
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default EditProfileScreen;
|