309 lines
9.2 KiB
TypeScript
309 lines
9.2 KiB
TypeScript
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
Pressable,
|
|
TextInput,
|
|
Dimensions,
|
|
Platform,
|
|
Keyboard,
|
|
ScrollView,
|
|
ActivityIndicator,
|
|
} from 'react-native'
|
|
import { Image } from 'expo-image'
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
import { useTranslation } from 'react-i18next'
|
|
import BottomSheet, { BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView } from '@gorhom/bottom-sheet'
|
|
import { CloseIcon, AvatarUploadIcon } from '@/components/icon'
|
|
import { LinearGradient } from 'expo-linear-gradient'
|
|
import { type ImagePickerAsset } from 'expo-image-picker'
|
|
import { useUpdateProfile } from '@/hooks'
|
|
|
|
|
|
interface EditProfileDrawerProps {
|
|
visible: boolean
|
|
onClose: () => void
|
|
initialName?: string
|
|
initialAvatar?: string
|
|
onSave?: (data: { name: string; avatar?: string }) => void
|
|
}
|
|
|
|
export default function EditProfileDrawer({
|
|
visible,
|
|
onClose,
|
|
initialName = '',
|
|
initialAvatar,
|
|
onSave,
|
|
}: EditProfileDrawerProps) {
|
|
const { t } = useTranslation()
|
|
const bottomSheetRef = useRef<BottomSheet>(null)
|
|
const [name, setName] = useState(initialName)
|
|
const [avatar, setAvatar] = useState<string | undefined>(initialAvatar)
|
|
const [selectedImage, setSelectedImage] = useState<ImagePickerAsset | null>(null)
|
|
const insets = useSafeAreaInsets()
|
|
|
|
const { loading, pickImage, updateProfile } = useUpdateProfile()
|
|
|
|
const snapPoints = useMemo(() => [280], [])
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
bottomSheetRef.current?.expand()
|
|
// 重置为初始值
|
|
setName(initialName)
|
|
setAvatar(initialAvatar)
|
|
setSelectedImage(null)
|
|
} else {
|
|
bottomSheetRef.current?.close()
|
|
}
|
|
}, [visible, initialName, initialAvatar])
|
|
|
|
const handleSheetChanges = useCallback((index: number) => {
|
|
if (index === -1) {
|
|
onClose()
|
|
}
|
|
}, [onClose])
|
|
|
|
const renderBackdrop = useCallback(
|
|
(props: any) => (
|
|
<BottomSheetBackdrop
|
|
{...props}
|
|
disappearsOnIndex={-1}
|
|
appearsOnIndex={0}
|
|
opacity={0.5}
|
|
/>
|
|
),
|
|
[]
|
|
)
|
|
|
|
// 处理头像选择
|
|
const handleAvatarPress = useCallback(async () => {
|
|
const image = await pickImage()
|
|
if (image) {
|
|
setSelectedImage(image)
|
|
setAvatar(image.uri)
|
|
}
|
|
}, [pickImage])
|
|
|
|
const handleSave = async () => {
|
|
Keyboard.dismiss()
|
|
|
|
// 构建更新数据
|
|
const updateData: { name: string; image?: ImagePickerAsset } = {
|
|
name: name.trim(),
|
|
}
|
|
|
|
// 如果选择了新头像,添加到更新数据
|
|
if (selectedImage) {
|
|
updateData.image = selectedImage
|
|
}
|
|
|
|
// 调用更新接口
|
|
const result = await updateProfile(updateData)
|
|
|
|
if (!result.error) {
|
|
// 通知父组件更新
|
|
onSave?.({
|
|
name: updateData.name,
|
|
avatar: avatar,
|
|
})
|
|
onClose()
|
|
}
|
|
}
|
|
|
|
const handleClose = () => {
|
|
Keyboard.dismiss()
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<BottomSheet
|
|
ref={bottomSheetRef}
|
|
index={visible ? 0 : -1}
|
|
snapPoints={snapPoints}
|
|
onChange={handleSheetChanges}
|
|
enablePanDownToClose={!loading}
|
|
backgroundStyle={styles.bottomSheetBackground}
|
|
handleIndicatorStyle={styles.handleIndicator}
|
|
backdropComponent={renderBackdrop}
|
|
keyboardBehavior="interactive"
|
|
keyboardBlurBehavior="restore"
|
|
>
|
|
<BottomSheetScrollView
|
|
style={styles.scrollView}
|
|
contentContainerStyle={styles.scrollContent}
|
|
keyboardShouldPersistTaps="handled"
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<View style={styles.container}>
|
|
{/* 顶部关闭按钮 */}
|
|
<Pressable
|
|
style={styles.closeButton}
|
|
onPress={handleClose}
|
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
disabled={loading}
|
|
>
|
|
<CloseIcon />
|
|
</Pressable>
|
|
{/* 头像区域 */}
|
|
<View style={styles.avatarContainer}>
|
|
<View style={styles.avatarWrapper}>
|
|
<Image
|
|
source={avatar ? { uri: avatar } : require('@/assets/images/icon.png')}
|
|
style={styles.avatar}
|
|
contentFit="cover"
|
|
/>
|
|
<Pressable
|
|
style={styles.cameraButton}
|
|
onPress={handleAvatarPress}
|
|
disabled={loading}
|
|
>
|
|
<View style={styles.cameraIconContainer}>
|
|
{loading ? (
|
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
) : (
|
|
<AvatarUploadIcon />
|
|
)}
|
|
</View>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 输入框 */}
|
|
<TextInput
|
|
style={styles.input}
|
|
value={name}
|
|
onChangeText={setName}
|
|
placeholder={t('editProfile.namePlaceholder')}
|
|
placeholderTextColor="#666666"
|
|
returnKeyType="done"
|
|
onSubmitEditing={handleSave}
|
|
blurOnSubmit={true}
|
|
editable={!loading}
|
|
/>
|
|
|
|
{/* 保存按钮 */}
|
|
<Pressable
|
|
style={[styles.saveButtonContainer, loading && styles.saveButtonDisabled]}
|
|
onPress={handleSave}
|
|
disabled={loading || !name.trim()}
|
|
android_ripple={{ color: 'rgba(255, 255, 255, 0.1)' }}
|
|
>
|
|
<LinearGradient
|
|
colors={['#9966FF', '#FF6699', '#FF9966']}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
style={styles.saveButton}
|
|
>
|
|
{loading ? (
|
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
) : (
|
|
<Text style={styles.saveButtonText}>{t('editProfile.save')}</Text>
|
|
)}
|
|
</LinearGradient>
|
|
</Pressable>
|
|
</View>
|
|
</BottomSheetScrollView>
|
|
</BottomSheet>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
bottomSheetBackground: {
|
|
backgroundColor: '#1C1E22',
|
|
},
|
|
handleIndicator: {
|
|
backgroundColor: '#666666',
|
|
},
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#1C1E22',
|
|
paddingHorizontal: 16,
|
|
},
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
paddingTop: 32,
|
|
paddingBottom: Platform.OS === 'ios' ? 25 : 17,
|
|
paddingHorizontal: 0,
|
|
flexGrow: 1,
|
|
},
|
|
closeButton: {
|
|
position: 'absolute',
|
|
top: Platform.OS === 'ios' ? 16 : 16,
|
|
right: 16,
|
|
width: 24,
|
|
height: 24,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 10,
|
|
},
|
|
avatarContainer: {
|
|
alignItems: 'center',
|
|
},
|
|
avatarWrapper: {
|
|
position: 'relative',
|
|
width: 88,
|
|
height: 88,
|
|
},
|
|
avatar: {
|
|
width: 88,
|
|
height: 88,
|
|
borderRadius: 50,
|
|
overflow: 'hidden',
|
|
},
|
|
cameraButton: {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
right: 0,
|
|
width: 26,
|
|
height: 26,
|
|
borderRadius: 16,
|
|
backgroundColor: '#16181B',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderWidth: 2,
|
|
borderColor: '#FFFFFF',
|
|
},
|
|
cameraIconContainer: {
|
|
width: 13,
|
|
height: 13,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
input: {
|
|
backgroundColor: '#262A31',
|
|
borderRadius: 12,
|
|
paddingHorizontal: 16,
|
|
marginVertical: 24,
|
|
paddingVertical: 14,
|
|
color: '#F5F5F5',
|
|
fontWeight: '500',
|
|
fontSize: 14,
|
|
height: 48,
|
|
},
|
|
saveButtonContainer: {
|
|
width: '100%',
|
|
borderRadius: 12,
|
|
overflow: 'hidden',
|
|
height: 48,
|
|
},
|
|
saveButtonDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
saveButton: {
|
|
width: '100%',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderRadius: 12,
|
|
height: 48,
|
|
},
|
|
saveButtonText: {
|
|
color: '#F5F5F5',
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
},
|
|
})
|