/** * 文件预览组件 * 显示文档内容和评查点高亮 */ import { useState, useEffect, useRef, forwardRef, useImperativeHandle, 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; silentReplace?: boolean; // 是否静默替换(不显示面板) }; // AI建议替换参数 isTemplate?: boolean; } // 暴露给父组件的接口 export interface FilePreviewHandle { collaboraViewerRef: React.RefObject; } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { export const FilePreview = forwardRef(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) { // 获取文件类型 const real_path = fileContent.path || fileContent.template_contract_path || ''; const fileExtension = real_path.split('.').pop()?.toLowerCase(); // 是文档类型 并且 是模板文件 const isDocx = fileExtension === 'docx'; // 是模板文件 或 是pdf文件就用pdf渲染 const isPdf = fileExtension === 'pdf'; // ✅ 将所有hooks移到条件return之前,确保遵守React Hooks规则 // Refs const contentRef = useRef(null); const collaboraViewerRef = useRef(null); const prevTargetPageRef = useRef(undefined); // 暴露 collaboraViewerRef 给父组件 useImperativeHandle(ref, () => ({ collaboraViewerRef })); // 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 && (!numPages || targetPageNum <= numPages)) { if (targetPageNum > 0) { try { await customGotoPage(iframeWindow, targetPageNum); setPageInputValue(''); // toastService.success(`已跳转至第 ${targetPageNum} 页`); } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; toastService.error(`跳转失败: ${errorMessage}`); } // } else if (numPages && targetPageNum > numPages) { // toastService.warning(`页码不能超过总页数 ${numPages}`); } else { toastService.warning('请输入有效页码'); } }; // 处理回车键跳转 const handlePageInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handlePageJump(); } }; // 滚动到顶部(支持 PDF 和 DOCX) const handleScrollToTop = async () => { setIsScrollingToTop(true); try { if (isPdf) { // PDF文件:滚动到第一个页面元素 const firstPage = document.querySelector('[data-page-number="1"]'); if (firstPage) { firstPage.scrollIntoView({ behavior: 'smooth', block: 'start' }); // console.log('[FilePreview] PDF已返回顶部'); } else if (contentRef.current) { // 如果找不到页面元素,则滚动容器到顶部 contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }); // console.log('[FilePreview] PDF容器已返回顶部'); } } else if (isDocx) { // DOCX文件:模板预览模式尝试多种滚动方式,编辑模式使用Collabora命令 if (isTemplate) { // 模板预览模式:尝试多种滚动方式 console.log('[FilePreview] 尝试返回顶部...'); // 1. 尝试滚动 contentRef 容器 if (contentRef.current) { contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }); console.log('[FilePreview] 已滚动 contentRef 容器'); } // 2. 尝试滚动外层的 file-preview-isolation 容器 const isolationContainer = document.querySelector('.file-preview-isolation'); if (isolationContainer) { isolationContainer.scrollTo({ top: 0, behavior: 'smooth' }); console.log('[FilePreview] 已滚动 isolation 容器'); } // 3. 最后滚动整个窗口 window.scrollTo({ top: 0, behavior: 'smooth' }); console.log('[FilePreview] 已滚动窗口'); } else { // 编辑模式:使用Collabora UNO命令 if (!collaboraViewerRef.current?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); setIsScrollingToTop(false); return; } await collaboraViewerRef.current?.unoCommands.scrollToTop(); console.log('[FilePreview] DOCX已返回顶部(UNO命令)'); } } } 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; console.log('docx跳转的目标页',targetPage) // DOCX文件使用Collabora Online预览 // 如果是模板预览,使用只读模式;否则使用编辑模式 return ( ); } else { // 非PDF/DOCX文件显示不支持消息 return (

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

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