From 3c4b66b451be6c29bee60ec714d66408c1e62661 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 28 Jan 2026 11:18:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(DataManagement):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E9=A2=84=E8=A7=88=E5=8A=9F=E8=83=BD=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E7=A7=8D=E6=96=87=E4=BB=B6=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现文本、图片、视频、音频文件的预览功能 - 添加预览模态框支持不同文件类型的展示 - 集成文件类型检测和预览内容加载逻辑 - 添加预览加载状态和错误处理机制 - 实现大文件内容截断和滚动预览功能 - 添加预览窗口关闭和资源清理功能 --- .../Detail/components/Overview.tsx | 128 ++++++++++++----- .../Detail/useFilesOperation.ts | 135 ++++++++++++++---- 2 files changed, 194 insertions(+), 69 deletions(-) diff --git a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx index 801fa31..fb4a62b 100644 --- a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx +++ b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx @@ -1,4 +1,4 @@ -import { App, Button, Descriptions, DescriptionsProps, Modal, Table, Input } from "antd"; +import { App, Button, Descriptions, DescriptionsProps, Modal, Table, Input } from "antd"; import { formatBytes, formatDateTime } from "@/utils/unit"; import { Download, Trash2, Folder, File } from "lucide-react"; import { datasetTypeMap } from "../../dataset.const"; @@ -9,6 +9,15 @@ type DatasetFileRow = DatasetFile & { fileCount?: number; uploadTime?: string; }; + +const PREVIEW_MAX_HEIGHT = 500; +const PREVIEW_MODAL_WIDTH = { + text: 800, + media: 700, +}; +const PREVIEW_TEXT_FONT_SIZE = 12; +const PREVIEW_TEXT_PADDING = 12; +const PREVIEW_AUDIO_PADDING = 40; export default function Overview({ dataset, @@ -22,17 +31,21 @@ export default function Overview({ pagination, selectedFiles, previewVisible, - previewFileName, - previewContent, - setPreviewVisible, - handleDeleteFile, - handleDownloadFile, - handleBatchDeleteFiles, - handleBatchExport, - handleCreateDirectory, - handleDownloadDirectory, - handleDeleteDirectory, - } = filesOperation; + previewFileName, + previewContent, + previewFileType, + previewMediaUrl, + previewLoading, + closePreview, + handleDeleteFile, + handleDownloadFile, + handleBatchDeleteFiles, + handleBatchExport, + handleCreateDirectory, + handleDownloadDirectory, + handleDeleteDirectory, + handlePreviewFile, + } = filesOperation; // 基本信息 const items: DescriptionsProps["items"] = [ @@ -127,15 +140,16 @@ export default function Overview({ ); } - return ( + return ( - ); - }, + ); + }, }, { title: "大小", @@ -370,25 +384,63 @@ export default function Overview({ /> - {/* 文件预览弹窗 */} - setPreviewVisible(false)} - footer={null} - width={700} - > -
-          {previewContent}
-        
-
- - ); -} + {/* 文件预览弹窗 */} + + 关闭 + , + ]} + width={previewFileType === "text" ? PREVIEW_MODAL_WIDTH.text : PREVIEW_MODAL_WIDTH.media} + > + {previewFileType === "text" && ( +
+            {previewContent}
+          
+ )} + {previewFileType === "image" && ( +
+ {previewFileName} +
+ )} + {previewFileType === "video" && ( +
+ +
+ )} + {previewFileType === "audio" && ( +
+ +
+ )} +
+ + ); +} diff --git a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts index 4d316ce..f3c95cb 100644 --- a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts +++ b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts @@ -2,7 +2,7 @@ import type { Dataset, DatasetFile, } from "@/pages/DataManagement/dataset.model"; -import { App } from "antd"; +import { App } from "antd"; import { useState } from "react"; import { deleteDatasetFileUsingDelete, @@ -13,11 +13,17 @@ import { downloadDirectoryUsingGet, deleteDirectoryUsingDelete, } from "../dataset.api"; -import { useParams } from "react-router"; +import { useParams } from "react-router"; + +const MAX_PREVIEW_LENGTH = 50000; +const TEXT_FILE_EXTENSIONS = [".json", ".jsonl", ".txt", ".csv", ".tsv", ".xml", ".md", ".yaml", ".yml"]; +const IMAGE_FILE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"]; +const VIDEO_FILE_EXTENSIONS = [".mp4", ".webm", ".ogg", ".mov", ".avi"]; +const AUDIO_FILE_EXTENSIONS = [".mp3", ".wav", ".ogg", ".aac", ".flac", ".m4a"]; -export function useFilesOperation(dataset: Dataset) { - const { message } = App.useApp(); - const { id } = useParams(); // 获取动态路由参数 +export function useFilesOperation(dataset: Dataset) { + const { message } = App.useApp(); + const { id } = useParams(); // 获取动态路由参数 // 文件相关状态 const [fileList, setFileList] = useState([]); @@ -30,9 +36,12 @@ export function useFilesOperation(dataset: Dataset) { }>({ current: 1, pageSize: 10, total: 0, prefix: '' }); // 文件预览相关状态 - const [previewVisible, setPreviewVisible] = useState(false); - const [previewContent, setPreviewContent] = useState(""); - const [previewFileName, setPreviewFileName] = useState(""); + const [previewVisible, setPreviewVisible] = useState(false); + const [previewContent, setPreviewContent] = useState(""); + const [previewFileName, setPreviewFileName] = useState(""); + const [previewFileType, setPreviewFileType] = useState<"text" | "image" | "video" | "audio">("text"); + const [previewMediaUrl, setPreviewMediaUrl] = useState(""); + const [previewLoading, setPreviewLoading] = useState(false); const fetchFiles = async ( prefix?: string, @@ -90,17 +99,80 @@ export function useFilesOperation(dataset: Dataset) { setSelectedFiles([]); // 清空选中状态 }; - const handleShowFile = (file: DatasetFile) => async () => { - // 请求文件内容并弹窗预览 - try { - const res = await fetch(`/api/datasets/${dataset.id}/file/${file.id}`); - const data = await res.text(); - setPreviewFileName(file.fileName); - setPreviewContent(data); - setPreviewVisible(true); - } catch { - message.error({ content: "文件预览失败" }); + const resolvePreviewFileType = (fileName?: string) => { + const lowerName = (fileName || "").toLowerCase(); + if (TEXT_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) { + return "text"; } + if (IMAGE_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) { + return "image"; + } + if (VIDEO_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) { + return "video"; + } + if (AUDIO_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) { + return "audio"; + } + return null; + }; + + const handlePreviewFile = async (file: DatasetFile) => { + const datasetId = dataset?.id || id; + if (!datasetId) { + message.warning({ content: "数据集未就绪" }); + return; + } + if (file?.id?.startsWith("directory-")) { + return; + } + + const fileType = resolvePreviewFileType(file?.fileName); + if (!fileType) { + message.warning({ content: "不支持预览该文件类型" }); + return; + } + + const fileUrl = `/api/data-management/datasets/${datasetId}/files/${file.id}/download`; + setPreviewFileName(file.fileName); + setPreviewFileType(fileType); + setPreviewContent(""); + setPreviewMediaUrl(""); + + if (fileType === "text") { + setPreviewLoading(true); + try { + const response = await fetch(fileUrl); + if (!response.ok) { + throw new Error("下载失败"); + } + const text = await response.text(); + if (text.length > MAX_PREVIEW_LENGTH) { + setPreviewContent( + `${text.slice(0, MAX_PREVIEW_LENGTH)}\n\n... (内容过长,仅显示前 ${MAX_PREVIEW_LENGTH} 字符)` + ); + } else { + setPreviewContent(text); + } + setPreviewVisible(true); + } catch (error) { + console.error("Preview file content error:", error); + message.error({ content: "获取文件内容失败" }); + } finally { + setPreviewLoading(false); + } + return; + } + + setPreviewMediaUrl(fileUrl); + setPreviewVisible(true); + }; + + const closePreview = () => { + setPreviewVisible(false); + setPreviewContent(""); + setPreviewMediaUrl(""); + setPreviewFileName(""); + setPreviewFileType("text"); }; const handleDeleteFile = async (file: DatasetFile) => { @@ -139,19 +211,20 @@ export function useFilesOperation(dataset: Dataset) { setSelectedFiles, pagination, setPagination, - previewVisible, - setPreviewVisible, - previewContent, - previewFileName, - setPreviewContent, - setPreviewFileName, - fetchFiles, - setFileList, - handleBatchDeleteFiles, - handleDownloadFile, - handleShowFile, - handleDeleteFile, - handleBatchExport, + previewVisible, + previewContent, + previewFileName, + previewFileType, + previewMediaUrl, + previewLoading, + closePreview, + fetchFiles, + setFileList, + handleBatchDeleteFiles, + handleDownloadFile, + handlePreviewFile, + handleDeleteFile, + handleBatchExport, handleCreateDirectory: async (directoryName: string) => { const currentPrefix = pagination.prefix || ""; try {