Bläddra i källkod

♻️ refactor(avatar): 重构头像上传组件,分离业务逻辑

- 移除AvatarUpload组件中的API调用和用户认证依赖,使其更通用
- 新增onUploadError回调函数,提供错误处理机制
- 添加userId和username属性,支持外部传入用户信息
- 将头像上传后的资料更新逻辑移至ProfileEditPage页面
- 优化错误提示方式,支持自定义错误处理或默认alert

✨ feat(avatar): 增强头像上传错误处理能力

- 实现文件类型验证错误的捕获与传递
- 添加文件大小超限错误的处理机制
- 支持上传过程中各类异常的捕获和反馈

🔧 chore(profile): 调整个人资料页面,适配头像组件变更

- 在ProfileEditPage中实现头像上传后的资料更新逻辑
- 添加toast提示,优化用户操作反馈
- 传入必要的用户信息至AvatarUpload组件
yourname 9 månader sedan
förälder
incheckning
4808621a2e
2 ändrade filer med 79 tillägg och 105 borttagningar
  1. 47 103
      src/client/mobile/components/AvatarUpload.tsx
  2. 32 2
      src/client/mobile/pages/ProfileEditPage.tsx

+ 47 - 103
src/client/mobile/components/AvatarUpload.tsx

@@ -1,9 +1,6 @@
 import React, { useState, useRef } from 'react';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { silverUserProfileClient } from '@/client/api';
 import { uploadMinIOWithPolicy } from '@/client/utils/minio';
 import { compressImage, isImageFile, checkFileSize } from '@/client/utils/upload';
-import { useAuth } from '../hooks/AuthProvider';
 import { AvatarCropper } from './AvatarCropper';
 
 // 水墨风格色彩系统
