/** * 文件预览组件 * 显示文档内容和评查点高亮 */ import { useState, useEffect, useRef, ChangeEvent } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import { DOCUMENT_URL } from '~/api/axios-client'; // 设置worker路径为public目录下的worker文件 // 使用已经下载的兼容版本 (pdfjs-dist v2.12.313) pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js'; // 导入统一的ReviewPoint类型 import { type ReviewPoint } from './'; import { toastService } from '../ui/Toast'; /** * 自定义样式 * 这些样式解决了PDF页面在放大时互相重叠的问题 */ const styles = { pdfContainer: { display: 'flex', flexDirection: 'column' as const, alignItems: 'center', width: '100%', position: 'relative' as const, }, pageContainer: { display: 'flex', flexDirection: 'column' as const, alignItems: 'center', width: '100%', position: 'relative' as const, } }; // 定义文档内容类型 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; // 新增目标页码参数 isStructuredView?: boolean; // 是否显示结构化视图 } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, isStructuredView = false }: FilePreviewProps) { const [zoomLevel, setZoomLevel] = useState(100); // const [highlightsVisible, setHighlightsVisible] = useState(true); const contentRef = useRef(null); const [numPages, setNumPages] = useState(null); const [loadError, setLoadError] = useState(null); const [pageInputValue, setPageInputValue] = useState(''); // 拖拽状态管理 const [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式 const [isDragging, setIsDragging] = useState(false); const [dragCursor, setDragCursor] = useState('default'); const lastMousePosRef = useRef({ x: 0, y: 0 }); // 放大文档 const handleZoomIn = () => { if (zoomLevel < 200) { setZoomLevel(prevZoom => prevZoom + 10); } }; // 缩小文档 const handleZoomOut = () => { if (zoomLevel > 50) { setZoomLevel(prevZoom => prevZoom - 10); } }; // 切换拖拽模式 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); }; // 处理页码跳转 const handlePageJump = () => { if (!pageInputValue || !numPages) return; const targetPageNum = parseInt(pageInputValue, 10); // 验证页码是否在有效范围内 if (targetPageNum > 0 && targetPageNum <= numPages) { // 找到目标页面元素并滚动到该位置 const pageElement = document.getElementById(`page-${targetPageNum}`); if (pageElement) { pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } else { // 页码超出范围,显示错误信息或重置输入 toastService.warning(`请输入有效页码 (1-${numPages})`); setPageInputValue(''); } }; // 处理回车键跳转 const handlePageInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handlePageJump(); } }; // PDF文档加载成功回调函数 function onDocumentLoadSuccess({ numPages }: { numPages: number }) { setNumPages(numPages); // console.log("PDF加载成功,页数:", numPages); } // 计算页面在缩放后的实际间距 const calculatePageMargin = (zoomFactor: number) => { // 基础间距为30px,随着缩放倍数线性增加 const baseMargin = 30; // 页面缩放后,需要额外添加的间距 = (缩放倍数 - 1) * 页面高度 const additionalMargin = Math.max(0, (zoomFactor - 1) * 800); // 800是估计的页面高度 return baseMargin + additionalMargin; }; // 滚动到顶部 const handleScrollToTop = () => { if (contentRef.current) { contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }); } }; /** * 渲染PDF文档的所有页面 * * 功能描述: * 1. 生成PDF所有页面的渲染数组,每个页面包含页码标识和实际页面内容 * 2. 处理页面缩放,通过CSS transform实现页面大小调整 * 3. 在每个页面上标记对应的评查点高亮区域 * 4. 处理评查点的激活状态,显示特殊的高亮效果 * * @returns {JSX.Element[] | null} 返回所有页面组件的数组,如果没有页数信息则返回null */ const renderAllPages = () => { // 如果还没有获取到PDF总页数,返回null if (!numPages) return null; // 用于存储所有页面组件的数组 const pages = []; // 遍历每一页,生成对应的页面组件 for (let i = 1; i <= numPages; i++) { // 计算当前缩放级别下的页面容器样式 const zoomFactor = zoomLevel / 100; const pageContainerStyle = { ...styles.pageContainer, marginBottom: `${calculatePageMargin(zoomFactor)}px`, // 动态计算页面间距 }; // 为结构化视图和普通视图创建不同的ID const pageId = isStructuredView ? `page-${i}-structured` : `page-${i}`; // 为每一页创建组件 pages.push(
{/* 页码标识,显示在页面上方 */}
第 {i} 页
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
{/* 渲染PDF页面组件 */} {/* 渲染评查点高亮区域 */} {/* {highlightsVisible && pageReviewPoints.map(point => { // 判断当前评查点是否为激活状态(被选中) const isActive = point.id === activeReviewPointId; return ( // 评查点高亮区域
); })} */}
); } // 返回所有页面组件数组 return pages; }; // 渲染文档内容 const renderDocumentContent = () => { const real_path = fileContent.path || fileContent.template_contract_path || ''; // 如果路径无效,显示错误信息 if (!real_path) { if(!fileContent.template_contract_path){ return (

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

); } return (

无法加载文件:路径无效

); } // console.log('real_path',real_path); // 获取文件扩展名 const fileExtension = real_path.split('.').pop()?.toLowerCase(); // PDF内容渲染 const renderPdfContent = () => (
100 ? `${zoomLevel}%` : '100%', overflow: 'visible' }} > { console.error("PDF加载错误:", error); setLoadError("PDF文档加载失败:" + (error.message || "未知错误")); }} className="w-full" error={
PDF文档加载失败,请检查链接或网络连接。
} noData={
无数据
} loading={
PDF加载中...
} > {renderAllPages()}
); // 结构化数据渲染 const renderStructuredData = () => (
结构化数据:
{fileContent.ocrResult ? (
              {JSON.stringify(fileContent.ocrResult, null, 2)}
            
) : (

无结构化数据可显示

)}
); // 根据文件类型选择不同的渲染方式 if (fileExtension === 'pdf') { // 结构化视图模式:显示PDF和结构化数据 if (isStructuredView) { return (
{renderPdfContent()} {renderStructuredData()}
); } // 普通模式:仅显示PDF return renderPdfContent(); } else { // 非PDF文件显示不支持消息 return (

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

); } }; return (
{isStructuredView ? '模板预览' : '文件预览'}
{/* 页码跳转控件 */}
{numPages && ( / {numPages} )}
比例:{zoomLevel}%
); }