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 {
|
||||
App,
|
||||
Breadcrumb,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Empty,
|
||||
Input,
|
||||
Modal,
|
||||
Spin,
|
||||
Tag,
|
||||
Table,
|
||||
Tooltip,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
exportKnowledgeItemsUsingGet,
|
||||
queryKnowledgeDirectoriesUsingGet,
|
||||
queryKnowledgeItemsUsingGet,
|
||||
queryKnowledgeItemPreviewStatusUsingGet,
|
||||
queryKnowledgeSetByIdUsingGet,
|
||||
} from "../knowledge-management.api";
|
||||
import {
|
||||
@@ -64,42 +66,16 @@ const PREVIEW_TEXT_FONT_SIZE = 12;
|
||||
const PREVIEW_TEXT_PADDING = 12;
|
||||
const PREVIEW_AUDIO_PADDING = 40;
|
||||
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";
|
||||
|
||||
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";
|
||||
@@ -155,6 +131,10 @@ const KnowledgeSetDetail = () => {
|
||||
const [previewFileType, setPreviewFileType] = useState<PreviewFileType>("text");
|
||||
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
|
||||
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 [fileKeyword, setFileKeyword] = useState("");
|
||||
@@ -333,21 +313,65 @@ const KnowledgeSetDetail = () => {
|
||||
return "文件";
|
||||
};
|
||||
|
||||
const resolveOfficePreviewDisplay = (record: KnowledgeItemView) => {
|
||||
const fileName = resolvePreviewFileName(record);
|
||||
if (!isOfficeFileName(fileName)) {
|
||||
return null;
|
||||
const clearOfficePreviewPolling = useCallback(() => {
|
||||
if (officePreviewPollingRef.current) {
|
||||
window.clearTimeout(officePreviewPollingRef.current);
|
||||
officePreviewPollingRef.current = 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,
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearOfficePreviewPolling();
|
||||
};
|
||||
};
|
||||
}, [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) => {
|
||||
if (!id) return;
|
||||
@@ -359,22 +383,48 @@ const KnowledgeSetDetail = () => {
|
||||
|
||||
if (isOfficeFileName(fileName)) {
|
||||
setPreviewFileType("pdf");
|
||||
setPreviewVisible(true);
|
||||
setPreviewLoadingItemId(record.id);
|
||||
setOfficePreviewStatus("PROCESSING");
|
||||
setOfficePreviewError("");
|
||||
officePreviewItemRef.current = record.id;
|
||||
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 status = normalizeOfficePreviewStatus(data?.status);
|
||||
if (status === "READY") {
|
||||
setPreviewMediaUrl(previewUrl);
|
||||
setPreviewVisible(true);
|
||||
setOfficePreviewStatus("READY");
|
||||
setPreviewLoadingItemId(null);
|
||||
} else if (status === "FAILED") {
|
||||
message.error(data?.previewError || "转换失败,请稍后重试");
|
||||
setOfficePreviewStatus("FAILED");
|
||||
setOfficePreviewError(data?.previewError || "转换失败,请稍后重试");
|
||||
setPreviewLoadingItemId(null);
|
||||
} else {
|
||||
message.info("已开始转换,请稍后重试");
|
||||
setOfficePreviewStatus("PROCESSING");
|
||||
pollOfficePreviewStatus(id, record.id, 0);
|
||||
}
|
||||
fetchItems();
|
||||
} catch (error) {
|
||||
console.error("触发预览转换失败", error);
|
||||
message.error("触发预览转换失败");
|
||||
setOfficePreviewStatus("FAILED");
|
||||
setOfficePreviewError("触发预览转换失败");
|
||||
} finally {
|
||||
setPreviewLoadingItemId(null);
|
||||
}
|
||||
@@ -412,11 +462,15 @@ const KnowledgeSetDetail = () => {
|
||||
};
|
||||
|
||||
const closePreview = () => {
|
||||
clearOfficePreviewPolling();
|
||||
officePreviewItemRef.current = null;
|
||||
setPreviewVisible(false);
|
||||
setPreviewContent("");
|
||||
setPreviewMediaUrl("");
|
||||
setPreviewFileName("");
|
||||
setPreviewFileType("text");
|
||||
setOfficePreviewStatus(null);
|
||||
setOfficePreviewError("");
|
||||
};
|
||||
|
||||
const handleReadItem = async (record: KnowledgeItemView) => {
|
||||
@@ -758,20 +812,8 @@ 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
|
||||
@@ -1084,11 +1126,39 @@ const KnowledgeSetDetail = () => {
|
||||
</div>
|
||||
)}
|
||||
{previewFileType === "pdf" && (
|
||||
<iframe
|
||||
src={previewMediaUrl}
|
||||
title={previewFileName || "PDF 预览"}
|
||||
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
||||
/>
|
||||
<>
|
||||
{previewMediaUrl ? (
|
||||
<iframe
|
||||
src={previewMediaUrl}
|
||||
title={previewFileName || "PDF 预览"}
|
||||
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" && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
|
||||
Reference in New Issue
Block a user