Files
leaudit-platform-frontend/app/routes/monaco-demo.tsx
T
LiangShiyong d88cfc818b feat: 1. 实现一键替换。
2. 优化追加附件和模板上传的样式。
2025-12-03 12:07:56 +08:00

816 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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>
);
}