完善评查详情

This commit is contained in:
2025-04-18 15:41:43 +08:00
parent 119f9197b2
commit 01d93522b8
25 changed files with 1731 additions and 511 deletions
+147 -60
View File
@@ -3,6 +3,11 @@
* 显示文档内容和评查点高亮
*/
import { useState, useEffect, useRef } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
// 设置worker路径为public目录下的worker文件
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
// 导入统一的ReviewPoint类型
import { type ReviewPoint } from './';
@@ -11,6 +16,12 @@ import { type ReviewPoint } from './';
interface FileContent {
title: string;
contractNumber: string;
path: string;
ocrResult?: {
__meta?: {
page_offset?: number;
};
}; // 添加ocrResult属性
parties: {
partyA: {
name: string;
@@ -35,12 +46,15 @@ interface FilePreviewProps {
fileContent: FileContent;
reviewPoints: ReviewPoint[];
activeReviewPointId: string | null;
targetPage?: number; // 新增目标页码参数
}
export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }: FilePreviewProps) {
export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, targetPage }: FilePreviewProps) {
const [zoomLevel, setZoomLevel] = useState(100);
const [highlightsVisible, setHighlightsVisible] = useState(true);
const contentRef = useRef<HTMLDivElement>(null);
const [numPages, setNumPages] = useState<number | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
// 放大文档
const handleZoomIn = () => {
@@ -77,6 +91,28 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
}
}, [activeReviewPointId]);
// 处理页面跳转
useEffect(() => {
if (targetPage && numPages && targetPage <= numPages) {
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);
}
const pageElement = document.getElementById(`page-${newTargetPage}`);
if (pageElement) {
console.log(`跳转到第${newTargetPage}`);
pageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [targetPage, numPages, fileContent]);
// 获取评查点对应的样式类
const getHighlightClass = (status: string) => {
switch (status) {
@@ -91,69 +127,114 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
}
};
// PDF文档加载成功回调函数
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
setNumPages(numPages);
console.log("PDF加载成功,页数:", numPages);
}
/**
* 渲染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++) {
// 查找该页面上的评查点,基于position.section匹配页面ID
const pageReviewPoints = reviewPoints.filter(point =>
point.position && point.position.section === `page-${i}`
);
// console.log("pageReviewPoints-------",pageReviewPoints);
// 为每一页创建组件
pages.push(
<div key={i} id={`page-${i}`} className="mb-6">
{/* 页码标识,显示在页面上方 */}
<div className="text-center text-gray-500 text-sm mb-2"> {i} </div>
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
<div style={{
transform: `scale(${zoomLevel / 100})`, // 根据zoomLevel应用缩放
transformOrigin: 'top center', // 缩放原点设置为顶部中心
position: 'relative' // 相对定位,作为评查点高亮的定位参考
}}>
{/* 渲染PDF页面组件 */}
<Page
pageNumber={i} // 当前页码
renderTextLayer={true} // 启用文本层,使文本可选择
renderAnnotationLayer={true} // 启用注释层,显示PDF内置注释
className="border border-gray-300 shadow-md" // 添加边框和阴影样式
/>
{/* 渲染评查点高亮区域 */}
{highlightsVisible && pageReviewPoints.map(point => {
// 判断当前评查点是否为激活状态(被选中)
const isActive = point.id === activeReviewPointId;
return (
// 评查点高亮区域
<div
key={point.id}
// 添加多个类名:基础高亮区域、PDF专用高亮、状态样式类、激活状态类
className={`highlight-area pdf-highlight ${getHighlightClass(point.status)} ${isActive ? 'highlight-focus' : ''}`}
// 设置唯一标识,用于滚动定位和DOM查询
data-review-id={point.id}
// 设置高亮区域的样式
style={{
position: 'absolute', // 绝对定位,相对于页面容器
zIndex: isActive ? 20 : 10, // 激活状态时提高层级,确保在最上层显示
boxShadow: isActive ? '0 0 0 2px yellow, 0 0 10px rgba(0,0,0,0.3)' : '', // 激活时添加特殊阴影效果
// 根据评查点的位置信息计算高亮区域位置,实际项目中需根据真实位置坐标计算
top: `${point.position?.index ? point.position.index * 20 : 20}px`,
left: '50px',
width: '300px',
height: '30px',
}}
/>
);
})}
</div>
</div>
);
}
// 返回所有页面组件数组
return pages;
};
// 渲染文档内容
const renderDocumentContent = () => {
return (
<div className="word-document" ref={contentRef} style={{transform: `scale(${zoomLevel/100})`, transformOrigin: 'center top'}}>
<h1>{fileContent.title}</h1>
<p style={{textAlign: 'right'}}>{fileContent.contractNumber}</p>
<div style={{margin: '20px 0'}}>
<p><strong></strong>{fileContent.parties.partyA.name}</p>
<p>{fileContent.parties.partyA.address}</p>
<p>{fileContent.parties.partyA.representative}</p>
<p>{fileContent.parties.partyA.phone}</p>
<p>&nbsp;</p>
<p><strong></strong>{fileContent.parties.partyB.name}</p>
<p>{fileContent.parties.partyB.address}</p>
<p>{fileContent.parties.partyB.representative}</p>
<p>{fileContent.parties.partyB.phone}</p>
</div>
<p></p>
{fileContent.sections.map((section, sectionIndex) => (
<div key={sectionIndex}>
<h2>{section.title}</h2>
{renderSectionContent(section.content, section.title, sectionIndex)}
</div>
))}
</div>
<Document
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={(error) => {
console.error("PDF加载错误:", error);
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
}}
className="flex flex-col items-center"
error={<div className="text-red-500">PDF文档加载失败</div>}
noData={<div></div>}
loading={<div className="text-center py-10">PDF加载中...</div>}
>
{renderAllPages()}
</Document>
);
};
// 渲染章节内容,处理高亮
const renderSectionContent = (content: string, sectionTitle: string, sectionIndex: number) => {
const lines = content.split('\n');
return lines.map((line, lineIndex) => {
// 查找该行对应的评查点
const reviewPoint = reviewPoints.find(point =>
point.position &&
point.position.section === sectionTitle &&
point.position.index === lineIndex
);
if (reviewPoint && highlightsVisible) {
// 如果有对应的评查点,添加高亮
const isActive = reviewPoint.id === activeReviewPointId;
return (
<div
key={`${sectionIndex}-${lineIndex}`}
className={`highlight-area ${getHighlightClass(reviewPoint.status)} ${isActive ? 'highlight-focus' : ''}`}
data-review-id={reviewPoint.id}
style={isActive ? {zIndex: 20, boxShadow: '0 0 0 2px yellow, 0 0 10px rgba(0,0,0,0.3)'} : {}}
>
<p>{line}</p>
</div>
);
} else {
// 没有评查点,正常显示
return <p key={`${sectionIndex}-${lineIndex}`}>{line}</p>;
}
});
};
return (
<div className="file-preview">
<div className="file-preview-header py-2 px-4">
@@ -182,8 +263,14 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
</button>
</div>
</div>
<div className="file-preview-content">
{renderDocumentContent()}
<div className="file-preview-content" ref={contentRef}>
{loadError ? (
<div className="text-red-500 p-4">
<p>{loadError}</p>
</div>
) : (
renderDocumentContent()
)}
</div>
</div>
);