feat(knowledge): 添加知识库条目预览功能

- 集成 docx4j 和 LibreOffice 实现 Office 文档转 PDF 预览
- 新增 KnowledgeItemPreviewService 处理预览转换逻辑
- 添加异步任务 KnowledgeItemPreviewAsyncService 进行文档转换
- 实现预览状态管理包括待转换、转换中、就绪和失败状态
- 在前端界面添加 Office 文档预览状态标签显示
- 支持 DOC/DOCX 文件在线预览功能
- 添加预览元数据存储和管理机制
This commit is contained in:
2026-02-01 20:05:25 +08:00
parent 551248ec76
commit 40889baacc
13 changed files with 854 additions and 6 deletions

View File

@@ -8,6 +8,7 @@ import {
Empty,
Input,
Modal,
Tag,
Table,
Tooltip,
} from "antd";
@@ -21,6 +22,7 @@ import {
deleteKnowledgeItemByIdUsingDelete,
deleteKnowledgeSetByIdUsingDelete,
downloadKnowledgeItemFileUsingGet,
convertKnowledgeItemPreviewUsingPost,
exportKnowledgeItemsUsingGet,
queryKnowledgeDirectoriesUsingGet,
queryKnowledgeItemsUsingGet,
@@ -61,6 +63,53 @@ const PREVIEW_MODAL_WIDTH = {
const PREVIEW_TEXT_FONT_SIZE = 12;
const PREVIEW_TEXT_PADDING = 12;
const PREVIEW_AUDIO_PADDING = 40;
const OFFICE_FILE_EXTENSIONS = [".doc", ".docx"];
type OfficePreviewStatus = "UNSET" | "PENDING" | "PROCESSING" | "READY" | "FAILED";
const OFFICE_PREVIEW_STATUS_META: Record<OfficePreviewStatus, { label: string; color: string }> = {
UNSET: { label: "未转换", color: "default" },
PENDING: { label: "待转换", color: "default" },
PROCESSING: { label: "转换中", color: "processing" },
READY: { label: "可预览", color: "success" },
FAILED: { label: "转换失败", color: "error" },
};
type OfficePreviewMetadata = {
previewStatus?: string;
previewError?: string;
previewUpdatedAt?: string;
previewPdfPath?: string;
};
const isOfficeFileName = (fileName?: string) => {
const lowerName = (fileName || "").toLowerCase();
return OFFICE_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext));
};
const parseOfficePreviewMetadata = (metadata?: string): OfficePreviewMetadata => {
if (!metadata) {
return {};
}
try {
const parsed = JSON.parse(metadata) as OfficePreviewMetadata;
return parsed || {};
} catch (error) {
console.warn("解析预览元数据失败", error);
return {};
}
};
const normalizeOfficePreviewStatus = (status?: string): OfficePreviewStatus => {
if (!status) {
return "UNSET";
}
const upper = status.toUpperCase();
if (upper === "PENDING" || upper === "PROCESSING" || upper === "READY" || upper === "FAILED") {
return upper as OfficePreviewStatus;
}
return "UNSET";
};
type KnowledgeItemRow = KnowledgeItemView & {
isDirectory?: boolean;
@@ -284,20 +333,60 @@ const KnowledgeSetDetail = () => {
return "文件";
};
const resolveOfficePreviewDisplay = (record: KnowledgeItemView) => {
const fileName = resolvePreviewFileName(record);
if (!isOfficeFileName(fileName)) {
return null;
}
const previewMetadata = parseOfficePreviewMetadata(record.metadata);
const status = normalizeOfficePreviewStatus(previewMetadata.previewStatus);
const meta = OFFICE_PREVIEW_STATUS_META[status];
return {
status,
label: meta.label,
color: meta.color,
error: previewMetadata.previewError,
};
};
const handlePreviewItemFile = async (record: KnowledgeItemView) => {
if (!id) return;
const fileName = resolvePreviewFileName(record);
const previewUrl = `/api/data-management/knowledge-sets/${id}/items/${record.id}/preview`;
setPreviewFileName(fileName);
setPreviewContent("");
setPreviewMediaUrl("");
if (isOfficeFileName(fileName)) {
setPreviewFileType("pdf");
setPreviewLoadingItemId(record.id);
try {
const { data } = await convertKnowledgeItemPreviewUsingPost(id, record.id);
const status = normalizeOfficePreviewStatus(data?.status);
if (status === "READY") {
setPreviewMediaUrl(previewUrl);
setPreviewVisible(true);
} else if (status === "FAILED") {
message.error(data?.previewError || "转换失败,请稍后重试");
} else {
message.info("已开始转换,请稍后重试");
}
fetchItems();
} catch (error) {
console.error("触发预览转换失败", error);
message.error("触发预览转换失败");
} finally {
setPreviewLoadingItemId(null);
}
return;
}
const fileType = resolvePreviewFileType(fileName);
if (!fileType) {
message.warning("不支持预览该文件类型");
return;
}
const previewUrl = `/api/data-management/knowledge-sets/${id}/items/${record.id}/preview`;
setPreviewFileName(fileName);
setPreviewFileType(fileType);
setPreviewContent("");
setPreviewMediaUrl("");
if (fileType === "text") {
setPreviewLoadingItemId(record.id);
@@ -669,8 +758,20 @@ const KnowledgeSetDetail = () => {
const isFileRecord =
record.contentType === KnowledgeContentType.FILE ||
record.sourceType === KnowledgeSourceType.FILE_UPLOAD;
const officePreview = isFileRecord ? resolveOfficePreviewDisplay(record) : null;
return (
<>
{officePreview && (
<Tooltip
title={
officePreview.status === "FAILED"
? officePreview.error || officePreview.label
: officePreview.label
}
>
<Tag color={officePreview.color}>{officePreview.label}</Tag>
</Tooltip>
)}
{isFileRecord && (
<Tooltip title="预览">
<Button

View File

@@ -101,6 +101,16 @@ export function downloadKnowledgeItemFileUsingGet(setId: string, itemId: string,
return download(`/api/data-management/knowledge-sets/${setId}/items/${itemId}/file`, null, fileName || "");
}
// 知识条目预览状态
export function queryKnowledgeItemPreviewStatusUsingGet(setId: string, itemId: string) {
return get(`/api/data-management/knowledge-sets/${setId}/items/${itemId}/preview/status`);
}
// 触发知识条目预览转换
export function convertKnowledgeItemPreviewUsingPost(setId: string, itemId: string) {
return post(`/api/data-management/knowledge-sets/${setId}/items/${itemId}/preview/convert`, {});
}
// 导出知识条目
export function exportKnowledgeItemsUsingGet(setId: string) {
return download(`/api/data-management/knowledge-sets/${setId}/items/export`);