diff --git a/app/routes/rules.new1.tsx b/app/routes/rules.new1.tsx index fe3ed81..140cad5 100644 --- a/app/routes/rules.new1.tsx +++ b/app/routes/rules.new1.tsx @@ -1,40 +1,83 @@ +/** + * 文档预览与内容抽取模块 + * + * 依赖包说明: + * 1. react-pdf - PDF文档预览 + * 安装命令: npm install react-pdf + * 或: yarn add react-pdf + * + * 2. mammoth - Word文档转HTML预览 + * 安装命令: npm install mammoth + * 或: yarn add mammoth + * + * 3. @remix-run/react, @remix-run/node - Remix框架组件 + * 安装命令: npm install @remix-run/react @remix-run/node + * 或: yarn add @remix-run/react @remix-run/node + * + * 注意事项: + * - react-pdf需要pdfjs-dist作为依赖,安装react-pdf时会自动安装 + * - 需要引入PDF.js worker文件,本代码通过CDN方式引入 + * - 如需本地加载PDF.js worker文件,请安装pdfjs-dist并修改worker配置 + */ + import { useState, useEffect, useRef } from "react"; import { useLoaderData } from "@remix-run/react"; import { Document, Page, pdfjs } from "react-pdf"; import type { LoaderFunctionArgs } from "@remix-run/node"; import mammoth from "mammoth"; -// 设置 pdfjs 工作线程 +/** + * 设置 pdfjs 工作线程 + * 使用 CDN 上的 worker.js 文件处理 PDF 解析 + */ pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`; -// 模拟后端返回的抽取内容数据 +/** + * 模拟后端返回的文档抽取内容数据 + * 实际应用中应从API获取 + */ const mockExtractedContent = [ { id: 1, text: "合同条款", page: 2, position: { start: 50, end: 60 } }, { id: 2, text: "签署日期", page: 5, position: { start: 120, end: 130 } }, { id: 3, text: "责任划分", page: 3, position: { start: 80, end: 90 } }, ]; +/** + * 文档抽取内容接口定义 + */ interface ExtractedContent { - id: number; - text: string; - page: number; - position: { start: number; end: number }; + id: number; // 内容唯一标识 + text: string; // 抽取的文本内容 + page: number; // 所在页码 + position: { // 在页面中的位置信息 + start: number; + end: number; + }; } +/** + * Loader 函数返回数据接口定义 + */ interface LoaderData { - fileUrl: string; - initialPage: number; - extractedContent: ExtractedContent[]; - fileType: "pdf" | "docx"; - urls: Record; + fileUrl: string; // 当前文档URL + initialPage: number; // 初始页码 + extractedContent: ExtractedContent[]; // 抽取内容数组 + fileType: "pdf" | "docx"; // 文档类型 + urls: Record; // 可用文档URL列表 } -// 定义文档加载成功回调类型 +/** + * PDF文档加载成功回调接口 + */ interface DocumentLoadSuccess { - numPages: number; + numPages: number; // 文档总页数 } -// 根据URL判断文件类型 +/** + * 根据URL判断文件类型 + * @param url 文档URL + * @returns 文档类型:"pdf" 或 "docx" + */ function getFileTypeFromUrl(url: string): "pdf" | "docx" { const lowerCaseUrl = url.toLowerCase(); if (lowerCaseUrl.endsWith(".pdf")) { @@ -46,15 +89,15 @@ function getFileTypeFromUrl(url: string): "pdf" | "docx" { return "pdf"; } -// Remix Loader 函数 +/** + * Remix Loader 函数 - 请求处理和数据加载 + */ export const loader = async ({ request }: LoaderFunctionArgs) => { + // 从URL获取查询参数 const url = new URL(request.url); const page = url.searchParams.get("page") || 1; - // 实际文档 URL (PDF示例) - // const fileUrl = "http://172.18.0.100:9000/docauditai/documents/%E5%90%88%E5%90%8C%E6%96%87%E6%A1%A3/2025/04%E6%9C%8816%E6%97%A5/%E7%AC%AC16%E5%8F%B7--%E9%94%80%E5%94%AE%E6%97%A0%E6%A0%87%E5%BF%97%E5%A4%96%E5%9B%BD%E5%8D%B7%E7%83%9F_10%E6%97%B626%E5%88%8632%E7%A7%92/%E7%AC%AC16%E5%8F%B7--%E9%94%80%E5%94%AE%E6%97%A0%E6%A0%87%E5%BF%97%E5%A4%96%E5%9B%BD%E5%8D%B7%E7%83%9F.pdf"; - - // 示例文档URLs + // 示例文档URLs集合 const urls = { // 1. 原始文档URL - 可能有CORS限制 original: "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx", @@ -64,127 +107,108 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { proxy: "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx", // 4. 本地服务器上的文档 (假设已经部署) local: "/uploads/sample.docx", - // 5. PDF示例 (如果Word文档问题无法解决) + // 5. PDF示例 pdf: "http://172.18.0.100:9000/docauditai/documents/%E5%90%88%E5%90%8C%E6%96%87%E6%A1%A3/2025/04%E6%9C%8816%E6%97%A5/%E7%AC%AC16%E5%8F%B7--%E9%94%80%E5%94%AE%E6%97%A0%E6%A0%87%E5%BF%97%E5%A4%96%E5%9B%BD%E5%8D%B7%E7%83%9F_10%E6%97%B626%E5%88%8632%E7%A7%92/%E7%AC%AC16%E5%8F%B7--%E9%94%80%E5%94%AE%E6%97%A0%E6%A0%87%E5%BF%97%E5%A4%96%E5%9B%BD%E5%8D%B7%E7%83%9F.pdf" }; - // 使用本地文档或通过CORS代理的URL - const fileUrl = urls.public; // 可以切换到其他URL进行测试 + // 使用默认文档URL + const fileUrl = urls.pdf; // 判断文件类型 const fileType = getFileTypeFromUrl(fileUrl); + // 返回加载的数据 return { fileUrl, initialPage: Number(page), extractedContent: mockExtractedContent, fileType, - urls // 传递所有URL供前端选择 + urls }; }; +/** + * 文档预览组件 + */ export default function Documents() { + // 从loader获取数据 const { fileUrl, extractedContent, fileType, urls } = useLoaderData(); - const [numPages, setNumPages] = useState(null); - const [scrollToPage, setScrollToPage] = useState(null); - const [docxLoading, setDocxLoading] = useState(false); // 设置为false以避免加载指示器 - const [loadError, setLoadError] = useState(null); - const [debugInfo, setDebugInfo] = useState([]); - const docxContainerRef = useRef(null); - const [docxContentPositions, setDocxContentPositions] = useState<{[id: number]: number}>({}); - const [currentUrl, setCurrentUrl] = useState(fileUrl); - // 默认使用iframe模式 - const [showIframe, setShowIframe] = useState(true); - const [docxHtml, setDocxHtml] = useState(""); + + // 状态管理 + const [numPages, setNumPages] = useState(null); // PDF总页数 + const [scrollToPage, setScrollToPage] = useState(null); // 滚动目标页码 + const [docxLoading, setDocxLoading] = useState(false); // Word文档加载状态 + const [loadError, setLoadError] = useState(null); // 加载错误信息 + const [debugInfo, setDebugInfo] = useState([]); // 调试信息 + const [docxHtml, setDocxHtml] = useState(""); // 转换后的HTML内容 + const [currentUrl, setCurrentUrl] = useState(fileUrl); // 当前文档URL + + // 引用 + const docxContainerRef = useRef(null); // Word文档容器引用 - // 处理抽取内容点击 + /** + * 处理抽取内容点击事件 - 仅对PDF文档生效 + * @param item 被点击的抽取内容项 + */ const handleContentClick = (item: ExtractedContent) => { - setScrollToPage(item.page); + // 仅对PDF文档执行交互操作 if (fileType === "pdf") { - // 使用ID滚动到指定页面 + setScrollToPage(item.page); + // 对于PDF,滚动到指定页面 const pageElement = document.getElementById(`page-${item.page}`); if (pageElement) { pageElement.scrollIntoView({ behavior: 'smooth' }); } - } else if (fileType === "docx" && !showIframe) { - // 对于Word文档,滚动到提取内容位置 (仅本地渲染模式) - const position = docxContentPositions[item.id]; - if (position !== undefined && docxContainerRef.current) { - // 找到Word内容容器内的位置并滚动 - docxContainerRef.current.scrollTop = position; - - // 高亮显示这个区域(模拟) - highlightDocxContent(item); - } - } else if (fileType === "docx" && showIframe) { - // 对于iframe中的Word文档,我们只能切换到特定iframe页面 - // 这里我们无法控制iframe内部的滚动,只能提示用户 - addDebugInfo(`在iframe中无法直接定位到"${item.text}",请在文档中手动查找`); } + // DOCX文档不执行任何交互操作 }; - // 模拟在Word文档中高亮内容 - const highlightDocxContent = (item: ExtractedContent) => { - // 移除之前的高亮 - const previousHighlights = document.querySelectorAll('.docx-highlight'); - previousHighlights.forEach(el => el.classList.remove('docx-highlight')); - - // 由于我们没有确切的位置信息,这里使用一个模拟的方法 - // 实际项目中,您需要一个更精确的方法来找到文本位置 - if (docxContainerRef.current) { - const textNodes = Array.from(docxContainerRef.current.querySelectorAll('p, span, div')) - .filter(node => node.textContent?.includes(item.text)); - - textNodes.forEach(node => { - node.classList.add('docx-highlight'); - }); - } - }; - - // PDF文档加载成功回调 + /** + * PDF文档加载成功回调函数 + * @param param0 包含numPages的对象 + */ function onDocumentLoadSuccess({ numPages }: DocumentLoadSuccess) { setNumPages(numPages); console.log("PDF加载成功,页数:", numPages); } - // 简化的调试日志 + /** + * 添加调试信息 + * @param info 调试信息文本 + */ const addDebugInfo = (info: string) => { console.log(info); setDebugInfo(prev => [...prev, `${new Date().toISOString().split('T')[1].split('.')[0]}: ${info}`]); }; - // 切换到不同的文档URL + /** + * 切换文档URL + * @param urlKey URL键名 + */ const switchDocumentUrl = (urlKey: keyof typeof urls) => { setCurrentUrl(urls[urlKey]); setDebugInfo([]); setLoadError(null); setDocxLoading(false); - setShowIframe(true); addDebugInfo(`切换到新的文档URL: ${urls[urlKey]}`); }; - // 切换到iframe模式 (当直接加载文档有CORS问题时) - const switchToIframeMode = () => { - setShowIframe(true); - setDocxLoading(false); - addDebugInfo("切换到iframe嵌入模式"); - }; - - // 使用mammoth处理Word文档 + /** + * Word文档处理逻辑 + */ useEffect(() => { - if (fileType === "docx" && docxContainerRef.current && !showIframe) { + if (fileType === "docx" && docxContainerRef.current) { setDocxLoading(true); - setDebugInfo([]); // 清空之前的调试信息 + setDebugInfo([]); // 清空调试信息 addDebugInfo(`准备加载Word文档: ${currentUrl}`); const loadDocx = async () => { try { - // 获取文件 + // 1. 获取文档文件 addDebugInfo(`开始获取文件...`); let response; try { response = await fetch(currentUrl, { - // 添加CORS相关选项 mode: 'cors', credentials: 'omit', headers: { @@ -197,12 +221,13 @@ export default function Documents() { throw new Error(`网络请求失败: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`); } + // 检查响应状态 if (!response.ok) { throw new Error(`文档无法访问,状态码: ${response.status}`); } addDebugInfo(`文档下载成功,状态码: ${response.status}`); - // 转换为ArrayBuffer + // 2. 将响应转换为ArrayBuffer addDebugInfo(`开始读取响应内容为ArrayBuffer...`); let buffer; try { @@ -213,10 +238,10 @@ export default function Documents() { throw new Error(`转换文档内容失败: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`); } - // 使用mammoth.js将Word转换为HTML,添加自定义选项 + // 3. 使用mammoth.js将Word转换为HTML addDebugInfo("使用mammoth开始转换文档为HTML..."); try { - // 添加自定义样式映射 + // 自定义样式映射 const styleMap = ` p[style-name='Heading 1'] => h1:fresh p[style-name='Heading 2'] => h2:fresh @@ -225,13 +250,14 @@ export default function Documents() { table => table.docx-table `; - // 创建简化版的转换选项 + // 转换选项 const options = { arrayBuffer: buffer, styleMap: styleMap, includeDefaultStyleMap: true }; + // 执行转换 const result = await mammoth.convertToHtml(options); // 检查转换警告 @@ -243,60 +269,18 @@ export default function Documents() { addDebugInfo("文档转换成功,获取到HTML内容"); - // 为生成的HTML文档添加包装容器和样式 + // 4. 为生成的HTML添加包装容器和样式 const enhancedHtml = `
${result.value}
-

