feat: 接入pdf文件的显示高亮效果
This commit is contained in:
@@ -3,45 +3,19 @@
|
|||||||
* 显示文档内容和评查点高亮
|
* 显示文档内容和评查点高亮
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, useRef, ChangeEvent } from 'react';
|
import { useState, useEffect, useRef, ChangeEvent } from 'react';
|
||||||
import { Document, Page, pdfjs } from 'react-pdf';
|
import { pdfjs } from 'react-pdf';
|
||||||
import { DOCUMENT_URL } from '~/api/axios-client';
|
import { DOCUMENT_URL } from '~/api/axios-client';
|
||||||
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
|
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
|
||||||
import { requestPageInfo, customGotoPage } from '~/components/collabora/lib';
|
import { requestPageInfo, customGotoPage } from '~/components/collabora/lib';
|
||||||
|
import { PdfPreview } from './previewComponents/PdfPreview';
|
||||||
// 导入react-pdf的CSS样式(文本层和注释层必需)
|
|
||||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
|
||||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
|
||||||
|
|
||||||
// 设置worker路径为public目录下的worker文件
|
// 设置worker路径为public目录下的worker文件
|
||||||
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
|
|
||||||
// 2025/09/28 使用新版本的pdfjs-dist v4.8.69
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||||
|
|
||||||
// 导入统一的ReviewPoint类型
|
// 导入统一的ReviewPoint类型
|
||||||
import { type ReviewPoint } from './';
|
import { type ReviewPoint } from './';
|
||||||
import { toastService } from '../ui/Toast';
|
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 {
|
interface FileContent {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -78,6 +52,7 @@ interface FilePreviewProps {
|
|||||||
reviewPoints?: ReviewPoint[]; // 设为可选
|
reviewPoints?: ReviewPoint[]; // 设为可选
|
||||||
activeReviewPointResultId: string | null;
|
activeReviewPointResultId: string | null;
|
||||||
targetPage?: number; // 新增目标页码参数
|
targetPage?: number; // 新增目标页码参数
|
||||||
|
charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息
|
||||||
isStructuredView?: boolean; // 是否显示结构化视图
|
isStructuredView?: boolean; // 是否显示结构化视图
|
||||||
userInfo?: {
|
userInfo?: {
|
||||||
sub: string;
|
sub: string;
|
||||||
@@ -86,21 +61,36 @@ interface FilePreviewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, isStructuredView = false, userInfo }: FilePreviewProps) {
|
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, isStructuredView = false, userInfo }: FilePreviewProps) {
|
||||||
const [zoomLevel, setZoomLevel] = useState(100);
|
|
||||||
// const [highlightsVisible, setHighlightsVisible] = useState(true);
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
|
||||||
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
|
|
||||||
const [numPages, setNumPages] = useState<number | null>(null);
|
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
|
||||||
const [pageInputValue, setPageInputValue] = useState<string>('');
|
|
||||||
|
|
||||||
// 获取文件类型
|
// 获取文件类型
|
||||||
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
||||||
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
||||||
const isDocx = fileExtension === 'docx';
|
const isDocx = fileExtension === 'docx';
|
||||||
const isPdf = fileExtension === 'pdf';
|
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 (
|
||||||
|
<PdfPreview
|
||||||
|
filePath={real_path}
|
||||||
|
targetPage={targetPage}
|
||||||
|
charPositions={charPositions}
|
||||||
|
isStructuredView={isStructuredView}
|
||||||
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
|
pageOffset={pageOffset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOCX 和其他文件类型继续使用原有逻辑
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
|
||||||
|
const [numPages, setNumPages] = useState<number | null>(null);
|
||||||
|
const [pageInputValue, setPageInputValue] = useState<string>('');
|
||||||
|
|
||||||
// DOCX 页数获取: 使用 requestPageInfo 方法
|
// DOCX 页数获取: 使用 requestPageInfo 方法
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDocx) return;
|
if (!isDocx) return;
|
||||||
@@ -144,44 +134,28 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
};
|
};
|
||||||
}, [isDocx]);
|
}, [isDocx]);
|
||||||
|
|
||||||
// 拖拽状态管理
|
// 拖拽状态管理(仅用于 DOCX)
|
||||||
const [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式
|
const [dragMode, setDragMode] = useState(false);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [dragCursor, setDragCursor] = useState('default');
|
const [dragCursor, setDragCursor] = useState('default');
|
||||||
const lastMousePosRef = useRef({ x: 0, y: 0 });
|
const lastMousePosRef = useRef({ x: 0, y: 0 });
|
||||||
|
|
||||||
// 放大文档
|
// 放大文档(仅用于 DOCX)
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
if (isDocx) {
|
|
||||||
// DOCX 文件:调用 Collabora UNO 命令
|
|
||||||
if (!collaboraViewerRef.current?.isReady) {
|
if (!collaboraViewerRef.current?.isReady) {
|
||||||
toastService.warning('文档尚未加载完成,请稍候...');
|
toastService.warning('文档尚未加载完成,请稍候...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
collaboraViewerRef.current?.unoCommands.zoomIn();
|
collaboraViewerRef.current?.unoCommands.zoomIn();
|
||||||
} else if (isPdf) {
|
|
||||||
// PDF 文件:修改 zoomLevel 状态
|
|
||||||
if (zoomLevel < 200) {
|
|
||||||
setZoomLevel(prevZoom => prevZoom + 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 缩小文档
|
// 缩小文档(仅用于 DOCX)
|
||||||
const handleZoomOut = () => {
|
const handleZoomOut = () => {
|
||||||
if (isDocx) {
|
|
||||||
// DOCX 文件:调用 Collabora UNO 命令
|
|
||||||
if (!collaboraViewerRef.current?.isReady) {
|
if (!collaboraViewerRef.current?.isReady) {
|
||||||
toastService.warning('文档尚未加载完成,请稍候...');
|
toastService.warning('文档尚未加载完成,请稍候...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
collaboraViewerRef.current?.unoCommands.zoomOut();
|
collaboraViewerRef.current?.unoCommands.zoomOut();
|
||||||
} else if (isPdf) {
|
|
||||||
// PDF 文件:修改 zoomLevel 状态
|
|
||||||
if (zoomLevel > 50) {
|
|
||||||
setZoomLevel(prevZoom => prevZoom - 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换拖拽模式
|
// 切换拖拽模式
|
||||||
@@ -316,15 +290,13 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
setPageInputValue(value);
|
setPageInputValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理页码跳转
|
// 处理页码跳转(仅用于 DOCX)
|
||||||
const handlePageJump = async () => {
|
const handlePageJump = async () => {
|
||||||
if (!pageInputValue) return;
|
if (!pageInputValue) return;
|
||||||
|
|
||||||
const targetPageNum = parseInt(pageInputValue, 10);
|
const targetPageNum = parseInt(pageInputValue, 10);
|
||||||
|
|
||||||
if (isDocx) {
|
|
||||||
// DOCX 文件:调用自定义页面跳转
|
|
||||||
const iframeWindow = collaboraViewerRef.current?.getIframeWindow?.();
|
const iframeWindow = collaboraViewerRef.current?.getIframeWindow?.();
|
||||||
|
|
||||||
if (!iframeWindow) {
|
if (!iframeWindow) {
|
||||||
toastService.warning('文档尚未加载完成,请稍候...');
|
toastService.warning('文档尚未加载完成,请稍候...');
|
||||||
return;
|
return;
|
||||||
@@ -333,7 +305,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
if (targetPageNum > 0) {
|
if (targetPageNum > 0) {
|
||||||
try {
|
try {
|
||||||
await customGotoPage(iframeWindow, targetPageNum);
|
await customGotoPage(iframeWindow, targetPageNum);
|
||||||
// 跳转成功,清空输入框
|
|
||||||
setPageInputValue('');
|
setPageInputValue('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||||
@@ -343,22 +314,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
toastService.warning('请输入有效页码');
|
toastService.warning('请输入有效页码');
|
||||||
setPageInputValue('');
|
setPageInputValue('');
|
||||||
}
|
}
|
||||||
} else if (isPdf) {
|
|
||||||
// PDF 文件:验证页码并滚动到目标页面
|
|
||||||
if (!numPages) return;
|
|
||||||
|
|
||||||
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('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理回车键跳转
|
// 处理回车键跳转
|
||||||
@@ -368,129 +323,13 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// PDF文档加载成功回调函数
|
// 滚动到顶部(仅用于 DOCX)
|
||||||
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 = () => {
|
const handleScrollToTop = () => {
|
||||||
if (isDocx) {
|
|
||||||
// DOCX 文件:调用 Collabora UNO 命令
|
|
||||||
if (!collaboraViewerRef.current?.isReady) {
|
if (!collaboraViewerRef.current?.isReady) {
|
||||||
toastService.warning('文档尚未加载完成,请稍候...');
|
toastService.warning('文档尚未加载完成,请稍候...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
collaboraViewerRef.current?.unoCommands.scrollToTop();
|
collaboraViewerRef.current?.unoCommands.scrollToTop();
|
||||||
} else {
|
|
||||||
// PDF 文件:滚动容器到顶部
|
|
||||||
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(
|
|
||||||
<div key={i} id={pageId} style={pageContainerStyle}>
|
|
||||||
{/* 页码标识,显示在页面上方 */}
|
|
||||||
<div className="text-center text-gray-500 text-sm mb-2">第 {i} 页</div>
|
|
||||||
|
|
||||||
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
|
|
||||||
<div
|
|
||||||
className="page-wrapper"
|
|
||||||
style={{
|
|
||||||
// transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
|
|
||||||
// transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
|
||||||
position: 'relative', // 相对定位,作为评查点高亮的定位参考
|
|
||||||
display: 'inline-block', // 内联块级元素,宽度由内容决定
|
|
||||||
margin: '0 auto', // 水平居中
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 渲染PDF页面组件 */}
|
|
||||||
<Page
|
|
||||||
pageNumber={i} // 当前页码
|
|
||||||
scale={zoomLevel / 100}
|
|
||||||
devicePixelRatio={window.devicePixelRatio || 1} //根据设备像素比渲染
|
|
||||||
renderTextLayer={false} // 停用文本层,使文本可选择
|
|
||||||
renderAnnotationLayer={false} // 停用注释层,显示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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染文档内容
|
// 渲染文档内容
|
||||||
@@ -511,68 +350,9 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('real_path',real_path);
|
|
||||||
|
|
||||||
// PDF内容渲染
|
|
||||||
const renderPdfContent = () => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...styles.pdfContainer,
|
|
||||||
// 当缩放大于100%时设置最小宽度,确保出现横向滚动条
|
|
||||||
// minWidth: zoomLevel > 100 ? `${zoomLevel}%` : '100%',
|
|
||||||
width: '100%',
|
|
||||||
overflow: 'visible'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Document
|
|
||||||
file={`/api/pdf-proxy?path=${encodeURIComponent(real_path)}`}
|
|
||||||
onLoadSuccess={onDocumentLoadSuccess}
|
|
||||||
onLoadError={(error) => {
|
|
||||||
console.error("PDF加载错误:", error);
|
|
||||||
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
|
||||||
noData={<div>无数据</div>}
|
|
||||||
loading={<div className="text-center py-10">PDF加载中...</div>}
|
|
||||||
>
|
|
||||||
{renderAllPages()}
|
|
||||||
</Document>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 结构化数据渲染
|
|
||||||
const renderStructuredData = () => (
|
|
||||||
<div className="structured-view p-4 text-left mt-4 border-t border-gray-200">
|
|
||||||
<div className="text-xs font-medium mb-2">结构化数据:</div>
|
|
||||||
{fileContent.ocrResult ? (
|
|
||||||
<div className="overflow-auto max-h-[300px]">
|
|
||||||
<pre className="text-xs whitespace-pre-wrap bg-gray-50 p-3 rounded">
|
|
||||||
{JSON.stringify(fileContent.ocrResult, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500 p-4 text-center">
|
|
||||||
<p>无结构化数据可显示</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 根据文件类型选择不同的渲染方式
|
// 根据文件类型选择不同的渲染方式
|
||||||
if (fileExtension === 'pdf') {
|
// 注意:PDF 文件已在组件开头使用 PdfPreview 组件提前返回
|
||||||
// 结构化视图模式:显示PDF和结构化数据
|
if (fileExtension === 'docx') {
|
||||||
if (isStructuredView) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{renderPdfContent()}
|
|
||||||
{renderStructuredData()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 普通模式:仅显示PDF
|
|
||||||
return renderPdfContent();
|
|
||||||
} else if (fileExtension === 'docx') {
|
|
||||||
// DOCX文件使用Collabora Online预览
|
// DOCX文件使用Collabora Online预览
|
||||||
return (
|
return (
|
||||||
<CollaboraViewer
|
<CollaboraViewer
|
||||||
@@ -650,9 +430,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-2 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
|
||||||
比例:{zoomLevel}%
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 flex-shrink-0 ${dragMode ? 'active bg-green-300' : ''}`}
|
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 flex-shrink-0 ${dragMode ? 'active bg-green-300' : ''}`}
|
||||||
title="切换拖拽模式"
|
title="切换拖拽模式"
|
||||||
@@ -711,13 +488,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
padding: 0
|
padding: 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loadError ? (
|
{renderDocumentContent()}
|
||||||
<div className="text-red-500 p-4">
|
|
||||||
<p>{loadError}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
renderDocumentContent()
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,6 +68,16 @@ const getRuleTypeText = (type?: string): string => {
|
|||||||
return ruleTypeMap[type] || type;
|
return ruleTypeMap[type] || type;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符位置类型定义
|
||||||
|
* 用于定位文档中具体的文字位置
|
||||||
|
*/
|
||||||
|
export interface CharPosition {
|
||||||
|
box: number[][]; // 字符边界框坐标
|
||||||
|
char: string; // 字符内容
|
||||||
|
score: number; // OCR识别置信度
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评查点类型定义
|
* 评查点类型定义
|
||||||
* 用于展示单个评查结果
|
* 用于展示单个评查结果
|
||||||
@@ -136,7 +146,7 @@ interface ReviewPointsListProps {
|
|||||||
reviewPoints: ReviewPoint[];
|
reviewPoints: ReviewPoint[];
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
activeReviewPointResultId: string | null;
|
activeReviewPointResultId: string | null;
|
||||||
onReviewPointSelect: (id: string, page?: number) => void;
|
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[]) => void;
|
||||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,8 +829,8 @@ export function ReviewPointsList({
|
|||||||
const config = singleReviewPoint.config as {
|
const config = singleReviewPoint.config as {
|
||||||
logic?: string;
|
logic?: string;
|
||||||
pairs?: Array<{
|
pairs?: Array<{
|
||||||
sourceField: Record<string, { page: number; value: string }>;
|
sourceField: Record<string, { page: number; value: string; char_positions?: CharPosition[] }>;
|
||||||
targetField: Record<string, { page: number; value: string }>;
|
targetField: Record<string, { page: number; value: string; char_positions?: CharPosition[] }>;
|
||||||
res: boolean;
|
res: boolean;
|
||||||
compareMethod?: string;
|
compareMethod?: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -870,7 +880,8 @@ export function ReviewPointsList({
|
|||||||
data: {
|
data: {
|
||||||
key: string;
|
key: string;
|
||||||
page: number;
|
page: number;
|
||||||
value: string
|
value: string;
|
||||||
|
char_positions?: CharPosition[];
|
||||||
};
|
};
|
||||||
res: boolean;
|
res: boolean;
|
||||||
compareMethod?: string;
|
compareMethod?: string;
|
||||||
@@ -883,8 +894,8 @@ export function ReviewPointsList({
|
|||||||
const fieldMap = new Map<string, Array<{
|
const fieldMap = new Map<string, Array<{
|
||||||
targetField: string;
|
targetField: string;
|
||||||
data: {
|
data: {
|
||||||
source: { key: string; page: number; value: string };
|
source: { key: string; page: number; value: string; char_positions?: CharPosition[] };
|
||||||
target: { key: string; page: number; value: string };
|
target: { key: string; page: number; value: string; char_positions?: CharPosition[] };
|
||||||
};
|
};
|
||||||
res: boolean;
|
res: boolean;
|
||||||
compareMethod?: string;
|
compareMethod?: string;
|
||||||
@@ -1102,7 +1113,7 @@ export function ReviewPointsList({
|
|||||||
for (const item of chain) {
|
for (const item of chain) {
|
||||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||||
hasPage = true;
|
hasPage = true;
|
||||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page));
|
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1116,7 +1127,7 @@ export function ReviewPointsList({
|
|||||||
// 遍历chain找到第一个有效的page
|
// 遍历chain找到第一个有效的page
|
||||||
for (const item of chain) {
|
for (const item of chain) {
|
||||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page));
|
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1152,11 +1163,11 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (item.data.page) {
|
if (item.data.page) {
|
||||||
// console.log('currentitem-------', reviewPoint);
|
console.log('点击了长链条评查点', item.data.char_positions);
|
||||||
// 假设onReviewPointSelect在作用域内可用
|
// 假设onReviewPointSelect在作用域内可用
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, Number(item.data.page));
|
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
||||||
@@ -1237,9 +1248,10 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (chain[0].data.page) {
|
if (chain[0].data.page) {
|
||||||
|
console.log('点击了短链1左', chain[0].data.char_positions)
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, chain[0].data.page);
|
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
||||||
@@ -1263,9 +1275,10 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (chain[1].data.page) {
|
if (chain[1].data.page) {
|
||||||
|
console.log('点击了短链2右', chain[1].data.char_positions)
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, chain[1].data.page);
|
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
||||||
@@ -1343,6 +1356,7 @@ export function ReviewPointsList({
|
|||||||
res: boolean;
|
res: boolean;
|
||||||
page?: number | string;
|
page?: number | string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
char_positions?: CharPosition[];
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1404,7 +1418,8 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page));
|
console.log("点击了其他评查点", mainTypeValue)
|
||||||
|
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]));
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]));
|
||||||
}else{
|
}else{
|
||||||
@@ -1415,7 +1430,7 @@ export function ReviewPointsList({
|
|||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page));
|
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1483,6 +1498,7 @@ export function ReviewPointsList({
|
|||||||
fields?: Record<string, {
|
fields?: Record<string, {
|
||||||
page: number | string;
|
page: number | string;
|
||||||
value: string;
|
value: string;
|
||||||
|
char_positions?: CharPosition[];
|
||||||
}>;
|
}>;
|
||||||
message?: string;
|
message?: string;
|
||||||
res?: boolean;
|
res?: boolean;
|
||||||
@@ -1525,7 +1541,8 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(value.page));
|
console.log("点击了大模型的评查点", value.char_positions)
|
||||||
|
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
|
||||||
}else{
|
}else{
|
||||||
@@ -1537,7 +1554,7 @@ export function ReviewPointsList({
|
|||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(value.page));
|
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
|
||||||
}else{
|
}else{
|
||||||
@@ -1652,6 +1669,7 @@ export function ReviewPointsList({
|
|||||||
interface RuleFieldValue {
|
interface RuleFieldValue {
|
||||||
page?: number | string;
|
page?: number | string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
char_positions?: CharPosition[];
|
||||||
type: Record<string, boolean>;
|
type: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1670,7 +1688,7 @@ export function ReviewPointsList({
|
|||||||
// 使用类型断言获取config对象的具体结构
|
// 使用类型断言获取config对象的具体结构
|
||||||
const config = rule.config as {
|
const config = rule.config as {
|
||||||
res: boolean;
|
res: boolean;
|
||||||
fields: Record<string, { page: number; value: string }>;
|
fields: Record<string, { page: number; value: string; char_positions?: CharPosition[] }>;
|
||||||
logic?: string;
|
logic?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1717,7 +1735,7 @@ export function ReviewPointsList({
|
|||||||
// 使用类型断言获取config对象的具体结构
|
// 使用类型断言获取config对象的具体结构
|
||||||
const config = rule.config as {
|
const config = rule.config as {
|
||||||
res: boolean;
|
res: boolean;
|
||||||
field: Record<string, { page: string | number; value: string }>;
|
field: Record<string, { page: string | number; value: string; char_positions?: CharPosition[] }>;
|
||||||
formatType?: string;
|
formatType?: string;
|
||||||
parameters?: string;
|
parameters?: string;
|
||||||
};
|
};
|
||||||
@@ -1751,7 +1769,7 @@ export function ReviewPointsList({
|
|||||||
logic: string;
|
logic: string;
|
||||||
res: boolean;
|
res: boolean;
|
||||||
conditions: Array<{
|
conditions: Array<{
|
||||||
field: Record<string, { page: number | string; value: string }>;
|
field: Record<string, { page: number | string; value: string; char_positions?: CharPosition[] }>;
|
||||||
value: string;
|
value: string;
|
||||||
operator: string;
|
operator: string;
|
||||||
res: boolean;
|
res: boolean;
|
||||||
@@ -1786,7 +1804,7 @@ export function ReviewPointsList({
|
|||||||
// 使用类型断言获取config对象的具体结构
|
// 使用类型断言获取config对象的具体结构
|
||||||
const config = rule.config as {
|
const config = rule.config as {
|
||||||
res: boolean;
|
res: boolean;
|
||||||
field: Record<string, { page: number | string; value: string }>;
|
field: Record<string, { page: number | string; value: string; char_positions?: CharPosition[] }>;
|
||||||
pattern?: string;
|
pattern?: string;
|
||||||
matchType?: string;
|
matchType?: string;
|
||||||
selectedFields?: string[];
|
selectedFields?: string[];
|
||||||
@@ -1821,6 +1839,7 @@ export function ReviewPointsList({
|
|||||||
res: boolean;
|
res: boolean;
|
||||||
page?: number | string;
|
page?: number | string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
char_positions?: CharPosition[];
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
}> = [];
|
}> = [];
|
||||||
@@ -1833,6 +1852,7 @@ export function ReviewPointsList({
|
|||||||
res: boolean;
|
res: boolean;
|
||||||
page?: number | string;
|
page?: number | string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
char_positions?: CharPosition[];
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
}> = {};
|
}> = {};
|
||||||
@@ -1844,9 +1864,10 @@ export function ReviewPointsList({
|
|||||||
const typeKey = Object.keys(fieldValue.type)[0]; // 获取类型名称(exists/logic/regex/format)
|
const typeKey = Object.keys(fieldValue.type)[0]; // 获取类型名称(exists/logic/regex/format)
|
||||||
const typeValue = fieldValue.type[typeKey]; // 获取类型值(true/false)
|
const typeValue = fieldValue.type[typeKey]; // 获取类型值(true/false)
|
||||||
|
|
||||||
// 提取页码和值
|
// 提取页码、值和字符位置
|
||||||
const page = fieldValue.page;
|
const page = fieldValue.page;
|
||||||
const value = fieldValue.value;
|
const value = fieldValue.value;
|
||||||
|
const char_positions = fieldValue.char_positions;
|
||||||
|
|
||||||
// 如果是第一次遇到这个fieldKey,创建新条目
|
// 如果是第一次遇到这个fieldKey,创建新条目
|
||||||
if (!fieldKeyMap[fieldKey]) {
|
if (!fieldKeyMap[fieldKey]) {
|
||||||
@@ -1863,7 +1884,8 @@ export function ReviewPointsList({
|
|||||||
fieldKeyMap[fieldKey].fieldValue.type[typeKey] = {
|
fieldKeyMap[fieldKey].fieldValue.type[typeKey] = {
|
||||||
res: typeValue,
|
res: typeValue,
|
||||||
page,
|
page,
|
||||||
value
|
value,
|
||||||
|
char_positions
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2270,7 +2292,7 @@ export function ReviewPointsList({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ userSelect: 'text' }}
|
style={{ userSelect: 'text' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// console.log('reviewPoint', reviewPoint);
|
console.log('reviewPoint', reviewPoint);
|
||||||
handleReviewPointClick(reviewPoint.id);
|
handleReviewPointClick(reviewPoint.id);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ export default function ReviewDetails() {
|
|||||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||||
const [templateTargetPage, setTemplateTargetPage] = useState<number | undefined>(undefined);
|
const [templateTargetPage, setTemplateTargetPage] = useState<number | undefined>(undefined);
|
||||||
|
const [charPositions, setCharPositions] = useState<Array<{ box: number[][], char: string, score: number }> | undefined>(undefined);
|
||||||
const [pendingUpdate, setPendingUpdate] = useState<{
|
const [pendingUpdate, setPendingUpdate] = useState<{
|
||||||
reviewPointResultId: string;
|
reviewPointResultId: string;
|
||||||
newStatus: string;
|
newStatus: string;
|
||||||
@@ -367,19 +368,22 @@ export default function ReviewDetails() {
|
|||||||
setActiveTab(tabKey);
|
setActiveTab(tabKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReviewPointSelect = (reviewPointId: string, page?: number) => {
|
const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>) => {
|
||||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||||
if (reviewPointId === activeReviewPointResultId && page) {
|
if (reviewPointId === activeReviewPointResultId && page) {
|
||||||
setTargetPage(undefined);
|
setTargetPage(undefined);
|
||||||
// 使用setTimeout确保状态更新后再设置新的targetPage
|
setCharPositions(undefined);
|
||||||
|
// 使用setTimeout确保状态更新后再设置新的targetPage和charPositions
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setActiveReviewPointResultId(reviewPointId);
|
setActiveReviewPointResultId(reviewPointId);
|
||||||
setTargetPage(page);
|
setTargetPage(page);
|
||||||
|
setCharPositions(charPos);
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
// 正常设置activeReviewPointId和targetPage
|
// 正常设置activeReviewPointId、targetPage和charPositions
|
||||||
setActiveReviewPointResultId(reviewPointId);
|
setActiveReviewPointResultId(reviewPointId);
|
||||||
setTargetPage(page);
|
setTargetPage(page);
|
||||||
|
setCharPositions(charPos);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -718,6 +722,7 @@ export default function ReviewDetails() {
|
|||||||
reviewPoints={reviewData.reviewPoints}
|
reviewPoints={reviewData.reviewPoints}
|
||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
|
charPositions={charPositions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -744,6 +749,7 @@ export default function ReviewDetails() {
|
|||||||
reviewPoints={reviewData.reviewPoints}
|
reviewPoints={reviewData.reviewPoints}
|
||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
|
charPositions={charPositions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user