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 { useEffect, useMemo, useState } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Eye } from "lucide-react";
|
import { Eye } from "lucide-react";
|
||||||
|
import {
|
||||||
|
PREVIEW_TEXT_MAX_LENGTH,
|
||||||
|
resolvePreviewFileType,
|
||||||
|
truncatePreviewText,
|
||||||
|
type PreviewFileType,
|
||||||
|
} from "@/utils/filePreview";
|
||||||
import {
|
import {
|
||||||
createAnnotationTaskUsingPost,
|
createAnnotationTaskUsingPost,
|
||||||
getAnnotationTaskByIdUsingGet,
|
getAnnotationTaskByIdUsingGet,
|
||||||
@@ -53,6 +59,7 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|||||||
!!value && typeof value === "object" && !Array.isArray(value);
|
!!value && typeof value === "object" && !Array.isArray(value);
|
||||||
|
|
||||||
const DEFAULT_SEGMENTATION_ENABLED = true;
|
const DEFAULT_SEGMENTATION_ENABLED = true;
|
||||||
|
const FILE_PREVIEW_MAX_HEIGHT = 500;
|
||||||
const SEGMENTATION_OPTIONS = [
|
const SEGMENTATION_OPTIONS = [
|
||||||
{ label: "需要切片段", value: true },
|
{ label: "需要切片段", value: true },
|
||||||
{ label: "不需要切片段", value: false },
|
{ label: "不需要切片段", value: false },
|
||||||
@@ -116,7 +123,7 @@ export default function CreateAnnotationTask({
|
|||||||
const [fileContent, setFileContent] = useState("");
|
const [fileContent, setFileContent] = useState("");
|
||||||
const [fileContentLoading, setFileContentLoading] = useState(false);
|
const [fileContentLoading, setFileContentLoading] = useState(false);
|
||||||
const [previewFileName, setPreviewFileName] = 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 [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
||||||
|
|
||||||
// 任务详情加载状态(编辑模式)
|
// 任务详情加载状态(编辑模式)
|
||||||
@@ -297,57 +304,32 @@ export default function CreateAnnotationTask({
|
|||||||
|
|
||||||
// 预览文件内容
|
// 预览文件内容
|
||||||
const handlePreviewFileContent = async (file: DatasetPreviewFile) => {
|
const handlePreviewFileContent = async (file: DatasetPreviewFile) => {
|
||||||
const fileName = file.fileName?.toLowerCase() || '';
|
const fileType = resolvePreviewFileType(file.fileName);
|
||||||
|
if (!fileType) {
|
||||||
// 文件类型扩展名映射
|
|
||||||
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) {
|
|
||||||
message.warning("不支持预览该文件类型");
|
message.warning("不支持预览该文件类型");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFileContentLoading(true);
|
setFileContentLoading(true);
|
||||||
setPreviewFileName(file.fileName);
|
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 {
|
try {
|
||||||
if (isTextFile) {
|
if (fileType === "text") {
|
||||||
// 文本文件:获取内容
|
// 文本文件:获取内容
|
||||||
const response = await fetch(fileUrl);
|
const response = await fetch(previewUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('下载失败');
|
throw new Error('下载失败');
|
||||||
}
|
}
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
// 限制预览内容长度
|
setFileContent(truncatePreviewText(text, PREVIEW_TEXT_MAX_LENGTH));
|
||||||
const maxLength = 50000;
|
} else {
|
||||||
if (text.length > maxLength) {
|
// 媒体/PDF 文件:直接使用预览地址
|
||||||
setFileContent(text.substring(0, maxLength) + '\n\n... (内容过长,仅显示前 50000 字符)');
|
setPreviewMediaUrl(previewUrl);
|
||||||
} 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");
|
|
||||||
}
|
}
|
||||||
setFileContentVisible(true);
|
setFileContentVisible(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -878,7 +860,7 @@ export default function CreateAnnotationTask({
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className="mb-2 text-xs text-gray-500">点击文件名可预览文件内容(支持文本、图片、音频、视频)</div>
|
<div className="mb-2 text-xs text-gray-500">点击文件名可预览文件内容(支持文本、图片、音频、视频、PDF)</div>
|
||||||
<Table
|
<Table
|
||||||
dataSource={datasetPreviewData}
|
dataSource={datasetPreviewData}
|
||||||
columns={[
|
columns={[
|
||||||
@@ -942,7 +924,7 @@ export default function CreateAnnotationTask({
|
|||||||
{previewFileType === "text" && (
|
{previewFileType === "text" && (
|
||||||
<pre
|
<pre
|
||||||
style={{
|
style={{
|
||||||
maxHeight: '500px',
|
maxHeight: `${FILE_PREVIEW_MAX_HEIGHT}px`,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
@@ -960,16 +942,23 @@ export default function CreateAnnotationTask({
|
|||||||
<img
|
<img
|
||||||
src={previewMediaUrl}
|
src={previewMediaUrl}
|
||||||
alt={previewFileName}
|
alt={previewFileName}
|
||||||
style={{ maxWidth: '100%', maxHeight: '500px', objectFit: 'contain' }}
|
style={{ maxWidth: '100%', maxHeight: `${FILE_PREVIEW_MAX_HEIGHT}px`, objectFit: 'contain' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{previewFileType === "pdf" && (
|
||||||
|
<iframe
|
||||||
|
src={previewMediaUrl}
|
||||||
|
title={previewFileName || "PDF 预览"}
|
||||||
|
style={{ width: '100%', height: `${FILE_PREVIEW_MAX_HEIGHT}px`, border: 'none' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{previewFileType === "video" && (
|
{previewFileType === "video" && (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<video
|
<video
|
||||||
src={previewMediaUrl}
|
src={previewMediaUrl}
|
||||||
controls
|
controls
|
||||||
style={{ maxWidth: '100%', maxHeight: '500px' }}
|
style={{ maxWidth: '100%', maxHeight: `${FILE_PREVIEW_MAX_HEIGHT}px` }}
|
||||||
>
|
>
|
||||||
您的浏览器不支持视频播放
|
您的浏览器不支持视频播放
|
||||||
</video>
|
</video>
|
||||||
|
|||||||
@@ -332,6 +332,14 @@ export default function Overview({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
loading={previewLoading && previewFileName === record.fileName}
|
||||||
|
onClick={() => handlePreviewFile(record)}
|
||||||
|
>
|
||||||
|
预览
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
@@ -549,6 +557,13 @@ export default function Overview({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{previewFileType === "pdf" && (
|
||||||
|
<iframe
|
||||||
|
src={previewMediaUrl}
|
||||||
|
title={previewFileName || "PDF 预览"}
|
||||||
|
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{previewFileType === "video" && (
|
{previewFileType === "video" && (
|
||||||
<div style={{ textAlign: "center" }}>
|
<div style={{ textAlign: "center" }}>
|
||||||
<video
|
<video
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import type {
|
|||||||
} 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 { PREVIEW_TEXT_MAX_LENGTH, resolvePreviewFileType, truncatePreviewText } from "@/utils/filePreview";
|
import {
|
||||||
|
PREVIEW_TEXT_MAX_LENGTH,
|
||||||
|
resolvePreviewFileType,
|
||||||
|
truncatePreviewText,
|
||||||
|
type PreviewFileType,
|
||||||
|
} from "@/utils/filePreview";
|
||||||
import {
|
import {
|
||||||
deleteDatasetFileUsingDelete,
|
deleteDatasetFileUsingDelete,
|
||||||
downloadFileByIdUsingGet,
|
downloadFileByIdUsingGet,
|
||||||
@@ -35,7 +40,7 @@ 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 [previewFileType, setPreviewFileType] = useState<PreviewFileType>("text");
|
||||||
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
||||||
const [previewLoading, setPreviewLoading] = useState(false);
|
const [previewLoading, setPreviewLoading] = useState(false);
|
||||||
|
|
||||||
@@ -111,7 +116,7 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
return;
|
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);
|
setPreviewFileName(file.fileName);
|
||||||
setPreviewFileType(fileType);
|
setPreviewFileType(fileType);
|
||||||
setPreviewContent("");
|
setPreviewContent("");
|
||||||
@@ -120,7 +125,7 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
if (fileType === "text") {
|
if (fileType === "text") {
|
||||||
setPreviewLoading(true);
|
setPreviewLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(fileUrl);
|
const response = await fetch(previewUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("下载失败");
|
throw new Error("下载失败");
|
||||||
}
|
}
|
||||||
@@ -136,7 +141,7 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreviewMediaUrl(fileUrl);
|
setPreviewMediaUrl(previewUrl);
|
||||||
setPreviewVisible(true);
|
setPreviewVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ import CreateKnowledgeSet from "../components/CreateKnowledgeSet";
|
|||||||
import KnowledgeItemEditor from "../components/KnowledgeItemEditor";
|
import KnowledgeItemEditor from "../components/KnowledgeItemEditor";
|
||||||
import ImportKnowledgeItemsDialog from "../components/ImportKnowledgeItemsDialog";
|
import ImportKnowledgeItemsDialog from "../components/ImportKnowledgeItemsDialog";
|
||||||
import { formatDate } from "@/utils/unit";
|
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_MAX_HEIGHT = 500;
|
||||||
const PREVIEW_MODAL_WIDTH = {
|
const PREVIEW_MODAL_WIDTH = {
|
||||||
@@ -67,7 +72,7 @@ const KnowledgeSetDetail = () => {
|
|||||||
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 [previewFileType, setPreviewFileType] = useState<PreviewFileType>("text");
|
||||||
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
||||||
const [previewLoadingItemId, setPreviewLoadingItemId] = useState<string | null>(null);
|
const [previewLoadingItemId, setPreviewLoadingItemId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -560,6 +565,13 @@ const KnowledgeSetDetail = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{previewFileType === "pdf" && (
|
||||||
|
<iframe
|
||||||
|
src={previewMediaUrl}
|
||||||
|
title={previewFileName || "PDF 预览"}
|
||||||
|
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{previewFileType === "video" && (
|
{previewFileType === "video" && (
|
||||||
<div style={{ textAlign: "center" }}>
|
<div style={{ textAlign: "center" }}>
|
||||||
<video
|
<video
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ const IMAGE_FILE_EXTENSIONS = [
|
|||||||
];
|
];
|
||||||
const VIDEO_FILE_EXTENSIONS = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
|
const VIDEO_FILE_EXTENSIONS = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
|
||||||
const AUDIO_FILE_EXTENSIONS = [".mp3", ".wav", ".ogg", ".aac", ".flac", ".m4a"];
|
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 => {
|
export const resolvePreviewFileType = (fileName?: string): PreviewFileType | null => {
|
||||||
const lowerName = (fileName || "").toLowerCase();
|
const lowerName = (fileName || "").toLowerCase();
|
||||||
@@ -39,6 +40,9 @@ export const resolvePreviewFileType = (fileName?: string): PreviewFileType | nul
|
|||||||
if (AUDIO_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
|
if (AUDIO_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
|
||||||
return "audio";
|
return "audio";
|
||||||
}
|
}
|
||||||
|
if (PDF_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext))) {
|
||||||
|
return "pdf";
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user