Procházet zdrojové kódy

✨ feat(components): 为MinioUploader添加尺寸模式支持

- 新增size属性,支持default/compact/minimal三种尺寸模式
- 实现基于尺寸的样式适配,包括容器大小、图标尺寸和间距调整
- 在minimal模式下简化布局,移除部分描述文本以节省空间
- AvatarSelector组件采用minimal尺寸以优化头像选择器布局

💄 style(components): 优化MinioUploader视觉样式

- 调整文件图标尺寸和颜色,增强视觉区分度
- 优化上传进度条样式和高度,提升不同尺寸下的显示效果
- 统一间距和内边距样式,提升整体视觉一致性
- 改进按钮尺寸适配,确保在各种尺寸模式下的可用性
yourname před 4 měsíci
rodič
revize
de9b4b1d86

+ 1 - 0
src/client/admin-shadcn/components/AvatarSelector.tsx

@@ -192,6 +192,7 @@ const AvatarSelector: React.FC<AvatarSelectorProps> = ({
                   maxSize={maxSize}
                   onUploadSuccess={handleUploadSuccess}
                   buttonText={uploadButtonText}
+                  size="minimal"
                 />
                 <p className="text-sm text-gray-500 mt-2">
                   上传后请从下方列表中选择新上传的头像

+ 88 - 35
src/client/admin-shadcn/components/MinioUploader.tsx

@@ -6,8 +6,6 @@ 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';
-import type { UploadRequestOption } from 'rc-upload/lib/interface';
-import type { RcFile } from 'rc-upload/lib/interface';
 
 interface MinioUploaderProps {
   /** 上传路径 */
@@ -26,6 +24,8 @@ interface MinioUploaderProps {
   buttonText?: string;
   /** 自定义提示文本 */
   tipText?: string;
+  /** 组件尺寸模式 */
+  size?: 'default' | 'compact' | 'minimal';
 }
 
 // 定义上传文件状态
@@ -48,12 +48,55 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
   onUploadSuccess,
   onUploadError,
   buttonText = '点击或拖拽上传文件',
-  tipText = '支持单文件或多文件上传,单个文件大小不超过500MB'
+  tipText = '支持单文件或多文件上传,单个文件大小不超过500MB',
+  size = 'default'
 }) => {
   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 => 
@@ -241,47 +284,54 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
   };
 
   // 渲染文件图标
-  const renderFileIcon = (type?: string) => {
+  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="h-8 w-8 text-blue-500" />;
+      return <FileText className={`${sizeClass} text-blue-500`} />;
     } else if (type?.startsWith('video/')) {
-      return <FileText className="h-8 w-8 text-red-500" />;
+      return <FileText className={`${sizeClass} text-red-500`} />;
     } else if (type?.startsWith('audio/')) {
-      return <FileText className="h-8 w-8 text-purple-500" />;
+      return <FileText className={`${sizeClass} text-purple-500`} />;
     } else if (type?.includes('pdf')) {
-      return <FileText className="h-8 w-8 text-red-500" />;
+      return <FileText className={`${sizeClass} text-red-500`} />;
     } else if (type?.includes('word')) {
-      return <FileText className="h-8 w-8 text-blue-600" />;
+      return <FileText className={`${sizeClass} text-blue-600`} />;
     } else if (type?.includes('excel') || type?.includes('sheet')) {
-      return <FileText className="h-8 w-8 text-green-500" />;
+      return <FileText className={`${sizeClass} text-green-500`} />;
     } else {
-      return <FileText className="h-8 w-8 text-gray-500" />;
+      return <FileText className={`${sizeClass} text-gray-500`} />;
     }
   };
 
+  const sizeConfig = getSizeConfig();
+
   return (
-    <div className="space-y-4">
+    <div className={sizeConfig.spacing}>
       {/* 拖拽上传区域 */}
       <div
-        className={`relative border-2 border-dashed rounded-lg p-6 transition-all ${
-          dragActive 
-            ? 'border-primary bg-primary/5' 
+        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 space-y-4">
-          <Upload className={`h-12 w-12 ${dragActive ? 'text-primary' : 'text-gray-400'}`} />
+        <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="text-lg font-medium">{buttonText}</p>
-            <p className="text-sm text-gray-500 mt-1">{tipText}</p>
+            <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';
@@ -303,35 +353,38 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
       {/* 上传进度列表 */}
       {fileList.length > 0 && (
         <Card>
-          <CardContent className="pt-6">
-            <h3 className="text-lg font-semibold mb-4">上传进度</h3>
-            <div className="space-y-4">
+          <CardContent className={sizeConfig.cardPadding}>
+            <h3 className={`${sizeConfig.title} font-semibold mb-3`}>上传进度</h3>
+            <div className={sizeConfig.fileList}>
               {fileList.map(item => (
-                <div key={item.uid} className="flex items-center space-x-4 p-4 border rounded-lg">
+                <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)}
+                    {renderFileIcon(item.type, size === 'minimal' ? 'small' : 'normal')}
                   </div>
                   <div className="flex-1 min-w-0">
-                    <div className="flex justify-between items-center mb-2">
-                      <p className="text-sm font-medium truncate">{item.name}</p>
-                      <div className="flex items-center space-x-2">
+                    <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="sm"
+                          size={size === 'minimal' ? 'icon' : 'sm'}
                           onClick={() => handleRemove(item.uid)}
                           disabled={item.status === 'uploading'}
+                          className={size === 'minimal' ? 'h-6 w-6' : ''}
                         >
-                          <X className="h-4 w-4" />
+                          <X className={size === 'minimal' ? 'h-3 w-3' : 'h-4 w-4'} />
                         </Button>
                       </div>
                     </div>
                     {item.status === 'uploading' && (
-                      <div className="space-y-2">
-                        <Progress value={item.percent} className="h-2" />
-                        <p className="text-xs text-gray-500">
-                          {Math.round(item.percent)}% - {formatFileSize(item.size * (item.percent / 100))} / {formatFileSize(item.size)}
-                        </p>
+                      <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>