瀏覽代碼

✨ feat(admin): 重构文件管理和用户管理界面,集成新的Minio上传组件

- 将MinioUploader从Ant Design迁移到shadcn/ui组件库,支持拖拽上传和传统模式
- 添加文件大小格式化辅助函数和文件类型图标显示
- 重构Files页面使用新的表格组件和对话框组件
- 在用户管理页面集成头像选择器组件,支持头像上传和预览
- 优化用户界面交互体验,使用toast通知替代message组件
- 添加文件预览和下载功能,支持图片和视频文件类型
- 实现响应式设计,支持不同尺寸模式和显示模式

♻️ refactor(admin): 优化组件结构和代码组织

- 移除Ant Design相关依赖,统一使用shadcn/ui组件
- 重构表单处理逻辑,使用react-hook-form和zod进行表单验证
- 优化类型定义,使用InferResponseType和InferRequestType自动推断类型
- 改进错误处理机制,使用toast组件显示操作结果
- 统一分页组件使用,优化数据表格的显示效果

📝 docs(admin): 更新组件文档和类型定义

- 添加MinioUploader组件的完整属性文档
- 更新用户管理和文件管理的类型定义
- 补充组件使用示例和配置说明
yourname 3 月之前
父節點
當前提交
90ac8d0971

+ 326 - 102
src/client/admin/components/MinioUploader.tsx

@@ -1,11 +1,10 @@
 import React, { useState, useCallback } from 'react';
 import React, { useState, useCallback } from 'react';
-import { Upload, Progress, message, Tag, Space, Typography, Button } from 'antd';
-import { UploadOutlined, CloseOutlined, CheckCircleOutlined, SyncOutlined } from '@ant-design/icons';
-import { App } from 'antd';
-import type { UploadFile, UploadProps } from 'antd';
-import type { RcFile } from 'rc-upload/lib/interface';
-import type { UploadFileStatus } from 'antd/es/upload/interface';
-import type { UploadRequestOption } from 'rc-upload/lib/interface';
+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';
 import { uploadMinIOWithPolicy, MinioProgressEvent } from '@/client/utils/minio';
 
 
 interface MinioUploaderProps {
 interface MinioUploaderProps {
@@ -25,8 +24,29 @@ interface MinioUploaderProps {
   buttonText?: string;
   buttonText?: string;
   /** 自定义提示文本 */
   /** 自定义提示文本 */
   tipText?: 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> = ({
 const MinioUploader: React.FC<MinioUploaderProps> = ({
   uploadPath = '/',
   uploadPath = '/',
@@ -36,11 +56,58 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
   onUploadSuccess,
   onUploadSuccess,
   onUploadError,
   onUploadError,
   buttonText = '点击或拖拽上传文件',
   buttonText = '点击或拖拽上传文件',
-  tipText = '支持单文件或多文件上传,单个文件大小不超过500MB'
+  tipText = '支持单文件或多文件上传,单个文件大小不超过500MB',
+  uploadMode = 'dragdrop',
+  showUploadList = true,
+  uploadListTitle = '上传进度',
+  size = 'default',
+  displayMode = 'full'
 }) => {
 }) => {
-  const { message: antdMessage } = App.useApp();
   const [fileList, setFileList] = useState<UploadFile[]>([]);
   const [fileList, setFileList] = useState<UploadFile[]>([]);
   const [uploadingFiles, setUploadingFiles] = useState<Set<string>>(new Set());
   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) => {
   const handleProgress = useCallback((uid: string, event: MinioProgressEvent) => {
@@ -49,7 +116,7 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
         if (item.uid === uid) {
         if (item.uid === uid) {
           return {
           return {
             ...item,
             ...item,
-            status: event.stage === 'error' ? ('error' as UploadFileStatus) : ('uploading' as UploadFileStatus),
+            status: event.stage === 'error' ? 'error' : 'uploading',
             percent: event.progress,
             percent: event.progress,
             error: event.stage === 'error' ? event.message : undefined
             error: event.stage === 'error' ? event.message : undefined
           };
           };
@@ -66,9 +133,8 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
         if (item.uid === uid) {
         if (item.uid === uid) {
           return {
           return {
             ...item,
             ...item,
-            status: 'success' as UploadFileStatus,
+            status: 'success',
             percent: 100,
             percent: 100,
-            response: { fileKey: result.fileKey },
             url: result.fileUrl,
             url: result.fileUrl,
           };
           };
         }
         }
@@ -82,9 +148,9 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
       return newSet;
       return newSet;
     });
     });
     
     
-    antdMessage.success(`文件 "${file.name}" 上传成功`);
+    // toast.success(`文件 "${file.name}" 上传成功`);
     onUploadSuccess?.(result.fileKey, result.fileUrl, file);
     onUploadSuccess?.(result.fileKey, result.fileUrl, file);
-  }, [antdMessage, onUploadSuccess]);
+  }, [onUploadSuccess]);
 
 
   // 处理上传失败
   // 处理上传失败
   const handleError = useCallback((uid: string, error: Error, file: File) => {
   const handleError = useCallback((uid: string, error: Error, file: File) => {
@@ -93,7 +159,7 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
         if (item.uid === uid) {
         if (item.uid === uid) {
           return {
           return {
             ...item,
             ...item,
-            status: 'error' as UploadFileStatus,
+            status: 'error',
             percent: 0,
             percent: 0,
             error: error.message || '上传失败'
             error: error.message || '上传失败'
           };
           };
@@ -108,27 +174,23 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
       return newSet;
       return newSet;
     });
     });
     
     
-    antdMessage.error(`文件 "${file.name}" 上传失败: ${error.message}`);
+    // toast.error(`文件 "${file.name}" 上传失败: ${error.message}`);
     onUploadError?.(error, file);
     onUploadError?.(error, file);
-  }, [antdMessage, onUploadError]);
+  }, [onUploadError]);
 
 
   // 自定义上传逻辑
   // 自定义上传逻辑
