/** * 文件预览组件 * 显示文档内容和评查点高亮 */ import { useState, useEffect, useRef, ChangeEvent } from 'react'; import { pdfjs } from 'react-pdf'; import { DOCUMENT_URL } from '~/api/axios-client'; import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer'; import { requestPageInfo, customGotoPage } from '~/components/collabora/lib'; import { PdfPreview } from './previewComponents/PdfPreview'; // 设置worker路径为public目录下的worker文件 pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js'; // 导入统一的ReviewPoint类型 import { type ReviewPoint } from './'; import { toastService } from '../ui/Toast'; // 定义文档内容类型 interface FileContent { title: string; contractNumber: string; path: string; ocrResult?: { __meta?: { page_offset?: number; }; }; // 添加ocrResult属性 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 }>; // 字符位置信息 isStructuredView?: boolean; // 是否显示结构化视图 userInfo?: { sub: string; nick_name: string; }; // 用户信息(用于 Collabora) } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, isStructuredView = false, userInfo }: 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'; // 如果是PDF文件,直接使用PdfPreview组件 if (isPdf && real_path) { // console.log('fileContent', fileContent) // console.log('activeReviewPointResultId', activeReviewPointResultId) const pageOffset = fileContent.ocrResult?.__meta?.page_offset || 0; return ( ); } // DOCX 和其他文件类型继续使用原有逻辑 const contentRef = useRef(null); const collaboraViewerRef = useRef(null); const [numPages, setNumPages] = useState(null); const [pageInputValue, setPageInputValue] = useState(''); // DOCX 页数获取: 使用 requestPageInfo 方法 useEffect(() => { if (!isDocx) return; // console.log('[FilePreview] DOCX 文档加载,尝试获取页数'); let intervalCleared = false; // 等待 CollaboraViewer 准备就绪 const checkInterval = setInterval(() => { if (intervalCleared) return; if (!collaboraViewerRef.current?.isReady) { console.log('[FilePreview] 等待 Collabora 就绪...'); return; } // console.log('[FilePreview] Collabora 已就绪,尝试获取页数'); 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]); // 拖拽状态管理(仅用于 DOCX) const [dragMode, setDragMode] = useState(false); const [isDragging, setIsDragging] = useState(false); const [dragCursor, setDragCursor] = useState('default'); const lastMousePosRef = useRef({ x: 0, y: 0 }); // 放大文档(仅用于 DOCX) const handleZoomIn = () => { if (!collaboraViewerRef.current?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); return; } collaboraViewerRef.current?.unoCommands.zoomIn(); }; // 缩小文档(仅用于 DOCX) const handleZoomOut = () => { if (!collaboraViewerRef.current?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); return; } collaboraViewerRef.current?.unoCommands.zoomOut(); }; // 切换拖拽模式 const toggleDragMode = () => { setDragMode(prev => !prev); setDragCursor(prev => prev === 'default' ? 'grab' : 'default'); setIsDragging(false); }; // 处理拖拽开始 const handleMouseDown = (e: React.MouseEvent) => { if (!dragMode || e.button !== 0) return; // 只在拖拽模式下响应左键点击 // 防止选中文本 e.preventDefault(); // 设置拖拽状态 setIsDragging(true); setDragCursor('grabbing'); // 记录鼠标初始位置 lastMousePosRef.current = { x: e.clientX, y: e.clientY }; }; // 处理拖拽过程 const handleMouseMove = (e: React.MouseEvent) => { if (!dragMode || !isDragging || !contentRef.current) return; // 计算鼠标移动距离 const dx = e.clientX - lastMousePosRef.current.x; const dy = e.clientY - lastMousePosRef.current.y; // 更新容器滚动位置 contentRef.current.scrollLeft -= dx; contentRef.current.scrollTop -= dy; // 更新鼠标位置记录 lastMousePosRef.current = { x: e.clientX, y: e.clientY }; }; // 处理拖拽结束 const handleMouseUp = () => { if (!dragMode) return; setIsDragging(false); setDragCursor('grab'); }; // 监听鼠标离开窗口事件 useEffect(() => { const handleMouseLeave = () => { if (dragMode && isDragging) { setIsDragging(false); setDragCursor('grab'); } }; document.addEventListener('mouseleave', handleMouseLeave); document.addEventListener('mouseup', handleMouseUp as EventListener); return () => { document.removeEventListener('mouseleave', handleMouseLeave); document.removeEventListener('mouseup', handleMouseUp as EventListener); }; }, [isDragging, dragMode]); // 处理页面跳转 const prevTargetPageRef = useRef(undefined); useEffect(() => { // 调试信息:记录组件状态 // console.log(`FilePreview更新 - isStructuredView:${isStructuredView}, targetPage:${targetPage}, activeReviewPointResultId:${activeReviewPointResultId}, numPages:${numPages}`); // 如果有目标页码,并且与上次相同,提示用户 if(targetPage && numPages && targetPage <= numPages && targetPage === prevTargetPageRef.current){ // toastService.success(`已跳转至目标页码`); } // 如果有目标页码,并且与上次不同或activeReviewPointId变化了,则执行跳转 if (targetPage && numPages && targetPage <= numPages) { // if (targetPage && numPages && targetPage <= numPages && (targetPage !== prevTargetPageRef.current || activeReviewPointResultId)) { 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' : ''}`; // console.log(`尝试跳转到元素ID: ${pageElementId}`); const pageElement = document.getElementById(pageElementId); if (pageElement) { // console.log(`跳转到第${newTargetPage}页,对应评查点结果ID: ${activeReviewPointResultId}`); pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } else { console.warn(`未找到页面元素: ${pageElementId}`); } } }, [targetPage, numPages, fileContent, activeReviewPointResultId, isStructuredView]); // 获取评查点对应的样式类 // 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 = () => { if (!collaboraViewerRef.current?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); return; } collaboraViewerRef.current?.unoCommands.scrollToTop(); }; // 渲染文档内容 const renderDocumentContent = () => { // 如果路径无效,显示错误信息 if (!real_path) { if(!fileContent.template_contract_path){ return ( 无法加载文件:合同模板未上传 ); } return ( 无法加载文件:路径无效 ); } // 根据文件类型选择不同的渲染方式 // 注意:PDF 文件已在组件开头使用 PdfPreview 组件提前返回 if (fileExtension === 'docx') { // DOCX文件使用Collabora Online预览 return ( ); } else { // 非PDF/DOCX文件显示不支持消息 return ( 暂不支持预览此类型的文件:{fileExtension} ); } }; return ( {isStructuredView ? '模板预览' : '文件预览'} 返回顶部 {/* 页码跳转控件 */} {numPages && ( / {numPages} )} 拖拽模式{dragMode ? '(已激活)' : ''} { // 添加键盘导航支持 const scrollAmount = 50; if (e.key === 'ArrowUp') { if (contentRef.current) contentRef.current.scrollTop -= scrollAmount; e.preventDefault(); } else if (e.key === 'ArrowDown') { if (contentRef.current) contentRef.current.scrollTop += scrollAmount; e.preventDefault(); } else if (e.key === 'ArrowLeft') { if (contentRef.current) contentRef.current.scrollLeft -= scrollAmount; e.preventDefault(); } else if (e.key === 'ArrowRight') { if (contentRef.current) contentRef.current.scrollLeft += scrollAmount; e.preventDefault(); } }} style={{ position: 'relative', height: '100%', width: '100%', display: 'block', border: 'none', background: 'transparent', textAlign: 'center', padding: 0 }} > {renderDocumentContent()} ); }
无法加载文件:合同模板未上传
无法加载文件:路径无效
暂不支持预览此类型的文件:{fileExtension}