d88cfc818b
2. 优化追加附件和模板上传的样式。
816 lines
29 KiB
TypeScript
816 lines
29 KiB
TypeScript
/**
|
||
* Monaco Editor 差异对比演示页面
|
||
*
|
||
* 功能:
|
||
* - 展示两份合同文本的差异对比
|
||
* - 支持逐行高亮显示差异
|
||
* - 提供差异导航功能
|
||
* - 后续可扩展文件上传功能
|
||
*/
|
||
|
||
import { type MetaFunction, type ClientLoaderFunctionArgs } from "@remix-run/node";
|
||
import { useState, useRef, useEffect } from "react";
|
||
import { pdfjs } from 'react-pdf';
|
||
import mammoth from 'mammoth';
|
||
import { toastService } from '~/components/ui/Toast';
|
||
|
||
// 动态导入 Monaco Editor(仅客户端)
|
||
let DiffEditor: any = null;
|
||
let editor: any = null;
|
||
|
||
if (typeof window !== 'undefined') {
|
||
import('@monaco-editor/react').then((mod) => {
|
||
DiffEditor = mod.DiffEditor;
|
||
});
|
||
import('monaco-editor').then((mod) => {
|
||
editor = mod.editor;
|
||
});
|
||
}
|
||
|
||
// 设置 PDF.js worker(与 pdf-demo.tsx 相同)
|
||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "Monaco Diff Editor 演示 - 合同对比" },
|
||
{ name: "description", content: "使用 Monaco Editor 进行合同文本差异对比" }
|
||
];
|
||
};
|
||
|
||
// 文档类型枚举
|
||
type DocumentType = 'pdf' | 'docx' | 'unknown';
|
||
|
||
// PDF 类型枚举
|
||
type PdfType = 'text' | 'scanned' | 'unknown';
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 示例合同文本 A(原始版本)
|
||
const CONTRACT_A = `中国烟草合同(原始版本)
|
||
|
||
第一条 合同双方
|
||
甲方:中国烟草总公司广东省公司
|
||
乙方:XX供应商有限公司
|
||
|
||
第二条 合同标的
|
||
甲方向乙方采购烟草包装材料,具体型号为:
|
||
1. 硬盒包装纸 10000箱
|
||
2. 烟用滤棒 5000箱
|
||
总金额:人民币壹佰万元整(¥1,000,000.00)
|
||
|
||
第三条 交付时间
|
||
乙方应在签订合同后30个工作日内完成全部交付。
|
||
|
||
第四条 质量标准
|
||
产品应符合国家烟草行业标准 YC/T 207-2014。
|
||
|
||
第五条 付款方式
|
||
甲方在收到货物并验收合格后,于15个工作日内支付全部款项。
|
||
|
||
第六条 违约责任
|
||
1. 乙方延期交付,每延迟一天支付合同总额0.5%的违约金。
|
||
2. 产品质量不合格,乙方应无偿更换并承担相应损失。
|
||
|
||
第七条 争议解决
|
||
本合同履行过程中发生的争议,由双方协商解决;协商不成的,提交广州仲裁委员会仲裁。
|
||
|
||
第八条 其他约定
|
||
本合同一式两份,甲乙双方各执一份,具有同等法律效力。
|
||
|
||
签订日期:2024年1月15日
|
||
`;
|
||
|
||
// 示例合同文本 B(修改版本)
|
||
const CONTRACT_B = `中国烟草合同(修订版本)
|
||
|
||
第一条 合同双方
|
||
甲方:中国烟草总公司广东省公司
|
||
乙方:XX供应商有限公司
|
||
|
||
第二条 合同标的
|
||
甲方向乙方采购烟草包装材料,具体型号为:
|
||
1. 硬盒包装纸 15000箱(数量增加)
|
||
2. 烟用滤棒 5000箱
|
||
3. 商标纸 3000箱(新增项目)
|
||
总金额:人民币壹佰伍拾万元整(¥1,500,000.00)
|
||
|
||
第三条 交付时间
|
||
乙方应在签订合同后45个工作日内完成全部交付。
|
||
如遇不可抗力,交付时间可顺延,但乙方应及时通知甲方。
|
||
|
||
第四条 质量标准
|
||
产品应符合国家烟草行业标准 YC/T 207-2014 及甲方企业标准。
|
||
|
||
第五条 付款方式
|
||
1. 签订合同后,甲方支付合同总额30%作为预付款;
|
||
2. 收到货物并验收合格后,于15个工作日内支付剩余70%款项。
|
||
|
||
第六条 违约责任
|
||
1. 乙方延期交付,每延迟一天支付合同总额1%的违约金(违约金比例提高)。
|
||
2. 产品质量不合格,乙方应无偿更换并承担相应损失。
|
||
3. 甲方延期付款,每延迟一天支付未付款项0.05%的违约金。
|
||
|
||
第七条 保密条款(新增)
|
||
双方应对合同内容及执行过程中获悉的商业秘密承担保密义务,保密期限为合同终止后2年。
|
||
|
||
第八条 争议解决
|
||
本合同履行过程中发生的争议,由双方协商解决;协商不成的,提交广州仲裁委员会仲裁。
|
||
|
||
第九条 其他约定
|
||
本合同一式两份,甲乙双方各执一份,具有同等法律效力。
|
||
|
||
签订日期:2024年3月20日
|
||
`;
|
||
|
||
export default function MonacoDemoPage() {
|
||
const [originalText, setOriginalText] = useState(CONTRACT_A);
|
||
const [modifiedText, setModifiedText] = useState(CONTRACT_B);
|
||
const diffEditorRef = useRef<any>(null);
|
||
const [diffCount, setDiffCount] = useState<number>(0);
|
||
const [currentDiff, setCurrentDiff] = useState<number>(0);
|
||
const [editorLoaded, setEditorLoaded] = 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);
|
||
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<string> => {
|
||
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;
|
||
};
|
||
|
||
// 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. 检测文件类型
|
||
const fileType = detectFileType(filePath);
|
||
|
||
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);
|
||
|
||
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('不支持的文件类型');
|
||
setTextContent('');
|
||
}
|
||
} catch (error) {
|
||
console.error('文档加载失败:', error);
|
||
toastService.error(`文档加载失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||
setDocInfo(null);
|
||
setTextContent('');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 检查 Monaco Editor 是否已加载
|
||
useEffect(() => {
|
||
if (typeof window !== 'undefined') {
|
||
Promise.all([
|
||
import('@monaco-editor/react'),
|
||
import('monaco-editor')
|
||
]).then(([reactMod, editorMod]) => {
|
||
DiffEditor = reactMod.DiffEditor;
|
||
editor = editorMod.editor;
|
||
setEditorLoaded(true);
|
||
});
|
||
}
|
||
}, []);
|
||
|
||
// Monaco Editor 挂载后的回调
|
||
const handleEditorDidMount = (editorInstance: any) => {
|
||
diffEditorRef.current = editorInstance;
|
||
|
||
// 获取差异数量
|
||
const lineChanges = editor.getLineChanges();
|
||
if (lineChanges) {
|
||
setDiffCount(lineChanges.length);
|
||
console.log(`发现 ${lineChanges.length} 处差异:`, lineChanges);
|
||
}
|
||
};
|
||
|
||
// 跳转到下一个差异
|
||
const goToNextDiff = () => {
|
||
if (!diffEditorRef.current) return;
|
||
|
||
const lineChanges = diffEditorRef.current.getLineChanges();
|
||
if (!lineChanges || lineChanges.length === 0) return;
|
||
|
||
const nextIndex = (currentDiff + 1) % lineChanges.length;
|
||
const nextChange = lineChanges[nextIndex];
|
||
|
||
// 跳转到差异位置(修改后的编辑器)
|
||
const modifiedEditor = diffEditorRef.current.getModifiedEditor();
|
||
modifiedEditor.revealLineInCenter(nextChange.modifiedStartLineNumber);
|
||
modifiedEditor.setPosition({
|
||
lineNumber: nextChange.modifiedStartLineNumber,
|
||
column: 1
|
||
});
|
||
|
||
setCurrentDiff(nextIndex);
|
||
};
|
||
|
||
// 跳转到上一个差异
|
||
const goToPreviousDiff = () => {
|
||
if (!diffEditorRef.current) return;
|
||
|
||
const lineChanges = diffEditorRef.current.getLineChanges();
|
||
if (!lineChanges || lineChanges.length === 0) return;
|
||
|
||
const prevIndex = currentDiff === 0 ? lineChanges.length - 1 : currentDiff - 1;
|
||
const prevChange = lineChanges[prevIndex];
|
||
|
||
// 跳转到差异位置(修改后的编辑器)
|
||
const modifiedEditor = diffEditorRef.current.getModifiedEditor();
|
||
modifiedEditor.revealLineInCenter(prevChange.modifiedStartLineNumber);
|
||
modifiedEditor.setPosition({
|
||
lineNumber: prevChange.modifiedStartLineNumber,
|
||
column: 1
|
||
});
|
||
|
||
setCurrentDiff(prevIndex);
|
||
};
|
||
|
||
// 重置为示例文本
|
||
const resetToExample = () => {
|
||
setOriginalText(CONTRACT_A);
|
||
setModifiedText(CONTRACT_B);
|
||
setCurrentDiff(0);
|
||
setUseExample(true);
|
||
setDoc1Info(null);
|
||
setDoc2Info(null);
|
||
|
||
// 重新计算差异数量
|
||
setTimeout(() => {
|
||
if (diffEditorRef.current) {
|
||
const lineChanges = diffEditorRef.current.getLineChanges();
|
||
if (lineChanges) {
|
||
setDiffCount(lineChanges.length);
|
||
}
|
||
}
|
||
}, 100);
|
||
};
|
||
|
||
// 从URL参数加载文档(支持 PDF 和 Word)
|
||
const loadDocumentsFromUrl = () => {
|
||
if (typeof window === 'undefined') return;
|
||
|
||
const searchParams = new URLSearchParams(window.location.search);
|
||
const doc1Param = searchParams.get('doc1') || searchParams.get('pdf1'); // 兼容旧参数名
|
||
const doc2Param = searchParams.get('doc2') || searchParams.get('pdf2'); // 兼容旧参数名
|
||
|
||
// 只有在传参时才加载文档,否则使用默认的 mock 数据(示例合同)
|
||
if (doc1Param || doc2Param) {
|
||
setUseExample(false);
|
||
|
||
// 文档1
|
||
if (doc1Param) {
|
||
const doc1Url = doc1Param.startsWith('/') ? doc1Param : '/' + doc1Param; // 相对路径,确保以 / 开头
|
||
setDoc1Url(doc1Url);
|
||
loadDocumentAndExtractText(doc1Url, doc1Param, setDoc1Info, setIsLoadingDoc1, setOriginalText);
|
||
}
|
||
|
||
// 文档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(() => {
|
||
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',
|
||
borderBottom: '1px solid #e0e0e0',
|
||
backgroundColor: '#fff',
|
||
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
|
||
}}>
|
||
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 600, color: '#333' }}>
|
||
<i className="ri-file-text-line" style={{ marginRight: '8px' }}></i>
|
||
Monaco Editor - 合同差异对比演示
|
||
</h1>
|
||
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: '14px' }}>
|
||
使用 Monaco Diff Editor 逐行对比两份合同文本的差异
|
||
</p>
|
||
</div>
|
||
|
||
{/* 工具栏 */}
|
||
<div style={{
|
||
padding: '12px 24px',
|
||
borderBottom: '1px solid #e0e0e0',
|
||
backgroundColor: '#f5f5f5',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between'
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||
{/* 差异统计 */}
|
||
<div style={{
|
||
padding: '6px 12px',
|
||
backgroundColor: '#fff',
|
||
border: '1px solid #d0d0d0',
|
||
borderRadius: '4px',
|
||
fontSize: '14px',
|
||
color: '#333'
|
||
}}>
|
||
<i className="ri-git-compare-line" style={{ marginRight: '6px', color: '#00684a' }}></i>
|
||
发现 <strong style={{ color: '#00684a' }}>{diffCount}</strong> 处差异
|
||
{diffCount > 0 && ` (当前: ${currentDiff + 1}/${diffCount})`}
|
||
</div>
|
||
|
||
{/* 导航按钮 */}
|
||
<button
|
||
onClick={goToPreviousDiff}
|
||
disabled={diffCount === 0}
|
||
style={{
|
||
padding: '6px 12px',
|
||
backgroundColor: '#fff',
|
||
border: '1px solid #d0d0d0',
|
||
borderRadius: '4px',
|
||
cursor: diffCount === 0 ? 'not-allowed' : 'pointer',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '6px',
|
||
opacity: diffCount === 0 ? 0.5 : 1
|
||
}}
|
||
>
|
||
<i className="ri-arrow-up-line"></i>
|
||
上一处差异
|
||
</button>
|
||
|
||
<button
|
||
onClick={goToNextDiff}
|
||
disabled={diffCount === 0}
|
||
style={{
|
||
padding: '6px 12px',
|
||
backgroundColor: '#fff',
|
||
border: '1px solid #d0d0d0',
|
||
borderRadius: '4px',
|
||
cursor: diffCount === 0 ? 'not-allowed' : 'pointer',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '6px',
|
||
opacity: diffCount === 0 ? 0.5 : 1
|
||
}}
|
||
>
|
||
<i className="ri-arrow-down-line"></i>
|
||
下一处差异
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '12px' }}>
|
||
{/* 重置按钮 */}
|
||
<button
|
||
onClick={resetToExample}
|
||
style={{
|
||
padding: '6px 12px',
|
||
backgroundColor: '#fff',
|
||
border: '1px solid #d0d0d0',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '6px'
|
||
}}
|
||
>
|
||
<i className="ri-refresh-line"></i>
|
||
重置示例
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 文档加载信息 */}
|
||
{!useExample && (doc1Info || doc2Info || isLoadingDoc1 || isLoadingDoc2) && 2==1 && (
|
||
<div style={{
|
||
padding: '12px 24px',
|
||
backgroundColor: '#fff3cd',
|
||
borderBottom: '1px solid #ffc107',
|
||
fontSize: '14px',
|
||
color: '#856404'
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '24px' }}>
|
||
<i className="ri-file-text-line" style={{ fontSize: '18px', marginTop: '2px' }}></i>
|
||
<div style={{ flex: 1 }}>
|
||
<strong>文档信息:</strong>
|
||
<div style={{ display: 'flex', gap: '24px', marginTop: '8px' }}>
|
||
{/* 文档 1 信息 */}
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>📄 文档1(左侧/原始)</div>
|
||
{isLoadingDoc1 ? (
|
||
<div style={{ color: '#666' }}>⏳ 加载中...</div>
|
||
) : doc1Info ? (
|
||
<div>
|
||
<div>类型: <span style={{
|
||
color: doc1Info.fileType === 'pdf' ? '#007bff' : doc1Info.fileType === 'docx' ? '#28a745' : '#dc3545',
|
||
fontWeight: 'bold'
|
||
}}>
|
||
{doc1Info.fileType === 'pdf' ? '📕 PDF文档' : doc1Info.fileType === 'docx' ? '📘 Word文档' : '❌ 未知类型'}
|
||
</span></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>
|
||
|
||
{/* 文档 2 信息 */}
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>📄 文档2(右侧/修改)</div>
|
||
{isLoadingDoc2 ? (
|
||
<div style={{ color: '#666' }}>⏳ 加载中...</div>
|
||
) : doc2Info ? (
|
||
<div>
|
||
<div>类型: <span style={{
|
||
color: doc2Info.fileType === 'pdf' ? '#007bff' : doc2Info.fileType === 'docx' ? '#28a745' : '#dc3545',
|
||
fontWeight: 'bold'
|
||
}}>
|
||
{doc2Info.fileType === 'pdf' ? '📕 PDF文档' : doc2Info.fileType === 'docx' ? '📘 Word文档' : '❌ 未知类型'}
|
||
</span></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>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 说明信息 */}
|
||
<div style={{
|
||
padding: '12px 24px',
|
||
backgroundColor: '#e7f3ff',
|
||
borderBottom: '1px solid #b3d9ff',
|
||
fontSize: '14px',
|
||
color: '#004085'
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
|
||
<i className="ri-information-line" style={{ fontSize: '18px', marginTop: '2px' }}></i>
|
||
<div style={{ flex: 1 }}>
|
||
<strong>差异高亮说明:</strong>
|
||
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
|
||
<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 和 Word,使用相对路径):
|
||
<code style={{
|
||
display: 'block',
|
||
marginTop: '4px',
|
||
padding: '8px',
|
||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||
borderRadius: '4px',
|
||
fontSize: '12px',
|
||
wordBreak: 'break-all'
|
||
}}>
|
||
/monaco-demo?doc1=相对路径1&doc2=相对路径2
|
||
</code>
|
||
<div style={{ marginTop: '4px', fontSize: '12px' }}>
|
||
<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>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Diff Editor 主体 */}
|
||
<div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
|
||
{editorLoaded && DiffEditor ? (
|
||
<DiffEditor
|
||
height="100%"
|
||
language="plaintext"
|
||
original={originalText}
|
||
modified={modifiedText}
|
||
onMount={handleEditorDidMount}
|
||
theme="vs"
|
||
options={{
|
||
// 编辑器选项
|
||
readOnly: true, // 只读模式
|
||
renderSideBySide: true, // 并排显示(false为内联模式)
|
||
ignoreTrimWhitespace: false, // 不忽略首尾空格差异
|
||
renderWhitespace: 'selection', // 显示选中区域的空格
|
||
fontSize: 14, // 字体大小
|
||
lineNumbers: 'on', // 显示行号
|
||
minimap: {
|
||
enabled: true // 显示缩略图
|
||
},
|
||
scrollBeyondLastLine: false, // 禁止滚动超过最后一行
|
||
wordWrap: 'on', // 自动换行
|
||
automaticLayout: true, // 自动调整布局
|
||
|
||
// Diff 特定选项
|
||
renderIndicators: true, // 显示差异指示器
|
||
diffWordWrap: 'on', // Diff 模式下自动换行
|
||
enableSplitViewResizing: true, // 允许调整分屏比例
|
||
diffAlgorithm: 'advanced', // 使用高级差异算法
|
||
}}
|
||
/>
|
||
) : (
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
height: '100%',
|
||
backgroundColor: '#f5f5f5'
|
||
}}>
|
||
<div style={{ textAlign: 'center' }}>
|
||
<div style={{
|
||
width: '48px',
|
||
height: '48px',
|
||
border: '4px solid #f3f3f3',
|
||
borderTop: '4px solid #00684a',
|
||
borderRadius: '50%',
|
||
animation: 'spin 1s linear infinite',
|
||
margin: '0 auto 16px'
|
||
}}></div>
|
||
<div style={{ fontSize: '16px', color: '#333' }}>
|
||
正在加载 Monaco Editor...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 文档加载中的遮罩层 */}
|
||
{(isLoadingDoc1 || isLoadingDoc2) && (
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1000
|
||
}}>
|
||
<div style={{ textAlign: 'center' }}>
|
||
<div style={{
|
||
width: '48px',
|
||
height: '48px',
|
||
border: '4px solid #f3f3f3',
|
||
borderTop: '4px solid #00684a',
|
||
borderRadius: '50%',
|
||
animation: 'spin 1s linear infinite',
|
||
margin: '0 auto 16px'
|
||
}}></div>
|
||
<div style={{ fontSize: '16px', color: '#333' }}>
|
||
正在加载文档并提取文本...
|
||
</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>
|
||
)}
|
||
</div>
|
||
|
||
{/* 添加旋转动画 */}
|
||
<style>{`
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
`}</style>
|
||
|
||
{/* 页面底部信息 */}
|
||
<div style={{
|
||
padding: '8px 24px',
|
||
borderTop: '1px solid #e0e0e0',
|
||
backgroundColor: '#f9f9f9',
|
||
fontSize: '12px',
|
||
color: '#666',
|
||
display: 'flex',
|
||
justifyContent: 'space-between'
|
||
}}>
|
||
<span>
|
||
<i className="ri-code-line"></i> 基于 Monaco Editor (VS Code 核心编辑器)
|
||
</span>
|
||
<span>
|
||
提示:可使用鼠标滚轮缩放,Ctrl+F 搜索
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|