You've already forked DataMate
feat(file-preview): 增加PDF文件预览功能并优化预览逻辑
- 引入统一的文件预览工具函数和类型定义 - 添加PDF文件类型的识别和预览支持 - 使用iframe实现PDF文件在线预览 - 重构文件预览逻辑,统一处理不同文件类型的预览 - 优化文本内容预览的长度截取机制 - 更新预览按钮加载状态显示 - 统一预览窗口的最大高度配置 - 修改API调用路径为专门的预览接口
This commit is contained in:
@@ -6,6 +6,12 @@ import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Eye } from "lucide-react";
|
||||
import {
|
||||
PREVIEW_TEXT_MAX_LENGTH,
|
||||
resolvePreviewFileType,
|
||||
truncatePreviewText,
|
||||
type PreviewFileType,
|
||||
} from "@/utils/filePreview";
|
||||
import {
|
||||
createAnnotationTaskUsingPost,
|
||||
getAnnotationTaskByIdUsingGet,
|
||||
@@ -53,6 +59,7 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
|
||||
const DEFAULT_SEGMENTATION_ENABLED = true;
|
||||
const FILE_PREVIEW_MAX_HEIGHT = 500;
|
||||
const SEGMENTATION_OPTIONS = [
|
||||
{ label: "需要切片段", value: true },
|
||||
{ label: "不需要切片段", value: false },
|
||||
@@ -116,7 +123,7 @@ export default function CreateAnnotationTask({
|
||||
const [fileContent, setFileContent] = useState("");
|
||||
const [fileContentLoading, setFileContentLoading] = useState(false);
|
||||
const [previewFileName, setPreviewFileName] = useState("");
|
||||
const [previewFileType, setPreviewFileType] = useState<"text" | "image" | "video" | "audio">("text");
|
||||
const [previewFileType, setPreviewFileType] = useState<PreviewFileType>("text");
|
||||
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
||||
|
||||
// 任务详情加载状态(编辑模式)
|
||||
@@ -297,57 +304,32 @@ export default function CreateAnnotationTask({
|
||||
|
||||
// 预览文件内容
|
||||
const handlePreviewFileContent = async (file: DatasetPreviewFile) => {
|
||||
const fileName = file.fileName?.toLowerCase() || '';
|
||||
|
||||
// 文件类型扩展名映射
|
||||
const textExtensions = ['.json', '.jsonl', '.txt', '.csv', '.tsv', '.xml', '.md', '.yaml', '.yml'];
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
|
||||
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi'];
|
||||
const audioExtensions = ['.mp3', '.wav', '.ogg', '.aac', '.flac', '.m4a'];
|
||||
|
||||
const isTextFile = textExtensions.some(ext => fileName.endsWith(ext));
|
||||
const isImageFile = imageExtensions.some(ext => fileName.endsWith(ext));
|
||||
const isVideoFile = videoExtensions.some(ext => fileName.endsWith(ext));
|
||||
const isAudioFile = audioExtensions.some(ext => fileName.endsWith(ext));
|
||||
|
||||
if (!isTextFile && !isImageFile && !isVideoFile && !isAudioFile) {
|
||||
const fileType = resolvePreviewFileType(file.fileName);
|
||||
if (!fileType) {
|
||||
message.warning("不支持预览该文件类型");
|
||||
return;
|
||||
}
|
||||
|
||||
setFileContentLoading(true);
|
||||
setPreviewFileName(file.fileName);
|
||||
setPreviewFileType(fileType);
|
||||
setFileContent("");
|
||||
setPreviewMediaUrl("");
|
||||
|
||||
const fileUrl = `/api/data-management/datasets/${selectedDatasetId}/files/${file.id}/download`;
|
||||
const previewUrl = `/api/data-management/datasets/${selectedDatasetId}/files/${file.id}/preview`;
|
||||
|
||||
try {
|
||||
if (isTextFile) {
|
||||
if (fileType === "text") {
|
||||
// 文本文件:获取内容
|
||||
const response = await fetch(fileUrl);
|
||||
const response = await fetch(previewUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('下载失败');
|
||||
}
|
||||
const text = await response.text();
|
||||
// 限制预览内容长度
|
||||
const maxLength = 50000;
|
||||
if (text.length > maxLength) {
|
||||
setFileContent(text.substring(0, maxLength) + '\n\n... (内容过长,仅显示前 50000 字符)');
|
||||
setFileContent(truncatePreviewText(text, PREVIEW_TEXT_MAX_LENGTH));
|
||||
} else {
|
||||
setFileContent(text);
|
||||
}
|
||||
setPreviewFileType("text");
|
||||
} else if (isImageFile) {
|
||||
// 图片文件:直接使用 URL
|
||||
setPreviewMediaUrl(fileUrl);
|
||||
setPreviewFileType("image");
|
||||
} else if (isVideoFile) {
|
||||
// 视频文件:使用 URL
|
||||
setPreviewMediaUrl(fileUrl);
|
||||
setPreviewFileType("video");
|
||||
} else if (isAudioFile) {
|
||||
// 音频文件:使用 URL
|
||||
setPreviewMediaUrl(fileUrl);
|
||||
setPreviewFileType("audio");
|
||||
// 媒体/PDF 文件:直接使用预览地址
|
||||
setPreviewMediaUrl(previewUrl);
|
||||
}
|
||||
setFileContentVisible(true);
|
||||
} catch (error) {
|
||||
@@ -878,7 +860,7 @@ export default function CreateAnnotationTask({
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div className="mb-2 text-xs text-gray-500">点击文件名可预览文件内容(支持文本、图片、音频、视频)</div>
|
||||
<div className="mb-2 text-xs text-gray-500">点击文件名可预览文件内容(支持文本、图片、音频、视频、PDF)</div>
|
||||
<Table
|
||||
dataSource={datasetPreviewData}
|
||||
columns={[
|
||||
@@ -942,7 +924,7 @@ export default function CreateAnnotationTask({
|
||||
{previewFileType === "text" && (
|
||||
<pre
|
||||
style={{
|
||||
maxHeight: '500px',
|
||||
maxHeight: `${FILE_PREVIEW_MAX_HEIGHT}px`,
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '12px',
|
||||
@@ -960,16 +942,23 @@ export default function CreateAnnotationTask({
|
||||
<img
|
||||
src={previewMediaUrl}
|
||||
alt={previewFileName}
|
||||
style={{ maxWidth: '100%', maxHeight: '500px', objectFit: 'contain' }}
|
||||
style={{ maxWidth: '100%', maxHeight: `${FILE_PREVIEW_MAX_HEIGHT}px`, objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{previewFileType === "pdf" && (
|
||||
<iframe
|
||||
src={previewMediaUrl}
|
||||
title={previewFileName || "PDF 预览"}
|
||||
style={{ width: '100%', height: `${FILE_PREVIEW_MAX_HEIGHT}px`, border: 'none' }}
|
||||
/>
|
||||
)}
|
||||
{previewFileType === "video" && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<video
|
||||
src={previewMediaUrl}
|
||||
controls
|
||||
style={{ maxWidth: '100%', maxHeight: '500px' }}
|
||||
style={{ maxWidth: '100%', maxHeight: `${FILE_PREVIEW_MAX_HEIGHT}px` }}
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
|
||||
@@ -332,6 +332,14 @@ export default function Overview({
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
loading={previewLoading && previewFileName === record.fileName}
|
||||
onClick={() => handlePreviewFile(record)}
|
||||
>
|
||||
预览
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@@ -549,6 +557,13 @@ export default function Overview({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{previewFileType === "pdf" && (
|
||||
<iframe
|
||||
src={previewMediaUrl}
|
||||
title={previewFileName || "PDF 预览"}
|
||||
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
||||
/>
|
||||
)}
|
||||
{previewFileType === "video" && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<video
|
||||
|
||||
@@ -4,7 +4,12 @@ import type {
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { App } from "antd";
|
||||
import { useState } from "react";
|
||||
import { PREVIEW_TEXT_MAX_LENGTH, resolvePreviewFileType, truncatePreviewText } from "@/utils/filePreview";
|
||||
import {
|
||||
PREVIEW_TEXT_MAX_LENGTH,
|
||||
resolvePreviewFileType,
|
||||
truncatePreviewText,
|
||||
type PreviewFileType,
|
||||
} from "@/utils/filePreview";
|
||||
import {
|
||||
deleteDatasetFileUsingDelete,
|
||||
downloadFileByIdUsingGet,
|
||||
@@ -35,7 +40,7 @@ export function useFilesOperation(dataset: Dataset) {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState("");
|
||||
const [previewFileName, setPreviewFileName] = useState("");
|
||||
const [previewFileType, setPreviewFileType] = useState<"text" | "image" | "video" | "audio">("text");
|
||||
const [previewFileType, setPreviewFileType] = useState<PreviewFileType>("text");
|
||||
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
|
||||
@@ -111,7 +116,7 @@ export function useFilesOperation(dataset: Dataset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileUrl = `/api/data-management/datasets/${datasetId}/files/${file.id}/download`;
|
||||
const previewUrl = `/api/data-management/datasets/${datasetId}/files/${file.id}/preview`;
|
||||
setPreviewFileName(file.fileName);
|
||||
setPreviewFileType(fileType);
|
||||
setPreviewContent("");
|
||||
@@ -120,7 +125,7 @@ export function useFilesOperation(dataset: Dataset) {
|
||||
if (fileType === "text") {
|
||||
setPreviewLoading(true);
|
||||
try {
|
||||
const response = await fetch(fileUrl);
|
||||
const response = await fetch(previewUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error("下载失败");
|
||||
}
|
||||
@@ -136,7 +141,7 @@ export function useFilesOperation(dataset: Dataset) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewMediaUrl(fileUrl);
|
||||
setPreviewMediaUrl(previewUrl);
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,12 @@ import CreateKnowledgeSet from "../components/CreateKnowledgeSet";
|
||||
import KnowledgeItemEditor from "../components/KnowledgeItemEditor";
|
||||
import ImportKnowledgeItemsDialog from "../components/ImportKnowledgeItemsDialog";
|
||||
import { formatDate } from "@/utils/unit";
|
||||
import { PREVIEW_TEXT_MAX_LENGTH, resolvePreviewFileType, truncatePreviewText } from "@/utils/filePreview";
|
||||
import {
|
||||
PREVIEW_TEXT_MAX_LENGTH,
|
||||
resolvePreviewFileType,
|
||||
truncatePreviewText,
|
||||
type PreviewFileType,
|
||||
} from "@/utils/filePreview";
|
||||
|
||||
const PREVIEW_MAX_HEIGHT = 500;
|
||||
const PREVIEW_MODAL_WIDTH = {
|
||||
@@ -67,7 +72,7 @@ const KnowledgeSetDetail = () => {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState("");
|
||||
const [previewFileName, setPreviewFileName] = useState("");
|
||||
const [previewFileType, setPreviewFileType] = useState<"text" | "image" | "video" | "audio">("text");
|
||||
const [previewFileType, setPreviewFileType] = useState<PreviewFileType>("text");
|
||||
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
||||
const [previewLoadingItemId, setPreviewLoadingItemId] = useState<string | null>(null);
|
||||
|
||||
@@ -560,6 +565,13 @@ const KnowledgeSetDetail = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{previewFileType === "pdf" && (
|
||||
<iframe
|
||||
src={previewMediaUrl}
|
||||
title={previewFileName || "PDF 预览"}
|
||||
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
||||
/>
|
||||
)}
|
||||
{previewFileType === "video" && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<video
|
||||
|
||||
@@ -22,8 +22,9 @@ const IMAGE_FILE_EXTENSIONS = [
|
||||
];
|
||||
const VIDEO_FILE_EXTENSIONS = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
|
||||
const AUDIO_FILE_EXTENSIONS = [".mp3", ".wav", ".ogg", ".aac", ".flac", ".m4a"];
|
||||
const PDF_FILE_EXTENSIONS = [".pdf"];
|
||||
|
||||
export type PreviewFileType = "text" | "image" | "video" | "audio";
|
||||
export type PreviewFileType = "text" | "image" | "video" | "audio" | "pdf";
|
||||
|
||||
export const resolvePreviewFileType = (fileName?: string): PreviewFileType | null => {
|
||||
const lowerName = (fileName || "").toLowerCase();
|
||||
@@ -39,6 +40,9 @@ export const resolvePreviewFileType = (fileName?: string): PreviewFileType | nul
|
||||
if (AUDIO_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
|
||||
return "audio";
|
||||
}
|
||||
if (PDF_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
|
||||
return "pdf";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user