-  const customRequest = async (options: UploadRequestOption) => {
-    const { file, onSuccess, onError } = options;
-    const rcFile = file as RcFile;
-    const uid = rcFile.uid;
+  const handleUpload = async (file: File) => {
+    const uid = Date.now().toString() + Math.random().toString(36).substr(2, 9);
     
     
     // 添加到文件列表
     // 添加到文件列表
     setFileList(prev => [
     setFileList(prev => [
-      ...prev.filter(item => item.uid !== uid),
+      ...prev,
       {
       {
         uid,
         uid,
-        name: rcFile.name,
-        size: rcFile.size,
-        type: rcFile.type,
-        lastModified: rcFile.lastModified,
-        lastModifiedDate: new Date(rcFile.lastModified),
-        status: 'uploading' as UploadFileStatus,
+        name: file.name,
+        size: file.size,
+        type: file.type,
+        status: 'uploading',
         percent: 0,
         percent: 0,
       }
       }
     ]);
     ]);
@@ -137,38 +199,67 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
     setUploadingFiles(prev => new Set(prev).add(uid));
     setUploadingFiles(prev => new Set(prev).add(uid));
     
     
     try {
     try {
+      // 验证文件大小
+      const fileSizeMB = file.size / (1024 * 1024);
+      if (fileSizeMB > maxSize) {
+        throw new Error(`文件大小超过 ${maxSize}MB 限制`);
+      }
+      
       // 调用minio上传方法
       // 调用minio上传方法
       const result = await uploadMinIOWithPolicy(
       const result = await uploadMinIOWithPolicy(
         uploadPath,
         uploadPath,
-        options.file as unknown as File,
-        rcFile.name,
+        file,
+        file.name,
         {
         {
           onProgress: (event) => handleProgress(uid, event),
           onProgress: (event) => handleProgress(uid, event),
           signal: new AbortController().signal
           signal: new AbortController().signal
         }
         }
       );
       );
       
       
-      handleComplete(uid, result, rcFile as unknown as File);
-      onSuccess?.({}, rcFile);
+      handleComplete(uid, result, file);
     } catch (error) {
     } catch (error) {
-      handleError(uid, error instanceof Error ? error : new Error('未知错误'), rcFile as unknown as File);
-      onError?.(error instanceof Error ? error : new Error('未知错误'));
+      handleError(uid, error instanceof Error ? error : new Error('未知错误'), file);
     }
     }
   };
   };
 
 
-  // 处理文件移除
-  const handleRemove = (uid: string) => {
-    setFileList(prev => prev.filter(item => item.uid !== uid));
+  // 处理文件选择
+  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 beforeUpload = (file: File) => {
-    const fileSizeMB = file.size / (1024 * 1024);
-    if (fileSizeMB > maxSize!) {
-      message.error(`文件 "${file.name}" 大小超过 ${maxSize}MB 限制`);
-      return false;
+  // 处理拖拽
+  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);
     }
     }
-    return true;
+  };
+
+  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));
   };
   };
 
 
   // 渲染上传状态
   // 渲染上传状态
