feat(DataManagement): 添加文件预览功能支持多种文件类型

- 实现文本、图片、视频、音频文件的预览功能
- 添加预览模态框支持不同文件类型的展示
- 集成文件类型检测和预览内容加载逻辑
- 添加预览加载状态和错误处理机制
- 实现大文件内容截断和滚动预览功能
- 添加预览窗口关闭和资源清理功能
This commit is contained in:
2026-01-28 11:18:08 +08:00
parent 4233da5b91
commit 3c4b66b451
2 changed files with 194 additions and 69 deletions

View File

@@ -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 { formatBytes, formatDateTime } from "@/utils/unit";
import { Download, Trash2, Folder, File } from "lucide-react"; import { Download, Trash2, Folder, File } from "lucide-react";
import { datasetTypeMap } from "../../dataset.const"; import { datasetTypeMap } from "../../dataset.const";
@@ -9,6 +9,15 @@ type DatasetFileRow = DatasetFile & {
fileCount?: number; fileCount?: number;
uploadTime?: string; 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({ export default function Overview({
dataset, dataset,
@@ -22,17 +31,21 @@ export default function Overview({
pagination, pagination,
selectedFiles, selectedFiles,
previewVisible, previewVisible,
previewFileName, previewFileName,
previewContent, previewContent,
setPreviewVisible, previewFileType,
handleDeleteFile, previewMediaUrl,
handleDownloadFile, previewLoading,
handleBatchDeleteFiles, closePreview,
handleBatchExport, handleDeleteFile,
handleCreateDirectory, handleDownloadFile,
handleDownloadDirectory, handleBatchDeleteFiles,
handleDeleteDirectory, handleBatchExport,
} = filesOperation; handleCreateDirectory,
handleDownloadDirectory,
handleDeleteDirectory,
handlePreviewFile,
} = filesOperation;
// 基本信息 // 基本信息
const items: DescriptionsProps["items"] = [ const items: DescriptionsProps["items"] = [
@@ -127,15 +140,16 @@ export default function Overview({
); );
} }
return ( return (
<Button <Button
type="link" type="link"
onClick={() => {}} loading={previewLoading && previewFileName === record.fileName}
onClick={() => handlePreviewFile(record)}
> >
{content} {content}
</Button> </Button>
); );
}, },
}, },
{ {
title: "大小", title: "大小",
@@ -370,25 +384,63 @@ export default function Overview({
/> />
</div> </div>
</div> </div>
{/* 文件预览弹窗 */} {/* 文件预览弹窗 */}
<Modal <Modal
title={`文件预览:${previewFileName}`} title={`文件预览:${previewFileName}`}
open={previewVisible} open={previewVisible}
onCancel={() => setPreviewVisible(false)} onCancel={closePreview}
footer={null} footer={[
width={700} <Button key="close" onClick={closePreview}>
>
<pre </Button>,
style={{ ]}
whiteSpace: "pre-wrap", width={previewFileType === "text" ? PREVIEW_MODAL_WIDTH.text : PREVIEW_MODAL_WIDTH.media}
wordBreak: "break-all", >
fontSize: 14, {previewFileType === "text" && (
color: "#222", <pre
}} style={{
> maxHeight: `${PREVIEW_MAX_HEIGHT}px`,
{previewContent} overflow: "auto",
</pre> whiteSpace: "pre-wrap",
</Modal> wordBreak: "break-all",
</> fontSize: PREVIEW_TEXT_FONT_SIZE,
); color: "#222",
} backgroundColor: "#f5f5f5",
padding: `${PREVIEW_TEXT_PADDING}px`,
borderRadius: "4px",
}}
>
{previewContent}
</pre>
)}
{previewFileType === "image" && (
<div style={{ textAlign: "center" }}>
<img
src={previewMediaUrl}
alt={previewFileName}
style={{ maxWidth: "100%", maxHeight: `${PREVIEW_MAX_HEIGHT}px`, objectFit: "contain" }}
/>
</div>
)}
{previewFileType === "video" && (
<div style={{ textAlign: "center" }}>
<video
src={previewMediaUrl}
controls
style={{ maxWidth: "100%", maxHeight: `${PREVIEW_MAX_HEIGHT}px` }}
>
</video>
</div>
)}
{previewFileType === "audio" && (
<div style={{ textAlign: "center", padding: `${PREVIEW_AUDIO_PADDING}px 0` }}>
<audio src={previewMediaUrl} controls style={{ width: "100%" }}>
</audio>
</div>
)}
</Modal>
</>
);
}

View File

@@ -2,7 +2,7 @@ import type {
Dataset, Dataset,
DatasetFile, DatasetFile,
} from "@/pages/DataManagement/dataset.model"; } from "@/pages/DataManagement/dataset.model";
import { App } from "antd"; import { App } from "antd";
import { useState } from "react"; import { useState } from "react";
import { import {
deleteDatasetFileUsingDelete, deleteDatasetFileUsingDelete,
@@ -13,11 +13,17 @@ import {
downloadDirectoryUsingGet, downloadDirectoryUsingGet,
deleteDirectoryUsingDelete, deleteDirectoryUsingDelete,
} from "../dataset.api"; } 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) { export function useFilesOperation(dataset: Dataset) {
const { message } = App.useApp(); const { message } = App.useApp();
const { id } = useParams(); // 获取动态路由参数 const { id } = useParams(); // 获取动态路由参数
// 文件相关状态 // 文件相关状态
const [fileList, setFileList] = useState<DatasetFile[]>([]); const [fileList, setFileList] = useState<DatasetFile[]>([]);
@@ -30,9 +36,12 @@ export function useFilesOperation(dataset: Dataset) {
}>({ current: 1, pageSize: 10, total: 0, prefix: '' }); }>({ current: 1, pageSize: 10, total: 0, prefix: '' });
// 文件预览相关状态 // 文件预览相关状态
const [previewVisible, setPreviewVisible] = useState(false); const [previewVisible, setPreviewVisible] = useState(false);
const [previewContent, setPreviewContent] = useState(""); const [previewContent, setPreviewContent] = useState("");
const [previewFileName, setPreviewFileName] = 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 ( const fetchFiles = async (
prefix?: string, prefix?: string,
@@ -90,17 +99,80 @@ export function useFilesOperation(dataset: Dataset) {
setSelectedFiles([]); // 清空选中状态 setSelectedFiles([]); // 清空选中状态
}; };
const handleShowFile = (file: DatasetFile) => async () => { const resolvePreviewFileType = (fileName?: string) => {
// 请求文件内容并弹窗预览 const lowerName = (fileName || "").toLowerCase();
try { if (TEXT_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
const res = await fetch(`/api/datasets/${dataset.id}/file/${file.id}`); return "text";
const data = await res.text();
setPreviewFileName(file.fileName);
setPreviewContent(data);
setPreviewVisible(true);
} catch {
message.error({ content: "文件预览失败" });
} }
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) => { const handleDeleteFile = async (file: DatasetFile) => {
@@ -139,19 +211,20 @@ export function useFilesOperation(dataset: Dataset) {
setSelectedFiles, setSelectedFiles,
pagination, pagination,
setPagination, setPagination,
previewVisible, previewVisible,
setPreviewVisible, previewContent,
previewContent, previewFileName,
previewFileName, previewFileType,
setPreviewContent, previewMediaUrl,
setPreviewFileName, previewLoading,
fetchFiles, closePreview,
setFileList, fetchFiles,
handleBatchDeleteFiles, setFileList,
handleDownloadFile, handleBatchDeleteFiles,
handleShowFile, handleDownloadFile,
handleDeleteFile, handlePreviewFile,
handleBatchExport, handleDeleteFile,
handleBatchExport,
handleCreateDirectory: async (directoryName: string) => { handleCreateDirectory: async (directoryName: string) => {
const currentPrefix = pagination.prefix || ""; const currentPrefix = pagination.prefix || "";
try { try {