@@ -28,105 +25,25 @@ const COLORS = {
 interface AvatarUploadProps {
   currentAvatar?: string | null;
   onUploadSuccess?: (avatarFileId: number) => void;
+  onUploadError?: (error: Error) => void;
   size?: number;
+  userId?: number;
+  username?: string;
 }
 
 export const AvatarUpload: React.FC<AvatarUploadProps> = ({
   currentAvatar,
   onUploadSuccess,
+  onUploadError,
   size = 96,
+  userId,
+  username,
 }) => {
   const [isUploading, setIsUploading] = useState(false);
   const [previewUrl, setPreviewUrl] = useState<string | null>(null);
   const [showCropper, setShowCropper] = useState(false);
   const [cropImageUrl, setCropImageUrl] = useState<string | null>(null);
   const fileInputRef = useRef<HTMLInputElement>(null);
-  const { user } = useAuth();
-  const queryClient = useQueryClient();
-
-  // 上传头像的mutation
-  const uploadAvatarMutation = useMutation({
-    mutationFn: async (file: File) => {
-      if (!user?.id) {
-        throw new Error('用户未登录');
-      }
-
-      // 1. 压缩图片
-      const compressedFile = await compressImage(file, 800, 800, 0.8);
-      
-      // 2. 生成上传路径
-      const timestamp = Date.now();
-      const fileExtension = compressedFile.name.split('.').pop();
-      const fileKey = `avatars/${user.id}/${timestamp}.${fileExtension}`;
-      
-      // 3. 上传到MinIO
-      const result = await uploadMinIOWithPolicy(
-        'avatars',
-        compressedFile,
-        fileKey
-      );
-
-      // 4. 获取用户资料ID
-      const profileResponse = await silverUserProfileClient.$get({
-        query: { filters: JSON.stringify({ userId: user.id }) }
-      });
-      
-      let profileId: number;
-      if (profileResponse.status === 200) {
-        const profileData = await profileResponse.json();
-        if (profileData.data && profileData.data.length > 0) {
-          profileId = profileData.data[0].id;
-        } else {
-          // 创建新的用户资料
-          const createResponse = await silverUserProfileClient.$post({
-            json: {
-              realName: user.username || '用户',
-              age: 30,
-              gender: 1,
-              phone: '',
-              avatarFileId: result.fileId,
-            }
-          });
-          
-          if (createResponse.status !== 201) {
-            throw new Error('创建用户资料失败');
-          }
-          
-          const createdProfile = await createResponse.json();
-          profileId = createdProfile.id;
-        }
-      } else {
-        throw new Error('获取用户资料失败');
-      }
-
-      // 5. 更新用户资料头像
-      const updateResponse = await silverUserProfileClient[':id'].$put({
-        param: { id: profileId },
-        json: {
-          avatarFileId: result.fileId,
-        }
-      });
-
-      if (updateResponse.status !== 200) {
-        throw new Error('更新用户头像失败');
-      }
-
-      return result.fileId;
-    },
-    onSuccess: (fileId) => {
-      // 清除相关缓存
-      queryClient.invalidateQueries({ queryKey: ['userProfile'] });
-      queryClient.invalidateQueries({ queryKey: ['currentUser'] });
-      queryClient.invalidateQueries({ queryKey: ['silverUserProfile'] });
-      
-      if (onUploadSuccess) {
-        onUploadSuccess(fileId);
-      }
-    },
-    onError: (error) => {
-      console.error('头像上传失败:', error);
-    }
-  });
 
   const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
     const file = event.target.files?.[0];
@@ -134,13 +51,17 @@ export const AvatarUpload: React.FC<AvatarUploadProps> = ({
 
     // 验证文件类型
     if (!isImageFile(file)) {
-      alert('请选择图片文件');
+      const error = new Error('请选择图片文件');
+      if (onUploadError) onUploadError(error);
+      else alert(error.message);
       return;
     }
 
     // 验证文件大小 (2MB)
     if (!checkFileSize(file, 2)) {
-      alert('图片大小不能超过2MB');
+      const error = new Error('图片大小不能超过2MB');
+      if (onUploadError) onUploadError(error);
+      else alert(error.message);
       return;
     }
 
@@ -152,7 +73,9 @@ export const AvatarUpload: React.FC<AvatarUploadProps> = ({
       
     } catch (error) {
       console.error('文件处理失败:', error);
-      alert('文件处理失败,请重试');
+      const err = error as Error;
+      if (onUploadError) onUploadError(err);
+      else alert('文件处理失败,请重试');
     } finally {
       // 清除文件输入
       if (fileInputRef.current) {
@@ -162,26 +85,47 @@ export const AvatarUpload: React.FC<AvatarUploadProps> = ({
   };
 
   const handleCropComplete = async (croppedBlob: Blob) => {
+    if (!userId) {
+      const error = new Error('用户ID未提供');
+      if (onUploadError) onUploadError(error);
+      else alert(error.message);
+      return;
+    }
+
     try {
       setIsUploading(true);
       setShowCropper(false);
       
-      // 创建文件对象
-      const croppedFile = new File([croppedBlob], 'avatar.jpg', { type: 'image/jpeg' });
+      // 1. 压缩图片
+      const compressedFile = await compressImage(new File([croppedBlob], 'avatar.jpg', { type: 'image/jpeg' }), 800, 800, 0.8);
       
-      // 上传头像
-      await uploadAvatarMutation.mutateAsync(croppedFile);
+      // 2. 生成上传路径
+      const timestamp = Date.now();
+      const fileExtension = compressedFile.name.split('.').pop();
+      const fileKey = `avatars/${userId}/${timestamp}.${fileExtension}`;
       
-      // 清理预览URL
-      if (cropImageUrl) {
-        URL.revokeObjectURL(cropImageUrl);
+      // 3. 上传到MinIO
+      const result = await uploadMinIOWithPolicy(
+        'avatars',
+        compressedFile,
+        fileKey
+      );
+      
+      // 4. 回调成功
+      if (onUploadSuccess) {
+        onUploadSuccess(result.fileId);
       }
       
     } catch (error) {
       console.error('头像上传失败:', error);
-      alert('头像上传失败,请重试');
+      const err = error as Error;
+      if (onUploadError) onUploadError(err);
+      else alert('头像上传失败,请重试');
     } finally {
       setIsUploading(false);
+      if (cropImageUrl) {
+        URL.revokeObjectURL(cropImageUrl);
+      }
       setCropImageUrl(null);
     }
   };
@@ -224,11 +168,11 @@ export const AvatarUpload: React.FC<AvatarUploadProps> = ({
             style={{ borderRadius: '50%' }}
           />
         ) : (
-          <div 
+          <div
             className="w-full h-full flex items-center justify-center text-white font-bold"
             style={{ fontSize: size * 0.4 }}
           >
-            {user?.username?.charAt(0)?.toUpperCase() || '用'}
+            {username?.charAt(0)?.toUpperCase() || '用'}
           </div>
         )}
 
@@ -268,7 +212,7 @@ export const AvatarUpload: React.FC<AvatarUploadProps> = ({
       />
 
       {/* 上传进度提示 */}
-      {uploadAvatarMutation.isPending && (
+      {isUploading && (
         <div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2">
           <span className="text-xs" style={{ color: COLORS.text.secondary }}>
             上传中...

+ 32 - 2
src/client/mobile/pages/ProfileEditPage.tsx

@@ -163,8 +163,35 @@ const ProfileEditPage: React.FC = () => {
     }
   }, [user, profileData, form]);
 
-  const handleAvatarChange = (fileId: number) => {
+  const handleAvatarUpload = async (fileId: number) => {
     setAvatarFileId(fileId);
+    
+    // 如果用户资料已存在,立即更新头像
+    if (profileData?.id) {
+      try {
+        const response = await silverUserProfileClient[':id'].$put({
+          param: { id: profileData.id },
+          json: {
+            avatarFileId: fileId,
+          }
+        });
+
+        if (response.status !== 200) {
+          throw new Error('更新用户头像失败');
+        }
+
+        // 刷新用户资料数据
+        await queryClient.invalidateQueries({ queryKey: ['userProfile', user?.id] });
+        toast.success('头像更新成功');
+      } catch (error) {
+        console.error('更新用户头像失败:', error);
+        toast.error('头像更新失败,请重试');
+      }
+    }
+  };
+
+  const handleAvatarUploadError = (error: Error) => {
+    toast.error(error.message);
   };
 
   const onSubmit = async (data: ProfileFormData) => {
@@ -276,7 +303,10 @@ const ProfileEditPage: React.FC = () => {
                   <AvatarUpload
                     currentAvatar={avatarFileId ? profileData?.avatarFile?.fullUrl : ''}
                     size={80}
-                    onUploadSuccess={handleAvatarChange}
+                    userId={user?.id}
+                    username={user?.username}
+                    onUploadSuccess={handleAvatarUpload}
+                    onUploadError={handleAvatarUploadError}
                   />
                   <div>
                     <p className="text-sm text-gray-600">点击头像上传新照片</p>