文件预览页面demo完成,访问路径为:/rules/new1
This commit is contained in:
+141
-191
@@ -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 { useState, useEffect, useRef } from "react";
|
||||||
import { useLoaderData } from "@remix-run/react";
|
import { useLoaderData } from "@remix-run/react";
|
||||||
import { Document, Page, pdfjs } from "react-pdf";
|
import { Document, Page, pdfjs } from "react-pdf";
|
||||||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import mammoth from "mammoth";
|
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`;
|
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
|
||||||
|
|
||||||
// 模拟后端返回的抽取内容数据
|
/**
|
||||||
|
* 模拟后端返回的文档抽取内容数据
|
||||||
|
* 实际应用中应从API获取
|
||||||
|
*/
|
||||||
const mockExtractedContent = [
|
const mockExtractedContent = [
|
||||||
{ id: 1, text: "合同条款", page: 2, position: { start: 50, end: 60 } },
|
{ id: 1, text: "合同条款", page: 2, position: { start: 50, end: 60 } },
|
||||||
{ id: 2, text: "签署日期", page: 5, position: { start: 120, end: 130 } },
|
{ id: 2, text: "签署日期", page: 5, position: { start: 120, end: 130 } },
|
||||||
{ id: 3, text: "责任划分", page: 3, position: { start: 80, end: 90 } },
|
{ id: 3, text: "责任划分", page: 3, position: { start: 80, end: 90 } },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档抽取内容接口定义
|
||||||
|
*/
|
||||||
interface ExtractedContent {
|
interface ExtractedContent {
|
||||||
id: number;
|
id: number; // 内容唯一标识
|
||||||
text: string;
|
text: string; // 抽取的文本内容
|
||||||
page: number;
|
page: number; // 所在页码
|
||||||
position: { start: number; end: number };
|
position: { // 在页面中的位置信息
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loader 函数返回数据接口定义
|
||||||
|
*/
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
fileUrl: string;
|
fileUrl: string; // 当前文档URL
|
||||||
initialPage: number;
|
initialPage: number; // 初始页码
|
||||||
extractedContent: ExtractedContent[];
|
extractedContent: ExtractedContent[]; // 抽取内容数组
|
||||||
fileType: "pdf" | "docx";
|
fileType: "pdf" | "docx"; // 文档类型
|
||||||
urls: Record<string, string>;
|
urls: Record<string, string>; // 可用文档URL列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义文档加载成功回调类型
|
/**
|
||||||
|
* PDF文档加载成功回调接口
|
||||||
|
*/
|
||||||
interface DocumentLoadSuccess {
|
interface DocumentLoadSuccess {
|
||||||
numPages: number;
|
numPages: number; // 文档总页数
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据URL判断文件类型
|
/**
|
||||||
|
* 根据URL判断文件类型
|
||||||
|
* @param url 文档URL
|
||||||
|
* @returns 文档类型:"pdf" 或 "docx"
|
||||||
|
*/
|
||||||
function getFileTypeFromUrl(url: string): "pdf" | "docx" {
|
function getFileTypeFromUrl(url: string): "pdf" | "docx" {
|
||||||
const lowerCaseUrl = url.toLowerCase();
|
const lowerCaseUrl = url.toLowerCase();
|
||||||
if (lowerCaseUrl.endsWith(".pdf")) {
|
if (lowerCaseUrl.endsWith(".pdf")) {
|
||||||
@@ -46,15 +89,15 @@ function getFileTypeFromUrl(url: string): "pdf" | "docx" {
|
|||||||
return "pdf";
|
return "pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remix Loader 函数
|
/**
|
||||||
|
* Remix Loader 函数 - 请求处理和数据加载
|
||||||
|
*/
|
||||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
|
// 从URL获取查询参数
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const page = url.searchParams.get("page") || 1;
|
const page = url.searchParams.get("page") || 1;
|
||||||
|
|
||||||
// 实际文档 URL (PDF示例)
|
// 示例文档URLs集合
|
||||||
// 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 = {
|
const urls = {
|
||||||
// 1. 原始文档URL - 可能有CORS限制
|
// 1. 原始文档URL - 可能有CORS限制
|
||||||
original: "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx",
|
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",
|
proxy: "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx",
|
||||||
// 4. 本地服务器上的文档 (假设已经部署)
|
// 4. 本地服务器上的文档 (假设已经部署)
|
||||||
local: "/uploads/sample.docx",
|
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"
|
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
|
// 使用默认文档URL
|
||||||
const fileUrl = urls.public; // 可以切换到其他URL进行测试
|
const fileUrl = urls.pdf;
|
||||||
|
|
||||||
// 判断文件类型
|
// 判断文件类型
|
||||||
const fileType = getFileTypeFromUrl(fileUrl);
|
const fileType = getFileTypeFromUrl(fileUrl);
|
||||||
|
|
||||||
|
// 返回加载的数据
|
||||||
return {
|
return {
|
||||||
fileUrl,
|
fileUrl,
|
||||||
initialPage: Number(page),
|
initialPage: Number(page),
|
||||||
extractedContent: mockExtractedContent,
|
extractedContent: mockExtractedContent,
|
||||||
fileType,
|
fileType,
|
||||||
urls // 传递所有URL供前端选择
|
urls
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档预览组件
|
||||||
|
*/
|
||||||
export default function Documents() {
|
export default function Documents() {
|
||||||
|
// 从loader获取数据
|
||||||
const { fileUrl, extractedContent, fileType, urls } = useLoaderData<LoaderData>();
|
const { fileUrl, extractedContent, fileType, urls } = useLoaderData<LoaderData>();
|
||||||
const [numPages, setNumPages] = useState<number | null>(null);
|
|
||||||
const [scrollToPage, setScrollToPage] = useState<number | null>(null);
|
// 状态管理
|
||||||
const [docxLoading, setDocxLoading] = useState(false); // 设置为false以避免加载指示器
|
const [numPages, setNumPages] = useState<number | null>(null); // PDF总页数
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [scrollToPage, setScrollToPage] = useState<number | null>(null); // 滚动目标页码
|
||||||
const [debugInfo, setDebugInfo] = useState<string[]>([]);
|
const [docxLoading, setDocxLoading] = useState(false); // Word文档加载状态
|
||||||
const docxContainerRef = useRef<HTMLDivElement>(null);
|
const [loadError, setLoadError] = useState<string | null>(null); // 加载错误信息
|
||||||
const [docxContentPositions, setDocxContentPositions] = useState<{[id: number]: number}>({});
|
const [debugInfo, setDebugInfo] = useState<string[]>([]); // 调试信息
|
||||||
const [currentUrl, setCurrentUrl] = useState<string>(fileUrl);
|
const [docxHtml, setDocxHtml] = useState<string>(""); // 转换后的HTML内容
|
||||||
// 默认使用iframe模式
|
const [currentUrl, setCurrentUrl] = useState<string>(fileUrl); // 当前文档URL
|
||||||
const [showIframe, setShowIframe] = useState<boolean>(true);
|
|
||||||
const [docxHtml, setDocxHtml] = useState<string>("");
|
// 引用
|
||||||
|
const docxContainerRef = useRef<HTMLDivElement>(null); // Word文档容器引用
|
||||||
|
|
||||||
// 处理抽取内容点击
|
/**
|
||||||
|
* 处理抽取内容点击事件 - 仅对PDF文档生效
|
||||||
|
* @param item 被点击的抽取内容项
|
||||||
|
*/
|
||||||
const handleContentClick = (item: ExtractedContent) => {
|
const handleContentClick = (item: ExtractedContent) => {
|
||||||
setScrollToPage(item.page);
|
// 仅对PDF文档执行交互操作
|
||||||
if (fileType === "pdf") {
|
if (fileType === "pdf") {
|
||||||
// 使用ID滚动到指定页面
|
setScrollToPage(item.page);
|
||||||
|
// 对于PDF,滚动到指定页面
|
||||||
const pageElement = document.getElementById(`page-${item.page}`);
|
const pageElement = document.getElementById(`page-${item.page}`);
|
||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
pageElement.scrollIntoView({ behavior: 'smooth' });
|
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) => {
|
* PDF文档加载成功回调函数
|
||||||
// 移除之前的高亮
|
* @param param0 包含numPages的对象
|
||||||
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) {
|
function onDocumentLoadSuccess({ numPages }: DocumentLoadSuccess) {
|
||||||
setNumPages(numPages);
|
setNumPages(numPages);
|
||||||
console.log("PDF加载成功,页数:", numPages);
|
console.log("PDF加载成功,页数:", numPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 简化的调试日志
|
/**
|
||||||
|
* 添加调试信息
|
||||||
|
* @param info 调试信息文本
|
||||||
|
*/
|
||||||
const addDebugInfo = (info: string) => {
|
const addDebugInfo = (info: string) => {
|
||||||
console.log(info);
|
console.log(info);
|
||||||
setDebugInfo(prev => [...prev, `${new Date().toISOString().split('T')[1].split('.')[0]}: ${info}`]);
|
setDebugInfo(prev => [...prev, `${new Date().toISOString().split('T')[1].split('.')[0]}: ${info}`]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换到不同的文档URL
|
/**
|
||||||
|
* 切换文档URL
|
||||||
|
* @param urlKey URL键名
|
||||||
|
*/
|
||||||
const switchDocumentUrl = (urlKey: keyof typeof urls) => {
|
const switchDocumentUrl = (urlKey: keyof typeof urls) => {
|
||||||
setCurrentUrl(urls[urlKey]);
|
setCurrentUrl(urls[urlKey]);
|
||||||
setDebugInfo([]);
|
setDebugInfo([]);
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
setDocxLoading(false);
|
setDocxLoading(false);
|
||||||
setShowIframe(true);
|
|
||||||
addDebugInfo(`切换到新的文档URL: ${urls[urlKey]}`);
|
addDebugInfo(`切换到新的文档URL: ${urls[urlKey]}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换到iframe模式 (当直接加载文档有CORS问题时)
|
/**
|
||||||
const switchToIframeMode = () => {
|
* Word文档处理逻辑
|
||||||
setShowIframe(true);
|
*/
|
||||||
setDocxLoading(false);
|
|
||||||
addDebugInfo("切换到iframe嵌入模式");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用mammoth处理Word文档
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileType === "docx" && docxContainerRef.current && !showIframe) {
|
if (fileType === "docx" && docxContainerRef.current) {
|
||||||
setDocxLoading(true);
|
setDocxLoading(true);
|
||||||
setDebugInfo([]); // 清空之前的调试信息
|
setDebugInfo([]); // 清空调试信息
|
||||||
addDebugInfo(`准备加载Word文档: ${currentUrl}`);
|
addDebugInfo(`准备加载Word文档: ${currentUrl}`);
|
||||||
|
|
||||||
const loadDocx = async () => {
|
const loadDocx = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取文件
|
// 1. 获取文档文件
|
||||||
addDebugInfo(`开始获取文件...`);
|
addDebugInfo(`开始获取文件...`);
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(currentUrl, {
|
response = await fetch(currentUrl, {
|
||||||
// 添加CORS相关选项
|
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -197,12 +221,13 @@ export default function Documents() {
|
|||||||
throw new Error(`网络请求失败: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
|
throw new Error(`网络请求失败: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`文档无法访问,状态码: ${response.status}`);
|
throw new Error(`文档无法访问,状态码: ${response.status}`);
|
||||||
}
|
}
|
||||||
addDebugInfo(`文档下载成功,状态码: ${response.status}`);
|
addDebugInfo(`文档下载成功,状态码: ${response.status}`);
|
||||||
|
|
||||||
// 转换为ArrayBuffer
|
// 2. 将响应转换为ArrayBuffer
|
||||||
addDebugInfo(`开始读取响应内容为ArrayBuffer...`);
|
addDebugInfo(`开始读取响应内容为ArrayBuffer...`);
|
||||||
let buffer;
|
let buffer;
|
||||||
try {
|
try {
|
||||||
@@ -213,10 +238,10 @@ export default function Documents() {
|
|||||||
throw new Error(`转换文档内容失败: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`);
|
throw new Error(`转换文档内容失败: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用mammoth.js将Word转换为HTML,添加自定义选项
|
// 3. 使用mammoth.js将Word转换为HTML
|
||||||
addDebugInfo("使用mammoth开始转换文档为HTML...");
|
addDebugInfo("使用mammoth开始转换文档为HTML...");
|
||||||
try {
|
try {
|
||||||
// 添加自定义样式映射
|
// 自定义样式映射
|
||||||
const styleMap = `
|
const styleMap = `
|
||||||
p[style-name='Heading 1'] => h1:fresh
|
p[style-name='Heading 1'] => h1:fresh
|
||||||
p[style-name='Heading 2'] => h2:fresh
|
p[style-name='Heading 2'] => h2:fresh
|
||||||
@@ -225,13 +250,14 @@ export default function Documents() {
|
|||||||
table => table.docx-table
|
table => table.docx-table
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 创建简化版的转换选项
|
// 转换选项
|
||||||
const options = {
|
const options = {
|
||||||
arrayBuffer: buffer,
|
arrayBuffer: buffer,
|
||||||
styleMap: styleMap,
|
styleMap: styleMap,
|
||||||
includeDefaultStyleMap: true
|
includeDefaultStyleMap: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 执行转换
|
||||||
const result = await mammoth.convertToHtml(options);
|
const result = await mammoth.convertToHtml(options);
|
||||||
|
|
||||||
// 检查转换警告
|
// 检查转换警告
|
||||||
@@ -243,60 +269,18 @@ export default function Documents() {
|
|||||||
|
|
||||||
addDebugInfo("文档转换成功,获取到HTML内容");
|
addDebugInfo("文档转换成功,获取到HTML内容");
|
||||||
|
|
||||||
// 为生成的HTML文档添加包装容器和样式
|
// 4. 为生成的HTML添加包装容器和样式
|
||||||
const enhancedHtml = `
|
const enhancedHtml = `
|
||||||
<div class="document-container">
|
<div class="document-container">
|
||||||
${result.value}
|
${result.value}
|
||||||
<div class="format-note">
|
<div class="format-note">
|
||||||
<p>注意:本地转换使用了简化版格式,一些高级格式(如页眉页脚、复杂表格格式)可能无法完全显示。</p>
|
<p>注意:部分复杂格式(如页眉页脚、复杂表格样式)可能无法完全显示。</p>
|
||||||
<p>如需查看完整格式,请使用"嵌入模式"或下载文档。</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 存储HTML内容
|
// 更新状态
|
||||||
setDocxHtml(enhancedHtml);
|
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);
|
setDocxLoading(false);
|
||||||
} catch (mammothError) {
|
} catch (mammothError) {
|
||||||
addDebugInfo(`Mammoth转换失败: ${mammothError instanceof Error ? mammothError.message : String(mammothError)}`);
|
addDebugInfo(`Mammoth转换失败: ${mammothError instanceof Error ? mammothError.message : String(mammothError)}`);
|
||||||
@@ -312,9 +296,11 @@ export default function Documents() {
|
|||||||
|
|
||||||
loadDocx();
|
loadDocx();
|
||||||
}
|
}
|
||||||
}, [currentUrl, fileType, extractedContent, showIframe]);
|
}, [currentUrl, fileType]);
|
||||||
|
|
||||||
// 页面渲染完成后检查是否需要滚动
|
/**
|
||||||
|
* 页面滚动逻辑
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollToPage && fileType === "pdf") {
|
if (scrollToPage && fileType === "pdf") {
|
||||||
const pageElement = document.getElementById(`page-${scrollToPage}`);
|
const pageElement = document.getElementById(`page-${scrollToPage}`);
|
||||||
@@ -325,7 +311,10 @@ export default function Documents() {
|
|||||||
}
|
}
|
||||||
}, [scrollToPage, fileType]);
|
}, [scrollToPage, fileType]);
|
||||||
|
|
||||||
// 生成所有PDF页面的数组
|
/**
|
||||||
|
* 生成所有PDF页面的渲染数组
|
||||||
|
* @returns 页面组件数组
|
||||||
|
*/
|
||||||
const renderAllPages = () => {
|
const renderAllPages = () => {
|
||||||
if (!numPages) return null;
|
if (!numPages) return null;
|
||||||
|
|
||||||
@@ -347,45 +336,14 @@ export default function Documents() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-gray-50 p-6">
|
<div className="flex h-screen bg-gray-50">
|
||||||
{/* 文档展示区域 */}
|
{/* 文档展示区域 */}
|
||||||
<div className="flex-1 mr-6">
|
<div className="flex-1 mr-6 p-4">
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
<div className="bg-white p-4 rounded-lg shadow-md h-full flex flex-col">
|
||||||
<h1 className="text-2xl font-bold mb-4">文档预览 ({fileType.toUpperCase()})</h1>
|
<h1 className="text-2xl font-bold mb-4">文档预览 ({fileType.toUpperCase()})</h1>
|
||||||
|
|
||||||
{fileType === "docx" && (
|
{/* 文档内容显示区域 */}
|
||||||
<div className="bg-gray-100 p-3 mb-4 rounded flex flex-col">
|
<div className="w-full flex-1 overflow-auto bg-gray-100 rounded-lg p-4">
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<p className="text-sm text-gray-600">Word文档预览模式</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowIframe(!showIframe)}
|
|
||||||
className={`px-3 py-1 text-sm rounded ${showIframe ? 'bg-gray-200' : 'bg-blue-500 text-white'}`}
|
|
||||||
>
|
|
||||||
{showIframe ? "尝试本地渲染" : "使用嵌入模式"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => window.open(currentUrl, '_blank')}
|
|
||||||
className="px-3 py-1 bg-gray-500 text-white text-sm rounded"
|
|
||||||
>
|
|
||||||
下载文档
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!showIframe && (
|
|
||||||
<div className="text-xs text-gray-500 bg-yellow-50 p-2 rounded">
|
|
||||||
<p>本地渲染说明:</p>
|
|
||||||
<ul className="list-disc pl-5 mt-1">
|
|
||||||
<li>本地渲染使用mammoth.js库将Word文档转换为HTML</li>
|
|
||||||
<li>部分复杂格式(页眉页脚、复杂表格样式、特殊字体等)可能无法完全还原</li>
|
|
||||||
<li>嵌入模式使用Google Docs提供原生渲染,格式更完整但加载较慢</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="w-full h-[80vh] overflow-auto bg-gray-100 rounded-lg p-4">
|
|
||||||
{loadError ? (
|
{loadError ? (
|
||||||
<div className="text-red-500 flex flex-col items-center justify-center h-full">
|
<div className="text-red-500 flex flex-col items-center justify-center h-full">
|
||||||
<p className="mb-4">加载错误:</p>
|
<p className="mb-4">加载错误:</p>
|
||||||
@@ -405,9 +363,6 @@ export default function Documents() {
|
|||||||
<button onClick={() => switchDocumentUrl('proxy')} className="px-3 py-1 bg-blue-500 text-white rounded">
|
<button onClick={() => switchDocumentUrl('proxy')} className="px-3 py-1 bg-blue-500 text-white rounded">
|
||||||
使用CORS代理
|
使用CORS代理
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => switchToIframeMode()} className="px-3 py-1 bg-purple-500 text-white rounded">
|
|
||||||
使用iframe嵌入
|
|
||||||
</button>
|
|
||||||
<button onClick={() => switchDocumentUrl('pdf')} className="px-3 py-1 bg-yellow-500 text-white rounded">
|
<button onClick={() => switchDocumentUrl('pdf')} className="px-3 py-1 bg-yellow-500 text-white rounded">
|
||||||
切换到PDF
|
切换到PDF
|
||||||
</button>
|
</button>
|
||||||
@@ -418,6 +373,7 @@ export default function Documents() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : fileType === "pdf" ? (
|
) : fileType === "pdf" ? (
|
||||||
|
/* PDF 文档渲染 */
|
||||||
<Document
|
<Document
|
||||||
file={currentUrl}
|
file={currentUrl}
|
||||||
onLoadSuccess={onDocumentLoadSuccess}
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
@@ -433,8 +389,10 @@ export default function Documents() {
|
|||||||
{renderAllPages()}
|
{renderAllPages()}
|
||||||
</Document>
|
</Document>
|
||||||
) : (
|
) : (
|
||||||
|
/* Word 文档渲染 */
|
||||||
<>
|
<>
|
||||||
{docxLoading ? (
|
{docxLoading ? (
|
||||||
|
/* 加载状态显示 */
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
@@ -449,18 +407,8 @@ export default function Documents() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : showIframe ? (
|
|
||||||
// 嵌入模式显示Word文档
|
|
||||||
<div className="w-full h-full">
|
|
||||||
<iframe
|
|
||||||
src={`https://docs.google.com/viewer?url=${encodeURIComponent(currentUrl)}&embedded=true`}
|
|
||||||
className="w-full h-full"
|
|
||||||
frameBorder="0"
|
|
||||||
title="谷歌文档查看器"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
// 本地渲染模式 (只有用户特别点击按钮才显示)
|
/* 本地渲染的Word文档 */
|
||||||
<div
|
<div
|
||||||
ref={docxContainerRef}
|
ref={docxContainerRef}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
@@ -479,15 +427,16 @@ export default function Documents() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 抽取内容区域 */}
|
{/* 抽取内容区域 - 始终显示,但DOCX模式下不交互 */}
|
||||||
<div className="w-80 bg-white p-4 rounded-lg shadow-md">
|
<div className="w-80 bg-white p-4 rounded-lg shadow-md mr-4 my-4 overflow-auto">
|
||||||
<h2 className="text-xl font-semibold mb-4">抽取内容</h2>
|
<h2 className="text-xl font-semibold mb-4">抽取内容</h2>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{extractedContent.map((item) => (
|
{extractedContent.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => handleContentClick(item)}
|
onClick={() => handleContentClick(item)}
|
||||||
className="w-full text-left p-3 bg-gray-50 hover:bg-gray-100 cursor-pointer rounded-lg transition"
|
className={`w-full text-left p-3 ${fileType === "pdf" ? "bg-gray-50 hover:bg-gray-100 cursor-pointer" : "bg-gray-100"} rounded-lg transition`}
|
||||||
|
disabled={fileType === "docx"}
|
||||||
aria-label={`查看内容: ${item.text}`}
|
aria-label={`查看内容: ${item.text}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium">{item.text}</p>
|
<p className="text-sm font-medium">{item.text}</p>
|
||||||
@@ -500,13 +449,14 @@ export default function Documents() {
|
|||||||
{/* 添加自定义样式 */}
|
{/* 添加自定义样式 */}
|
||||||
<style dangerouslySetInnerHTML={{
|
<style dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
|
/* 高亮显示样式 */
|
||||||
.docx-highlight {
|
.docx-highlight {
|
||||||
background-color: #ffff00;
|
background-color: #ffff00;
|
||||||
outline: 2px solid orange;
|
outline: 2px solid orange;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 找到的内容高亮 */
|
/* 找到的内容高亮样式 */
|
||||||
.docx-content-found {
|
.docx-content-found {
|
||||||
background-color: rgba(255, 230, 0, 0.3);
|
background-color: rgba(255, 230, 0, 0.3);
|
||||||
outline: 1px solid orange;
|
outline: 1px solid orange;
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
// 简单测试docx-preview能否正常工作
|
|
||||||
import { renderAsync } from 'docx-preview';
|
|
||||||
|
|
||||||
async function testDocxPreview() {
|
|
||||||
try {
|
|
||||||
// DOCX文件URL
|
|
||||||
const fileUrl = "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx";
|
|
||||||
console.log("正在获取文件:", fileUrl);
|
|
||||||
|
|
||||||
// 获取文件
|
|
||||||
const response = await fetch(fileUrl);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`网络请求失败: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
console.log("文件下载成功,准备解析");
|
|
||||||
|
|
||||||
// 转换为ArrayBuffer
|
|
||||||
const buffer = await response.arrayBuffer();
|
|
||||||
console.log("获取到ArrayBuffer,长度:", buffer.byteLength);
|
|
||||||
|
|
||||||
// 创建容器
|
|
||||||
const container = document.getElementById('docx-container');
|
|
||||||
if (!container) {
|
|
||||||
throw new Error("找不到容器元素");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染文档
|
|
||||||
console.log("开始渲染文档...");
|
|
||||||
await renderAsync(buffer, container, null, {
|
|
||||||
className: "docx-viewer",
|
|
||||||
inWrapper: true,
|
|
||||||
ignoreWidth: false,
|
|
||||||
ignoreHeight: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("文档渲染成功");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("文档渲染失败:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后执行
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const app = document.getElementById('app');
|
|
||||||
if (app) {
|
|
||||||
// 创建容器
|
|
||||||
app.innerHTML = `
|
|
||||||
<div style="width: 100%; height: 100vh; display: flex; flex-direction: column;">
|
|
||||||
<h1>测试 docx-preview</h1>
|
|
||||||
<div id="docx-container" style="flex: 1; border: 1px solid #ccc; overflow: auto;"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
testDocxPreview();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Word文档预览方案测试</title>
|
|
||||||
<style>
|
|
||||||
body, html {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.tab {
|
|
||||||
padding: 10px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-bottom: none;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
.tab.active {
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
#preview-container {
|
|
||||||
flex: 1;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.preview-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.preview-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
#log {
|
|
||||||
height: 120px;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
color: #0f0;
|
|
||||||
padding: 10px;
|
|
||||||
font-family: monospace;
|
|
||||||
margin-top: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.download-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.download-button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.loading {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(255,255,255,0.8);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.spinner {
|
|
||||||
border: 5px solid #f3f3f3;
|
|
||||||
border-top: 5px solid #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Word文档预览方案测试</h1>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
<div class="tab active" data-target="office">Office Online</div>
|
|
||||||
<div class="tab" data-target="google">Google Docs</div>
|
|
||||||
<div class="tab" data-target="download">下载查看</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="preview-container">
|
|
||||||
<div id="loading" class="loading">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div>加载中,请稍候...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="office-preview" class="preview-content active">
|
|
||||||
<iframe id="office-iframe" src=""></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="google-preview" class="preview-content">
|
|
||||||
<iframe id="google-iframe" src=""></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="download-preview" class="preview-content">
|
|
||||||
<div class="download-container">
|
|
||||||
<p>在线预览无法工作?</p>
|
|
||||||
<p>您可以下载文档在本地打开</p>
|
|
||||||
<a id="download-link" href="" class="download-button" download target="_blank">
|
|
||||||
下载文档
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="log">
|
|
||||||
<div>测试页面已加载,正在尝试不同的文档预览方案...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 配置
|
|
||||||
const docUrl = "https://dev-xc-enroll.oss-cn-guangzhou.aliyuncs.com/uploads/7840-230620112939.docx";
|
|
||||||
const officeOnlineUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(docUrl)}`;
|
|
||||||
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(docUrl)}&embedded=true`;
|
|
||||||
|
|
||||||
// 日志函数
|
|
||||||
function log(message) {
|
|
||||||
const logContainer = document.getElementById('log');
|
|
||||||
const logEntry = document.createElement('div');
|
|
||||||
logEntry.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
|
|
||||||
logContainer.appendChild(logEntry);
|
|
||||||
logContainer.scrollTop = logContainer.scrollHeight;
|
|
||||||
console.log(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化函数
|
|
||||||
function init() {
|
|
||||||
// 设置iframe源
|
|
||||||
document.getElementById('office-iframe').src = officeOnlineUrl;
|
|
||||||
document.getElementById('google-iframe').src = googleDocsUrl;
|
|
||||||
document.getElementById('download-link').href = docUrl;
|
|
||||||
|
|
||||||
// 设置标签切换
|
|
||||||
const tabs = document.querySelectorAll('.tab');
|
|
||||||
tabs.forEach(tab => {
|
|
||||||
tab.addEventListener('click', function() {
|
|
||||||
// 移除所有标签的active类
|
|
||||||
tabs.forEach(t => t.classList.remove('active'));
|
|
||||||
|
|
||||||
// 添加当前标签的active类
|
|
||||||
this.classList.add('active');
|
|
||||||
|
|
||||||
// 隐藏所有内容
|
|
||||||
document.querySelectorAll('.preview-content').forEach(content => {
|
|
||||||
content.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 显示对应内容
|
|
||||||
const target = this.getAttribute('data-target');
|
|
||||||
document.getElementById(`${target}-preview`).classList.add('active');
|
|
||||||
|
|
||||||
log(`切换到 ${target} 预览模式`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听iframe加载事件
|
|
||||||
document.getElementById('office-iframe').addEventListener('load', function() {
|
|
||||||
log('Office Online预览已加载');
|
|
||||||
document.getElementById('loading').style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('google-iframe').addEventListener('load', function() {
|
|
||||||
log('Google Docs预览已加载');
|
|
||||||
});
|
|
||||||
|
|
||||||
log('页面初始化完成,正在加载Office Online预览...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后执行初始化
|
|
||||||
window.addEventListener('DOMContentLoaded', init);
|
|
||||||
|
|
||||||
// 添加错误处理
|
|
||||||
window.addEventListener('error', function(event) {
|
|
||||||
log(`错误: ${event.message}`);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user