Browse Source

✨ feat(ui): 重构文件管理组件并迁移到 shadcn/ui 设计系统

- 将 FilePreview、FileSelector、MinioUploader 组件从 antd 迁移到 shadcn/ui
- 新增文件类型图标和徽章显示,支持图片、视频、音频、文档等不同类型
- 重构 MinioUploader 拖拽上传区域,支持自定义文件选择和拖拽上传
- 优化 Files 页面布局,使用 Card、Table、Dialog 等 shadcn/ui 组件
- 新增文件大小格式化、文件类型标签、预览提示等用户体验优化
- 修复文件选择和上传状态的显示问题
yourname 4 months ago
parent
commit
8fe3270528

+ 169 - 110
src/client/admin-shadcn/components/FilePreview.tsx

@@ -1,30 +1,32 @@
 import React from 'react';
-import { Image, Spin, Empty } from 'antd';
-import { EyeOutlined, FileImageOutlined } from '@ant-design/icons';
 import { useQuery } from '@tanstack/react-query';
 import { fileClient } from '@/client/api';
 import type { InferResponseType } from 'hono/client';
+import { Eye, FileText, Image as ImageIcon } from 'lucide-react';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { Card, CardContent } from '@/client/components/ui/card';
+import { Badge } from '@/client/components/ui/badge';
+import { Button } from '@/client/components/ui/button';
+import { toast } from 'sonner';
 
 // 定义文件类型
 type FileItem = InferResponseType<typeof fileClient[':id']['$get'], 200>;
 
-interface FilePreviewProps {
-  fileIds?: number[];
-  files?: any[];
-  maxCount?: number;
-  size?: 'small' | 'medium' | 'large';
-  showCount?: boolean;
-  onFileClick?: (file: FileItem) => void;
-}
-
 interface FilePreviewItemProps {
   file: FileItem;
   size: 'small' | 'medium' | 'large';
   index?: number;
   total?: number;
+  onClick?: (file: FileItem) => void;
 }
 
