/** * 文件预览组件 * 显示文档内容和评查点高亮 */ import { useState, useEffect, useRef, ChangeEvent } from 'react'; import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer'; import { requestPageInfo, customGotoPage } from '~/components/collabora/lib'; import { PdfPreview } from './previewComponents/PdfPreview'; import { toastService } from '../ui/Toast'; // 直接从ReviewPointsList导入类型,避免循环依赖 import { type ReviewPoint } from './ReviewPointsList'; // 定义文档内容类型 interface FileContent { title: string; contractNumber: string; path: string; ocrResult?: { __meta?: { page_offset?: number; }; }; // 添加ocrResult属性 ocr_result?:{ __meta?: { page_offset?: number; }; }, parties: { partyA: { name: string; address: string; representative: string; phone: string; }; partyB: { name: string; address: string; representative: string; phone: string; }; }; sections: { title: string; content: string; }[]; template_contract_path?: string; } interface FilePreviewProps { fileContent: FileContent; reviewPoints?: ReviewPoint[]; // 设为可选 activeReviewPointResultId: string | null; targetPage?: number; // 新增目标页码参数 charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(仅用于PDF) highlightValue?: string; // 高亮文本值(用于DOCX) isStructuredView?: boolean; // 是否显示结构化视图 userInfo?: { sub: string; nick_name: string; }; // 用户信息(用于 Collabora) aiSuggestionReplace?: { searchText: string; replaceText: string; pageNumber: number; }; // AI建议替换参数 } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace }: FilePreviewProps) { // 获取文件类型 const real_path = fileContent.path || fileContent.template_contract_path || ''; const fileExtension = real_path.split('.').pop()?.toLowerCase(); const isDocx = fileExtension === 'docx'; const isPdf = fileExtension === 'pdf'; // ✅ 将所有hooks移到条件return之前,确保遵守React Hooks规则 // Refs const contentRef = useRef(null); const collaboraViewerRef = useRef(null); const prevTargetPageRef = useRef(undefined); // States const [numPages, setNumPages] = useState(null); const [pageInputValue, setPageInputValue] = useState(''); const [isDocumentLoading, setIsDocumentLoading] = useState(true); // 文档加载状态 const [isScrollingToTop, setIsScrollingToTop] = useState(false); // 返回顶部loading状态 const [isClearingHighlights, setIsClearingHighlights] = useState(false); // 清除高亮loading状态 // ✅ 将所有useEffect移到条件return之前 // 清除高亮:在组件卸载或文档路径变化时 useEffect(() => { // 返回清理函数 return () => { if (isDocx && collaboraViewerRef.current?.isReady) { console.log('[FilePreview] 🔥 文档切换,调用 clearAllHighlights'); // 调用暴露的清除方法 collaboraViewerRef.current.clearAllHighlights().catch(error => { console.error('[FilePreview] ✗ 清除高亮失败:', error); }); } }; }, [real_path, isDocx]); // 当文档路径变化时,清除旧文档的高亮 // 监听文档加载状态 useEffect(() => { if (!isDocx) { setIsDocumentLoading(false); // 非DOCX文件直接设为已加载 return; } // DOCX文件需要等待 Collabora 准备就绪 setIsDocumentLoading(true); const checkInterval = setInterval(() => { if (collaboraViewerRef.current?.isReady) { setIsDocumentLoading(false); clearInterval(checkInterval); } }, 200); return () => { clearInterval(checkInterval); }; }, [isDocx, real_path]); // 当文档路径变化时重新检测 // DOCX 页数获取: 使用 requestPageInfo 方法 useEffect(() => { if (!isDocx || isPdf) return; // PDF文件不需要执行 let intervalCleared = false; // 等待 CollaboraViewer 准备就绪 const checkInterval = setInterval(() => { if (intervalCleared) return; if (!collaboraViewerRef.current?.isReady) { console.log('[FilePreview] 等待 Collabora 就绪...'); return; } clearInterval(checkInterval); intervalCleared = true; const iframeWindow = collaboraViewerRef.current.getIframeWindow?.(); if (!iframeWindow) { console.warn('[FilePreview] 无法获取 iframe window'); return; } // 使用 requestPageInfo 获取页数 requestPageInfo(iframeWindow) .then((info) => { setNumPages(info.totalPages); }) .catch((error) => { console.warn('[FilePreview] 获取 DOCX 页数失败:', error.message); }); }, 500); // 清理定时器 return () => { clearInterval(checkInterval); }; }, [isDocx, isPdf]); // 处理页面跳转 useEffect(() => { if (isPdf) return; // PDF由PdfPreview处理 // 如果有目标页码,并且与上次相同,提示用户 if(targetPage && numPages && targetPage <= numPages && targetPage === prevTargetPageRef.current){ // toastService.success(`已跳转至目标页码`); } // 如果有目标页码,并且与上次不同或activeReviewPointId变化了,则执行跳转 if (targetPage && numPages && targetPage <= numPages) { prevTargetPageRef.current = targetPage; let newTargetPage = targetPage; // 页码偏移量 try { // 安全地访问ocrResult if (fileContent.ocrResult && fileContent.ocrResult.__meta && fileContent.ocrResult.__meta.page_offset) { // 可以根据需要使用page_offset调整目标页面 newTargetPage = targetPage + fileContent.ocrResult.__meta.page_offset; } } catch (error) { console.error("访问ocrResult时出错:", error); toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误')); } const pageElementId = `page-${newTargetPage}${isStructuredView ? '-structured' : ''}`; const pageElement = document.getElementById(pageElementId); if (pageElement) { pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } else { console.warn(`未找到页面元素: ${pageElementId}`); } } }, [targetPage, numPages, fileContent, activeReviewPointResultId, isStructuredView, isPdf]); // 调试日志 // console.log('[FilePreview] 组件渲染', { // real_path, // fileExtension, // isDocx, // isPdf, // hasPath: !!fileContent.path, // hasTemplatePath: !!fileContent.template_contract_path // }); // 如果是PDF文件,直接使用PdfPreview组件 if (isPdf && real_path) { // console.log('[FilePreview] 渲染PDF预览', { real_path, targetPage, charPositions }); // console.log('[FilePreview] 渲染PDF预览', { fileContent }); const pageOffset = fileContent.ocrResult?.__meta?.page_offset || fileContent.ocr_result?.__meta?.page_offset || 0; // console.log('pageOffset', pageOffset) return ( ); } // DOCX 和其他文件类型继续使用原有逻辑 // 获取评查点对应的样式类 // const getHighlightClass = (status: string) => { // switch (status) { // case 'warning': // return 'warning'; // case 'error': // return 'error'; // case 'success': // return 'success'; // default: // return 'warning'; // } // }; // 处理页码输入变化 const handlePageInputChange = (e: ChangeEvent) => { // 只允许输入数字 const value = e.target.value.replace(/\D/g, ''); setPageInputValue(value); }; // 处理页码跳转(仅用于 DOCX) const handlePageJump = async () => { if (!pageInputValue) return; const targetPageNum = parseInt(pageInputValue, 10); const iframeWindow = collaboraViewerRef.current?.getIframeWindow?.(); if (!iframeWindow) { toastService.warning('文档尚未加载完成,请稍候...'); return; } if (targetPageNum > 0) { try { await customGotoPage(iframeWindow, targetPageNum); setPageInputValue(''); } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; toastService.error(`跳转失败: ${errorMessage}`); } } else { toastService.warning('请输入有效页码'); setPageInputValue(''); } }; // 处理回车键跳转 const handlePageInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handlePageJump(); } }; // 滚动到顶部(仅用于 DOCX) const handleScrollToTop = async () => { if (!collaboraViewerRef.current?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); return; } setIsScrollingToTop(true); try { await collaboraViewerRef.current?.unoCommands.scrollToTop(); console.log('[FilePreview] 已返回顶部'); } catch (error) { console.error('[FilePreview] 返回顶部失败:', error); toastService.error('返回顶部失败'); } finally { // 延迟500ms后重置loading状态,给用户足够的视觉反馈 setTimeout(() => { setIsScrollingToTop(false); }, 500); } }; // 清除所有高亮(仅用于 DOCX) const handleClearAllHighlights = async () => { if (!collaboraViewerRef.current?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); return; } setIsClearingHighlights(true); try { await collaboraViewerRef.current.clearAllHighlights(); console.log('[FilePreview] 已清除所有高亮'); toastService.success('已清除所有高亮'); } catch (error) { console.error('[FilePreview] 清除高亮失败:', error); toastService.error('清除高亮失败'); } finally { // 延迟500ms后重置loading状态 setTimeout(() => { setIsClearingHighlights(false); }, 500); } }; // 渲染文档内容 const renderDocumentContent = () => { // 如果路径无效,显示错误信息 if (!real_path) { if(!fileContent.template_contract_path){ return (

无法加载文件:合同模板未上传

); } return (

无法加载文件:路径无效

); } // 根据文件类型选择不同的渲染方式 // 注意:PDF 文件已在组件开头使用 PdfPreview 组件提前返回 if (fileExtension === 'docx') { // 使用 highlightValue 作为高亮文本(用户点击评查点时传递的实际文本值) // 不再从 charPositions 提取,因为 charPositions 是 PDF 特有的坐标信息 const highlightText = highlightValue; // DOCX文件使用Collabora Online预览 return ( ); } else { // 非PDF/DOCX文件显示不支持消息 return (

暂不支持预览此类型的文件:{fileExtension}

); } }; return (
{isStructuredView ? '模板预览' : '文件预览'}
{/* 清除高亮按钮 - 仅在DOCX文档时显示 */} {isDocx && ( )} {/* 页码跳转控件 */}
{numPages && ( / {numPages} )}
{/* 缩放提示 - 仅在DOCX文档时显示 */} {isDocx && (
Ctrl+滚轮
)}
{renderDocumentContent()}
); }