1. 添加 mocano-editor demo

2. 添加 react-pdf 高亮效果的 demo
This commit is contained in:
2025-11-24 18:41:14 +08:00
parent 1b546e6818
commit 9376e8af6d
9 changed files with 1908 additions and 5 deletions
+1 -1
View File
@@ -282,7 +282,7 @@ export async function getEntryModules(userRole: string | null | undefined, userA
return [];
}
console.log(`✅ [getEntryModules] 找到 ${modules.length} 个已启用的入口模块`);
// console.log(`✅ [getEntryModules] 找到 ${modules.length} 个已启用的入口模块`);
// 为每个模块查询关联的 document_types
const modulesWithTypes = await Promise.all(
+4
View File
@@ -7,6 +7,10 @@ import { Document, Page, pdfjs } from 'react-pdf';
import { DOCUMENT_URL } from '~/api/axios-client';
import { CollaboraViewer } from '~/components/collabora/CollaboraViewer';
// 导入react-pdf的CSS样式(文本层和注释层必需)
import 'react-pdf/dist/esm/Page/TextLayer.css';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
// 设置worker路径为public目录下的worker文件
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
// 2025/09/28 使用新版本的pdfjs-dist v4.8.69
+6 -3
View File
@@ -87,11 +87,14 @@ function isPathAllowed(pathname: string, allowedPaths: string[]): boolean {
}
}
// 根路径特殊处理
if (pathname === '/' || pathname === '/home') {
return true; // 首页通常对所有已登录用户开放
// 根路径特殊处理(仅根路径 '/' 对所有已登录用户开放)
if (pathname === '/') {
return true; // 根路径重定向到首页,始终允许
}
// /home 路由需要检查路由权限,不再特殊处理
// 如果用户的 routes 数据中没有 /home,则返回 403
return false;
}
+377
View File
@@ -0,0 +1,377 @@
/**
* 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>
);
}
File diff suppressed because it is too large Load Diff