Merge branch 'shiy-login' into Wren

This commit is contained in:
2025-11-25 20:59:59 +08:00
24 changed files with 2559 additions and 1633 deletions
+200 -91
View File
@@ -13,6 +13,7 @@ import { useState, useRef, useEffect } from "react";
import { DiffEditor } from "@monaco-editor/react";
import type { editor } from "monaco-editor";
import { pdfjs } from 'react-pdf';
import mammoth from 'mammoth';
import { toastService } from '~/components/ui/Toast';
// 设置 PDF.js worker(与 pdf-demo.tsx 相同)
@@ -25,14 +26,26 @@ export const meta: MetaFunction = () => {
];
};
// 文档类型枚举
type DocumentType = 'pdf' | 'docx' | 'unknown';
// PDF 类型枚举
type PdfType = 'text' | 'scanned' | 'unknown';
// PDF 信息接口
// PDF 信息接口(内部使用)
interface PdfInfo {
type: PdfType;
numPages: number;
textLength: number;
confidence: number;
}
// 文档信息接口
interface DocumentInfo {
fileType: DocumentType;
pdfType?: PdfType; // 只有 PDF 才有
numPages?: number; // PDF 页数
textLength: number;
confidence: number; // 文本提取置信度 (0-1)
}
@@ -120,15 +133,28 @@ export default function MonacoDemoPage() {
const [diffCount, setDiffCount] = useState<number>(0);
const [currentDiff, setCurrentDiff] = useState<number>(0);
// PDF相关状态
const [pdf1Url, setPdf1Url] = useState<string>('');
const [pdf2Url, setPdf2Url] = useState<string>('');
const [pdf1Info, setPdf1Info] = useState<PdfInfo | null>(null);
const [pdf2Info, setPdf2Info] = useState<PdfInfo | null>(null);
const [isLoadingPdf1, setIsLoadingPdf1] = useState(false);
const [isLoadingPdf2, setIsLoadingPdf2] = useState(false);
// 文档相关状态
// 默认使用的测试文档路径(相对路径)
// 注意:文件名中使用的是中文全角括号()而不是英文半角括号()
const DEFAULT_DOC1_URL = '/testWork/(最终版)智慧法务平台建设采购项目合同(1).docx';
const DEFAULT_DOC2_URL = '/testWork/(最终版)智慧法务平台建设采购项目合同(2).docx';
const [doc1Url, setDoc1Url] = useState<string>('');
const [doc2Url, setDoc2Url] = useState<string>('');
const [doc1Info, setDoc1Info] = useState<DocumentInfo | null>(null);
const [doc2Info, setDoc2Info] = useState<DocumentInfo | null>(null);
const [isLoadingDoc1, setIsLoadingDoc1] = useState(false);
const [isLoadingDoc2, setIsLoadingDoc2] = useState(false);
const [useExample, setUseExample] = useState(true);
// 检测文件类型(根据文件路径)
const detectFileType = (filePath: string): DocumentType => {
const lowerPath = filePath.toLowerCase();
if (lowerPath.endsWith('.pdf')) return 'pdf';
if (lowerPath.endsWith('.docx') || lowerPath.endsWith('.doc')) return 'docx';
return 'unknown';
};
// PDF类型检测函数
const detectPdfType = async (pdfUrl: string): Promise<PdfInfo> => {
const loadingTask = pdfjs.getDocument(pdfUrl);
@@ -189,32 +215,82 @@ export default function MonacoDemoPage() {
return fullText;
};
// 加载PDF并提取文本
const loadPdfAndExtractText = async (pdfUrl: string, setPdfInfo: (info: PdfInfo | null) => void, setLoading: (loading: boolean) => void, setTextContent: (text: string) => void) => {
// Word文档文本提取函数
const extractTextFromWord = async (docUrl: string): Promise<string> => {
// 通过 fetch 获取文件
const response = await fetch(docUrl);
if (!response.ok) {
throw new Error(`无法加载文档: ${response.statusText}`);
}
// 获取 ArrayBuffer
const arrayBuffer = await response.arrayBuffer();
// 使用 mammoth 提取纯文本
const textResult = await mammoth.extractRawText({ arrayBuffer });
return textResult.value;
};
// 加载文档并提取文本(支持 PDF 和 Word)
const loadDocumentAndExtractText = async (
docUrl: string,
filePath: string,
setDocInfo: (info: DocumentInfo | null) => void,
setLoading: (loading: boolean) => void,
setTextContent: (text: string) => void
) => {
try {
setLoading(true);
// 1. 检测PDF类型
const pdfInfo = await detectPdfType(pdfUrl);
setPdfInfo(pdfInfo);
// 1. 检测文件类型
const fileType = detectFileType(filePath);
// 2. 提取文本
if (pdfInfo.type === 'text') {
const text = await extractTextFromPdf(pdfUrl);
if (fileType === 'pdf') {
// PDF 处理
const pdfInfo = await detectPdfType(docUrl);
const text = await extractTextFromPdf(docUrl);
const docInfo: DocumentInfo = {
fileType: 'pdf',
pdfType: pdfInfo.type,
numPages: pdfInfo.numPages,
textLength: pdfInfo.textLength,
confidence: pdfInfo.confidence
};
setDocInfo(docInfo);
setTextContent(text);
toastService.success(`PDF加载成功!共 ${pdfInfo.numPages} 页,提取了 ${pdfInfo.textLength} 个字符`);
} else if (pdfInfo.type === 'scanned') {
toastService.warning('检测到扫描版PDF,文本提取质量可能较低');
const text = await extractTextFromPdf(pdfUrl);
if (pdfInfo.type === 'text') {
toastService.success(`PDF加载成功!共 ${pdfInfo.numPages} 页,提取了 ${pdfInfo.textLength} 个字符`);
} else if (pdfInfo.type === 'scanned') {
toastService.warning('检测到扫描版PDF,文本提取质量可能较低');
} else {
toastService.error('无法识别PDF类型,可能是图片PDF');
}
} else if (fileType === 'docx') {
// Word 处理
const text = await extractTextFromWord(docUrl);
const docInfo: DocumentInfo = {
fileType: 'docx',
textLength: text.length,
confidence: 1.0 // Word 文档文本提取置信度为 100%
};
setDocInfo(docInfo);
setTextContent(text);
toastService.success(`Word文档加载成功!提取了 ${text.length} 个字符`);
} else {
toastService.error('无法识别PDF类型,可能是图片PDF');
toastService.error('不支持的文件类型');
setTextContent('');
}
} catch (error) {
console.error('PDF加载失败:', error);
toastService.error('PDF加载失败,请检查文件路径');
setPdfInfo(null);
console.error('文档加载失败:', error);
toastService.error(`文档加载失败: ${error instanceof Error ? error.message : '未知错误'}`);
setDocInfo(null);
setTextContent('');
} finally {
setLoading(false);
@@ -281,8 +357,8 @@ export default function MonacoDemoPage() {
setModifiedText(CONTRACT_B);
setCurrentDiff(0);
setUseExample(true);
setPdf1Info(null);
setPdf2Info(null);
setDoc1Info(null);
setDoc2Info(null);
// 重新计算差异数量
setTimeout(() => {
@@ -295,39 +371,81 @@ export default function MonacoDemoPage() {
}, 100);
};
// 从URL参数加载PDF
const loadPdfsFromUrl = () => {
// 从URL参数加载文档(支持 PDF 和 Word
const loadDocumentsFromUrl = () => {
if (typeof window === 'undefined') return;
const searchParams = new URLSearchParams(window.location.search);
const pdf1Path = searchParams.get('pdf1');
const pdf2Path = searchParams.get('pdf2');
const doc1Param = searchParams.get('doc1') || searchParams.get('pdf1'); // 兼容旧参数名
const doc2Param = searchParams.get('doc2') || searchParams.get('pdf2'); // 兼容旧参数名
if (pdf1Path || pdf2Path) {
// 只有在传参时才加载文档,否则使用默认的 mock 数据(示例合同)
if (doc1Param || doc2Param) {
setUseExample(false);
if (pdf1Path) {
const fullUrl = `/api/pdf-proxy?path=${encodeURIComponent(pdf1Path)}`;
setPdf1Url(fullUrl);
loadPdfAndExtractText(fullUrl, setPdf1Info, setIsLoadingPdf1, setOriginalText);
// 文档1
if (doc1Param) {
const doc1Url = doc1Param.startsWith('/') ? doc1Param : '/' + doc1Param; // 相对路径,确保以 / 开头
setDoc1Url(doc1Url);
loadDocumentAndExtractText(doc1Url, doc1Param, setDoc1Info, setIsLoadingDoc1, setOriginalText);
}
if (pdf2Path) {
const fullUrl = `/api/pdf-proxy?path=${encodeURIComponent(pdf2Path)}`;
setPdf2Url(fullUrl);
loadPdfAndExtractText(fullUrl, setPdf2Info, setIsLoadingPdf2, setModifiedText);
// 文档2
if (doc2Param) {
const doc2Url = doc2Param.startsWith('/') ? doc2Param : '/' + doc2Param; // 相对路径,确保以 / 开头
setDoc2Url(doc2Url);
loadDocumentAndExtractText(doc2Url, doc2Param, setDoc2Info, setIsLoadingDoc2, setModifiedText);
}
}
// 如果没有传参,则保持 useExample=true,显示默认的 CONTRACT_A 和 CONTRACT_B
};
// 组件挂载时读取URL参数
useEffect(() => {
loadPdfsFromUrl();
loadDocumentsFromUrl();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 监听文本变化,重新计算差异数量
useEffect(() => {
// 延迟一点确保编辑器已经渲染完成
const timer = setTimeout(() => {
if (diffEditorRef.current) {
const lineChanges = diffEditorRef.current.getLineChanges();
if (lineChanges) {
setDiffCount(lineChanges.length);
console.log(`✅ 重新计算差异: 发现 ${lineChanges.length} 处差异`);
}
}
}, 100);
return () => clearTimeout(timer);
}, [originalText, modifiedText]); // 当文本内容变化时重新计算
return (
<div className="monaco-demo-page" style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* 字符级差异高亮样式 - 加深高亮颜色 */}
<style>{`
/* 修改后的文本(绿色背景)- 字符级差异 */
.monaco-diff-editor .char-insert {
background-color: rgba(100, 150, 50, 0.6) !important;
}
/* 删除的文本(红色背景)- 字符级差异 */
.monaco-diff-editor .char-delete {
background-color: rgba(200, 50, 50, 0.5) !important;
}
/* 内联文本差异 */
.monaco-diff-editor .line-insert .char-insert {
background-color: rgba(100, 150, 50, 0.7) !important;
}
.monaco-diff-editor .line-delete .char-delete {
background-color: rgba(200, 50, 50, 0.6) !important;
}
`}</style>
{/* 页面头部 */}
<div style={{
padding: '16px 24px',
@@ -429,31 +547,11 @@ export default function MonacoDemoPage() {
<i className="ri-refresh-line"></i>
</button>
{/* 未来扩展:上传按钮 */}
<button
disabled
style={{
padding: '6px 12px',
backgroundColor: '#e0e0e0',
border: '1px solid #d0d0d0',
borderRadius: '4px',
cursor: 'not-allowed',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '6px',
opacity: 0.5
}}
>
<i className="ri-upload-2-line"></i>
</button>
</div>
</div>
{/* PDF加载信息 */}
{!useExample && (pdf1Info || pdf2Info || isLoadingPdf1 || isLoadingPdf2) && (
{/* 文档加载信息 */}
{!useExample && (doc1Info || doc2Info || isLoadingDoc1 || isLoadingDoc2) && 2==1 && (
<div style={{
padding: '12px 24px',
backgroundColor: '#fff3cd',
@@ -462,48 +560,58 @@ export default function MonacoDemoPage() {
color: '#856404'
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '24px' }}>
<i className="ri-file-pdf-line" style={{ fontSize: '18px', marginTop: '2px' }}></i>
<i className="ri-file-text-line" style={{ fontSize: '18px', marginTop: '2px' }}></i>
<div style={{ flex: 1 }}>
<strong>PDF文档信息</strong>
<strong></strong>
<div style={{ display: 'flex', gap: '24px', marginTop: '8px' }}>
{/* PDF 1 信息 */}
{/* 文档 1 信息 */}
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>📄 1/</div>
{isLoadingPdf1 ? (
{isLoadingDoc1 ? (
<div style={{ color: '#666' }}> ...</div>
) : pdf1Info ? (
) : doc1Info ? (
<div>
<div>: <span style={{
color: pdf1Info.type === 'text' ? '#28a745' : pdf1Info.type === 'scanned' ? '#ffc107' : '#dc3545',
color: doc1Info.fileType === 'pdf' ? '#007bff' : doc1Info.fileType === 'docx' ? '#28a745' : '#dc3545',
fontWeight: 'bold'
}}>
{pdf1Info.type === 'text' ? '✅ 文本PDF' : pdf1Info.type === 'scanned' ? '⚠️ 扫描PDF' : '❌ 未知类型'}
{doc1Info.fileType === 'pdf' ? '📕 PDF文档' : doc1Info.fileType === 'docx' ? '📘 Word文档' : '❌ 未知类型'}
</span></div>
<div>: {pdf1Info.numPages} </div>
<div>: {pdf1Info.textLength} </div>
<div>: {(pdf1Info.confidence * 100).toFixed(0)}%</div>
{doc1Info.fileType === 'pdf' && doc1Info.numPages && (
<div>: {doc1Info.numPages} </div>
)}
{doc1Info.fileType === 'pdf' && doc1Info.pdfType && (
<div>PDF类型: {doc1Info.pdfType === 'text' ? '✅ 文本' : doc1Info.pdfType === 'scanned' ? '⚠️ 扫描' : '❌ 未知'}</div>
)}
<div>: {doc1Info.textLength} </div>
<div>: {(doc1Info.confidence * 100).toFixed(0)}%</div>
</div>
) : (
<div style={{ color: '#999' }}></div>
)}
</div>
{/* PDF 2 信息 */}
{/* 文档 2 信息 */}
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>📄 2/</div>
{isLoadingPdf2 ? (
{isLoadingDoc2 ? (
<div style={{ color: '#666' }}> ...</div>
) : pdf2Info ? (
) : doc2Info ? (
<div>
<div>: <span style={{
color: pdf2Info.type === 'text' ? '#28a745' : pdf2Info.type === 'scanned' ? '#ffc107' : '#dc3545',
color: doc2Info.fileType === 'pdf' ? '#007bff' : doc2Info.fileType === 'docx' ? '#28a745' : '#dc3545',
fontWeight: 'bold'
}}>
{pdf2Info.type === 'text' ? '✅ 文本PDF' : pdf2Info.type === 'scanned' ? '⚠️ 扫描PDF' : '❌ 未知类型'}
{doc2Info.fileType === 'pdf' ? '📕 PDF文档' : doc2Info.fileType === 'docx' ? '📘 Word文档' : '❌ 未知类型'}
</span></div>
<div>: {pdf2Info.numPages} </div>
<div>: {pdf2Info.textLength} </div>
<div>: {(pdf2Info.confidence * 100).toFixed(0)}%</div>
{doc2Info.fileType === 'pdf' && doc2Info.numPages && (
<div>: {doc2Info.numPages} </div>
)}
{doc2Info.fileType === 'pdf' && doc2Info.pdfType && (
<div>PDF类型: {doc2Info.pdfType === 'text' ? '✅ 文本' : doc2Info.pdfType === 'scanned' ? '⚠️ 扫描' : '❌ 未知'}</div>
)}
<div>: {doc2Info.textLength} </div>
<div>: {(doc2Info.confidence * 100).toFixed(0)}%</div>
</div>
) : (
<div style={{ color: '#999' }}></div>
@@ -528,16 +636,16 @@ export default function MonacoDemoPage() {
<div style={{ flex: 1 }}>
<strong></strong>
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
<li><span style={{ color: '#28a745', fontWeight: 'bold' }}>绿</span></li>
<li><span style={{ color: '#dc3545', fontWeight: 'bold' }}></span></li>
<li><span style={{ color: '#ffc107', fontWeight: 'bold' }}></span></li>
<li><span style={{ color: '#dc3545', fontWeight: 'bold' }}></span></li>
<li><span style={{ color: '#28a745', fontWeight: 'bold' }}>绿</span></li>
<li><span style={{ color: '#666', fontWeight: 'bold' }}></span></li>
</ul>
{useExample && (
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid #b3d9ff' }}>
<strong>💡 使</strong>
<div style={{ marginTop: '4px' }}>
URL参数加载PDF文档进行对比
URL参数加载文档进行对比 PDF Word使
<code style={{
display: 'block',
marginTop: '4px',
@@ -547,10 +655,11 @@ export default function MonacoDemoPage() {
fontSize: '12px',
wordBreak: 'break-all'
}}>
/monaco-demo?pdf1=1&pdf2=2
/monaco-demo?doc1=1&doc2=2
</code>
<div style={{ marginTop: '4px', fontSize: '12px' }}>
: <code>/monaco-demo?pdf1=documents/contract_v1.pdf&pdf2=documents/contract_v2.pdf</code>
<div>Word示例: <code>/monaco-demo?doc1=testWork/(1).docx&doc2=testWork/(2).docx</code></div>
<div style={{ marginTop: '2px' }}>PDF示例: <code>/monaco-demo?doc1=testPDF/sample1.pdf&doc2=testPDF/sample2.pdf</code></div>
</div>
</div>
</div>
@@ -591,8 +700,8 @@ export default function MonacoDemoPage() {
}}
/>
{/* PDF加载中的遮罩层 */}
{(isLoadingPdf1 || isLoadingPdf2) && (
{/* 文档加载中的遮罩层 */}
{(isLoadingDoc1 || isLoadingDoc2) && (
<div style={{
position: 'absolute',
top: 0,
@@ -616,10 +725,10 @@ export default function MonacoDemoPage() {
margin: '0 auto 16px'
}}></div>
<div style={{ fontSize: '16px', color: '#333' }}>
PDF文档并提取文本...
...
</div>
{isLoadingPdf1 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 1</div>}
{isLoadingPdf2 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 2</div>}
{isLoadingDoc1 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 1</div>}
{isLoadingDoc2 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 2</div>}
</div>
</div>
)}
+13 -72
View File
@@ -39,7 +39,7 @@ interface HighlightArea {
// 基于坐标的字符数据
interface CharacterBox {
box: [number, number][]; // 4个点:左上、右上、右下、左下
box: [number, number][];
char: string;
page: number;
}
@@ -70,7 +70,7 @@ export default function PdfDemo() {
// PDF文件URL(使用示例PDF
// const [pdfUrl] = useState('/testPDF/sample.pdf'); // 使用包含真实文本层的PDF
// const [pdfUrl] = useState('/api/pdf-proxy?path=documents/mz/行政处罚决定书/2025/11月13日/第71号--未在当地烟草专卖批发企业进货_02时58分36秒/第71号--未在当地烟草专卖批发企业进货.pdf'); // 使用项目中的示例PDF
const [pdfUrl] = useState('/api/pdf-proxy?path=documents/mz/行政处罚决定书/2025/11月22日/第35号--无烟草专卖品准运证运输烟草专卖品_15时15分24秒/第35号--无烟草专卖品准运证运输烟草专卖品.pdf')
const [pdfUrl] = useState('/api/pdf-proxy?path=documents/mz/测试示范类型/2025/11月24日/第37号--涉嫌生产、销售伪劣产品罪_12时19分10秒/第37号--涉嫌生产、销售伪劣产品罪.pdf')
// PDF状态
const [numPages, setNumPages] = useState<number | null>(null);
@@ -227,87 +227,28 @@ export default function PdfDemo() {
// 获取Page容器(SVG实际渲染的坐标空间)
const pageContainer = canvas?.closest('.react-pdf__Page') as HTMLElement;
if (canvas && pageContainer && pdfOriginalWidthPt) {
// Canvas 内部绘制尺寸(考虑了 devicePixelRatio
const canvasInternalWidth = canvas.width;
const canvasInternalHeight = canvas.height;
if (canvas && pdfOriginalWidthPt) {
// Canvas 显示尺寸(浏览器中实际占用的像素)
const canvasDisplayWidth = canvas.offsetWidth;
const canvasDisplayHeight = canvas.offsetHeight;
// Page容器尺寸(SVG高亮渲染的实际坐标空间)
const pageContainerWidth = pageContainer.offsetWidth;
const pageContainerHeight = pageContainer.offsetHeight;
// 计算坐标缩放比例:Canvas显示尺寸 / PDF原始尺寸
const autoScale = canvasDisplayWidth / pdfOriginalWidthPt;
// 尝试多种计算方式
const scale1_canvasDisplay = canvasDisplayWidth / pdfOriginalWidthPt;
const scale2_canvasInternal = canvasInternalWidth / pdfOriginalWidthPt;
const scale3_pageContainer = pageContainerWidth / pdfOriginalWidthPt;
// 尝试反向计算:如果OCR尺寸比渲染尺寸大(需要缩小)
const scale4_inverseCanvasInternal = canvasDisplayWidth / canvasInternalWidth;
const scale5_inversePage = canvasDisplayWidth / pageContainerWidth;
// 计算如果要达到 0.83 的缩放比例,OCR原始尺寸应该是多少
const expectedOcrWidth = canvasDisplayWidth / 0.83;
console.log('📏 尺寸信息汇总:');
console.log(' 1️⃣ PDF原始尺寸 (page.view):', pdfOriginalWidthPt, 'x', pdfOriginalHeightPt, 'pt');
console.log(' 2️⃣ Page容器尺寸:', pageContainerWidth, 'x', pageContainerHeight, 'px');
console.log(' 3️⃣ Canvas显示尺寸:', canvasDisplayWidth, 'x', canvasDisplayHeight, 'px');
console.log(' 4️⃣ Canvas内部尺寸:', canvasInternalWidth, 'x', canvasInternalHeight, 'px');
console.log(' 5️⃣ 用户缩放 (scale):', scale);
console.log(' 6️⃣ devicePixelRatio:', window.devicePixelRatio || 1);
console.log('');
console.log('🎯 各种计算方式:');
console.log(' 方案1️⃣: Canvas显示 / PDF原始 =', scale1_canvasDisplay.toFixed(3), 'x');
console.log(' 方案2️⃣: Canvas内部 / PDF原始 =', scale2_canvasInternal.toFixed(3), 'x');
console.log(' 方案3️⃣: Page容器 / PDF原始 =', scale3_pageContainer.toFixed(3), 'x');
console.log(' 方案4️⃣: Canvas显示 / Canvas内部 =', scale4_inverseCanvasInternal.toFixed(3), 'x ⬅ 可能是这个!');
console.log(' 方案5️⃣: Canvas显示 / Page容器 =', scale5_inversePage.toFixed(3), 'x');
console.log('');
console.log('🔍 目标值分析:');
console.log(' - 手动校准的正确值: 0.83');
console.log(' - 反推OCR图像尺寸:', expectedOcrWidth.toFixed(0), 'x', (canvasDisplayHeight / 0.83).toFixed(0), 'px');
console.log(' - 比较: ', expectedOcrWidth.toFixed(0), 'vs Canvas内部', canvasInternalWidth);
// 使用最接近0.83的方案
let autoScale = scale1_canvasDisplay;
let scaleMethod = '方案1 (Canvas显示/PDF原始)';
// 检查哪个方案最接近0.83
const diff1 = Math.abs(scale1_canvasDisplay - 0.83);
const diff2 = Math.abs(scale2_canvasInternal - 0.83);
const diff3 = Math.abs(scale3_pageContainer - 0.83);
const diff4 = Math.abs(scale4_inverseCanvasInternal - 0.83);
const diff5 = Math.abs(scale5_inversePage - 0.83);
const minDiff = Math.min(diff1, diff2, diff3, diff4, diff5);
if (minDiff === diff4) {
autoScale = scale4_inverseCanvasInternal;
scaleMethod = '方案4 (Canvas显示/Canvas内部)';
} else if (minDiff === diff5) {
autoScale = scale5_inversePage;
scaleMethod = '方案5 (Canvas显示/Page容器)';
} else if (minDiff === diff2) {
autoScale = scale2_canvasInternal;
scaleMethod = '方案2 (Canvas内部/PDF原始)';
} else if (minDiff === diff3) {
autoScale = scale3_pageContainer;
scaleMethod = '方案3 (Page容器/PDF原始)';
}
console.log('');
console.log('✅ 自动选择:', scaleMethod, '=', autoScale.toFixed(3), 'x (最接近0.83)');
console.log('📏 PDF尺寸信息:');
console.log(' - PDF原始尺寸 (page.view):', pdfOriginalWidthPt, 'x', pdfOriginalHeightPt, 'pt');
console.log(' - Canvas显示尺寸 (offsetWidth):', canvasDisplayWidth, 'x', canvasDisplayHeight, 'px');
console.log(' - 用户缩放 (scale):', scale);
console.log(' - devicePixelRatio:', window.devicePixelRatio || 1);
console.log('🎯 自动计算坐标缩放:', autoScale.toFixed(3), 'x');
console.log(' 公式: Canvas显示宽度 / PDF原始宽度 =', canvasDisplayWidth, '/', pdfOriginalWidthPt);
// 保存原始宽度和自动计算的缩放比例
setPdfOriginalWidth(pdfOriginalWidthPt);
setCoordinateScale(autoScale);
setIsScaleAutoCalculated(true);
toastService.success(`自动校准完成: ${autoScale.toFixed(3)}x (${scaleMethod})`);
toastService.success(`自动校准完成: ${autoScale.toFixed(3)}x`);
} else {
console.warn('⚠️ 无法获取Canvas元素、Page容器或原始尺寸');
console.log('调试信息:', {
+53 -21
View File
@@ -178,8 +178,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const id = url.searchParams.get('id') || undefined;
const previousRoute = url.searchParams.get('previousRoute') || '';
// console.log("id-------",id);
// console.log("[Reviews Loader] 开始加载,id:", id, "previousRoute:", previousRoute);
if (!id) {
console.error("[Reviews Loader] 文件ID不能为空");
return Response.json({ result: false, message: '文件ID不能为空' });
}
@@ -190,16 +192,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 获取评查点数据,传递request对象
const reviewData = await getReviewPoints(id, request);
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
if ('error' in reviewData && reviewData.error) {
console.error("获取评查点数据错误:", reviewData.error);
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
return Response.json({ result: false, message: reviewData.error });
}
// 确保reviewData有效且具有预期的属性
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
// console.log("reviewData-------",JSON.stringify(reviewData.data));
return Response.json({
previousRoute: previousRoute,
document: reviewData.document,
@@ -211,12 +210,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
frontendJWT
});
} else {
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
console.error("[Reviews Loader] 返回的评查数据格式不正确,完整数据:", JSON.stringify(reviewData, null, 2));
return Response.json({ result: false, message: '返回的评查数据格式不正确' });
}
} catch (error) {
console.error('获取评查数据失败:', error);
return Response.json({ result: false, message: '获取评查数据失败' });
console.error('[Reviews Loader] 获取评查数据失败:', error);
console.error('[Reviews Loader] 错误堆栈:', error instanceof Error ? error.stack : '无堆栈信息');
return Response.json({ result: false, message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}` });
}
}
@@ -226,7 +226,7 @@ export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent") as string;
console.log('Action接收到请求, intent:', intent);
// console.log('Action接收到请求, intent:', intent);
if (intent === "updateReviewResult") {
const reviewPointResultId = formData.get("reviewPointResultId") as string;
@@ -292,23 +292,39 @@ export async function action({ request }: ActionFunctionArgs) {
export default function ReviewDetails() {
const navigate = useNavigate();
const loaderData = useLoaderData<typeof loader>();
// 调试:查看loaderData内容 - 强制刷新
console.log('[Reviews Component] loaderData keys:', Object.keys(loaderData));
console.log('[Reviews Component] loaderData:', loaderData);
const fetcher = useFetcher();
const { document, reviewPoints, statistics, reviewInfo, comparison_document, frontendJWT } = loaderData;
// 调试:查看解构后的数据
console.log('[Reviews Component] 解构后的document:', document);
console.log('[Reviews Component] 解构后的reviewPoints length:', reviewPoints?.length);
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
const [targetPage, setTargetPage] = 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<{
reviewPointResultId: string;
newStatus: string;
message: string;
} | null>(null);
// loader 数据加载出错
useEffect(()=>{
loadingBarService.hide();
console.log('[Reviews Component] useEffect检查loaderData:', {
hasResultKey: Object.keys(loaderData).find(key => key === 'result'),
resultValue: loaderData.result,
willNavigateBack: Object.keys(loaderData).find(key => key === 'result') && !loaderData.result
});
if(Object.keys(loaderData).find(key => key === 'result') && !loaderData.result){
messageService.show({
title: '错误',
@@ -367,19 +383,22 @@ export default function ReviewDetails() {
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能够触发
if (reviewPointId === activeReviewPointResultId && page) {
setTargetPage(undefined);
// 使用setTimeout确保状态更新后再设置新的targetPage
setCharPositions(undefined);
// 使用setTimeout确保状态更新后再设置新的targetPage和charPositions
setTimeout(() => {
setActiveReviewPointResultId(reviewPointId);
setTargetPage(page);
setCharPositions(charPos);
}, 0);
} else {
// 正常设置activeReviewPointIdtargetPage
// 正常设置activeReviewPointIdtargetPage和charPositions
setActiveReviewPointResultId(reviewPointId);
setTargetPage(page);
setCharPositions(charPos);
}
};
@@ -713,14 +732,26 @@ export default function ReviewDetails() {
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
{/* 左侧:文件预览 */}
<div className="w-full lg:w-[65%]">
<FilePreview
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
/>
{(() => {
console.log('[Reviews] 准备渲染FilePreview', {
hasDocument: !!document,
documentPath: document?.path,
targetPage,
hasCharPositions: !!charPositions,
charPositionsLength: charPositions?.length
});
return (
<FilePreview
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
charPositions={charPositions}
/>
);
})()}
</div>
{/* 右侧:评查结果 */}
<div className="w-full lg:w-[35%]">
<ReviewPointsList
@@ -739,11 +770,12 @@ export default function ReviewDetails() {
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
{/* 左侧:原文件预览 */}
<div className={`w-full ${comparison_document.template_contract_path ? 'lg:w-[38%]' : 'lg:w-[56%]'}`}>
<FilePreview
<FilePreview
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
charPositions={charPositions}
/>
</div>