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.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`; // 模拟后端返回的抽取内容数据 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 }; } interface LoaderData { fileUrl: string; initialPage: number; extractedContent: ExtractedContent[]; fileType: "pdf" | "docx"; urls: Record; } // 定义文档加载成功回调类型 interface DocumentLoadSuccess { numPages: number; } // 根据URL判断文件类型 function getFileTypeFromUrl(url: string): "pdf" | "docx" { const lowerCaseUrl = url.toLowerCase(); if (lowerCaseUrl.endsWith(".pdf")) { return "pdf"; } else if (lowerCaseUrl.endsWith(".docx") || lowerCaseUrl.endsWith(".doc")) { return "docx"; } // 默认当作PDF处理 return "pdf"; } // Remix Loader 函数 export const loader = async ({ request }: LoaderFunctionArgs) => { 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 const urls = { // 1. 原始文档URL - 可能有CORS限制 original: "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx", // 2. 公开示例文档 - 仍可能有CORS限制 public: "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx", // 3. 通过CORS代理 (示例) proxy: "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx", // 4. 本地服务器上的文档 (假设已经部署) local: "/uploads/sample.docx", // 5. PDF示例 (如果Word文档问题无法解决) 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进行测试 // 判断文件类型 const fileType = getFileTypeFromUrl(fileUrl); return { fileUrl, initialPage: Number(page), extractedContent: mockExtractedContent, fileType, urls // 传递所有URL供前端选择 }; }; export default function Documents() { 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 handleContentClick = (item: ExtractedContent) => { setScrollToPage(item.page); if (fileType === "pdf") { // 使用ID滚动到指定页面 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}",请在文档中手动查找`); } }; // 模拟在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文档加载成功回调 function onDocumentLoadSuccess({ numPages }: DocumentLoadSuccess) { setNumPages(numPages); console.log("PDF加载成功,页数:", numPages); } // 简化的调试日志 const addDebugInfo = (info: string) => { console.log(info); setDebugInfo(prev => [...prev, `${new Date().toISOString().split('T')[1].split('.')[0]}: ${info}`]); }; // 切换到不同的文档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文档 useEffect(() => { if (fileType === "docx" && docxContainerRef.current && !showIframe) { setDocxLoading(true); setDebugInfo([]); // 清空之前的调试信息 addDebugInfo(`准备加载Word文档: ${currentUrl}`); const loadDocx = async () => { try { // 获取文件 addDebugInfo(`开始获取文件...`); let response; try { response = await fetch(currentUrl, { // 添加CORS相关选项 mode: 'cors', credentials: 'omit', headers: { 'Access-Control-Allow-Origin': '*' } }); addDebugInfo(`fetch请求状态: ${response.status} ${response.statusText}`); } catch (fetchError) { addDebugInfo(`fetch请求失败: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`); throw new Error(`网络请求失败: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`); } if (!response.ok) { throw new Error(`文档无法访问,状态码: ${response.status}`); } addDebugInfo(`文档下载成功,状态码: ${response.status}`); // 转换为ArrayBuffer addDebugInfo(`开始读取响应内容为ArrayBuffer...`); let buffer; try { buffer = await response.arrayBuffer(); addDebugInfo(`获取到文档数据,大小: ${buffer.byteLength} 字节`); } catch (bufferError) { addDebugInfo(`读取为ArrayBuffer失败: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`); throw new Error(`转换文档内容失败: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`); } // 使用mammoth.js将Word转换为HTML,添加自定义选项 addDebugInfo("使用mammoth开始转换文档为HTML..."); try { // 添加自定义样式映射 const styleMap = ` p[style-name='Heading 1'] => h1:fresh p[style-name='Heading 2'] => h2:fresh p[style-name='Title'] => h1.title:fresh p[style-name='Subtitle'] => h2.subtitle:fresh table => table.docx-table `; // 创建简化版的转换选项 const options = { arrayBuffer: buffer, styleMap: styleMap, includeDefaultStyleMap: true }; const result = await mammoth.convertToHtml(options); // 检查转换警告 if (result.messages.length > 0) { result.messages.forEach(message => { addDebugInfo(`转换警告: [${message.type}] ${message.message}`); }); } addDebugInfo("文档转换成功,获取到HTML内容"); // 为生成的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)}`); throw new Error(`Word转HTML失败: ${mammothError instanceof Error ? mammothError.message : String(mammothError)}`); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); addDebugInfo(`文档处理错误: ${errorMessage}`); setLoadError(`加载Word文档失败: ${errorMessage}`); setDocxLoading(false); } }; loadDocx(); } }, [currentUrl, fileType, extractedContent, showIframe]); // 页面渲染完成后检查是否需要滚动 useEffect(() => { if (scrollToPage && fileType === "pdf") { const pageElement = document.getElementById(`page-${scrollToPage}`); if (pageElement) { pageElement.scrollIntoView({ behavior: 'smooth' }); } setScrollToPage(null); } }, [scrollToPage, fileType]); // 生成所有PDF页面的数组 const renderAllPages = () => { if (!numPages) return null; const pages = []; for (let i = 1; i <= numPages; i++) { pages.push(
第 {i} 页
); } return pages; }; return (
{/* 文档展示区域 */}

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

{fileType === "docx" && (

Word文档预览模式

{!showIframe && (

本地渲染说明:

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

加载错误:

{loadError}

调试信息:

{debugInfo.map((info, index) => (
{info}
))}

尝试其他方式:

下载文档
) : fileType === "pdf" ? ( { console.error("PDF加载错误:", error); setLoadError("PDF文档加载失败:" + (error.message || "未知错误")); }} className="flex flex-col items-center" error={
PDF文档加载失败,请检查链接或网络连接。
} noData={
无数据
} loading={
PDF加载中...
} > {renderAllPages()}
) : ( <> {docxLoading ? (

Word文档加载中...

{debugInfo.length > 0 && (

加载过程:

{debugInfo.map((info, index) => (
{info}
))}
)}
) : showIframe ? ( // 嵌入模式显示Word文档