Переглянути джерело

✨ feat(files): 新增文件预览和下载功能

- 添加文件预览功能,支持图片和视频类型文件
- 实现文件下载功能,通过创建临时链接下载文件
- 优化操作按钮样式和布局,增加预览和下载图标按钮
- 添加文件类型判断,对不可预览文件禁用预览按钮并显示提示
- 移除未使用的ClientItem类型和clientClient导入
- 优化文件名称显示样式,增加弹性布局
- 调整操作列宽度以适应新增按钮
- 为各操作按钮添加title提示信息,提升用户体验
yourname 4 місяців тому
батько
коміт
0947308612
1 змінених файлів з 73 додано та 9 видалено
  1. 73 9
      src/client/admin/pages/Files.tsx

+ 73 - 9
src/client/admin/pages/Files.tsx

@@ -1,9 +1,9 @@
 import React, { useState, useEffect } from 'react';
-import { Table, Button, Space, Input, Modal, Form, Select, DatePicker, Upload, Popconfirm } from 'antd';
+import { Table, Button, Space, Input, Modal, Form, Select, DatePicker, Upload, Popconfirm, Image } from 'antd';
 import { App } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined } from '@ant-design/icons';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, EyeOutlined } from '@ant-design/icons';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { fileClient, clientClient } from '@/client/api';
+import { fileClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import dayjs from 'dayjs';
 import { uploadMinIOWithPolicy } from '@/client/utils/minio';
@@ -11,7 +11,6 @@ import { uploadMinIOWithPolicy } from '@/client/utils/minio';
 // 定义类型
 type FileItem = InferResponseType<typeof fileClient.$get, 200>['data'][0];
 type FileListResponse = InferResponseType<typeof fileClient.$get, 200>;
-type ClientItem = InferResponseType<typeof clientClient.$get, 200>['data'][0];
 type UpdateFileRequest = InferRequestType<typeof fileClient[':id']['$put']>['json'];
 
 export const FilesPage: React.FC = () => {
@@ -35,6 +34,51 @@ export const FilesPage: React.FC = () => {
     if (!response.ok) throw new Error('Failed to fetch files');
     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 handleDownload = async (record: FileItem) => {
+    const url = await getFileUrl(record.id);
+    if (url) {
+      const a = document.createElement('a');
+      a.href = url;
+      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 isPreviewable = (fileType: string) => {
+    return fileType.startsWith('image/') || fileType.startsWith('video/');
+  };
   
   const { data, isLoading: loading, error: filesError } = useQuery({
     queryKey: ['files', pagination.current, pagination.pageSize, searchText],
@@ -159,6 +203,11 @@ export const FilesPage: React.FC = () => {
       key: 'name',
       width: 300,
       ellipsis: true,
+      render: (name: string, record: FileItem) => (
+        <div className="flex items-center">
+          <span className="flex-1">{name}</span>
+        </div>
+      ),
     },
     {
       title: '文件类型',
@@ -207,18 +256,32 @@ export const FilesPage: React.FC = () => {
     {
       title: '操作',
       key: 'action',
-      width: 120,
+      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-blue-600 hover:text-blue-800 hover:bg-blue-50"
-          >
-            编辑
-          </Button>
+            className="text-purple-600 hover:text-purple-800 hover:bg-purple-50"
+            title="编辑文件信息"
+          />
           <Popconfirm
             title="确认删除"
             description={`确定要删除文件"${record.name}"吗?此操作不可恢复。`}
@@ -232,6 +295,7 @@ export const FilesPage: React.FC = () => {
               danger
               icon={<DeleteOutlined />}
               className="hover:bg-red-50"
+              title="删除文件"
             >
               删除
             </Button>