Răsfoiți Sursa

✨ feat(FileSelector): 添加上传用户ID筛选功能并重构移动端组件

- 在FileSelector组件中添加uploadUserId属性,支持按指定用户ID筛选文件
- 重构移动端ClassroomLayout组件,移除重复的FileSelector实现,统一使用通用组件
- 删除移动端专用的FileSelector和MinioUploader组件文件,避免代码重复
- 优化文件查询逻辑,支持根据uploadUserId参数进行用户文件筛选

【重构】移动端组件结构,提升代码复用性和维护性
- 将移动端ClassroomLayout中的FileSelector引用从移动端专用组件改为通用组件
- 移除重复的移动端FileSelector实现,减少代码冗余
- 删除不再使用的MinioUploader组件,统一文件上传逻辑
yourname 6 luni în urmă
părinte
comite
b6895a5771

+ 16 - 2
src/client/components/FileSelector.tsx

@@ -26,6 +26,8 @@ export interface FileSelectorProps {
   description?: string;
   filterType?: 'image' | 'all' | string;
   allowMultiple?: boolean;
+  /** 指定要筛选的上传用户ID,为0或undefined时显示所有用户的文件 */
+  uploadUserId?: number;
 }
 
 export const FileSelector: React.FC<FileSelectorProps> = ({
@@ -42,6 +44,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
   description = '上传新文件或从已有文件中选择',
   filterType = 'all',
   allowMultiple = false,
+  uploadUserId = 0,
 }) => {
   const [isOpen, setIsOpen] = useState(false);
   const [selectedFile, setSelectedFile] = useState<FileType | null>(null);
@@ -102,13 +105,24 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
 
   // 获取文件列表
   const { data: filesData, isLoading, refetch } = useQuery({
-    queryKey: ['files-for-selection', filterType] as const,
+    queryKey: ['files-for-selection', filterType, uploadUserId] as const,
     queryFn: async () => {
+      // 构建筛选条件
+      const filters: any = {};
+      
+      // 如果指定了上传用户ID,则筛选该用户的文件
+      if (uploadUserId && uploadUserId > 0) {
+        filters.uploadUserId = uploadUserId;
+      }
+      
       const response = await fileClient.$get({
         query: {
           page: 1,
           pageSize: 50,
-          ...(filterType !== 'all' && { keyword: filterType })
+          ...(filterType !== 'all' && { keyword: filterType }),
+          ...(Object.keys(filters).length > 0 && {
+            filters: JSON.stringify(filters)
+          })
         }
       });
       if (response.status !== 200) throw new Error('获取文件列表失败');

+ 1 - 1
src/client/mobile/components/Classroom/ClassroomLayout.tsx

@@ -23,7 +23,7 @@ import { TeacherQuestionManagementButton } from './TeacherQuestionManagementButt
 import { AllMuteToggleButton } from './AllMuteToggleButton';
 import { StudentHandUpButton } from './StudentHandUpButton';
 import { StudentQuestionButton } from './StudentQuestionButton';
-import { FileSelector } from '@/client/mobile/components/FileSelector';
+import { FileSelector } from '@/client/components/FileSelector';
 import { MessageList } from './MessageList';
 
 interface ClassroomLayoutProps {

+ 0 - 532
src/client/mobile/components/FileSelector.tsx

@@ -1,532 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { Button } from '@/client/components/ui/button';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
-import { Card, CardContent } from '@/client/components/ui/card';
-import { toast } from 'sonner';
-import { fileClient } from '@/client/api';
-import MinioUploader from '@/client/mobile/components/MinioUploader';
-import { Check, Upload, Eye, X, File as FileIcon, Image as ImageIcon } from 'lucide-react';
-import { cn } from '@/client/lib/utils';
-import type { InferResponseType } from 'hono/client';
-import { useAuth } from '@/client/mobile/hooks/AuthProvider';
-
-type FileType = InferResponseType<typeof fileClient.$get, 200>['data'][0]
-
-export interface FileSelectorProps {
-  value?: number | null | number[];
-  onChange?: (fileId: number | null | number[]) => void;
-  accept?: string;
-  maxSize?: number;
-  uploadPath?: string;
-  uploadButtonText?: string;
-  previewSize?: 'small' | 'medium' | 'large';
-  showPreview?: boolean;
-  placeholder?: string;
-  title?: string;
-  description?: string;
-  filterType?: 'image' | 'all' | string;
-  allowMultiple?: boolean;
-  /** 是否只显示当前用户自己上传的文件 */
-  onlyCurrentUser?: boolean;
-}
-
-export const FileSelector: React.FC<FileSelectorProps> = ({
-  value,
-  onChange,
-  accept = '*/*',
-  maxSize = 10,
-  uploadPath = '/files',
-  uploadButtonText = '上传文件',
-  previewSize = 'medium',
-  showPreview = true,
-  placeholder = '选择文件',
-  title = '选择文件',
-  description = '上传新文件或从已有文件中选择',
-  filterType = 'all',
-  allowMultiple = false,
-  onlyCurrentUser = false,
-}) => {
-  const [isOpen, setIsOpen] = useState(false);
-  const [selectedFile, setSelectedFile] = useState<FileType | null>(null);
-  const [localSelectedFiles, setLocalSelectedFiles] = useState<number[]>([]);
-  const { user } = useAuth();
-
-  // 获取当前选中的文件详情 - 支持单值和数组
-  const { data: currentFiles } = useQuery<FileType[]>({
-    queryKey: ['file-details', value, allowMultiple],
-    queryFn: async (): Promise<FileType[]> => {
-      if (!value) return [];
-      
-      // 处理多选模式下的数组值
-      if (allowMultiple && Array.isArray(value)) {
-        if (value.length === 0) return [];
-        
-        // 批量获取多个文件详情
-        const filePromises = value.map(async (fileId) => {
-          try {
-            const response = await fileClient[':id']['$get']({ param: { id: fileId.toString() } });
-            if (response.status === 200) {
-              return response.json();
-            }
-            return null;
-          } catch (error) {
-            console.error(`获取文件 ${fileId} 详情失败:`, error);
-            return null;
-          }
-        });
-        
-        const files = await Promise.all(filePromises);
-        return files.filter(file => file !== null);
-      }
-      
-      // 处理单选模式下的单值
-      if (!Array.isArray(value)) {
-        const response = await fileClient[':id']['$get']({ param: { id: value.toString() } });
-        if (response.status !== 200) throw new Error('获取文件详情失败');
-        return [await response.json()];
-      }
-      
-      return [];
-    },
-    enabled: !!value,
-  });
-
-  // 当对话框打开时,设置当前选中的文件
-  useEffect(() => {
-    if (isOpen) {
-      if (allowMultiple) {
-        // 在多选模式下,使用 value 数组初始化本地选择
-        const initialSelection = Array.isArray(value) ? value : [];
-        setLocalSelectedFiles(initialSelection);
-      } else if (value && currentFiles && currentFiles.length > 0) {
-        setSelectedFile(currentFiles[0]);
-      }
-    }
-  }, [isOpen, value, currentFiles, allowMultiple]);
-
-  // 获取文件列表
-  const { data: filesData, isLoading, refetch } = useQuery({
-    queryKey: ['files-for-selection', filterType, onlyCurrentUser, user?.id] as const,
-    queryFn: async () => {
-      // 构建筛选条件
-      const filters: any = {};
-      
-      // 如果设置为只显示当前用户的文件,并且用户已登录
-      if (onlyCurrentUser && user?.id) {
-        filters.uploadUserId = user.id;
-      }
-      
-      const response = await fileClient.$get({
-        query: {
-          page: 1,
-          pageSize: 50,
-          ...(filterType !== 'all' && { keyword: filterType }),
-          ...(Object.keys(filters).length > 0 && {
-            filters: JSON.stringify(filters)
-          })
-        }
-      });
-      if (response.status !== 200) throw new Error('获取文件列表失败');
-      return response.json();
-    },
-    enabled: isOpen && (!onlyCurrentUser || !!user),
-  });
-
-  const files = filesData?.data?.filter((f) => {
-    if (filterType === 'all') return true;
-    if (filterType === 'image') return f?.type?.startsWith('image/');
-    return f?.type?.includes(filterType);
-  }) || [];
-
-  const handleSelectFile = (file: FileType) => {
-    if (allowMultiple) {
-      setLocalSelectedFiles(prev => {
-        const newSelection = prev.includes(file.id)
-          ? prev.filter(id => id !== file.id)
-          : [...prev, file.id];
-        return newSelection;
-      });
-    } else {
-      setSelectedFile(prevSelected => {
-        if (prevSelected?.id === file.id) {
-          return null;
-        }
-        return file;
-      });
-    }
-  };
-
-  const handleConfirm = () => {
-    if (allowMultiple) {
-      if (onChange) {
-        onChange(localSelectedFiles);
-      }
-      setIsOpen(false);
-      return;
-    }
-
-    if (!selectedFile) {
-      toast.warning('请选择一个文件');
-      return;
-    }
-    if (onChange) {
-      onChange(selectedFile.id);
-    }
-    setIsOpen(false);
-    setSelectedFile(null);
-  };
-
-  const handleCancel = () => {
-    setIsOpen(false);
-    setSelectedFile(null);
-    // 取消时重置为初始的 value 值
-    const initialSelection = allowMultiple && Array.isArray(value) ? value : [];
-    setLocalSelectedFiles(initialSelection);
-  };
-
-  const handleUploadSuccess = () => {
-    toast.success('文件上传成功!请从列表中选择新上传的文件');
-    refetch();
-  };
-
-  const getPreviewSize = () => {
-    switch (previewSize) {
-      case 'small':
-        return 'h-16 w-16';
-      case 'medium':
-        return 'h-24 w-24';
-      case 'large':
-        return 'h-32 w-32';
-      default:
-        return 'h-24 w-24';
-    }
-  };
-
-  const getFileIcon = (fileType: string) => {
-    if (fileType.startsWith('image/')) {
-      return <ImageIcon className="h-8 w-8 text-gray-400" />;
-    }
-    if (fileType.startsWith('video/')) {
-      return <FileIcon className="h-8 w-8 text-blue-500" />;
-    }
-    if (fileType.startsWith('audio/')) {
-      return <FileIcon className="h-8 w-8 text-green-500" />;
-    }
-    if (fileType.includes('pdf')) {
-      return <FileIcon className="h-8 w-8 text-red-500" />;
-    }
-    if (fileType.includes('text')) {
-      return <FileIcon className="h-8 w-8 text-gray-600" />;
-    }
-    return <FileIcon className="h-8 w-8 text-gray-400" />;
-  };
-
-  const handleRemoveFile = (e: React.MouseEvent) => {
-    e.stopPropagation();
-    if (allowMultiple && Array.isArray(value)) {
-      // 在多选模式下,移除所有选中文件
-      onChange?.([]);
-    } else {
-      // 在单选模式下,设置为null
-      onChange?.(null);
-    }
-  };
-
-  const isSelected = (fileId: number) => {
-    if (allowMultiple) {
-      return localSelectedFiles.includes(fileId);
-    }
-    return selectedFile?.id === fileId;
-  };
-
-  return (
-    <>
-      <div className="space-y-4">
-        {showPreview && (
-          <div className="flex items-start space-x-4">
-            {/* 预览区域 */}
-            <div className="flex flex-wrap gap-2">
-              {allowMultiple && Array.isArray(currentFiles) && currentFiles.length > 0 ? (
-                // 多选模式下的预览
-                currentFiles.map((file) => (
-                  <div key={file.id} className="relative group">
-                    <div
-                      className={cn(
-                        getPreviewSize(),
-                        "border-2 border-dashed cursor-pointer hover:border-primary transition-colors rounded-lg overflow-hidden flex items-center justify-center bg-gray-100"
-                      )}
-                      onClick={() => setIsOpen(true)}
-                    >
-                      {file?.type ? (
-                        <div className="w-full h-full flex items-center justify-center">
-                          {file.type.startsWith('image/') ? (
-                            <img
-                              src={file.fullUrl}
-                              alt={file.name}
-                              className="w-full h-full object-cover"
-                            />
-                          ) : (
-                            <div className="flex flex-col items-center justify-center text-gray-400">
-                              {getFileIcon(file.type)}
-                              <span className="text-xs mt-1 text-center px-1 truncate max-w-full">
-                                {file.name}
-                              </span>
-                            </div>
-                          )}
-                        </div>
-                      ) : (
-                        <div className="flex flex-col items-center justify-center text-gray-400">
-                          <FileIcon className="h-8 w-8 mb-1" />
-                          <span className="text-xs">{placeholder}</span>
-                        </div>
-                      )}
-                    </div>
-                    
-                    <button
-                      type="button"
-                      className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
-                      onClick={(e) => {
-                        e.stopPropagation();
-                        if (allowMultiple && Array.isArray(value)) {
-                          const newValue = value.filter(id => id !== file.id);
-                          onChange?.(newValue);
-                        }
-                      }}
-                    >
-                      <X className="h-3 w-3" />
-                    </button>
-                  </div>
-                ))
-              ) : !allowMultiple && currentFiles && currentFiles.length > 0 ? (
-                // 单选模式下的预览
-                <div className="relative group">
-                  <div
-                    className={cn(
-                      getPreviewSize(),
-                      "border-2 border-dashed cursor-pointer hover:border-primary transition-colors rounded-lg overflow-hidden flex items-center justify-center bg-gray-100"
-                    )}
-                    onClick={() => setIsOpen(true)}
-                  >
-                    {currentFiles[0]?.type ? (
-                      <div className="w-full h-full flex items-center justify-center">
-                        {currentFiles[0].type.startsWith('image/') ? (
-                          <img
-                            src={currentFiles[0].fullUrl}
-                            alt={currentFiles[0].name}
-                            className="w-full h-full object-cover"
-                          />
-                        ) : (
-                          <div className="flex flex-col items-center justify-center text-gray-400">
-                            {getFileIcon(currentFiles[0].type)}
-                            <span className="text-xs mt-1 text-center">{currentFiles[0].name}</span>
-                          </div>
-                        )}
-                      </div>
-                    ) : (
-                      <div className="flex flex-col items-center justify-center text-gray-400">
-                        <FileIcon className="h-8 w-8 mb-1" />
-                        <span className="text-xs">{placeholder}</span>
-                      </div>
-                    )}
-                  </div>
-                  
-                  {currentFiles[0] && (
-                    <button
-                      type="button"
-                      className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
-                      onClick={handleRemoveFile}
-                    >
-                      <X className="h-3 w-3" />
-                    </button>
-                  )}
-                </div>
-              ) : (
-                // 没有选中文件时的占位符
-                <div
-                  className={cn(
-                    getPreviewSize(),
-                    "border-2 border-dashed cursor-pointer hover:border-primary transition-colors rounded-lg overflow-hidden flex items-center justify-center bg-gray-100"
-                  )}
-                  onClick={() => setIsOpen(true)}
-                >
-                  <div className="flex flex-col items-center justify-center text-gray-400">
-                    <FileIcon className="h-8 w-8 mb-1" />
-                    <span className="text-xs">{placeholder}</span>
-                  </div>
-                </div>
-              )}
-            </div>
-            
-            <div className="space-y-2">
-              <Button
-                type="button"
-                variant="outline"
-                onClick={() => setIsOpen(true)}
-                className="text-sm"
-              >
-                {((allowMultiple && currentFiles && currentFiles.length > 0) ||
-                  (!allowMultiple && currentFiles && currentFiles.length > 0)) ? '更换文件' : placeholder}
-              </Button>
-              {!allowMultiple && currentFiles && currentFiles.length > 0 && (
-                <p className="text-xs text-muted-foreground truncate w-40 sm:w-64">
-                  当前: {currentFiles[0].name}
-                </p>
-              )}
-              {allowMultiple && currentFiles && currentFiles.length > 0 && (
-                <p className="text-xs text-muted-foreground">
-                  已选择 {currentFiles.length} 个文件
-                </p>
-              )}
-            </div>
-          </div>
-        )}
-
-        {!showPreview && (
-          <Button 
-            type="button" 
-            variant="outline" 
-            onClick={() => setIsOpen(true)}
-            className="w-full"
-          >
-            {currentFiles ? '更换文件' : placeholder}
-          </Button>
-        )}
-      </div>
-
-      <Dialog open={isOpen} onOpenChange={setIsOpen}>
-        <DialogContent className="max-w-4xl max-h-[90vh]">
-          <DialogHeader>
-            <DialogTitle>{title}</DialogTitle>
-            <DialogDescription>
-              {description}
-            </DialogDescription>
-          </DialogHeader>
-
-          <div className="space-y-4">
-            {/* 文件列表 */}
-            <div className="space-y-2 max-h-96 overflow-y-auto p-1">
-              {isLoading ? (
-                <Card>
-                  <CardContent className="text-center py-8">
-                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
-                    <p className="text-gray-500 mt-2">加载中...</p>
-                  </CardContent>
-                </Card>
-              ) : (
-                <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3">
-                  {/* 上传区域 - 作为第一项 */}
-                  <div className="relative cursor-pointer transition-all duration-200">
-                    <div className="rounded-lg border-2 border-dashed border-gray-300 hover:border-primary transition-colors hover:scale-105">
-                      <div className="p-2 h-20 flex items-center justify-center">
-                        <MinioUploader
-                          uploadPath={uploadPath}
-                          accept={accept}
-                          maxSize={maxSize}
-                          onUploadSuccess={handleUploadSuccess}
-                          buttonText="上传"
-                          size="minimal"
-                          displayMode="card"
-                          showUploadList={false}
-                        />
-                      </div>
-                    </div>
-                    <p className="text-xs text-center mt-1 text-muted-foreground">
-                      上传新文件
-                    </p>
-                  </div>
-
-                  {/* 现有文件列表 */}
-                  {files.map((file) => (
-                    <div
-                      key={file.id}
-                      className={cn(
-                        "relative cursor-pointer transition-all duration-200",
-                        "hover:scale-105"
-                      )}
-                      onClick={() => handleSelectFile(file)}
-                    >
-                      <div
-                        className={cn(
-                          "relative rounded-lg overflow-hidden border-2 aspect-square",
-                          isSelected(file.id)
-                            ? "border-primary ring-2 ring-primary ring-offset-2"
-                            : "border-gray-200 hover:border-primary"
-                        )}
-                      >
-                        {file?.type?.startsWith('image/') ? (
-                          <img
-                            src={file.fullUrl}
-                            alt={file.name}
-                            className="w-full h-full object-cover"
-                          />
-                        ) : (
-                          <div className="w-full h-full flex flex-col items-center justify-center bg-gray-50 p-2">
-                            {file.type && getFileIcon(file.type)}
-                            <p className="text-xs text-center mt-1 truncate max-w-full">
-                              {file.name}
-                            </p>
-                          </div>
-                        )}
-                        
-                        {isSelected(file.id) && (
-                          <div className="absolute inset-0 bg-primary/20 flex items-center justify-center">
-                            <Check className="h-6 w-6 text-white bg-primary rounded-full p-1" />
-                          </div>
-                        )}
-                        
-                        <div className="absolute top-1 right-1">
-                          <Eye
-                            className="h-4 w-4 text-white bg-black/50 rounded-full p-0.5 cursor-pointer hover:bg-black/70"
-                            onClick={(e) => {
-                              e.stopPropagation();
-                              window.open(file.fullUrl, '_blank');
-                            }}
-                          />
-                        </div>
-                      </div>
-                      
-                      <p className="text-xs text-center mt-1 truncate">
-                        {file.name}
-                      </p>
-                    </div>
-                  ))}
-                  
-                  {/* 空状态 - 当没有文件时显示 */}
-                  {files.length === 0 && (
-                    <div className="col-span-full">
-                      <Card>
-                        <CardContent className="text-center py-8">
-                          <div className="flex flex-col items-center">
-                            <Upload className="h-12 w-12 text-gray-400 mb-4" />
-                            <p className="text-gray-600">暂无文件</p>
-                            <p className="text-sm text-gray-500 mt-2">请上传文件</p>
-                          </div>
-                        </CardContent>
-                      </Card>
-                    </div>
-                  )}
-                </div>
-              )}
-            </div>
-          </div>
-
-          <DialogFooter>
-            <Button type="button" variant="outline" onClick={handleCancel}>
-              取消
-            </Button>
-            <Button
-              type="button"
-              onClick={handleConfirm}
-              disabled={allowMultiple ? localSelectedFiles.length === 0 : !selectedFile}
-            >
-              {allowMultiple ? `确认选择 (${localSelectedFiles.length})` : '确认选择'}
-            </Button>
-          </DialogFooter>
-        </DialogContent>
-      </Dialog>
-    </>
-  );
-};
-
-export default FileSelector;

+ 0 - 484
src/client/mobile/components/MinioUploader.tsx

@@ -1,484 +0,0 @@
-import React, { useState, useCallback } from 'react';
-import { Button } from '@/client/components/ui/button';
-import { Card, CardContent } from '@/client/components/ui/card';
-import { Progress } from '@/client/components/ui/progress';
-import { Badge } from '@/client/components/ui/badge';
-import { toast } from 'sonner';
-import { Upload, X, CheckCircle, Loader2, FileText } from 'lucide-react';
-import { uploadMinIOWithPolicy, MinioProgressEvent } from '@/client/utils/minio';
-
-interface MinioUploaderProps {
-  /** 上传路径 */
-  uploadPath: string;
-  /** 允许的文件类型,如['image/*', '.pdf'] */
-  accept?: string;
-  /** 最大文件大小(MB) */
-  maxSize?: number;
-  /** 是否允许多文件上传 */
-  multiple?: boolean;
-  /** 上传成功回调 */
-  onUploadSuccess?: (fileKey: string, fileUrl: string, file: File) => void;
-  /** 上传失败回调 */
-  onUploadError?: (error: Error, file: File) => void;
-  /** 自定义上传按钮文本 */
-  buttonText?: string;
-  /** 自定义提示文本 */
-  tipText?: string;
-  /** 上传模式:拖放模式或传统模式 */
-  uploadMode?: 'dragdrop' | 'traditional';
-  /** 是否显示已上传文件列表 */
-  showUploadList?: boolean;
-  /** 已上传文件列表标题 */
-  uploadListTitle?: string;
-  /** 组件尺寸模式 */
-  size?: 'default' | 'compact' | 'minimal';
-  /** 显示模式:卡片模式或完整模式 */
-  displayMode?: 'full' | 'card';
-}
-
-// 定义上传文件状态
-interface UploadFile {
-  uid: string;
-  name: string;
-  size: number;
-  type?: string;
-  status: 'uploading' | 'success' | 'error';
-  percent: number;
-  error?: string;
-  url?: string;
-}
-
-const MinioUploader: React.FC<MinioUploaderProps> = ({
-  uploadPath = '/',
-  accept,
-  maxSize = 500, // 默认最大500MB
-  multiple = false,
-  onUploadSuccess,
-  onUploadError,
-  buttonText = '点击或拖拽上传文件',
-  tipText = '支持单文件或多文件上传,单个文件大小不超过500MB',
-  uploadMode = 'dragdrop',
-  showUploadList = true,
-  uploadListTitle = '上传进度',
-  size = 'default',
-  displayMode = 'full'
-}) => {
-  const [fileList, setFileList] = useState<UploadFile[]>([]);
-  const [uploadingFiles, setUploadingFiles] = useState<Set<string>>(new Set());
-  const [dragActive, setDragActive] = useState(false);
-
-  // 根据尺寸模式获取样式配置
-  const getSizeConfig = () => {
-    switch (size) {
-      case 'minimal':
-        return {
-          container: 'p-3',
-          icon: 'h-8 w-8',
-          title: 'text-sm',
-          description: 'text-xs',
-          button: 'h-8 px-3 text-xs',
-          spacing: 'space-y-2',
-          fileList: 'space-y-2',
-          cardPadding: 'p-3',
-          progressHeight: 'h-1'
-        };
-      case 'compact':
-        return {
-          container: 'p-4',
-          icon: 'h-10 w-10',
-          title: 'text-base',
-          description: 'text-sm',
-          button: 'h-9 px-4 text-sm',
-          spacing: 'space-y-3',
-          fileList: 'space-y-3',
-          cardPadding: 'p-4',
-          progressHeight: 'h-2'
-        };
-      default:
-        return {
-          container: 'p-6',
-          icon: 'h-12 w-12',
-          title: 'text-lg',
-          description: 'text-sm',
-          button: 'h-10 px-4',
-          spacing: 'space-y-4',
-          fileList: 'space-y-4',
-          cardPadding: 'p-6',
-          progressHeight: 'h-2'
-        };
-    }
-  };
-
-  // 处理上传进度
-  const handleProgress = useCallback((uid: string, event: MinioProgressEvent) => {
-    setFileList(prev => 
-      prev.map(item => {
-        if (item.uid === uid) {
-          return {
-            ...item,
-            status: event.stage === 'error' ? 'error' : 'uploading',
-            percent: event.progress,
-            error: event.stage === 'error' ? event.message : undefined
-          };
-        }
-        return item;
-      })
-    );
-  }, []);
-
-  // 处理上传成功
-  const handleComplete = useCallback((uid: string, result: { fileKey: string; fileUrl: string }, file: File) => {
-    setFileList(prev => 
-      prev.map(item => {
-        if (item.uid === uid) {
-          return {
-            ...item,
-            status: 'success',
-            percent: 100,
-            url: result.fileUrl,
-          };
-        }
-        return item;
-      })
-    );
-    
-    setUploadingFiles(prev => {
-      const newSet = new Set(prev);
-      newSet.delete(uid);
-      return newSet;
-    });
-    
-    // toast.success(`文件 "${file.name}" 上传成功`);
-    onUploadSuccess?.(result.fileKey, result.fileUrl, file);
-  }, [onUploadSuccess]);
-
-  // 处理上传失败
-  const handleError = useCallback((uid: string, error: Error, file: File) => {
-    setFileList(prev => 
-      prev.map(item => {
-        if (item.uid === uid) {
-          return {
-            ...item,
-            status: 'error',
-            percent: 0,
-            error: error.message || '上传失败'
-          };
-        }
-        return item;
-      })
-    );
-    
-    setUploadingFiles(prev => {
-      const newSet = new Set(prev);
-      newSet.delete(uid);
-      return newSet;
-    });
-    
-    // toast.error(`文件 "${file.name}" 上传失败: ${error.message}`);
-    onUploadError?.(error, file);
-  }, [onUploadError]);
-
-  // 自定义上传逻辑
-  const handleUpload = async (file: File) => {
-    const uid = Date.now().toString() + Math.random().toString(36).substr(2, 9);
-    
-    // 添加到文件列表
-    setFileList(prev => [
-      ...prev,
-      {
-        uid,
-        name: file.name,
-        size: file.size,
-        type: file.type,
-        status: 'uploading',
-        percent: 0,
-      }
-    ]);
-    
-    // 添加到上传中集合
-    setUploadingFiles(prev => new Set(prev).add(uid));
-    
-    try {
-      // 验证文件大小
-      const fileSizeMB = file.size / (1024 * 1024);
-      if (fileSizeMB > maxSize) {
-        throw new Error(`文件大小超过 ${maxSize}MB 限制`);
-      }
-      
-      // 调用minio上传方法
-      const result = await uploadMinIOWithPolicy(
-        uploadPath,
-        file,
-        file.name,
-        {
-          onProgress: (event) => handleProgress(uid, event),
-          signal: new AbortController().signal
-        }
-      );
-      
-      handleComplete(uid, result, file);
-    } catch (error) {
-      handleError(uid, error instanceof Error ? error : new Error('未知错误'), file);
-    }
-  };
-
-  // 处理文件选择
-  const handleFileSelect = (files: FileList) => {
-    if (!files || files.length === 0) return;
-
-    const fileArray = Array.from(files);
-    
-    if (!multiple && fileArray.length > 1) {
-      toast.error('不支持多文件上传');
-      return;
-    }
-
-    fileArray.forEach(file => handleUpload(file));
-  };
-
-  // 处理拖拽
-  const handleDrag = (e: React.DragEvent) => {
-    e.preventDefault();
-    e.stopPropagation();
-    
-    if (e.type === 'dragenter' || e.type === 'dragover') {
-      setDragActive(true);
-    } else if (e.type === 'dragleave') {
-      setDragActive(false);
-    }
-  };
-
-  const handleDrop = (e: React.DragEvent) => {
-    e.preventDefault();
-    e.stopPropagation();
-    setDragActive(false);
-    
-    const files = e.dataTransfer.files;
-    handleFileSelect(files);
-  };
-
-  // 处理文件移除
-  const handleRemove = (uid: string) => {
-    setFileList(prev => prev.filter(item => item.uid !== uid));
-  };
-
-  // 渲染上传状态
-  const renderUploadStatus = (item: UploadFile) => {
-    switch (item.status) {
-      case 'uploading':
-        return (
-          <div className="flex items-center gap-2">
-            <Loader2 className="h-4 w-4 animate-spin" />
-            <span className="text-sm">{Math.round(item.percent)}%</span>
-          </div>
-        );
-      case 'success':
-        return (
-          <div className="flex items-center gap-2">
-            <CheckCircle className="h-4 w-4 text-green-500" />
-            <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
-              上传成功
-            </Badge>
-          </div>
-        );
-      case 'error':
-        return (
-          <div className="flex items-center gap-2">
-            <div className="h-4 w-4 text-red-500">×</div>
-            <Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">
-              {item.error || '上传失败'}
-            </Badge>
-          </div>
-        );
-      default:
-        return null;
-    }
-  };
-
-  // 渲染文件图标
-  const renderFileIcon = (type?: string, iconSize: 'small' | 'normal' = 'normal') => {
-    const sizeClass = iconSize === 'small' ? 'h-4 w-4' : 'h-8 w-8';
-    
-    if (type?.startsWith('image/')) {
-      return <FileText className={`${sizeClass} text-blue-500`} />;
-    } else if (type?.startsWith('video/')) {
-      return <FileText className={`${sizeClass} text-red-500`} />;
-    } else if (type?.startsWith('audio/')) {
-      return <FileText className={`${sizeClass} text-purple-500`} />;
-    } else if (type?.includes('pdf')) {
-      return <FileText className={`${sizeClass} text-red-500`} />;
-    } else if (type?.includes('word')) {
-      return <FileText className={`${sizeClass} text-blue-600`} />;
-    } else if (type?.includes('excel') || type?.includes('sheet')) {
-      return <FileText className={`${sizeClass} text-green-500`} />;
-    } else {
-      return <FileText className={`${sizeClass} text-gray-500`} />;
-    }
-  };
-
-  const sizeConfig = getSizeConfig();
-
-  // 卡片模式渲染
-  if (displayMode === 'card') {
-    return (
-      <div className="h-full flex items-center justify-center">
-        <button
-          type="button"
-          className={`flex flex-col items-center justify-center w-full h-full text-muted-foreground hover:text-primary transition-colors cursor-pointer
-            ${size === 'minimal' ? 'text-xs' : 'text-sm'}`}
-          onClick={() => {
-            const input = document.createElement('input');
-            input.type = 'file';
-            input.accept = accept || '';
-            input.multiple = multiple;
-            input.onchange = (e) => {
-              const files = (e.target as HTMLInputElement).files;
-              if (files) handleFileSelect(files);
-            };
-            input.click();
-          }}
-        >
-          <Upload className={`${size === 'minimal' ? 'h-6 w-6 mb-1' : 'h-8 w-8 mb-2'}`} />
-          <span>{buttonText}</span>
-        </button>
-      </div>
-    );
-  }
-
-  return (
-    <div className={sizeConfig.spacing}>
-      {/* 上传区域 - 根据模式显示不同界面 */}
-      {uploadMode === 'dragdrop' ? (
-        <div
-          className={`relative border-2 border-dashed rounded-lg transition-all ${
-            dragActive
-              ? 'border-primary bg-primary/5'
-              : 'border-gray-300 hover:border-primary/50'
-          } ${sizeConfig.container}`}
-          onDragEnter={handleDrag}
-          onDragLeave={handleDrag}
-          onDragOver={handleDrag}
-          onDrop={handleDrop}
-        >
-          <div className={`flex flex-col items-center justify-center ${sizeConfig.spacing}`}>
-            <Upload className={`${sizeConfig.icon} ${dragActive ? 'text-primary' : 'text-gray-400'}`} />
-            <div className="text-center">
-              <p className={`${sizeConfig.title} font-medium`}>{buttonText}</p>
-              {size !== 'minimal' && (
-                <p className={`${sizeConfig.description} text-gray-500 mt-1`}>{tipText}</p>
-              )}
-            </div>
-            <Button
-              type="button"
-              variant="outline"
-              size={size === 'minimal' ? 'sm' : size === 'compact' ? 'sm' : 'default'}
-              onClick={() => {
-                const input = document.createElement('input');
-                input.type = 'file';
-                input.accept = accept || '';
-                input.multiple = multiple;
-                input.onchange = (e) => {
-                  const files = (e.target as HTMLInputElement).files;
-                  if (files) handleFileSelect(files);
-                };
-                input.click();
-              }}
-            >
-              <Upload className="h-4 w-4 mr-2" />
-              选择文件
-            </Button>
-          </div>
-        </div>
-      ) : (
-        <Card>
-          <CardContent className={sizeConfig.cardPadding}>
-            <div className={`flex flex-col items-center justify-center ${sizeConfig.spacing}`}>
-              <Upload className={`${sizeConfig.icon} text-gray-400`} />
-              <div className="text-center">
-                <p className={`${sizeConfig.title} font-medium`}>{buttonText}</p>
-                {size !== 'minimal' && (
-                  <p className={`${sizeConfig.description} text-gray-500 mt-1`}>{tipText}</p>
-                )}
-              </div>
-              <Button
-                type="button"
-                variant="outline"
-                size={size === 'minimal' ? 'sm' : size === 'compact' ? 'sm' : 'default'}
-                onClick={() => {
-                  const input = document.createElement('input');
-                  input.type = 'file';
-                  input.accept = accept || '';
-                  input.multiple = multiple;
-                  input.onchange = (e) => {
-                    const files = (e.target as HTMLInputElement).files;
-                    if (files) handleFileSelect(files);
-                  };
-                  input.click();
-                }}
-              >
-                <Upload className="h-4 w-4 mr-2" />
-                选择文件
-              </Button>
-            </div>
-          </CardContent>
-        </Card>
-      )}
-
-      {/* 上传进度列表 */}
-      {showUploadList && fileList.length > 0 && (
-        <Card>
-          <CardContent className={sizeConfig.cardPadding}>
-            <h3 className={`${sizeConfig.title} font-semibold mb-3`}>{uploadListTitle}</h3>
-            <div className={sizeConfig.fileList}>
-              {fileList.map(item => (
-                <div key={item.uid} className={`flex items-center space-x-3 p-3 border rounded-lg ${size === 'minimal' ? 'text-sm' : ''}`}>
-                  <div className="flex-shrink-0">
-                    {renderFileIcon(item.type, size === 'minimal' ? 'small' : 'normal')}
-                  </div>
-                  <div className="flex-1 min-w-0">
-                    <div className="flex justify-between items-center mb-1">
-                      <p className={`${size === 'minimal' ? 'text-xs' : 'text-sm'} font-medium truncate`}>{item.name}</p>
-                      <div className="flex items-center space-x-1">
-                        {renderUploadStatus(item)}
-                        <Button
-                          variant="ghost"
-                          size={size === 'minimal' ? 'icon' : 'sm'}
-                          onClick={() => handleRemove(item.uid)}
-                          disabled={item.status === 'uploading'}
-                          className={size === 'minimal' ? 'h-6 w-6' : ''}
-                        >
-                          <X className={size === 'minimal' ? 'h-3 w-3' : 'h-4 w-4'} />
-                        </Button>
-                      </div>
-                    </div>
-                    {item.status === 'uploading' && (
-                      <div className="space-y-1">
-                        <Progress value={item.percent} className={sizeConfig.progressHeight} />
-                        {size !== 'minimal' && (
-                          <p className={`${sizeConfig.description} text-gray-500`}>
-                            {Math.round(item.percent)}% - {formatFileSize(item.size * (item.percent / 100))} / {formatFileSize(item.size)}
-                          </p>
-                        )}
-                      </div>
-                    )}
-                  </div>
-                </div>
-              ))}
-            </div>
-          </CardContent>
-        </Card>
-      )}
-    </div>
-  );
-};
-
-// 辅助函数:格式化文件大小
-const formatFileSize = (bytes: number): string => {
-  if (bytes === 0) return '0 Bytes';
-  const k = 1024;
-  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
-  const i = Math.floor(Math.log(bytes) / Math.log(k));
-  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
-};
-
-export default MinioUploader;