You've already forked DataMate
feat(knowledge): 添加Office文档预览轮询机制
- 引入useRef钩子用于管理轮询定时器和当前处理项目 - 添加Spin组件用于预览加载状态显示 - 新增queryKnowledgeItemPreviewStatusUsingGet API调用接口 - 设置OFFICE_PREVIEW_POLL_INTERVAL和OFFICE_PREVIEW_POLL_MAX_TIMES常量 - 移除原有的Office预览元数据解析相关代码 - 添加officePreviewStatus、officePreviewError状态管理 - 实现pollOfficePreviewStatus函数进行预览状态轮询 - 添加clearOfficePreviewPolling清理轮询定时器功能 - 在handlePreviewItemFile中集成预览状态轮询逻辑 - 更新关闭预览时清理轮询和重置状态 - 移除表格中的Office预览标签显示 - 优化PDF预览界面,在无预览URL时显示加载或错误状态
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Empty,
|
Empty,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
|
Spin,
|
||||||
Tag,
|
Tag,
|
||||||
Table,
|
Table,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
exportKnowledgeItemsUsingGet,
|
exportKnowledgeItemsUsingGet,
|
||||||
queryKnowledgeDirectoriesUsingGet,
|
queryKnowledgeDirectoriesUsingGet,
|
||||||
queryKnowledgeItemsUsingGet,
|
queryKnowledgeItemsUsingGet,
|
||||||
|
queryKnowledgeItemPreviewStatusUsingGet,
|
||||||
queryKnowledgeSetByIdUsingGet,
|
queryKnowledgeSetByIdUsingGet,
|
||||||
} from "../knowledge-management.api";
|
} from "../knowledge-management.api";
|
||||||
import {
|
import {
|
||||||
@@ -64,42 +66,16 @@ const PREVIEW_TEXT_FONT_SIZE = 12;
|
|||||||
const PREVIEW_TEXT_PADDING = 12;
|
const PREVIEW_TEXT_PADDING = 12;
|
||||||
const PREVIEW_AUDIO_PADDING = 40;
|
const PREVIEW_AUDIO_PADDING = 40;
|
||||||
const OFFICE_FILE_EXTENSIONS = [".doc", ".docx"];
|
const OFFICE_FILE_EXTENSIONS = [".doc", ".docx"];
|
||||||
|
const OFFICE_PREVIEW_POLL_INTERVAL = 2000;
|
||||||
|
const OFFICE_PREVIEW_POLL_MAX_TIMES = 60;
|
||||||
|
|
||||||
type OfficePreviewStatus = "UNSET" | "PENDING" | "PROCESSING" | "READY" | "FAILED";
|
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 isOfficeFileName = (fileName?: string) => {
|
||||||
const lowerName = (fileName || "").toLowerCase();
|
const lowerName = (fileName || "").toLowerCase();
|
||||||
return OFFICE_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext));
|
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 => {
|
const normalizeOfficePreviewStatus = (status?: string): OfficePreviewStatus => {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return "UNSET";
|
return "UNSET";
|
||||||
@@ -155,6 +131,10 @@ const KnowledgeSetDetail = () => {
|
|||||||
const [previewFileType, setPreviewFileType] = useState<PreviewFileType>("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);
|
||||||
|
const [officePreviewStatus, setOfficePreviewStatus] = useState<OfficePreviewStatus | null>(null);
|
||||||
|
const [officePreviewError, setOfficePreviewError] = useState("");
|
||||||
|
const officePreviewPollingRef = useRef<number | null>(null);
|
||||||
|
const officePreviewItemRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const [filePrefix, setFilePrefix] = useState("");
|
const [filePrefix, setFilePrefix] = useState("");
|
||||||
const [fileKeyword, setFileKeyword] = useState("");
|
const [fileKeyword, setFileKeyword] = useState("");
|
||||||
@@ -333,21 +313,65 @@ const KnowledgeSetDetail = () => {
|
|||||||
return "文件";
|
return "文件";
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveOfficePreviewDisplay = (record: KnowledgeItemView) => {
|
const clearOfficePreviewPolling = useCallback(() => {
|
||||||
const fileName = resolvePreviewFileName(record);
|
if (officePreviewPollingRef.current) {
|
||||||
if (!isOfficeFileName(fileName)) {
|
window.clearTimeout(officePreviewPollingRef.current);
|
||||||
return null;
|
officePreviewPollingRef.current = null;
|
||||||
}
|
}
|
||||||
const previewMetadata = parseOfficePreviewMetadata(record.metadata);
|
}, []);
|
||||||
const status = normalizeOfficePreviewStatus(previewMetadata.previewStatus);
|
|
||||||
const meta = OFFICE_PREVIEW_STATUS_META[status];
|
useEffect(() => {
|
||||||
return {
|
return () => {
|
||||||
status,
|
clearOfficePreviewPolling();
|
||||||
label: meta.label,
|
|
||||||
color: meta.color,
|
|
||||||
error: previewMetadata.previewError,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
}, [clearOfficePreviewPolling]);
|
||||||
|
|
||||||
|
const pollOfficePreviewStatus = useCallback(
|
||||||
|
async (setId: string, itemId: string, attempt: number) => {
|
||||||
|
clearOfficePreviewPolling();
|
||||||
|
officePreviewPollingRef.current = window.setTimeout(async () => {
|
||||||
|
if (officePreviewItemRef.current !== itemId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await queryKnowledgeItemPreviewStatusUsingGet(setId, itemId);
|
||||||
|
const status = normalizeOfficePreviewStatus(data?.status);
|
||||||
|
if (status === "READY") {
|
||||||
|
setPreviewMediaUrl(`/api/data-management/knowledge-sets/${setId}/items/${itemId}/preview`);
|
||||||
|
setOfficePreviewStatus("READY");
|
||||||
|
setOfficePreviewError("");
|
||||||
|
setPreviewLoadingItemId(null);
|
||||||
|
fetchItems();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === "FAILED") {
|
||||||
|
setOfficePreviewStatus("FAILED");
|
||||||
|
setOfficePreviewError(data?.previewError || "转换失败,请稍后重试");
|
||||||
|
setPreviewLoadingItemId(null);
|
||||||
|
fetchItems();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attempt >= OFFICE_PREVIEW_POLL_MAX_TIMES - 1) {
|
||||||
|
setOfficePreviewStatus("FAILED");
|
||||||
|
setOfficePreviewError("转换超时,请稍后重试");
|
||||||
|
setPreviewLoadingItemId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pollOfficePreviewStatus(setId, itemId, attempt + 1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("轮询预览状态失败", error);
|
||||||
|
if (attempt >= OFFICE_PREVIEW_POLL_MAX_TIMES - 1) {
|
||||||
|
setOfficePreviewStatus("FAILED");
|
||||||
|
setOfficePreviewError("转换超时,请稍后重试");
|
||||||
|
setPreviewLoadingItemId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pollOfficePreviewStatus(setId, itemId, attempt + 1);
|
||||||
|
}
|
||||||
|
}, OFFICE_PREVIEW_POLL_INTERVAL);
|
||||||
|
},
|
||||||
|
[clearOfficePreviewPolling, fetchItems]
|
||||||
|
);
|
||||||
|
|
||||||
const handlePreviewItemFile = async (record: KnowledgeItemView) => {
|
const handlePreviewItemFile = async (record: KnowledgeItemView) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -359,22 +383,48 @@ const KnowledgeSetDetail = () => {
|
|||||||
|
|
||||||
if (isOfficeFileName(fileName)) {
|
if (isOfficeFileName(fileName)) {
|
||||||
setPreviewFileType("pdf");
|
setPreviewFileType("pdf");
|
||||||
|
setPreviewVisible(true);
|
||||||
setPreviewLoadingItemId(record.id);
|
setPreviewLoadingItemId(record.id);
|
||||||
|
setOfficePreviewStatus("PROCESSING");
|
||||||
|
setOfficePreviewError("");
|
||||||
|
officePreviewItemRef.current = record.id;
|
||||||
try {
|
try {
|
||||||
|
const { data: statusData } = await queryKnowledgeItemPreviewStatusUsingGet(id, record.id);
|
||||||
|
const currentStatus = normalizeOfficePreviewStatus(statusData?.status);
|
||||||
|
if (currentStatus === "READY") {
|
||||||
|
setPreviewMediaUrl(previewUrl);
|
||||||
|
setOfficePreviewStatus("READY");
|
||||||
|
setPreviewLoadingItemId(null);
|
||||||
|
fetchItems();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStatus === "FAILED") {
|
||||||
|
setOfficePreviewStatus("PROCESSING");
|
||||||
|
}
|
||||||
|
if (currentStatus === "PROCESSING" || currentStatus === "PENDING") {
|
||||||
|
pollOfficePreviewStatus(id, record.id, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { data } = await convertKnowledgeItemPreviewUsingPost(id, record.id);
|
const { data } = await convertKnowledgeItemPreviewUsingPost(id, record.id);
|
||||||
const status = normalizeOfficePreviewStatus(data?.status);
|
const status = normalizeOfficePreviewStatus(data?.status);
|
||||||
if (status === "READY") {
|
if (status === "READY") {
|
||||||
setPreviewMediaUrl(previewUrl);
|
setPreviewMediaUrl(previewUrl);
|
||||||
setPreviewVisible(true);
|
setOfficePreviewStatus("READY");
|
||||||
|
setPreviewLoadingItemId(null);
|
||||||
} else if (status === "FAILED") {
|
} else if (status === "FAILED") {
|
||||||
message.error(data?.previewError || "转换失败,请稍后重试");
|
setOfficePreviewStatus("FAILED");
|
||||||
|
setOfficePreviewError(data?.previewError || "转换失败,请稍后重试");
|
||||||
|
setPreviewLoadingItemId(null);
|
||||||
} else {
|
} else {
|
||||||
message.info("已开始转换,请稍后重试");
|
setOfficePreviewStatus("PROCESSING");
|
||||||
|
pollOfficePreviewStatus(id, record.id, 0);
|
||||||
}
|
}
|
||||||
fetchItems();
|
fetchItems();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("触发预览转换失败", error);
|
console.error("触发预览转换失败", error);
|
||||||
message.error("触发预览转换失败");
|
message.error("触发预览转换失败");
|
||||||
|
setOfficePreviewStatus("FAILED");
|
||||||
|
setOfficePreviewError("触发预览转换失败");
|
||||||
} finally {
|
} finally {
|
||||||
setPreviewLoadingItemId(null);
|
setPreviewLoadingItemId(null);
|
||||||
}
|
}
|
||||||
@@ -412,11 +462,15 @@ const KnowledgeSetDetail = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closePreview = () => {
|
const closePreview = () => {
|
||||||
|
clearOfficePreviewPolling();
|
||||||
|
officePreviewItemRef.current = null;
|
||||||
setPreviewVisible(false);
|
setPreviewVisible(false);
|
||||||
setPreviewContent("");
|
setPreviewContent("");
|
||||||
setPreviewMediaUrl("");
|
setPreviewMediaUrl("");
|
||||||
setPreviewFileName("");
|
setPreviewFileName("");
|
||||||
setPreviewFileType("text");
|
setPreviewFileType("text");
|
||||||
|
setOfficePreviewStatus(null);
|
||||||
|
setOfficePreviewError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReadItem = async (record: KnowledgeItemView) => {
|
const handleReadItem = async (record: KnowledgeItemView) => {
|
||||||
@@ -758,20 +812,8 @@ const KnowledgeSetDetail = () => {
|
|||||||
const isFileRecord =
|
const isFileRecord =
|
||||||
record.contentType === KnowledgeContentType.FILE ||
|
record.contentType === KnowledgeContentType.FILE ||
|
||||||
record.sourceType === KnowledgeSourceType.FILE_UPLOAD;
|
record.sourceType === KnowledgeSourceType.FILE_UPLOAD;
|
||||||
const officePreview = isFileRecord ? resolveOfficePreviewDisplay(record) : null;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{officePreview && (
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
officePreview.status === "FAILED"
|
|
||||||
? officePreview.error || officePreview.label
|
|
||||||
: officePreview.label
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tag color={officePreview.color}>{officePreview.label}</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{isFileRecord && (
|
{isFileRecord && (
|
||||||
<Tooltip title="预览">
|
<Tooltip title="预览">
|
||||||
<Button
|
<Button
|
||||||
@@ -1084,11 +1126,39 @@ const KnowledgeSetDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{previewFileType === "pdf" && (
|
{previewFileType === "pdf" && (
|
||||||
|
<>
|
||||||
|
{previewMediaUrl ? (
|
||||||
<iframe
|
<iframe
|
||||||
src={previewMediaUrl}
|
src={previewMediaUrl}
|
||||||
title={previewFileName || "PDF 预览"}
|
title={previewFileName || "PDF 预览"}
|
||||||
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${PREVIEW_MAX_HEIGHT}px`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 12,
|
||||||
|
color: "#666",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{officePreviewStatus === "FAILED" ? (
|
||||||
|
<>
|
||||||
|
<div>转换失败</div>
|
||||||
|
<div>{officePreviewError || "请稍后重试"}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Spin />
|
||||||
|
<div>正在转换,请稍候...</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{previewFileType === "video" && (
|
{previewFileType === "video" && (
|
||||||
<div style={{ textAlign: "center" }}>
|
<div style={{ textAlign: "center" }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user