9376e8af6d
2. 添加 react-pdf 高亮效果的 demo
378 lines
13 KiB
TypeScript
378 lines
13 KiB
TypeScript
/**
|
||
* Monaco Editor 差异对比演示页面
|
||
*
|
||
* 功能:
|
||
* - 展示两份合同文本的差异对比
|
||
* - 支持逐行高亮显示差异
|
||
* - 提供差异导航功能
|
||
* - 后续可扩展文件上传功能
|
||
*/
|
||
|
||
import { type MetaFunction } from "@remix-run/node";
|
||
import { useState, useRef } from "react";
|
||
import { DiffEditor } from "@monaco-editor/react";
|
||
import type { editor } from "monaco-editor";
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "Monaco Diff Editor 演示 - 合同对比" },
|
||
{ name: "description", content: "使用 Monaco Editor 进行合同文本差异对比" }
|
||
];
|
||
};
|
||
|
||
// 示例合同文本 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<editor.IStandaloneDiffEditor | null>(null);
|
||
const [diffCount, setDiffCount] = useState<number>(0);
|
||
const [currentDiff, setCurrentDiff] = useState<number>(0);
|
||
|
||
// Monaco Editor 挂载后的回调
|
||
const handleEditorDidMount = (editor: editor.IStandaloneDiffEditor) => {
|
||
diffEditorRef.current = editor;
|
||
|
||
// 获取差异数量
|
||
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);
|
||
|
||
// 重新计算差异数量
|
||
setTimeout(() => {
|
||
if (diffEditorRef.current) {
|
||
const lineChanges = diffEditorRef.current.getLineChanges();
|
||
if (lineChanges) {
|
||
setDiffCount(lineChanges.length);
|
||
}
|
||
}
|
||
}, 100);
|
||
};
|
||
|
||
return (
|
||
<div className="monaco-demo-page" style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||
{/* 页面头部 */}
|
||
<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>
|
||
|
||
{/* 未来扩展:上传按钮 */}
|
||
<button
|
||
disabled
|
||
style={{
|
||
padding: '6px 12px',
|
||
backgroundColor: '#e0e0e0',
|
||
border: '1px solid #d0d0d0',
|
||
borderRadius: '4px',
|
||
cursor: 'not-allowed',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '6px',
|
||
opacity: 0.5
|
||
}}
|
||
>
|
||
<i className="ri-upload-2-line"></i>
|
||
上传文件(待开发)
|
||
</button>
|
||
</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>
|
||
<strong>差异高亮说明:</strong>
|
||
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
|
||
<li><span style={{ color: '#28a745', fontWeight: 'bold' }}>绿色</span>:新增的内容</li>
|
||
<li><span style={{ color: '#dc3545', fontWeight: 'bold' }}>红色</span>:删除的内容</li>
|
||
<li><span style={{ color: '#ffc107', fontWeight: 'bold' }}>黄色背景</span>:修改的行内差异</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Diff Editor 主体 */}
|
||
<div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
|
||
<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>
|
||
|
||
{/* 页面底部信息 */}
|
||
<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>
|
||
);
|
||
}
|