From 93bae2de1707e11cd9b877df2d9618af6c16aeb8 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 24 Nov 2025 19:46:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=201.=20=E4=BF=AE=E5=A4=8D=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E7=A4=BA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/monaco-demo.tsx | 282 ++++++++++++++++++++++++- app/routes/role-permissions._index.tsx | 71 ++----- 2 files changed, 303 insertions(+), 50 deletions(-) diff --git a/app/routes/monaco-demo.tsx b/app/routes/monaco-demo.tsx index 8aaa4c6..3dc6ea8 100644 --- a/app/routes/monaco-demo.tsx +++ b/app/routes/monaco-demo.tsx @@ -9,9 +9,14 @@ */ import { type MetaFunction } from "@remix-run/node"; -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { DiffEditor } from "@monaco-editor/react"; import type { editor } from "monaco-editor"; +import { pdfjs } from 'react-pdf'; +import { toastService } from '~/components/ui/Toast'; + +// 设置 PDF.js worker(与 pdf-demo.tsx 相同) +pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js'; export const meta: MetaFunction = () => { return [ @@ -20,6 +25,17 @@ export const meta: MetaFunction = () => { ]; }; +// PDF 类型枚举 +type PdfType = 'text' | 'scanned' | 'unknown'; + +// PDF 信息接口 +interface PdfInfo { + type: PdfType; + numPages: number; + textLength: number; + confidence: number; // 文本提取置信度 (0-1) +} + // 示例合同文本 A(原始版本) const CONTRACT_A = `中国烟草合同(原始版本) @@ -104,6 +120,107 @@ export default function MonacoDemoPage() { const [diffCount, setDiffCount] = useState(0); const [currentDiff, setCurrentDiff] = useState(0); + // PDF相关状态 + const [pdf1Url, setPdf1Url] = useState(''); + const [pdf2Url, setPdf2Url] = useState(''); + const [pdf1Info, setPdf1Info] = useState(null); + const [pdf2Info, setPdf2Info] = useState(null); + const [isLoadingPdf1, setIsLoadingPdf1] = useState(false); + const [isLoadingPdf2, setIsLoadingPdf2] = useState(false); + const [useExample, setUseExample] = useState(true); + + // PDF类型检测函数 + const detectPdfType = async (pdfUrl: string): Promise => { + const loadingTask = pdfjs.getDocument(pdfUrl); + const pdf = await loadingTask.promise; + + let totalTextLength = 0; + const pagesToCheck = Math.min(pdf.numPages, 3); // 检查前3页 + + for (let i = 1; i <= pagesToCheck; i++) { + const page = await pdf.getPage(i); + const textContent = await page.getTextContent(); + const pageText = textContent.items + .map((item: any) => item.str) + .join(''); + totalTextLength += pageText.length; + } + + // 计算平均每页文字数量 + const avgTextPerPage = totalTextLength / pagesToCheck; + + // 计算置信度(0-1) + const confidence = Math.min(avgTextPerPage / 500, 1); + + // 判断PDF类型 + let type: PdfType; + if (avgTextPerPage > 100) { + type = 'text'; + } else if (avgTextPerPage > 10) { + type = 'scanned'; + } else { + type = 'unknown'; + } + + return { + type, + numPages: pdf.numPages, + textLength: totalTextLength, + confidence + }; + }; + + // PDF文本提取函数 + const extractTextFromPdf = async (pdfUrl: string): Promise => { + const loadingTask = pdfjs.getDocument(pdfUrl); + const pdf = await loadingTask.promise; + + let fullText = ''; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const textContent = await page.getTextContent(); + const pageText = textContent.items + .map((item: any) => item.str) + .join(' '); + fullText += `\n========== 第 ${i} 页 ==========\n${pageText}\n`; + } + + return fullText; + }; + + // 加载PDF并提取文本 + const loadPdfAndExtractText = async (pdfUrl: string, setPdfInfo: (info: PdfInfo | null) => void, setLoading: (loading: boolean) => void, setTextContent: (text: string) => void) => { + try { + setLoading(true); + + // 1. 检测PDF类型 + const pdfInfo = await detectPdfType(pdfUrl); + setPdfInfo(pdfInfo); + + // 2. 提取文本 + if (pdfInfo.type === 'text') { + const text = await extractTextFromPdf(pdfUrl); + setTextContent(text); + toastService.success(`PDF加载成功!共 ${pdfInfo.numPages} 页,提取了 ${pdfInfo.textLength} 个字符`); + } else if (pdfInfo.type === 'scanned') { + toastService.warning('检测到扫描版PDF,文本提取质量可能较低'); + const text = await extractTextFromPdf(pdfUrl); + setTextContent(text); + } else { + toastService.error('无法识别PDF类型,可能是图片PDF'); + setTextContent(''); + } + } catch (error) { + console.error('PDF加载失败:', error); + toastService.error('PDF加载失败,请检查文件路径'); + setPdfInfo(null); + setTextContent(''); + } finally { + setLoading(false); + } + }; + // Monaco Editor 挂载后的回调 const handleEditorDidMount = (editor: editor.IStandaloneDiffEditor) => { diffEditorRef.current = editor; @@ -163,6 +280,9 @@ export default function MonacoDemoPage() { setOriginalText(CONTRACT_A); setModifiedText(CONTRACT_B); setCurrentDiff(0); + setUseExample(true); + setPdf1Info(null); + setPdf2Info(null); // 重新计算差异数量 setTimeout(() => { @@ -175,6 +295,37 @@ export default function MonacoDemoPage() { }, 100); }; + // 从URL参数加载PDF + const loadPdfsFromUrl = () => { + if (typeof window === 'undefined') return; + + const searchParams = new URLSearchParams(window.location.search); + const pdf1Path = searchParams.get('pdf1'); + const pdf2Path = searchParams.get('pdf2'); + + if (pdf1Path || pdf2Path) { + setUseExample(false); + + if (pdf1Path) { + const fullUrl = `/api/pdf-proxy?path=${encodeURIComponent(pdf1Path)}`; + setPdf1Url(fullUrl); + loadPdfAndExtractText(fullUrl, setPdf1Info, setIsLoadingPdf1, setOriginalText); + } + + if (pdf2Path) { + const fullUrl = `/api/pdf-proxy?path=${encodeURIComponent(pdf2Path)}`; + setPdf2Url(fullUrl); + loadPdfAndExtractText(fullUrl, setPdf2Info, setIsLoadingPdf2, setModifiedText); + } + } + }; + + // 组件挂载时读取URL参数 + useEffect(() => { + loadPdfsFromUrl(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
{/* 页面头部 */} @@ -301,6 +452,69 @@ export default function MonacoDemoPage() {
+ {/* PDF加载信息 */} + {!useExample && (pdf1Info || pdf2Info || isLoadingPdf1 || isLoadingPdf2) && ( +
+
+ +
+ PDF文档信息: +
+ {/* PDF 1 信息 */} +
+
📄 文档1(左侧/原始)
+ {isLoadingPdf1 ? ( +
⏳ 加载中...
+ ) : pdf1Info ? ( +
+
类型: + {pdf1Info.type === 'text' ? '✅ 文本PDF' : pdf1Info.type === 'scanned' ? '⚠️ 扫描PDF' : '❌ 未知类型'} +
+
页数: {pdf1Info.numPages} 页
+
字符数: {pdf1Info.textLength} 个
+
置信度: {(pdf1Info.confidence * 100).toFixed(0)}%
+
+ ) : ( +
未加载
+ )} +
+ + {/* PDF 2 信息 */} +
+
📄 文档2(右侧/修改)
+ {isLoadingPdf2 ? ( +
⏳ 加载中...
+ ) : pdf2Info ? ( +
+
类型: + {pdf2Info.type === 'text' ? '✅ 文本PDF' : pdf2Info.type === 'scanned' ? '⚠️ 扫描PDF' : '❌ 未知类型'} +
+
页数: {pdf2Info.numPages} 页
+
字符数: {pdf2Info.textLength} 个
+
置信度: {(pdf2Info.confidence * 100).toFixed(0)}%
+
+ ) : ( +
未加载
+ )} +
+
+
+
+
+ )} + {/* 说明信息 */}
-
+
差异高亮说明:
  • 绿色:新增的内容
  • 红色:删除的内容
  • 黄色背景:修改的行内差异
+ + {useExample && ( +
+ 💡 使用提示: +
+ 您可以通过URL参数加载PDF文档进行对比: + + /monaco-demo?pdf1=路径1&pdf2=路径2 + +
+ 示例: /monaco-demo?pdf1=documents/contract_v1.pdf&pdf2=documents/contract_v2.pdf +
+
+
+ )}
@@ -353,8 +590,49 @@ export default function MonacoDemoPage() { diffAlgorithm: 'advanced', // 使用高级差异算法 }} /> + + {/* PDF加载中的遮罩层 */} + {(isLoadingPdf1 || isLoadingPdf2) && ( +
+
+
+
+ 正在加载PDF文档并提取文本... +
+ {isLoadingPdf1 &&
📄 加载文档1
} + {isLoadingPdf2 &&
📄 加载文档2
} +
+
+ )}
+ {/* 添加旋转动画 */} + + {/* 页面底部信息 */}
- +
- +

- 权限不足 + 暂无数据

- 您没有访问角色权限管理的权限,需要 省级管理员 角色或 system:rbac:manage 权限 + 系统中暂无角色、路由或用户数据,请稍后重试或联系管理员