diff --git a/app/(tabs)/my.tsx b/app/(tabs)/my.tsx
index ca70c7f..4837c79 100644
--- a/app/(tabs)/my.tsx
+++ b/app/(tabs)/my.tsx
@@ -154,7 +154,7 @@ export default function My() {
{/* 个人信息区 */}
@@ -260,7 +260,10 @@ export default function My() {
visible={editDrawerVisible}
onClose={() => setEditDrawerVisible(false)}
initialName={profileName}
- onSave={(name) => setProfileName(name)}
+ initialAvatar={session?.user?.image}
+ onSave={(data) => {
+ setProfileName(data.name)
+ }}
/>
)
diff --git a/components/drawer/EditProfileDrawer.tsx b/components/drawer/EditProfileDrawer.tsx
index 788149a..04fb647 100644
--- a/components/drawer/EditProfileDrawer.tsx
+++ b/components/drawer/EditProfileDrawer.tsx
@@ -9,6 +9,7 @@ import {
Platform,
Keyboard,
ScrollView,
+ ActivityIndicator,
} from 'react-native'
import { Image } from 'expo-image'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -16,51 +17,54 @@ 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?: any
- onSave?: (name: string) => void
+ initialAvatar?: string
+ onSave?: (data: { name: string; avatar?: string }) => void
}
export default function EditProfileDrawer({
visible,
onClose,
- initialName = '乔乔乔',
+ initialName = '',
initialAvatar,
onSave,
}: EditProfileDrawerProps) {
const { t } = useTranslation()
const bottomSheetRef = useRef(null)
const [name, setName] = useState(initialName)
+ const [avatar, setAvatar] = useState(initialAvatar)
+ const [selectedImage, setSelectedImage] = useState(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])
+ }, [visible, initialName, initialAvatar])
- // 当抽屉打开时,重置名字为初始值
- useEffect(() => {
- if (visible) {
- setName(initialName)
- }
- }, [visible, initialName])
-
const handleSheetChanges = useCallback((index: number) => {
if (index === -1) {
onClose()
}
}, [onClose])
-
+
const renderBackdrop = useCallback(
(props: any) => (
{
+ // 处理头像选择
+ const handleAvatarPress = useCallback(async () => {
+ const image = await pickImage()
+ if (image) {
+ setSelectedImage(image)
+ setAvatar(image.uri)
+ }
+ }, [pickImage])
+
+ const handleSave = async () => {
Keyboard.dismiss()
- onSave?.(name)
- onClose()
+
+ // 构建更新数据
+ 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 = () => {
@@ -90,7 +123,7 @@ export default function EditProfileDrawer({
index={visible ? 0 : -1}
snapPoints={snapPoints}
onChange={handleSheetChanges}
- enablePanDownToClose
+ enablePanDownToClose={!loading}
backgroundStyle={styles.bottomSheetBackground}
handleIndicatorStyle={styles.handleIndicator}
backdropComponent={renderBackdrop}
@@ -109,6 +142,7 @@ export default function EditProfileDrawer({
style={styles.closeButton}
onPress={handleClose}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ disabled={loading}
>
@@ -116,34 +150,44 @@ export default function EditProfileDrawer({
-
+
-
+ {loading ? (
+
+ ) : (
+
+ )}
{/* 输入框 */}
-
+
{/* 保存按钮 */}
-
- {t('editProfile.save')}
+ {loading ? (
+
+ ) : (
+ {t('editProfile.save')}
+ )}
@@ -242,6 +290,9 @@ const styles = StyleSheet.create({
overflow: 'hidden',
height: 48,
},
+ saveButtonDisabled: {
+ opacity: 0.6,
+ },
saveButton: {
width: '100%',
alignItems: 'center',
@@ -255,4 +306,3 @@ const styles = StyleSheet.create({
fontWeight: '500',
},
})
-
diff --git a/hooks/index.ts b/hooks/index.ts
index e0071dd..344be5b 100644
--- a/hooks/index.ts
+++ b/hooks/index.ts
@@ -10,3 +10,4 @@ export { useTags } from './use-tags'
export { useDebounce } from './use-debounce'
export { useWorksSearch } from './use-works-search'
export { useChangePassword } from './use-change-password'
+export { useUpdateProfile } from './use-update-profile'
diff --git a/hooks/use-update-profile.ts b/hooks/use-update-profile.ts
new file mode 100644
index 0000000..4f365f3
--- /dev/null
+++ b/hooks/use-update-profile.ts
@@ -0,0 +1,104 @@
+import { useState, useCallback } from 'react'
+import { ImagePickerAsset, launchImageLibraryAsync, MediaTypeOptions } from 'expo-image-picker'
+import Toast from '@/components/ui/Toast'
+import { updateUser } from '@/lib/auth'
+import { uploadFile } from '@/lib/uploadFile'
+import { type ApiError } from '@/lib/types'
+
+export interface UpdateProfileParams {
+ name?: string
+ image?: ImagePickerAsset
+}
+
+export function useUpdateProfile() {
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ /**
+ * 选择图片
+ */
+ const pickImage = useCallback(async (): Promise => {
+ try {
+ const result = await launchImageLibraryAsync({
+ mediaTypes: MediaTypeOptions.Images,
+ allowsEditing: true,
+ aspect: [1, 1],
+ quality: 0.8,
+ })
+
+ if (result.canceled) {
+ return null
+ }
+
+ return result.assets[0] || null
+ } catch (err) {
+ console.error('选择图片失败:', err)
+ Toast.show('选择图片失败')
+ return null
+ }
+ }, [])
+
+ /**
+ * 更新用户资料
+ */
+ const updateProfile = useCallback(async (params: UpdateProfileParams) => {
+ setLoading(true)
+ setError(null)
+
+ try {
+ const updateData: Record = {}
+
+ // 如果有新头像,先上传图片
+ if (params.image) {
+ Toast.showLoading({ title: '上传头像中...' })
+ try {
+ const imageUrl = await uploadFile({
+ uri: params.image.uri,
+ mimeType: params.image.mimeType || 'image/jpeg',
+ fileName: `avatar_${Date.now()}.jpg`,
+ })
+ updateData.image = imageUrl
+ Toast.hideLoading()
+ } catch (err) {
+ Toast.hideLoading()
+ throw new Error('头像上传失败')
+ }
+ }
+
+ // 更新用户名
+ if (params.name) {
+ updateData.name = params.name
+ }
+
+ // 如果有数据需要更新
+ if (Object.keys(updateData).length > 0) {
+ Toast.showLoading({ title: '保存中...' })
+ const result = await updateUser(updateData)
+ Toast.hideLoading()
+
+ if (result.error) {
+ throw result.error
+ }
+
+ Toast.show('保存成功')
+ return { data: result.data, error: null }
+ }
+
+ return { data: null, error: null }
+ } catch (err) {
+ const errorObj = err as ApiError
+ setError(errorObj)
+ Toast.show(errorObj.message || '保存失败,请稍后重试')
+ return { data: null, error: errorObj }
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ return {
+ loading,
+ error,
+ pickImage,
+ updateProfile,
+ }
+}
diff --git a/lib/auth.ts b/lib/auth.ts
index 4480b31..c633847 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -118,7 +118,7 @@ export const authClient = createAuthClient({
],
})
-export const { signIn, signUp, signOut, useSession, $Infer, admin, forgetPassword, resetPassword, emailOtp, changePassword } =
+export const { signIn, signUp, signOut, useSession, $Infer, admin, forgetPassword, resetPassword, emailOtp, changePassword, updateUser } =
authClient
// 导出 loomart API(来自 createSkerClientPlugin)