From d535d0ac1b8f4c44e97b99f468c0214f33a7dba9 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 1 Feb 2026 22:02:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(knowledge):=20=E6=B7=BB=E5=8A=A0Office?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E9=A2=84=E8=A7=88=E8=BD=AE=E8=AF=A2=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入useRef钩子用于管理轮询定时器和当前处理项目 - 添加Spin组件用于预览加载状态显示 - 新增queryKnowledgeItemPreviewStatusUsingGet API调用接口 - 设置OFFICE_PREVIEW_POLL_INTERVAL和OFFICE_PREVIEW_POLL_MAX_TIMES常量 - 移除原有的Office预览元数据解析相关代码 - 添加officePreviewStatus、officePreviewError状态管理 - 实现pollOfficePreviewStatus函数进行预览状态轮询 - 添加clearOfficePreviewPolling清理轮询定时器功能 - 在handlePreviewItemFile中集成预览状态轮询逻辑 - 更新关闭预览时清理轮询和重置状态 - 移除表格中的Office预览标签显示 - 优化PDF预览界面,在无预览URL时显示加载或错误状态 --- .../Detail/KnowledgeSetDetail.tsx | 194 ++++++++++++------ 1 file changed, 132 insertions(+), 62 deletions(-) diff --git a/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx index dd77586..8910329 100644 --- a/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx +++ b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx @@ -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 = { - 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("text"); const [previewMediaUrl, setPreviewMediaUrl] = useState(""); const [previewLoadingItemId, setPreviewLoadingItemId] = useState(null); + const [officePreviewStatus, setOfficePreviewStatus] = useState(null); + const [officePreviewError, setOfficePreviewError] = useState(""); + const officePreviewPollingRef = useRef(null); + const officePreviewItemRef = useRef(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 && ( - - {officePreview.label} - - )} {isFileRecord && (