feat(file-preview): 增加PDF文件预览功能并优化预览逻辑

- 引入统一的文件预览工具函数和类型定义
- 添加PDF文件类型的识别和预览支持
- 使用iframe实现PDF文件在线预览
- 重构文件预览逻辑,统一处理不同文件类型的预览
- 优化文本内容预览的长度截取机制
- 更新预览按钮加载状态显示
- 统一预览窗口的最大高度配置
- 修改API调用路径为专门的预览接口
This commit is contained in:
2026-01-30 17:32:36 +08:00
parent bd37858ccc
commit 6dfed934a5
5 changed files with 82 additions and 57 deletions

View File

@@ -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 字符)');
} 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");
setFileContent(truncatePreviewText(text, PREVIEW_TEXT_MAX_LENGTH));
} else {
// 媒体/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>

View File

@@ -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

View File

@@ -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);
};

View File

@@ -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

View File

@@ -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;
};