@@ -176,85 +267,218 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
     switch (item.status) {
     switch (item.status) {
       case 'uploading':
       case 'uploading':
         return (
         return (
-          <Space>
-            <SyncOutlined spin size={12} />
-            <span>{item.percent}%</span>
-          </Space>
+          <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 'done':
+      case 'success':
         return (
         return (
-          <Space>
-            <CheckCircleOutlined style={{ color: '#52c41a' }} size={12} />
-            <Tag color="success">上传成功</Tag>
-          </Space>
+          <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':
       case 'error':
         return (
         return (
-          <Space>
-            <CloseOutlined style={{ color: '#ff4d4f' }} size={12} />
-            <Tag color="error">{item.error || '上传失败'}</Tag>
-          </Space>
+          <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:
       default:
         return null;
         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 (
   return (
-    <div className="minio-uploader">
-      <Upload.Dragger
-        name="files"
-        accept={accept}
-        multiple={multiple}
-        customRequest={customRequest}
-        beforeUpload={beforeUpload}
-        showUploadList={false}
-        disabled={uploadingFiles.size > 0 && !multiple}
-      >
-        <div className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-gray-300 rounded-md transition-all hover:border-primary">
-          <UploadOutlined style={{ fontSize: 24, color: '#1890ff' }} />
-          <Typography.Text className="mt-2">{buttonText}</Typography.Text>
-          <Typography.Text type="secondary" className="mt-1">
-            {tipText}
-          </Typography.Text>
+    <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>
         </div>
-      </Upload.Dragger>
+      ) : (
+        <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>
+      )}
 
 
       {/* 上传进度列表 */}
       {/* 上传进度列表 */}
-      {fileList.length > 0 && (
-        <div className="mt-4 space-y-3">
-          {fileList.map(item => (
-            <div key={item.uid} className="flex items-center p-3 border rounded-md">
-              <div className="flex-1 min-w-0">
-                <div className="flex justify-between items-center mb-1">
-                  <Typography.Text ellipsis className="max-w-xs">
-                    {item.name}
-                  </Typography.Text>
-                  <div className="flex items-center space-x-2">
-                    {renderUploadStatus(item)}
-                    <Button
-                      type="text"
-                      size="small"
-                      icon={<CloseOutlined />}
-                      onClick={() => handleRemove(item.uid)}
-                      disabled={item.status === 'uploading'}
-                    />
+      {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>
                 </div>
-                {item.status === 'uploading' && (
-                  <Progress 
-                    percent={item.percent}
-                    size="small" 
-                    status={item.percent === 100 ? 'success' : undefined}
-                  />
-                )}
-              </div>
+              ))}
             </div>
             </div>
-          ))}
-        </div>
+          </CardContent>
+        </Card>
       )}
       )}
     </div>
     </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;
 export default MinioUploader;

+ 1 - 1
src/client/admin/layouts/MainLayout.tsx

@@ -197,7 +197,7 @@ export const MainLayout = () => {
                 <Button variant="ghost" className="relative h-8 w-8 rounded-full">
                 <Button variant="ghost" className="relative h-8 w-8 rounded-full">
                   <Avatar className="h-8 w-8">
                   <Avatar className="h-8 w-8">
                     <AvatarImage
                     <AvatarImage
-                      src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
+                      src={user?.avatarFile?.fullUrl || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
                       alt={user?.username || 'User'}
                       alt={user?.username || 'User'}
                     />
                     />
                     <AvatarFallback>
                     <AvatarFallback>

+ 404 - 359
src/client/admin/pages/Files.tsx

@@ -1,33 +1,54 @@
-import React, { useState, useEffect } from 'react';
-import { Table, Button, Space, Input, Modal, Form, Select, DatePicker, Upload, Popconfirm, Image } from 'antd';
-import { App } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, EyeOutlined } from '@ant-design/icons';
+import React, { useState } from 'react';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Badge } from '@/client/components/ui/badge';
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/client/components/ui/alert-dialog';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { Eye, Download, Edit, Trash2, Search, FileText, Upload } from 'lucide-react';
 import { fileClient } from '@/client/api';
 import { fileClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
-import { uploadMinIOWithPolicy } from '@/client/utils/minio';
+import MinioUploader from '@/client/admin/components/MinioUploader';
+import { UpdateFileDto } from '@/server/modules/files/file.schema';
+import * as z from 'zod';
 
 
 // 定义类型
 // 定义类型
 type FileItem = InferResponseType<typeof fileClient.$get, 200>['data'][0];
 type FileItem = InferResponseType<typeof fileClient.$get, 200>['data'][0];
 type FileListResponse = InferResponseType<typeof fileClient.$get, 200>;
 type FileListResponse = InferResponseType<typeof fileClient.$get, 200>;
 type UpdateFileRequest = InferRequestType<typeof fileClient[':id']['$put']>['json'];
 type UpdateFileRequest = InferRequestType<typeof fileClient[':id']['$put']>['json'];
+type FileFormData = z.infer<typeof UpdateFileDto>;
 
 
 export const FilesPage: React.FC = () => {
 export const FilesPage: React.FC = () => {
-  const { message } = App.useApp();
-  const [form] = Form.useForm();
-  const [modalVisible, setModalVisible] = useState(false);
-  const [editingKey, setEditingKey] = useState<number | null>(null);
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
+  const [editingFile, setEditingFile] = useState<FileItem | null>(null);
   const [searchText, setSearchText] = useState('');
   const [searchText, setSearchText] = useState('');
   const [pagination, setPagination] = useState({
   const [pagination, setPagination] = useState({
     current: 1,
     current: 1,
     pageSize: 10,
     pageSize: 10,
     total: 0,
     total: 0,
   });
   });
+  const [deleteFileId, setDeleteFileId] = useState<number | null>(null);
+  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
 
 
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   
   
-  
+  // 表单初始化
+  const form = useForm<FileFormData>({
+    resolver: zodResolver(UpdateFileDto),
+    defaultValues: {
+      name: '',
+      description: '',
+    },
+  });
+
   // 获取文件列表数据
   // 获取文件列表数据
   const fetchFiles = async ({ page, pageSize }: { page: number; pageSize: number }): Promise<FileListResponse> => {
   const fetchFiles = async ({ page, pageSize }: { page: number; pageSize: number }): Promise<FileListResponse> => {
     const response = await fileClient.$get({ query: { page, pageSize, keyword: searchText } });
     const response = await fileClient.$get({ query: { page, pageSize, keyword: searchText } });
@@ -35,389 +56,413 @@ export const FilesPage: React.FC = () => {
     return await response.json() as FileListResponse;
     return await response.json() as FileListResponse;
   };
   };
 
 
-  // 获取文件下载URL
-  const getFileUrl = async (fileId: number) => {
-    try {
-      const response = await fileClient[':id']['url'].$get({ param: { id: fileId } });
-      if (!response.ok) throw new Error('获取文件URL失败');
-      const data = await response.json();
-      return data.url;
-    } catch (error) {
-      message.error('获取文件URL失败');
-      return null;
+  const { data, isLoading, error } = useQuery({
+    queryKey: ['files', pagination.current, pagination.pageSize, searchText],
+    queryFn: () => fetchFiles({ page: pagination.current, pageSize: pagination.pageSize }),
+  });
+
+  // 更新文件记录
+  const updateFile = useMutation({
+    mutationFn: ({ id, data }: { id: number; data: UpdateFileRequest }) =>
+      fileClient[':id'].$put({ param: { id: id.toString() }, json: data }),
+    onSuccess: () => {
+      toast.success('文件记录更新成功');
+      queryClient.invalidateQueries({ queryKey: ['files'] });
+      setIsModalOpen(false);
+      setEditingFile(null);
+    },
+    onError: (error: Error) => {
+      toast.error(`操作失败: ${error.message}`);
     }
     }
-  };
+  });
 
 
-  // 获取文件下载URL
-  const getFileDownloadUrl = async (fileId: number) => {
-    try {
-      const response = await fileClient[':id']['download'].$get({ param: { id: fileId } });
-      if (!response.ok) throw new Error('获取文件下载URL失败');
-      const data = await response.json();
-      return data;
-    } catch (error) {
-      message.error('获取文件下载URL失败');
-      return null;
+  // 删除文件记录
+  const deleteFile = useMutation({
+    mutationFn: (id: number) => fileClient[':id'].$delete({ param: { id: id.toString() } }),
+    onSuccess: () => {
+      toast.success('文件记录删除成功');
+      queryClient.invalidateQueries({ queryKey: ['files'] });
+    },
+    onError: (error: Error) => {
+      toast.error(`删除失败: ${error.message}`);
     }
     }
-  };
+  });
 
 
   // 处理文件下载
   // 处理文件下载
-  const handleDownload = async (record: FileItem) => {
-    const result = await getFileDownloadUrl(record.id);
-    if (result?.url) {
-      const a = document.createElement('a');
-      a.href = result.url;
-      a.download = result.filename || record.name;
-      document.body.appendChild(a);
-      a.click();
-      document.body.removeChild(a);
-    }
+  const handleDownload = (record: FileItem) => {
+    const a = document.createElement('a');
+    a.href = record.fullUrl;
+    a.download = record.name;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
   };
   };
 
 
   // 处理文件预览
   // 处理文件预览
-  const handlePreview = async (record: FileItem) => {
-    const url = await getFileUrl(record.id);
-    if (url) {
-      if (record.type.startsWith('image/')) {
-        window.open(url, '_blank');
-      } else if (record.type.startsWith('video/')) {
-        window.open(url, '_blank');
-      } else {
-        message.warning('该文件类型不支持预览');
-      }
+  const handlePreview = (record: FileItem) => {
+    if (isPreviewable(record.type)) {
+      window.open(record.fullUrl, '_blank');
+    } else {
+      toast.warning('该文件类型不支持预览');
     }
     }
   };
   };
 
 
   // 检查是否为可预览的文件类型
   // 检查是否为可预览的文件类型
-  const isPreviewable = (fileType: string) => {
+  const isPreviewable = (fileType: string | null) => {
+    if (!fileType) return false;
     return fileType.startsWith('image/') || fileType.startsWith('video/');
     return fileType.startsWith('image/') || fileType.startsWith('video/');
   };
   };
-  
-  const { data, isLoading: loading, error: filesError } = useQuery({
-    queryKey: ['files', pagination.current, pagination.pageSize, searchText],
-    queryFn: () => fetchFiles({ page: pagination.current, pageSize: pagination.pageSize }),
-  });
 
 
-  // 错误处理
-  if (filesError) {
-    message.error(`获取文件列表失败: ${filesError instanceof Error ? filesError.message : '未知错误'}`);
-  }
-  
-  // 从API响应获取分页数据
-  const tablePagination = data?.pagination || pagination;
-  
-  // 搜索
-  const handleSearch = () => {
-    setPagination({ ...pagination, current: 1 });
+  // 处理上传成功回调
+  const handleUploadSuccess = (fileKey: string, fileUrl: string, file: File) => {
+    toast.success('文件上传成功');
+    queryClient.invalidateQueries({ queryKey: ['files'] });
   };
   };
-  
-  // 分页变化
-  const handleTableChange = (newPagination: any) => {
-    setPagination(newPagination);
+
+  // 处理上传失败回调
+  const handleUploadError = (error: Error, file: File) => {
+    toast.error(`上传失败: ${error instanceof Error ? error.message : '未知错误'}`);
   };
   };
-  
+
   // 显示编辑弹窗
   // 显示编辑弹窗
-  const showModal = (record: FileItem) => {
-    setModalVisible(true);
-    setEditingKey(record.id);
-    form.setFieldsValue({
+  const showEditModal = (record: FileItem) => {
+    setEditingFile(record);
+    setIsModalOpen(true);
+    form.reset({
       name: record.name,
       name: record.name,
-      description: record.description,
-      type: record.type,
-      size: record.size,
+      description: record.description || '',
     });
     });
   };
   };
 
 
-  // 关闭弹窗
-  const handleCancel = () => {
-    setModalVisible(false);
-    form.resetFields();
-  };
-  
-  // 更新文件记录
-  const updateFile = useMutation({
-    mutationFn: ({ id, data }: { id: number; data: UpdateFileRequest }) =>
-      fileClient[':id'].$put({ param: { id }, json: data }),
-    onSuccess: () => {
-      message.success('文件记录更新成功');
-      queryClient.invalidateQueries({ queryKey: ['files'] });
-      setModalVisible(false);
-    },
-    onError: (error: Error) => {
-      message.error(`操作失败: ${error instanceof Error ? error.message : '未知错误'}`);
+  // 处理表单提交
+  const handleFormSubmit = async (data: FileFormData) => {
+    if (editingFile) {
+      await updateFile.mutateAsync({ 
+        id: editingFile.id, 
+        data: {
+          name: data.name,
+          description: data.description,
+        }
+      });
     }
     }
-  });
-  
-  // 删除文件记录
-  const deleteFile = useMutation({
-    mutationFn: (id: number) => fileClient[':id'].$delete({ param: { id } }),
-    onSuccess: () => {
-      message.success('文件记录删除成功');
-      queryClient.invalidateQueries({ queryKey: ['files'] });
-    },
-    onError: (error: Error) => {
-      message.error(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
-    }
-  });
-  
-  // 直接上传文件
-  const handleDirectUpload = async () => {
-    const input = document.createElement('input');
-    input.type = 'file';
-    input.multiple = false;
-    
-    input.onchange = async (e) => {
-      const file = (e.target as HTMLInputElement).files?.[0];
-      if (!file) return;
-      
-      try {
-        message.loading('正在上传文件...');
-        await uploadMinIOWithPolicy('/files', file, file.name);
-        message.success('文件上传成功');
-        queryClient.invalidateQueries({ queryKey: ['files'] });
-      } catch (error) {
-        message.error(`上传失败: ${error instanceof Error ? error.message : '未知错误'}`);
-      }
-    };
-    
-    input.click();
   };
   };
-  
-  // 提交表单(仅用于编辑已上传文件)
-  const handleSubmit = async () => {
-    try {
-      const values = await form.validateFields();
-      
-      const payload = {
-        name: values.name,
-        description: values.description,
-      };
-      
-      if (editingKey) {
-        await updateFile.mutateAsync({ id: editingKey, data: payload });
-      }
-    } catch (error) {
-      message.error('表单验证失败,请检查输入');
+
+  // 处理删除确认
+  const handleDeleteConfirm = () => {
+    if (deleteFileId) {
+      deleteFile.mutate(deleteFileId);
+      setIsDeleteDialogOpen(false);
+      setDeleteFileId(null);
     }
     }
   };
   };
-  
-  // 表格列定义
-  const columns = [
-    {
-      title: '文件ID',
-      dataIndex: 'id',
-      key: 'id',
-      width: 80,
-      align: 'center' as const,
-    },
-    {
-      title: '文件名称',
-      dataIndex: 'name',
-      key: 'name',
-      width: 300,
-      ellipsis: true,
-      render: (name: string, record: FileItem) => (
-        <div className="flex items-center">
-          <span className="flex-1">{name}</span>
-        </div>
-      ),
-    },
-    {
-      title: '文件类型',
-      dataIndex: 'type',
-      key: 'type',
-      width: 120,
-      render: (type: string) => (
-        <span className="inline-block px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded-full">
-          {type}
-        </span>
-      ),
-    },
-    {
-      title: '文件大小',
-      dataIndex: 'size',
-      key: 'size',
-      width: 120,
-      render: (size: number) => (
-        <span className="text-sm">
-          {size ? `${(size / 1024).toFixed(2)} KB` : '-'}
-        </span>
-      ),
-    },
-    {
-      title: '上传时间',
-      dataIndex: 'uploadTime',
-      key: 'uploadTime',
-      width: 180,
-      render: (time: string) => (
-        <span className="text-sm text-gray-600">
-          {time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-'}
-        </span>
-      ),
-    },
-    {
-      title: '上传用户',
-      dataIndex: 'uploadUser',
-      key: 'uploadUser',
-      width: 120,
-      render: (uploadUser?: { username: string; nickname?: string }) => (
-        <span className="text-sm">
-          {uploadUser ? (uploadUser.nickname || uploadUser.username) : '-'}
-        </span>
-      ),
-    },
-    {
-      title: '操作',
-      key: 'action',
-      width: 200,
-      fixed: 'right' as const,
-      render: (_: any, record: FileItem) => (
-        <Space size="small">
-          <Button
-            type="text"
-            icon={<EyeOutlined />}
-            onClick={() => handlePreview(record)}
-            className="text-green-600 hover:text-green-800 hover:bg-green-50"
-            disabled={!isPreviewable(record.type)}
-            title={isPreviewable(record.type) ? '预览文件' : '该文件类型不支持预览'}
-          />
-          <Button
-            type="text"
-            icon={<DownloadOutlined />}
-            onClick={() => handleDownload(record)}
-            className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
-            title="下载文件"
-          />
-          <Button
-            type="text"
-            icon={<EditOutlined />}
-            onClick={() => showModal(record)}
-            className="text-purple-600 hover:text-purple-800 hover:bg-purple-50"
-            title="编辑文件信息"
-          />
-          <Popconfirm
-            title="确认删除"
-            description={`确定要删除文件"${record.name}"吗?此操作不可恢复。`}
-            onConfirm={() => deleteFile.mutate(record.id)}
-            okText="确认"
-            cancelText="取消"
-            okButtonProps={{ danger: true }}
-          >
-            <Button
-              type="text"
-              danger
-              icon={<DeleteOutlined />}
-              className="hover:bg-red-50"
-              title="删除文件"
-            >
-              删除
-            </Button>
-          </Popconfirm>
-        </Space>
-      ),
-    },
-  ];
-  
+
+  const handleSearch = () => {
+    setPagination({ ...pagination, current: 1 });
+  };
+
+  // 格式化文件大小
+  const formatFileSize = (bytes: number | null) => {
+    if (!bytes || bytes === 0) return '0 Bytes';
+    const k = 1024;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+  };
+
+  // 分页数据
+  const tablePagination = data?.pagination || pagination;
+
+  if (error) {
+    return (
+      <div className="p-6">
+        <Card>
+          <CardContent className="text-center py-8">
+            <FileText className="h-12 w-12 mx-auto text-gray-400 mb-4" />
+            <p className="text-gray-600">获取文件列表失败</p>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
   return (
   return (
-    <div className="p-6">
-      <div className="mb-6 flex justify-between items-center">
-        <h2 className="text-2xl font-bold text-gray-900">文件管理</h2>
-        <Button
-          type="primary"
-          icon={<UploadOutlined />}
-          onClick={handleDirectUpload}
-          className="h-10 flex items-center"
-        >
+    <div className="p-6 space-y-6">
+      <div className="flex justify-between items-center">
+        <h1 className="text-3xl font-bold">文件管理</h1>
+        <Button onClick={() => setIsUploadModalOpen(true)}>
+          <Upload className="h-4 w-4 mr-2" />
           上传文件
           上传文件
         </Button>
         </Button>
       </div>
       </div>
       
       
-      <div className="mb-6">
-        <div className="flex items-center gap-4">
-          <Input
-            placeholder="搜索文件名称或类型"
-            prefix={<SearchOutlined />}
-            value={searchText}
-            onChange={(e) => setSearchText(e.target.value)}
-            onPressEnter={handleSearch}
-            className="w-80 h-10"
-            allowClear
-          />
-          <Button
-            type="default"
-            onClick={handleSearch}
-            className="h-10"
-          >
-            搜索
-          </Button>
-        </div>
-      </div>
-      
-      <div className="bg-white rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
-        <Table
-          columns={columns}
-          dataSource={data?.data || []}
-          rowKey="id"
-          loading={loading}
-          pagination={{
-            ...tablePagination,
-            showSizeChanger: true,
-            showQuickJumper: true,
-            showTotal: (total, range) =>
-              `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`,
-          }}
-          onChange={handleTableChange}
-          bordered={false}
-          scroll={{ x: 'max-content' }}
-          className="[&_.ant-table]:!rounded-lg [&_.ant-table-thead>tr>th]:!bg-gray-50 [&_.ant-table-thead>tr>th]:!font-semibold [&_.ant-table-thead>tr>th]:!text-gray-700 [&_.ant-table-thead>tr>th]:!border-b-2 [&_.ant-table-thead>tr>th]:!border-gray-200"
-          rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
-        />
-      </div>
-      
-      <Modal
-        title="编辑文件信息"
-        open={modalVisible}
-        onCancel={handleCancel}
-        footer={[
-          <Button key="cancel" onClick={handleCancel}>
-            取消
-          </Button>,
-          <Button
-            key="submit"
-            type="primary"
-            onClick={handleSubmit}
-            loading={updateFile.isPending}
-          >
-            确定
-          </Button>,
-        ]}
-        width={600}
-        centered
-        destroyOnClose
-        maskClosable={false}
-      >
-        <Form form={form} layout="vertical">
-          <Form.Item name="name" label="文件名称">
-            <Input className="h-10" />
-          </Form.Item>
+      <Card>
+        <CardHeader>
+          <CardTitle>文件列表</CardTitle>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4 flex gap-4">
+            <div className="flex-1">
+              <Input
+                placeholder="搜索文件名称或类型"
+                value={searchText}
+                onChange={(e) => setSearchText(e.target.value)}
+                onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
+                className="max-w-sm"
+              />
+            </div>
+            <Button onClick={handleSearch}>
+              <Search className="h-4 w-4 mr-2" />
+              搜索
+            </Button>
+          </div>
+
+          <div className="overflow-x-auto">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead className="w-16">ID</TableHead>
+                  <TableHead>预览</TableHead>
+                  <TableHead>文件名称</TableHead>
+                  <TableHead>文件类型</TableHead>
+                  <TableHead>文件大小</TableHead>
+                  <TableHead>上传时间</TableHead>
+                  <TableHead>上传用户</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {isLoading ? (
+                  <TableRow>
+                    <TableCell colSpan={7} className="text-center">
+                      <div className="flex justify-center items-center py-8">
+                        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ) : data?.data?.length === 0 ? (
+                  <TableRow>
+                    <TableCell colSpan={7} className="text-center py-8">
+                      <FileText className="h-12 w-12 mx-auto text-gray-400 mb-4" />
+                      <p className="text-gray-600">暂无文件</p>
+                    </TableCell>
+                  </TableRow>
+                ) : (
+                  data?.data?.map((file) => (
+                    <TableRow key={file.id}>
+                      <TableCell className="font-medium">{file.id}</TableCell>
+                      <TableCell>
+                        {isPreviewable(file.type) ? (
+                          <img
+                            src={file.fullUrl}
+                            alt={file.name}
+                            className="w-12 h-12 object-cover rounded border cursor-pointer hover:opacity-80 transition-opacity"
+                            onClick={() => handlePreview(file)}
+                            title="点击查看大图"
+                          />
+                        ) : (
+                          <div className="w-12 h-12 flex items-center justify-center bg-gray-100 rounded border">
+                            <FileText className="h-6 w-6 text-gray-400" />
+                          </div>
+                        )}
+                      </TableCell>
+                      <TableCell>
+                        <div className="max-w-xs truncate" title={file.name}>
+                          {file.name}
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        <Badge variant="secondary">{file.type}</Badge>
+                      </TableCell>
+                      <TableCell>{formatFileSize(file.size)}</TableCell>
+                      <TableCell>
+                        {file.uploadTime ? dayjs(file.uploadTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
+                      </TableCell>
+                      <TableCell>
+                        {file.uploadUser ? (file.uploadUser.nickname || file.uploadUser.username) : '-'}
+                      </TableCell>
+                      <TableCell className="text-right">
+                        <div className="flex justify-end gap-2">
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => handlePreview(file)}
+                            disabled={!isPreviewable(file.type)}
+                            title={isPreviewable(file.type) ? '预览文件' : '该文件类型不支持预览'}
+                          >
+                            <Eye className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => handleDownload(file)}
+                            title="下载文件"
+                          >
+                            <Download className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => showEditModal(file)}
+                            title="编辑文件信息"
+                          >
+                            <Edit className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => {
+                              setDeleteFileId(file.id);
+                              setIsDeleteDialogOpen(true);
+                            }}
+                            className="text-red-600 hover:text-red-700"
+                            title="删除文件"
+                          >
+                            <Trash2 className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      </TableCell>
+                    </TableRow>
+                  ))
+                )}
+              </TableBody>
+            </Table>
+          </div>
+
+          {/* 分页 */}
+          {tablePagination.total > 0 && (
+            <div className="flex justify-between items-center mt-4">
+              <div className="text-sm text-gray-600">
+                显示 {((tablePagination.current - 1) * tablePagination.pageSize + 1)}-
+                {Math.min(tablePagination.current * tablePagination.pageSize, tablePagination.total)} 条,
+                共 {tablePagination.total} 条
+              </div>
+              <div className="flex gap-2">
+                <Button
+                  variant="outline"
+                  size="sm"
+                  disabled={tablePagination.current <= 1}
+                  onClick={() => setPagination({ ...pagination, current: tablePagination.current - 1 })}
+                >
+                  上一页
+                </Button>
+                <span className="px-3 py-1 text-sm">
+                  第 {tablePagination.current} 页
+                </span>
+                <Button
+                  variant="outline"
+                  size="sm"
+                  disabled={tablePagination.current >= Math.ceil(tablePagination.total / tablePagination.pageSize)}
+                  onClick={() => setPagination({ ...pagination, current: tablePagination.current + 1 })}
+                >
+                  下一页
+                </Button>
+              </div>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 上传文件对话框 */}
+      <Dialog open={isUploadModalOpen} onOpenChange={setIsUploadModalOpen}>
+        <DialogContent className="sm:max-w-[600px]">
+          <DialogHeader>
+            <DialogTitle>上传文件</DialogTitle>
+            <DialogDescription>
+              选择要上传的文件,支持拖拽上传
+            </DialogDescription>
+          </DialogHeader>
           
           
-          <Form.Item name="description" label="文件描述">
-            <Input.TextArea
-              rows={4}
-              placeholder="请输入文件描述"
-              className="rounded-md"
+          <div className="py-4">
+            <MinioUploader
+              uploadPath="/files"
+              maxSize={500}
+              multiple={false}
+              onUploadSuccess={(fileKey, fileUrl, file) => {
+                handleUploadSuccess(fileKey, fileUrl, file);
+                setIsUploadModalOpen(false);
+              }}
+              onUploadError={handleUploadError}
+              buttonText="点击或拖拽上传文件"
+              tipText="支持单文件上传,单个文件大小不超过500MB"
+              size="default"
             />
             />
-          </Form.Item>
-          
-          <Form.Item name="type" label="文件类型" hidden>
-            <Input />
-          </Form.Item>
+          </div>
           
           
-          <Form.Item name="size" label="文件大小" hidden>
-            <Input />
-          </Form.Item>
-        </Form>
-      </Modal>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setIsUploadModalOpen(false)}>
+              取消
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+
+      {/* 编辑对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px]">
+          <DialogHeader>
+            <DialogTitle>编辑文件信息</DialogTitle>
+            <DialogDescription>
+              修改文件的基本信息
+            </DialogDescription>
+          </DialogHeader>
+          <Form {...form}>
+            <form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-4">
+              <FormField
+                control={form.control}
+                name="name"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>文件名称</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入文件名称" {...field} />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+              <FormField
+                control={form.control}
+                name="description"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>文件描述</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入文件描述" {...field} />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+              <DialogFooter>
+                <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                  取消
+                </Button>
+                <Button type="submit" disabled={updateFile.isPending}>
+                  {updateFile.isPending ? '保存中...' : '保存'}
+                </Button>
+              </DialogFooter>
+            </form>
+          </Form>
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>确认删除</AlertDialogTitle>
+            <AlertDialogDescription>
+              确定要删除这个文件记录吗?此操作不可恢复。
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>取消</AlertDialogCancel>
+            <AlertDialogAction onClick={handleDeleteConfirm} className="bg-red-600 hover:bg-red-700">
+              确认删除
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
     </div>
     </div>
   );
   );
 };
 };

+ 68 - 1
src/client/admin/pages/Users.tsx

@@ -12,6 +12,7 @@ import { Badge } from '@/client/components/ui/badge';
 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
 import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
 import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
 import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
 import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
+import AvatarSelector from '@/client/admin/components/AvatarSelector';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { toast } from 'sonner';
 import { toast } from 'sonner';
@@ -42,6 +43,7 @@ export const UsersPage = () => {
   const [editingUser, setEditingUser] = useState<any>(null);
   const [editingUser, setEditingUser] = useState<any>(null);
   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
   const [userToDelete, setUserToDelete] = useState<number | null>(null);
   const [userToDelete, setUserToDelete] = useState<number | null>(null);
+  // Avatar selector is now integrated, no separate state needed
 
 
   const [isCreateForm, setIsCreateForm] = useState(true);
   const [isCreateForm, setIsCreateForm] = useState(true);
   
   
@@ -128,6 +130,7 @@ export const UsersPage = () => {
       email: user.email,
       email: user.email,
       phone: user.phone,
       phone: user.phone,
       name: user.name,
       name: user.name,
+      avatarFileId: user.avatarFileId,
       isDisabled: user.isDisabled,
       isDisabled: user.isDisabled,
     });
     });
     setIsModalOpen(true);
     setIsModalOpen(true);
@@ -266,6 +269,7 @@ export const UsersPage = () => {
             <Table>
             <Table>
               <TableHeader>
               <TableHeader>
                 <TableRow>
                 <TableRow>
+                  <TableHead>头像</TableHead>
                   <TableHead>用户名</TableHead>
                   <TableHead>用户名</TableHead>
                   <TableHead>昵称</TableHead>
                   <TableHead>昵称</TableHead>
                   <TableHead>邮箱</TableHead>
                   <TableHead>邮箱</TableHead>
@@ -279,6 +283,23 @@ export const UsersPage = () => {
               <TableBody>
               <TableBody>
                 {users.map((user) => (
                 {users.map((user) => (
                   <TableRow key={user.id}>
                   <TableRow key={user.id}>
+                    <TableCell>
+                      <div className="w-10 h-10">
+                        {user.avatarFile?.fullUrl ? (
+                          <img
+                            src={user.avatarFile.fullUrl}
+                            alt={user.username}
+                            className="w-10 h-10 rounded-full object-cover"
+                          />
+                        ) : (
+                          <div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
+                            <span className="text-sm font-medium text-gray-500">
+                              {user.username?.charAt(0)?.toUpperCase() || 'U'}
+                            </span>
+                          </div>
+                        )}
+                      </div>
+                    </TableCell>
                     <TableCell className="font-medium">{user.username}</TableCell>
                     <TableCell className="font-medium">{user.username}</TableCell>
                     <TableCell>{user.nickname || '-'}</TableCell>
                     <TableCell>{user.nickname || '-'}</TableCell>
                     <TableCell>{user.email || '-'}</TableCell>
                     <TableCell>{user.email || '-'}</TableCell>
@@ -336,7 +357,7 @@ export const UsersPage = () => {
 
 
       {/* 创建/编辑用户对话框 */}
       {/* 创建/编辑用户对话框 */}
       <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
       <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
-        <DialogContent className="sm:max-w-[500px]">
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
           <DialogHeader>
           <DialogHeader>
             <DialogTitle>
             <DialogTitle>
               {editingUser ? '编辑用户' : '创建用户'}
               {editingUser ? '编辑用户' : '创建用户'}
@@ -439,6 +460,28 @@ export const UsersPage = () => {
                   )}
                   )}
                 />
                 />
 
 
+                <FormField
+                  control={createForm.control}
+                  name="avatarFileId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>头像</FormLabel>
+                      <FormControl>
+                        <AvatarSelector
+                          value={field.value || undefined}
+                          onChange={(value) => field.onChange(value)}
+                          maxSize={2}
+                          uploadPath="/avatars"
+                          uploadButtonText="上传头像"
+                          previewSize="medium"
+                          placeholder="选择头像"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
                 <FormField
                 <FormField
                   control={createForm.control}
                   control={createForm.control}
                   name="isDisabled"
                   name="isDisabled"
@@ -560,6 +603,28 @@ export const UsersPage = () => {
                   )}
                   )}
                 />
                 />
 
 
+                <FormField
+                  control={updateForm.control}
+                  name="avatarFileId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>头像</FormLabel>
+                      <FormControl>
+                        <AvatarSelector
+                          value={field.value || undefined}
+                          onChange={(value) => field.onChange(value)}
+                          maxSize={2}
+                          uploadPath="/avatars"
+                          uploadButtonText="上传头像"
+                          previewSize="medium"
+                          placeholder="选择头像"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
                 <FormField
                 <FormField
                   control={updateForm.control}
                   control={updateForm.control}
                   name="isDisabled"
                   name="isDisabled"
@@ -595,6 +660,8 @@ export const UsersPage = () => {
         </DialogContent>
         </DialogContent>
       </Dialog>
       </Dialog>
 
 
+      {/* Avatar selector is now integrated within the form */}
+
       {/* 删除确认对话框 */}
       {/* 删除确认对话框 */}
       <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
       <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
         <DialogContent>
         <DialogContent>