注意:本地转换使用了简化版格式,一些高级格式(如页眉页脚、复杂表格格式)可能无法完全显示。

-

如需查看完整格式,请使用"嵌入模式"或下载文档。

+

注意:部分复杂格式(如页眉页脚、复杂表格样式)可能无法完全显示。

`; - // 存储HTML内容 + // 更新状态 setDocxHtml(enhancedHtml); - - // 查找匹配的内容并创建位置映射 - setTimeout(() => { - try { - if (docxContainerRef.current) { - const positionsMap: {[id: number]: number} = {}; - - extractedContent.forEach((item) => { - // 在HTML内容中查找文本 - // 使用更安全的查询方式 - if (docxContainerRef.current) { - // 获取所有可能包含文本的元素 - const elements = docxContainerRef.current.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, span'); - - // 转为数组并过滤包含目标文本的元素 - const textElements = Array.from(elements).filter(element => - element.textContent?.includes(item.text) - ); - - if (textElements.length > 0) { - // 使用找到的第一个元素的位置 - const element = textElements[0]; - const rect = element.getBoundingClientRect(); - const containerRect = docxContainerRef.current.getBoundingClientRect(); - // 计算相对于容器的位置 - positionsMap[item.id] = rect.top - containerRect.top + docxContainerRef.current.scrollTop; - - // 标记找到的元素 - element.classList.add('docx-content-found'); - } - } - }); - - setDocxContentPositions(positionsMap); - addDebugInfo(`已创建 ${Object.keys(positionsMap).length} 个内容位置映射`); - } - } catch (positionError) { - addDebugInfo(`创建位置映射时出错: ${positionError instanceof Error ? positionError.message : String(positionError)}`); - } - }, 500); - setDocxLoading(false); } catch (mammothError) { addDebugInfo(`Mammoth转换失败: ${mammothError instanceof Error ? mammothError.message : String(mammothError)}`); @@ -312,9 +296,11 @@ export default function Documents() { loadDocx(); } - }, [currentUrl, fileType, extractedContent, showIframe]); + }, [currentUrl, fileType]); - // 页面渲染完成后检查是否需要滚动 + /** + * 页面滚动逻辑 + */ useEffect(() => { if (scrollToPage && fileType === "pdf") { const pageElement = document.getElementById(`page-${scrollToPage}`); @@ -325,7 +311,10 @@ export default function Documents() { } }, [scrollToPage, fileType]); - // 生成所有PDF页面的数组 + /** + * 生成所有PDF页面的渲染数组 + * @returns 页面组件数组 + */ const renderAllPages = () => { if (!numPages) return null; @@ -347,45 +336,14 @@ export default function Documents() { }; return ( -
+
{/* 文档展示区域 */} -
-
+
+

文档预览 ({fileType.toUpperCase()})

- {fileType === "docx" && ( -
-
-

Word文档预览模式

-
- - -
-
- {!showIframe && ( -
-

本地渲染说明:

-
    -
  • 本地渲染使用mammoth.js库将Word文档转换为HTML
  • -
  • 部分复杂格式(页眉页脚、复杂表格样式、特殊字体等)可能无法完全还原
  • -
  • 嵌入模式使用Google Docs提供原生渲染,格式更完整但加载较慢
  • -
-
- )} -
- )} - -
+ {/* 文档内容显示区域 */} +
{loadError ? (

加载错误:

@@ -405,9 +363,6 @@ export default function Documents() { - @@ -418,6 +373,7 @@ export default function Documents() {
) : fileType === "pdf" ? ( + /* PDF 文档渲染 */ ) : ( + /* Word 文档渲染 */ <> {docxLoading ? ( + /* 加载状态显示 */
@@ -449,18 +407,8 @@ export default function Documents() {
)}
- ) : showIframe ? ( - // 嵌入模式显示Word文档 -
- -
- -
- -
- -
-
-

在线预览无法工作?

-

您可以下载文档在本地打开

- - 下载文档 - -
-
-
- -
-
测试页面已加载,正在尝试不同的文档预览方案...
-
-
- - - - \ No newline at end of file