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
+69
View File
@@ -9,6 +9,7 @@
"@ant-design/icons": "^5.6.1",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/theme-one-dark": "^6.1.2",
"@monaco-editor/react": "^4.7.0",
"@remix-run/node": "^2.16.2",
"@remix-run/react": "^2.16.2",
"@remix-run/serve": "^2.16.2",
@@ -29,6 +30,7 @@
"jszip": "^3.10.1",
"katex": "^0.16.22",
"mammoth": "^1.9.0",
"monaco-editor": "^0.55.1",
"pdf-lib": "^1.17.1",
"pg": "^8.14.1",
"pm2": "^6.0.8",
@@ -1596,6 +1598,29 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.7.0",
"resolved": "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.7.0.tgz",
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.7.0",
"resolved": "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.7.0.tgz",
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
"license": "MIT",
"dependencies": {
"@monaco-editor/loader": "^1.5.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -3353,6 +3378,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
@@ -6794,6 +6826,15 @@
"jszip": ">=3.0.0"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -11495,6 +11536,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmmirror.com/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -18281,6 +18334,16 @@
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
"license": "MIT"
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
@@ -23621,6 +23684,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+2
View File
@@ -20,6 +20,7 @@
"@ant-design/icons": "^5.6.1",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/theme-one-dark": "^6.1.2",
"@monaco-editor/react": "^4.7.0",
"@remix-run/node": "^2.16.2",
"@remix-run/react": "^2.16.2",
"@remix-run/serve": "^2.16.2",
@@ -40,6 +41,7 @@
"jszip": "^3.10.1",
"katex": "^0.16.22",
"mammoth": "^1.9.0",
"monaco-editor": "^0.55.1",
"pdf-lib": "^1.17.1",
"pg": "^8.14.1",
"pm2": "^6.0.8",
Binary file not shown.
+1 -1
View File
@@ -53,7 +53,7 @@ export default defineConfig({
server: {
host: '0.0.0.0',
// port: 5173,
port: Number(process.env.PORT) || 51703,
port: Number(process.env.PORT) || 51709,
open: true,
// open: false,
allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表1