From bf11241d68b043b105925f50e4e3ab80001b977e Mon Sep 17 00:00:00 2001 From: imeepos Date: Fri, 23 Jan 2026 19:42:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=E5=92=8C=E5=A4=B4=E5=83=8F=E7=BC=96=E8=BE=91=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 Better Auth 的 updateUser API 更新用户信息,使用 SDK 的 FileController 上传头像。 ## 新增功能 - 创建 useUpdateProfile hook 处理用户信息更新 - 支持选择图片上传头像(使用 expo-image-picker) - 先上传头像到 S3,再更新用户信息 ## 更新文件 - lib/auth.ts: 导出 updateUser 方法 - hooks/use-update-profile.ts: 新建更新资料 hook - hooks/index.ts: 导出 useUpdateProfile - components/drawer/EditProfileDrawer.tsx: - 添加头像选择功能(点击相机按钮) - 调用 updateProfile API 保存更改 - 添加加载状态和禁用状态 - 更新 onSave 回调参数类型 - app/(tabs)/my.tsx: - 传递 initialAvatar 给编辑抽屉 - 显示用户真实头像(如有) - 更新 onSave 回调处理 ## 功能流程 1. 点击相机按钮选择图片 2. 调用 uploadFile 上传到 S3 3. 调用 updateUser 更新用户信息 4. 保存成功后刷新 session Co-Authored-By: Claude --- app/(tabs)/my.tsx | 7 +- components/drawer/EditProfileDrawer.tsx | 118 +++++++++++++++++------- hooks/index.ts | 1 + hooks/use-update-profile.ts | 104 +++++++++++++++++++++ lib/auth.ts | 2 +- 5 files changed, 195 insertions(+), 37 deletions(-) create mode 100644 hooks/use-update-profile.ts 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)