You've already forked DataMate
feat(DataManagement): 添加文件预览功能支持多种文件类型
- 实现文本、图片、视频、音频文件的预览功能 - 添加预览模态框支持不同文件类型的展示 - 集成文件类型检测和预览内容加载逻辑 - 添加预览加载状态和错误处理机制 - 实现大文件内容截断和滚动预览功能 - 添加预览窗口关闭和资源清理功能
This commit is contained in:
@@ -10,6 +10,15 @@ type DatasetFileRow = DatasetFile & {
|
|||||||
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,
|
||||||
filesOperation,
|
filesOperation,
|
||||||
@@ -24,7 +33,10 @@ export default function Overview({
|
|||||||
previewVisible,
|
previewVisible,
|
||||||
previewFileName,
|
previewFileName,
|
||||||
previewContent,
|
previewContent,
|
||||||
setPreviewVisible,
|
previewFileType,
|
||||||
|
previewMediaUrl,
|
||||||
|
previewLoading,
|
||||||
|
closePreview,
|
||||||
handleDeleteFile,
|
handleDeleteFile,
|
||||||
handleDownloadFile,
|
handleDownloadFile,
|
||||||
handleBatchDeleteFiles,
|
handleBatchDeleteFiles,
|
||||||
@@ -32,6 +44,7 @@ export default function Overview({
|
|||||||
handleCreateDirectory,
|
handleCreateDirectory,
|
||||||
handleDownloadDirectory,
|
handleDownloadDirectory,
|
||||||
handleDeleteDirectory,
|
handleDeleteDirectory,
|
||||||
|
handlePreviewFile,
|
||||||
} = filesOperation;
|
} = filesOperation;
|
||||||
|
|
||||||
// 基本信息
|
// 基本信息
|
||||||
@@ -130,7 +143,8 @@ 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>
|
||||||
@@ -374,20 +388,58 @@ export default function Overview({
|
|||||||
<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}>
|
||||||
|
关闭
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={previewFileType === "text" ? PREVIEW_MODAL_WIDTH.text : PREVIEW_MODAL_WIDTH.media}
|
||||||
>
|
>
|
||||||
|
{previewFileType === "text" && (
|
||||||
<pre
|
<pre
|
||||||
style={{
|
style={{
|
||||||
|
maxHeight: `${PREVIEW_MAX_HEIGHT}px`,
|
||||||
|
overflow: "auto",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
wordBreak: "break-all",
|
wordBreak: "break-all",
|
||||||
fontSize: 14,
|
fontSize: PREVIEW_TEXT_FONT_SIZE,
|
||||||
color: "#222",
|
color: "#222",
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
|
padding: `${PREVIEW_TEXT_PADDING}px`,
|
||||||
|
borderRadius: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{previewContent}
|
{previewContent}
|
||||||
</pre>
|
</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>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import {
|
|||||||
} 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(); // 获取动态路由参数
|
||||||
@@ -33,6 +39,9 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
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) => {
|
||||||
@@ -140,16 +212,17 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
pagination,
|
pagination,
|
||||||
setPagination,
|
setPagination,
|
||||||
previewVisible,
|
previewVisible,
|
||||||
setPreviewVisible,
|
|
||||||
previewContent,
|
previewContent,
|
||||||
previewFileName,
|
previewFileName,
|
||||||
setPreviewContent,
|
previewFileType,
|
||||||
setPreviewFileName,
|
previewMediaUrl,
|
||||||
|
previewLoading,
|
||||||
|
closePreview,
|
||||||
fetchFiles,
|
fetchFiles,
|
||||||
setFileList,
|
setFileList,
|
||||||
handleBatchDeleteFiles,
|
handleBatchDeleteFiles,
|
||||||
handleDownloadFile,
|
handleDownloadFile,
|
||||||
handleShowFile,
|
handlePreviewFile,
|
||||||
handleDeleteFile,
|
handleDeleteFile,
|
||||||
handleBatchExport,
|
handleBatchExport,
|
||||||
handleCreateDirectory: async (directoryName: string) => {
|
handleCreateDirectory: async (directoryName: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user