-const FilePreviewItem: React.FC<FilePreviewItemProps> = ({ file, size, index, total }) => {
+const FilePreviewItem: React.FC<FilePreviewItemProps> = ({ 
+  file, 
+  size, 
+  index, 
+  total, 
+  onClick 
+}) => {
   const getSize = () => {
     switch (size) {
       case 'small':
@@ -39,91 +41,133 @@ const FilePreviewItem: React.FC<FilePreviewItemProps> = ({ file, size, index, to
   };
 
   const { width, height } = getSize();
-
   const isImage = file.type?.startsWith('image/');
-  const previewText = isImage ? '预览' : '查看';
+  const isVideo = file.type?.startsWith('video/');
+
+  const handlePreview = () => {
+    if (onClick) {
+      onClick(file);
+    } else if (isImage || isVideo) {
+      window.open(file.fullUrl, '_blank');
+    } else {
+      toast.warning('该文件类型不支持预览');
+    }
+  };
+
+  // 获取文件图标
+  const getFileIcon = (type?: string) => {
+    if (!type) return <FileText className="h-8 w-8 text-gray-400" />;
+    
+    if (type.startsWith('image/')) {
+      return <ImageIcon className="h-8 w-8 text-blue-500" />;
+    } else if (type.startsWith('video/')) {
+      return <FileText className="h-8 w-8 text-red-500" />;
+    } else if (type.startsWith('audio/')) {
+      return <FileText className="h-8 w-8 text-purple-500" />;
+    } else if (type.includes('pdf')) {
+      return <FileText className="h-8 w-8 text-red-500" />;
+    } else if (type.includes('word')) {
+      return <FileText className="h-8 w-8 text-blue-600" />;
+    } else if (type.includes('excel') || type.includes('sheet')) {
+      return <FileText className="h-8 w-8 text-green-500" />;
+    } else {
+      return <FileText className="h-8 w-8 text-gray-500" />;
+    }
+  };
+
+  // 获取文件类型标签
+  const getFileTypeBadge = (type: string) => {
+    if (type.startsWith('image/')) {
+      return { text: '图片', color: 'bg-blue-100 text-blue-800' };
+    } else if (type.startsWith('video/')) {
+      return { text: '视频', color: 'bg-red-100 text-red-800' };
+    } else if (type.startsWith('audio/')) {
+      return { text: '音频', color: 'bg-purple-100 text-purple-800' };
+    } else if (type.includes('pdf')) {
+      return { text: 'PDF', color: 'bg-red-100 text-red-800' };
+    } else if (type.includes('word')) {
+      return { text: '文档', color: 'bg-blue-100 text-blue-800' };
+    } else if (type.includes('excel') || type.includes('sheet')) {
+      return { text: '表格', color: 'bg-green-100 text-green-800' };
+    } else {
+      return { text: '文件', color: 'bg-gray-100 text-gray-800' };
+    }
+  };
 
   return (
     <div
-      style={{
-        position: 'relative',
-        width,
-        height,
-        border: '1px solid #d9d9d9',
-        borderRadius: 4,
-        overflow: 'hidden',
-        backgroundColor: '#fafafa',
-      }}
+      className="relative group cursor-pointer"
+      style={{ width, height }}
+      onClick={handlePreview}
     >
-      {isImage ? (
-        <Image
-          src={file.fullUrl}
-          alt={file.name}
-          style={{
-            width: '100%',
-            height: '100%',
-            objectFit: 'cover',
-          }}
-          preview={{
-            mask: (
-              <div
-                style={{
-                  display: 'flex',
-                  flexDirection: 'column',
-                  alignItems: 'center',
-                  justifyContent: 'center',
-                  color: 'white',
-                  backgroundColor: 'rgba(0,0,0,0.7)',
-                  height: '100%',
-                  fontSize: size === 'small' ? 10 : 12,
-                }}
-              >
-                <EyeOutlined style={{ fontSize: size === 'small' ? 14 : 16 }} />
-                <span style={{ marginTop: 2 }}>{previewText}</span>
-              </div>
-            ),
-          }}
-        />
-      ) : (
-        <div
-          style={{
-            display: 'flex',
-            flexDirection: 'column',
-            alignItems: 'center',
-            justifyContent: 'center',
-            height: '100%',
-            color: '#666',
-            fontSize: size === 'small' ? 10 : 12,
-          }}
-        >
-          <FileImageOutlined style={{ fontSize: size === 'small' ? 16 : 20, marginBottom: 2 }} />
-          <span style={{ textAlign: 'center', lineHeight: 1.2 }}>
-            {file.name.length > 8 ? `${file.name.substring(0, 6)}...` : file.name}
-          </span>
-        </div>
-      )}
+      {/* 文件预览容器 */}
+      <div className={`
+        relative overflow-hidden rounded-lg border transition-all duration-200
+        ${isImage ? 'border-gray-200' : 'border-gray-300 bg-gray-50'}
+        group-hover:shadow-md group-hover:border-primary
+      `}>
+        {isImage ? (
+          // 图片预览
+          <img
+            src={file.fullUrl}
+            alt={file.name}
+            className="w-full h-full object-cover"
+            loading="lazy"
+          />
+        ) : (
+          // 非图片文件预览
+          <div className="w-full h-full flex flex-col items-center justify-center">
+            {getFileIcon(file.type)}
+            <span className="text-xs text-center mt-1 px-1 truncate max-w-full">
+              {file.name.length > 8 ? `${file.name.substring(0, 6)}...` : file.name}
+            </span>
+          </div>
+        )}
 
-      {/* 序号标记 */}
-      {index !== undefined && total !== undefined && total > 1 && (
-        <div
-          style={{
-            position: 'absolute',
-            top: 0,
-            right: 0,
-            backgroundColor: 'rgba(0,0,0,0.5)',
-            color: 'white',
-            fontSize: 10,
-            padding: '2px 4px',
-            borderRadius: '0 0 0 4px',
-          }}
-        >
-          {index + 1}
+        {/* 悬停遮罩 */}
+        <div className={`
+          absolute inset-0 bg-black/60 flex flex-col items-center justify-center
+          opacity-0 group-hover:opacity-100 transition-opacity duration-200
+          text-white text-xs
+        `}>
+          <Eye className="h-4 w-4 mb-1" />
+          <span>{isImage || isVideo ? '预览' : '查看'}</span>
         </div>
-      )}
+
+        {/* 序号标记 */}
+        {index !== undefined && total !== undefined && total > 1 && (
+          <div className={`
+            absolute top-1 right-1 bg-black/70 text-white text-xs
+            px-1.5 py-0.5 rounded
+          `}>
+            {index + 1}
+          </div>
+        )}
+      </div>
+
+      {/* 文件类型标签 */}
+      <Badge 
+        className={`
+          absolute bottom-1 left-1 text-xs px-1 py-0
+          ${getFileTypeBadge(file.type).color}
+        `}
+      >
+        {getFileTypeBadge(file.type).text}
+      </Badge>
     </div>
   );
 };
 
+interface FilePreviewProps {
+  fileIds?: number[];
+  files?: any[];
+  maxCount?: number;
+  size?: 'small' | 'medium' | 'large';
+  showCount?: boolean;
+  onFileClick?: (file: FileItem) => void;
+  className?: string;
+}
+
 const FilePreview: React.FC<FilePreviewProps> = ({
   fileIds = [],
   files = [],
@@ -131,6 +175,7 @@ const FilePreview: React.FC<FilePreviewProps> = ({
   size = 'medium',
   showCount = true,
   onFileClick,
+  className = '',
 }) => {
   // 合并文件ID和文件对象
   const allFileIds = [...fileIds, ...(files?.map(f => f.id) || [])];
@@ -163,18 +208,39 @@ const FilePreview: React.FC<FilePreviewProps> = ({
     gcTime: 10 * 60 * 1000, // 10分钟
   });
 
+  // 加载状态
   if (isLoading) {
     return (
-      <div style={{ display: 'flex', justifyContent: 'center', padding: 20 }}>
-        <Spin tip="加载图片中..." />
+      <div className={`flex justify-center py-8 ${className}`}>
+        <div className="space-y-2">
+          <div className="flex gap-2 justify-center">
+            {[...Array(Math.min(maxCount, 3))].map((_, i) => (
+              <Skeleton key={i} className={`rounded-lg ${size === 'small' ? 'w-12 h-12' : size === 'medium' ? 'w-20 h-20' : 'w-24 h-24'}`} />
+            ))}
+          </div>
+          <p className="text-sm text-gray-500 text-center">加载中...</p>
+        </div>
       </div>
     );
   }
 
+  // 错误状态
   if (error) {
     return (
-      <div style={{ display: 'flex', justifyContent: 'center', padding: 20 }}>
-        <Empty description="加载图片失败" />
+      <div className={`flex flex-col items-center justify-center py-8 ${className}`}>
+        <FileText className="h-12 w-12 text-gray-400 mb-2" />
+        <p className="text-sm text-gray-600">加载图片失败</p>
+        <Button 
+          variant="outline" 
+          size="sm" 
+          className="mt-2"
+          onClick={() => {
+            // 这里可以添加重试逻辑
+            toast.info('请刷新页面重试');
+          }}
+        >
+          重试
+        </Button>
       </div>
     );
   }
@@ -182,24 +248,19 @@ const FilePreview: React.FC<FilePreviewProps> = ({
   const displayFiles = fileDetails?.slice(0, maxCount) || [];
   const remainingCount = Math.max(0, (fileDetails?.length || 0) - maxCount);
 
+  // 空状态
   if (displayFiles.length === 0) {
     return (
-      <div style={{ display: 'flex', justifyContent: 'center', padding: 10 }}>
-        <Empty description="暂无图片" image={Empty.PRESENTED_IMAGE_SIMPLE} />
+      <div className={`flex flex-col items-center justify-center py-6 ${className}`}>
+        <FileText className="h-12 w-12 text-gray-400 mb-2" />
+        <p className="text-sm text-gray-600">暂无图片</p>
       </div>
     );
   }
 
   return (
-    <div>
-      <div
-        style={{
-          display: 'flex',
-          flexWrap: 'wrap',
-          gap: 8,
-          alignItems: 'flex-start',
-        }}
-      >
+    <div className={className}>
+      <div className="flex flex-wrap gap-2 items-start">
         {displayFiles.map((file, index) => (
           <FilePreviewItem
             key={file.id}
@@ -207,23 +268,21 @@ const FilePreview: React.FC<FilePreviewProps> = ({
             size={size}
             index={index}
             total={displayFiles.length}
+            onClick={onFileClick}
           />
         ))}
       </div>
       
+      {/* 剩余数量提示 */}
       {showCount && remainingCount > 0 && (
-        <div
-          style={{
-            marginTop: 8,
-            fontSize: 12,
-            color: '#666',
-          }}
-        >
-          还有 {remainingCount} 张图片未显示
+        <div className="mt-2 text-sm text-gray-500">
+          还有 {remainingCount} 个文件未显示
         </div>
       )}
     </div>
   );
 };
 
-export default FilePreview;
+// 导出组件和类型
+export default FilePreview;
+export type { FilePreviewProps };

+ 171 - 96
src/client/admin-shadcn/components/FileSelector.tsx

@@ -1,9 +1,14 @@
 import React, { useState } from 'react';
-import { Modal, Button, Image, message } from 'antd';
 import { useQuery, useQueryClient } 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 { Badge } from '@/client/components/ui/badge';
+import { toast } from 'sonner';
 import { fileClient } from '@/client/api';
 import type { FileType } from '@/server/modules/files/file.schema';
 import MinioUploader from '@/client/admin-shadcn/components/MinioUploader';
+import { Check, Upload } from 'lucide-react';
 
 // 定义重载的 props 类型
 interface SingleFileSelectorProps {
@@ -89,7 +94,7 @@ const FileSelector: React.FC<FileSelectorProps> = ({
 
   const handleConfirm = () => {
     if (selectedFiles.length === 0) {
-      message.warning('请先选择文件');
+      toast.warning('请先选择文件');
       return;
     }
 
@@ -105,7 +110,7 @@ const FileSelector: React.FC<FileSelectorProps> = ({
   };
 
   const handleUploadSuccess = (fileKey: string, fileUrl: string, file: any) => {
-    message.success('文件上传成功!请从列表中选择新上传的文件');
+    toast.success('文件上传成功!请从列表中选择新上传的文件');
     // 刷新文件列表
     queryClient.invalidateQueries({ queryKey: ['files-for-selection', accept] });
   };
@@ -125,103 +130,173 @@ const FileSelector: React.FC<FileSelectorProps> = ({
     return selectedFiles.length > 0 ? `已选择: ${selectedFiles[0].name}` : '请选择文件';
   };
 
-  return (
-    <Modal
-      title="选择文件"
-      open={visible}
-      onCancel={onCancel}
-      width={800}
-      onOk={handleConfirm}
-      okText="确认选择"
-      cancelText="取消"
-      okButtonProps={{ disabled: selectedFiles.length === 0 }}
-    >
-      <div style={{ marginBottom: 16 }}>
-        <MinioUploader
-          uploadPath={uploadPath}
-          accept={accept}
-          maxSize={maxSize}
-          onUploadSuccess={handleUploadSuccess}
-          buttonText={uploadButtonText}
-        />
-        <span style={{ color: '#666', fontSize: '12px', marginLeft: 8 }}>
-          上传后请从下方列表中选择文件
-        </span>
-      </div>
-
-      <div style={{ marginBottom: 16 }}>
-        <div style={{
-          padding: '8px 12px',
-          backgroundColor: '#f6ffed',
-          border: '1px solid #b7eb8f',
-          borderRadius: '4px',
-          color: '#52c41a'
-        }}>
-          {getSelectionText()}
-        </div>
-      </div>
+  // 格式化文件大小
+  const formatFileSize = (bytes: number) => {
+    if (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];
+  };
 
-      <div style={{ maxHeight: 400, overflowY: 'auto' }}>
-        {isLoading ? (
-          <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
-            加载中...
-          </div>
-        ) : filteredFiles.length > 0 ? (
-          filteredFiles.map((file) => (
-            <div
-              key={file.id}
-              style={{
-                display: 'flex',
-                alignItems: 'center',
-                padding: '12px',
-                border: `1px solid ${isFileSelected(file) ? '#1890ff' : '#f0f0f0'}`,
-                borderRadius: '4px',
-                marginBottom: '8px',
-                cursor: 'pointer',
-                transition: 'all 0.3s',
-                backgroundColor: isFileSelected(file) ? '#e6f7ff' : ''
-              }}
-              onClick={() => handleSelectFile(file)}
-              onMouseEnter={(e) => {
-                if (!isFileSelected(file)) {
-                  e.currentTarget.style.backgroundColor = '#f6ffed';
-                  e.currentTarget.style.borderColor = '#b7eb8f';
-                }
-              }}
-              onMouseLeave={(e) => {
-                if (!isFileSelected(file)) {
-                  e.currentTarget.style.backgroundColor = '';
-                  e.currentTarget.style.borderColor = '#f0f0f0';
-                }
-              }}
-            >
-              <Image
-                src={file.fullUrl}
-                alt={file.name}
-                style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4, marginRight: 12 }}
-                preview={false}
+  // 获取文件类型的显示标签
+  const getFileTypeBadge = (type: string) => {
+    if (type.startsWith('image/')) {
+      return { text: '图片', color: 'bg-blue-100 text-blue-800' };
+    } else if (type.startsWith('video/')) {
+      return { text: '视频', color: 'bg-red-100 text-red-800' };
+    } else if (type.startsWith('audio/')) {
+      return { text: '音频', color: 'bg-purple-100 text-purple-800' };
+    } else if (type.includes('pdf')) {
+      return { text: 'PDF', color: 'bg-red-100 text-red-800' };
+    } else if (type.includes('word')) {
+      return { text: '文档', color: 'bg-blue-100 text-blue-800' };
+    } else if (type.includes('excel') || type.includes('sheet')) {
+      return { text: '表格', color: 'bg-green-100 text-green-800' };
+    } else {
+      return { text: '文件', color: 'bg-gray-100 text-gray-800' };
+    }
+  };
+
+  return (
+    <Dialog open={visible} onOpenChange={onCancel}>
+      <DialogContent className="max-w-4xl max-h-[80vh]">
+        <DialogHeader>
+          <DialogTitle>选择文件</DialogTitle>
+          <DialogDescription>
+            从已有文件中选择,或上传新文件
+          </DialogDescription>
+        </DialogHeader>
+        
+        <div className="space-y-4">
+          {/* 上传区域 */}
+          <Card>
+            <CardContent className="pt-6">
+              <MinioUploader
+                uploadPath={uploadPath}
+                accept={accept}
+                maxSize={maxSize}
+                onUploadSuccess={handleUploadSuccess}
+                buttonText={uploadButtonText}
               />
-              <div style={{ flex: 1 }}>
-                <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>{file.name}</div>
-                <div style={{ color: '#666', fontSize: '12px' }}>
-                  ID: {file.id} | 大小: {((file.size || 0) / 1024 / 1024).toFixed(2)}MB
-                </div>
+              <p className="text-sm text-gray-500 mt-2">
+                上传后请从下方列表中选择文件
+              </p>
+            </CardContent>
+          </Card>
+
+          {/* 选择状态显示 */}
+          <Card>
+            <CardContent className="pt-6">
+              <div className="bg-green-50 border border-green-200 rounded-md p-3">
+                <p className="text-sm text-green-700">{getSelectionText()}</p>
+              </div>
+            </CardContent>
+          </Card>
+
+          {/* 文件列表 */}
+          <div className="space-y-2 max-h-96 overflow-y-auto">
+            {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>
+            ) : filteredFiles.length > 0 ? (
+              <div className="grid gap-3">
+                {filteredFiles.map((file) => {
+                  const typeBadge = getFileTypeBadge(file.type);
+                  const isSelected = isFileSelected(file);
+                  
+                  return (
+                    <Card
+                      key={file.id}
+                      className={`cursor-pointer transition-all hover:shadow-md ${
+                        isSelected ? 'ring-2 ring-primary' : ''
+                      }`}
+                      onClick={() => handleSelectFile(file)}
+                    >
+                      <CardContent className="p-4">
+                        <div className="flex items-center space-x-4">
+                          {/* 文件预览图 */}
+                          <div className="relative">
+                            {file.type.startsWith('image/') ? (
+                              <img
+                                src={file.fullUrl}
+                                alt={file.name}
+                                className="w-16 h-16 object-cover rounded-md"
+                              />
+                            ) : (
+                              <div className="w-16 h-16 bg-gray-100 rounded-md flex items-center justify-center">
+                                <Upload className="h-8 w-8 text-gray-400" />
+                              </div>
+                            )}
+                            {isSelected && (
+                              <div className="absolute -top-2 -right-2 bg-primary text-white rounded-full p-1">
+                                <Check className="h-3 w-3" />
+                              </div>
+                            )}
+                          </div>
+
+                          {/* 文件信息 */}
+                          <div className="flex-1 min-w-0">
+                            <h4 className="text-sm font-medium truncate">{file.name}</h4>
+                            <div className="flex items-center space-x-2 mt-1">
+                              <Badge className={`${typeBadge.color} text-xs`}>
+                                {typeBadge.text}
+                              </Badge>
+                              <span className="text-xs text-gray-500">ID: {file.id}</span>
+                              <span className="text-xs text-gray-500">
+                                {formatFileSize(file.size || 0)}
+                              </span>
+                            </div>
+                          </div>
+
+                          {/* 选择按钮 */}
+                          <Button
+                            type="button"
+                            variant={isSelected ? "default" : "outline"}
+                            size="sm"
+                            onClick={(e) => {
+                              e.stopPropagation();
+                              handleSelectFile(file);
+                            }}
+                          >
+                            {isSelected ? '已选择' : '选择'}
+                          </Button>
+                        </div>
+                      </CardContent>
+                    </Card>
+                  );
+                })}
               </div>
-              <Button
-                type={isFileSelected(file) ? 'primary' : 'default'}
-                size="small"
-              >
-                {isFileSelected(file) ? '已选择' : '选择'}
-              </Button>
-            </div>
-          ))
-        ) : (
-          <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
-            暂无符合条件的文件,请先上传
+            ) : (
+              <Card>
+                <CardContent className="text-center py-8">
+                  <Upload className="h-12 w-12 mx-auto text-gray-400 mb-4" />
+                  <p className="text-gray-600">暂无符合条件的文件</p>
+                  <p className="text-sm text-gray-500 mt-2">请先上传文件或调整筛选条件</p>
+                </CardContent>
+              </Card>
+            )}
           </div>
-        )}
-      </div>
-    </Modal>
+        </div>
+
+        <DialogFooter>
+          <Button type="button" variant="outline" onClick={onCancel}>
+            取消
+          </Button>
+          <Button 
+            type="button" 
+            onClick={handleConfirm}
+            disabled={selectedFiles.length === 0}
+          >
+            确认选择
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
   );
 };
 

+ 196 - 99
src/client/admin-shadcn/components/MinioUploader.tsx

@@ -1,12 +1,13 @@
 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 type { UploadRequestOption } from 'rc-upload/lib/interface';
+import type { RcFile } from 'rc-upload/lib/interface';
 
 interface MinioUploaderProps {
   /** 上传路径 */
@@ -27,6 +28,17 @@ interface MinioUploaderProps {
   tipText?: string;
 }
 
+// 定义上传文件状态
+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 = '/',
@@ -38,9 +50,9 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
   buttonText = '点击或拖拽上传文件',
   tipText = '支持单文件或多文件上传,单个文件大小不超过500MB'
 }) => {
-  const { message: antdMessage } = App.useApp();
   const [fileList, setFileList] = useState<UploadFile[]>([]);
   const [uploadingFiles, setUploadingFiles] = useState<Set<string>>(new Set());
+  const [dragActive, setDragActive] = useState(false);
 
   // 处理上传进度
   const handleProgress = useCallback((uid: string, event: MinioProgressEvent) => {
@@ -49,7 +61,7 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
         if (item.uid === uid) {
           return {
             ...item,
-            status: event.stage === 'error' ? ('error' as UploadFileStatus) : ('uploading' as UploadFileStatus),
+            status: event.stage === 'error' ? 'error' : 'uploading',
             percent: event.progress,
             error: event.stage === 'error' ? event.message : undefined
           };
@@ -66,9 +78,8 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
         if (item.uid === uid) {
           return {
             ...item,
-            status: 'success' as UploadFileStatus,
+            status: 'success',
             percent: 100,
-            response: { fileKey: result.fileKey },
             url: result.fileUrl,
           };
         }
@@ -82,9 +93,9 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
       return newSet;
     });
     
-    antdMessage.success(`文件 "${file.name}" 上传成功`);
+    toast.success(`文件 "${file.name}" 上传成功`);
     onUploadSuccess?.(result.fileKey, result.fileUrl, file);
-  }, [antdMessage, onUploadSuccess]);
+  }, [onUploadSuccess]);
 
   // 处理上传失败
   const handleError = useCallback((uid: string, error: Error, file: File) => {
@@ -93,7 +104,7 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
         if (item.uid === uid) {
           return {
             ...item,
-            status: 'error' as UploadFileStatus,
+            status: 'error',
             percent: 0,
             error: error.message || '上传失败'
           };
@@ -108,27 +119,23 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
       return newSet;
     });
     
-    antdMessage.error(`文件 "${file.name}" 上传失败: ${error.message}`);
+    toast.error(`文件 "${file.name}" 上传失败: ${error.message}`);
     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 => [
-      ...prev.filter(item => item.uid !== uid),
+      ...prev,
       {
         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,
       }
     ]);
@@ -137,38 +144,67 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
     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,
-        options.file as unknown as File,
-        rcFile.name,
+        file,
+        file.name,
         {
           onProgress: (event) => handleProgress(uid, event),
           signal: new AbortController().signal
         }
       );
       
-      handleComplete(uid, result, rcFile as unknown as File);
-      onSuccess?.({}, rcFile);
+      handleComplete(uid, result, file);
     } 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 +212,146 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
     switch (item.status) {
       case 'uploading':
         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 (
-          <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':
         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:
         return null;
     }
   };
 
+  // 渲染文件图标
+  const renderFileIcon = (type?: string) => {
+    if (type?.startsWith('image/')) {
+      return <FileText className="h-8 w-8 text-blue-500" />;
+    } else if (type?.startsWith('video/')) {
+      return <FileText className="h-8 w-8 text-red-500" />;
+    } else if (type?.startsWith('audio/')) {
+      return <FileText className="h-8 w-8 text-purple-500" />;
+    } else if (type?.includes('pdf')) {
+      return <FileText className="h-8 w-8 text-red-500" />;
+    } else if (type?.includes('word')) {
+      return <FileText className="h-8 w-8 text-blue-600" />;
+    } else if (type?.includes('excel') || type?.includes('sheet')) {
+      return <FileText className="h-8 w-8 text-green-500" />;
+    } else {
+      return <FileText className="h-8 w-8 text-gray-500" />;
+    }
+  };
+
   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="space-y-4">
+      {/* 拖拽上传区域 */}
+      <div
+        className={`relative border-2 border-dashed rounded-lg p-6 transition-all ${
+          dragActive 
+            ? 'border-primary bg-primary/5' 
+            : 'border-gray-300 hover:border-primary/50'
+        }`}
+        onDragEnter={handleDrag}
+        onDragLeave={handleDrag}
+        onDragOver={handleDrag}
+        onDrop={handleDrop}
       >
-        <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="flex flex-col items-center justify-center space-y-4">
+          <Upload className={`h-12 w-12 ${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>
+          </div>
+          <Button
+            type="button"
+            variant="outline"
+            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>
-      </Upload.Dragger>
+      </div>
 
       {/* 上传进度列表 */}
       {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'}
-                    />
+        <Card>
+          <CardContent className="pt-6">
+            <h3 className="text-lg font-semibold mb-4">上传进度</h3>
+            <div className="space-y-4">
+              {fileList.map(item => (
+                <div key={item.uid} className="flex items-center space-x-4 p-4 border rounded-lg">
+                  <div className="flex-shrink-0">
+                    {renderFileIcon(item.type)}
+                  </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">
+                        {renderUploadStatus(item)}
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => handleRemove(item.uid)}
+                          disabled={item.status === 'uploading'}
+                        >
+                          <X className="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>
+                    )}
                   </div>
                 </div>
-                {item.status === 'uploading' && (
-                  <Progress 
-                    percent={item.percent}
-                    size="small" 
-                    status={item.percent === 100 ? 'success' : undefined}
-                  />
-                )}
-              </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;

+ 368 - 323
src/client/admin-shadcn/pages/Files.tsx

@@ -1,33 +1,59 @@
-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 { 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, Upload, FileText } from 'lucide-react';
 import { fileClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import dayjs from 'dayjs';
 import { uploadMinIOWithPolicy } from '@/client/utils/minio';
+import * as z from 'zod';
 
 // 定义类型
 type FileItem = InferResponseType<typeof fileClient.$get, 200>['data'][0];
 type FileListResponse = InferResponseType<typeof fileClient.$get, 200>;
 type UpdateFileRequest = InferRequestType<typeof fileClient[':id']['$put']>['json'];
 
+// 表单验证schema
+const fileFormSchema = z.object({
+  name: z.string().min(1, '文件名称不能为空'),
+  description: z.string().optional(),
+});
+
+type FileFormData = z.infer<typeof fileFormSchema>;
+
 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 [editingFile, setEditingFile] = useState<FileItem | null>(null);
   const [searchText, setSearchText] = useState('');
   const [pagination, setPagination] = useState({
-    current: 1,
+    page: 1,
     pageSize: 10,
     total: 0,
   });
+  const [deleteFileId, setDeleteFileId] = useState<number | null>(null);
+  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
 
   const queryClient = useQueryClient();
   
-  
+  // 表单初始化
+  const form = useForm<FileFormData>({
+    resolver: zodResolver(fileFormSchema),
+    defaultValues: {
+      name: '',
+      description: '',
+    },
+  });
+
   // 获取文件列表数据
   const fetchFiles = async ({ page, pageSize }: { page: number; pageSize: number }): Promise<FileListResponse> => {
     const response = await fileClient.$get({ query: { page, pageSize, keyword: searchText } });
@@ -36,31 +62,63 @@ export const FilesPage: React.FC = () => {
   };
 
   // 获取文件下载URL
-  const getFileUrl = async (fileId: number) => {
+  const getFileDownloadUrl = async (fileId: number) => {
     try {
-      const response = await fileClient[':id']['url'].$get({ param: { id: fileId } });
-      if (!response.ok) throw new Error('获取文件URL失败');
+      const response = await fileClient[':id']['download'].$get({ param: { id: fileId } });
+      if (!response.ok) throw new Error('获取文件下载URL失败');
       const data = await response.json();
-      return data.url;
+      return data;
     } catch (error) {
-      message.error('获取文件URL失败');
+      toast.error('获取文件下载URL失败');
       return null;
     }
   };
 
-  // 获取文件下载URL
-  const getFileDownloadUrl = async (fileId: number) => {
+  // 获取文件预览URL
+  const getFilePreviewUrl = async (fileId: number) => {
     try {
-      const response = await fileClient[':id']['download'].$get({ param: { id: fileId } });
-      if (!response.ok) throw new Error('获取文件下载URL失败');
+      const response = await fileClient[':id']['url'].$get({ param: { id: fileId } });
+      if (!response.ok) throw new Error('获取文件URL失败');
       const data = await response.json();
-      return data;
+      return data.url;
     } catch (error) {
-      message.error('获取文件下载URL失败');
+      toast.error('获取文件URL失败');
       return null;
     }
   };
 
+  const { data, isLoading, error } = useQuery({
+    queryKey: ['files', pagination.page, pagination.pageSize, searchText],
+    queryFn: () => fetchFiles({ page: pagination.page, 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}`);
+    }
+  });
+
+  // 删除文件记录
+  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);
@@ -76,14 +134,14 @@ export const FilesPage: React.FC = () => {
 
   // 处理文件预览
   const handlePreview = async (record: FileItem) => {
-    const url = await getFileUrl(record.id);
+    const url = await getFilePreviewUrl(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('该文件类型不支持预览');
+        toast.warning('该文件类型不支持预览');
       }
     }
   };
@@ -92,75 +150,8 @@ export const FilesPage: React.FC = () => {
   const isPreviewable = (fileType: string) => {
     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 handleTableChange = (newPagination: any) => {
-    setPagination(newPagination);
-  };
-  
-  // 显示编辑弹窗
-  const showModal = (record: FileItem) => {
-    setModalVisible(true);
-    setEditingKey(record.id);
-    form.setFieldsValue({
-      name: record.name,
-      description: record.description,
-      type: record.type,
-      size: record.size,
-    });
-  };
-
-  // 关闭弹窗
-  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 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';
@@ -171,253 +162,307 @@ export const FilesPage: React.FC = () => {
       if (!file) return;
       
       try {
-        message.loading('正在上传文件...');
+        toast.loading('正在上传文件...');
         await uploadMinIOWithPolicy('/files', file, file.name);
-        message.success('文件上传成功');
+        toast.success('文件上传成功');
         queryClient.invalidateQueries({ queryKey: ['files'] });
       } catch (error) {
-        message.error(`上传失败: ${error instanceof Error ? error.message : '未知错误'}`);
+        toast.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 showEditModal = (record: FileItem) => {
+    setEditingFile(record);
+    setIsModalOpen(true);
+    form.reset({
+      name: record.name,
+      description: record.description || '',
+    });
+  };
+
+  // 处理表单提交
+  const handleFormSubmit = async (data: FileFormData) => {
+    if (editingFile) {
+      await updateFile.mutateAsync({ 
+        id: editingFile.id, 
+        data: {
+          name: data.name,
+          description: data.description,
+        }
+      });
     }
   };
-  
-  // 表格列定义
-  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 handleDeleteConfirm = () => {
+    if (deleteFileId) {
+      deleteFile.mutate(deleteFileId);
+      setIsDeleteDialogOpen(false);
+      setDeleteFileId(null);
+    }
+  };
+
+  const handleSearch = () => {
+    setPagination({ ...pagination, page: 1 });
+  };
+
+  // 格式化文件大小
+  const formatFileSize = (bytes: number) => {
+    if (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 (
-    <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={handleDirectUpload}>
+          <Upload className="h-4 w-4 mr-2" />
           上传文件
         </Button>
       </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>
-          
-          <Form.Item name="description" label="文件描述">
-            <Input.TextArea
-              rows={4}
-              placeholder="请输入文件描述"
-              className="rounded-md"
-            />
-          </Form.Item>
-          
-          <Form.Item name="type" label="文件类型" hidden>
-            <Input />
-          </Form.Item>
-          
-          <Form.Item name="size" label="文件大小" hidden>
-            <Input />
-          </Form.Item>
-        </Form>
-      </Modal>
+      <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 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>
+                        <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.page - 1) * tablePagination.pageSize + 1)}-
+                {Math.min(tablePagination.page * tablePagination.pageSize, tablePagination.total)} 条,
+                共 {tablePagination.total} 条
+              </div>
+              <div className="flex gap-2">
+                <Button
+                  variant="outline"
+                  size="sm"
+                  disabled={tablePagination.page <= 1}
+                  onClick={() => setPagination({ ...pagination, page: tablePagination.page - 1 })}
+                >
+                  上一页
+                </Button>
+                <span className="px-3 py-1 text-sm">
+                  第 {tablePagination.page} 页
+                </span>
+                <Button
+                  variant="outline"
+                  size="sm"
+                  disabled={tablePagination.page >= Math.ceil(tablePagination.total / tablePagination.pageSize)}
+                  onClick={() => setPagination({ ...pagination, page: tablePagination.page + 1 })}
+                >
+                  下一页
+                </Button>
+              </div>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 编辑对话框 */}
+      <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>
   );
 };