重新设计diff功能:使用Monaco Editor实现专业级文件对比
新增功能: - DiffViewer组件:基于Monaco Editor DiffEditor的专业代码对比 - DiffWindow组件:专用的diff对比窗口包装器 - 并排/内联视图切换功能 - 多语言语法高亮支持 - 智能文件类型检测 - 完整的工具栏(下载、刷新、视图切换、行号切换) 技术改进: - 替代原来的两个独立文件窗口方案 - 使用Monaco Editor提供VS Code同级的对比体验 - 支持大文件错误处理和SSH连接自动重连 - 专业的差异高亮显示(新增/删除/修改) 依赖更新: - 新增@monaco-editor/react依赖
This commit is contained in:
47
package-lock.json
generated
47
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
@@ -2950,6 +2951,29 @@
|
|||||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@monaco-editor/loader": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"state-local": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@monaco-editor/react": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@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/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -5311,6 +5335,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@types/verror": {
|
"node_modules/@types/verror": {
|
||||||
"version": "1.10.11",
|
"version": "1.10.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
||||||
@@ -12234,6 +12265,16 @@
|
|||||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/monaco-editor": {
|
||||||
|
"version": "0.53.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz",
|
||||||
|
"integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/trusted-types": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz",
|
||||||
@@ -14891,6 +14932,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/state-local": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useFileSelection } from "./hooks/useFileSelection";
|
|||||||
import { useDragAndDrop } from "./hooks/useDragAndDrop";
|
import { useDragAndDrop } from "./hooks/useDragAndDrop";
|
||||||
import { WindowManager, useWindowManager } from "./components/WindowManager";
|
import { WindowManager, useWindowManager } from "./components/WindowManager";
|
||||||
import { FileWindow } from "./components/FileWindow";
|
import { FileWindow } from "./components/FileWindow";
|
||||||
|
import { DiffWindow } from "./components/DiffWindow";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -1021,64 +1022,37 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在新窗口中打开两个文件进行对比
|
// 使用专用的DiffWindow进行文件对比
|
||||||
console.log('Opening diff comparison:', file1.name, 'vs', file2.name);
|
console.log('Opening diff comparison:', file1.name, 'vs', file2.name);
|
||||||
|
|
||||||
// 计算第一个窗口位置
|
// 计算窗口位置
|
||||||
const offsetX1 = 100;
|
const offsetX = 100;
|
||||||
const offsetY1 = 100;
|
const offsetY = 80;
|
||||||
|
|
||||||
// 计算第二个窗口位置(偏移)
|
// 创建diff窗口
|
||||||
const offsetX2 = 450;
|
const windowId = `diff-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
const offsetY2 = 120;
|
const createWindowComponent = (windowId: string) => (
|
||||||
|
<DiffWindow
|
||||||
// 创建第一个文件窗口
|
|
||||||
const windowId1 = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
const createWindowComponent1 = (windowId: string) => (
|
|
||||||
<FileWindow
|
|
||||||
windowId={windowId}
|
windowId={windowId}
|
||||||
file={file1}
|
file1={file1}
|
||||||
|
file2={file2}
|
||||||
sshSessionId={sshSessionId}
|
sshSessionId={sshSessionId}
|
||||||
sshHost={currentHost}
|
sshHost={currentHost}
|
||||||
initialX={offsetX1}
|
initialX={offsetX}
|
||||||
initialY={offsetY1}
|
initialY={offsetY}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
openWindow({
|
openWindow({
|
||||||
id: windowId1,
|
id: windowId,
|
||||||
type: 'file',
|
type: 'diff',
|
||||||
title: `${file1.name} (对比文件1)`,
|
title: `文件对比: ${file1.name} ↔ ${file2.name}`,
|
||||||
isMaximized: false,
|
isMaximized: false,
|
||||||
component: createWindowComponent1,
|
component: createWindowComponent,
|
||||||
zIndex: Date.now()
|
zIndex: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
// 稍后打开第二个文件窗口
|
toast.success(`正在对比文件: ${file1.name} 与 ${file2.name}`);
|
||||||
setTimeout(() => {
|
|
||||||
const windowId2 = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
const createWindowComponent2 = (windowId: string) => (
|
|
||||||
<FileWindow
|
|
||||||
windowId={windowId}
|
|
||||||
file={file2}
|
|
||||||
sshSessionId={sshSessionId}
|
|
||||||
sshHost={currentHost}
|
|
||||||
initialX={offsetX2}
|
|
||||||
initialY={offsetY2}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
openWindow({
|
|
||||||
id: windowId2,
|
|
||||||
type: 'file',
|
|
||||||
title: `${file2.name} (对比文件2)`,
|
|
||||||
isMaximized: false,
|
|
||||||
component: createWindowComponent2,
|
|
||||||
zIndex: Date.now() + 1
|
|
||||||
});
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
toast.success(`正在打开文件对比: ${file1.name} 与 ${file2.name}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤文件并添加新建的临时项目
|
// 过滤文件并添加新建的临时项目
|
||||||
|
|||||||
303
src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx
Normal file
303
src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { DiffEditor } from '@monaco-editor/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
RefreshCw,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
ArrowLeftRight,
|
||||||
|
FileText
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { readSSHFile, downloadSSHFile, getSSHStatus, connectSSH } from '@/ui/main-axios';
|
||||||
|
import type { FileItem, SSHHost } from '../../../../types/index.js';
|
||||||
|
|
||||||
|
interface DiffViewerProps {
|
||||||
|
file1: FileItem;
|
||||||
|
file2: FileItem;
|
||||||
|
sshSessionId: string;
|
||||||
|
sshHost: SSHHost;
|
||||||
|
onDownload1?: () => void;
|
||||||
|
onDownload2?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiffViewer({
|
||||||
|
file1,
|
||||||
|
file2,
|
||||||
|
sshSessionId,
|
||||||
|
sshHost,
|
||||||
|
onDownload1,
|
||||||
|
onDownload2
|
||||||
|
}: DiffViewerProps) {
|
||||||
|
const [content1, setContent1] = useState<string>('');
|
||||||
|
const [content2, setContent2] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [diffMode, setDiffMode] = useState<'side-by-side' | 'inline'>('side-by-side');
|
||||||
|
const [showLineNumbers, setShowLineNumbers] = useState(true);
|
||||||
|
|
||||||
|
// 确保SSH连接有效
|
||||||
|
const ensureSSHConnection = async () => {
|
||||||
|
try {
|
||||||
|
const status = await getSSHStatus(sshSessionId);
|
||||||
|
if (!status.connected) {
|
||||||
|
await connectSSH(sshSessionId, {
|
||||||
|
hostId: sshHost.id,
|
||||||
|
ip: sshHost.ip,
|
||||||
|
port: sshHost.port,
|
||||||
|
username: sshHost.username,
|
||||||
|
password: sshHost.password,
|
||||||
|
sshKey: sshHost.key,
|
||||||
|
keyPassword: sshHost.keyPassword,
|
||||||
|
authType: sshHost.authType,
|
||||||
|
credentialId: sshHost.credentialId,
|
||||||
|
userId: sshHost.userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SSH connection check/reconnect failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载文件内容
|
||||||
|
const loadFileContents = async () => {
|
||||||
|
if (file1.type !== 'file' || file2.type !== 'file') {
|
||||||
|
setError('只能对比文件类型的项目');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// 确保SSH连接有效
|
||||||
|
await ensureSSHConnection();
|
||||||
|
|
||||||
|
// 并行加载两个文件
|
||||||
|
const [response1, response2] = await Promise.all([
|
||||||
|
readSSHFile(sshSessionId, file1.path),
|
||||||
|
readSSHFile(sshSessionId, file2.path)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setContent1(response1.content || '');
|
||||||
|
setContent2(response2.content || '');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load files for diff:', error);
|
||||||
|
|
||||||
|
const errorData = error?.response?.data;
|
||||||
|
if (errorData?.tooLarge) {
|
||||||
|
setError(`文件过大: ${errorData.error}`);
|
||||||
|
} else if (error.message?.includes('connection') || error.message?.includes('established')) {
|
||||||
|
setError(`SSH连接失败。请检查与 ${sshHost.name} (${sshHost.ip}:${sshHost.port}) 的连接`);
|
||||||
|
} else {
|
||||||
|
setError(`加载文件失败: ${error.message || errorData?.error || '未知错误'}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const handleDownloadFile = async (file: FileItem) => {
|
||||||
|
try {
|
||||||
|
await ensureSSHConnection();
|
||||||
|
const response = await downloadSSHFile(sshSessionId, file.path);
|
||||||
|
|
||||||
|
if (response?.content) {
|
||||||
|
const byteCharacters = atob(response.content);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
const blob = new Blob([byteArray], { type: response.mimeType || 'application/octet-stream' });
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = response.fileName || file.name;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
toast.success(`文件下载成功: ${file.name}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to download file:', error);
|
||||||
|
toast.error(`下载失败: ${error.message || '未知错误'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取文件语言类型
|
||||||
|
const getFileLanguage = (fileName: string): string => {
|
||||||
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
'js': 'javascript',
|
||||||
|
'jsx': 'javascript',
|
||||||
|
'ts': 'typescript',
|
||||||
|
'tsx': 'typescript',
|
||||||
|
'py': 'python',
|
||||||
|
'java': 'java',
|
||||||
|
'c': 'c',
|
||||||
|
'cpp': 'cpp',
|
||||||
|
'cs': 'csharp',
|
||||||
|
'php': 'php',
|
||||||
|
'rb': 'ruby',
|
||||||
|
'go': 'go',
|
||||||
|
'rs': 'rust',
|
||||||
|
'html': 'html',
|
||||||
|
'css': 'css',
|
||||||
|
'scss': 'scss',
|
||||||
|
'less': 'less',
|
||||||
|
'json': 'json',
|
||||||
|
'xml': 'xml',
|
||||||
|
'yaml': 'yaml',
|
||||||
|
'yml': 'yaml',
|
||||||
|
'md': 'markdown',
|
||||||
|
'sql': 'sql',
|
||||||
|
'sh': 'shell',
|
||||||
|
'bash': 'shell',
|
||||||
|
'ps1': 'powershell',
|
||||||
|
'dockerfile': 'dockerfile'
|
||||||
|
};
|
||||||
|
return languageMap[ext || ''] || 'plaintext';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
useEffect(() => {
|
||||||
|
loadFileContents();
|
||||||
|
}, [file1, file2, sshSessionId]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex items-center justify-center bg-dark-bg">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||||
|
<p className="text-sm text-muted-foreground">正在加载文件对比...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex items-center justify-center bg-dark-bg">
|
||||||
|
<div className="text-center max-w-md">
|
||||||
|
<FileText className="w-16 h-16 mx-auto mb-4 text-red-500 opacity-50" />
|
||||||
|
<p className="text-red-500 mb-4">{error}</p>
|
||||||
|
<Button onClick={loadFileContents} variant="outline">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col bg-dark-bg">
|
||||||
|
{/* 工具栏 */}
|
||||||
|
<div className="flex-shrink-0 border-b border-dark-border p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">对比:</span>
|
||||||
|
<span className="font-medium text-green-400 mx-2">{file1.name}</span>
|
||||||
|
<ArrowLeftRight className="w-4 h-4 inline mx-1" />
|
||||||
|
<span className="font-medium text-blue-400">{file2.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 视图切换 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setDiffMode(diffMode === 'side-by-side' ? 'inline' : 'side-by-side')}
|
||||||
|
>
|
||||||
|
{diffMode === 'side-by-side' ? '并排' : '内联'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 行号切换 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowLineNumbers(!showLineNumbers)}
|
||||||
|
>
|
||||||
|
{showLineNumbers ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 下载按钮 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDownloadFile(file1)}
|
||||||
|
title={`下载 ${file1.name}`}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-1" />
|
||||||
|
{file1.name}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDownloadFile(file2)}
|
||||||
|
title={`下载 ${file2.name}`}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-1" />
|
||||||
|
{file2.name}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 刷新按钮 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadFileContents}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diff编辑器 */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<DiffEditor
|
||||||
|
original={content1}
|
||||||
|
modified={content2}
|
||||||
|
language={getFileLanguage(file1.name)}
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
renderSideBySide: diffMode === 'side-by-side',
|
||||||
|
lineNumbers: showLineNumbers ? 'on' : 'off',
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
fontSize: 13,
|
||||||
|
wordWrap: 'off',
|
||||||
|
automaticLayout: true,
|
||||||
|
readOnly: true,
|
||||||
|
originalEditable: false,
|
||||||
|
modifiedEditable: false,
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'visible',
|
||||||
|
horizontal: 'visible'
|
||||||
|
},
|
||||||
|
diffWordWrap: 'off',
|
||||||
|
ignoreTrimWhitespace: false
|
||||||
|
}}
|
||||||
|
loading={
|
||||||
|
<div className="h-full flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||||
|
<p className="text-sm text-muted-foreground">初始化编辑器...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
src/ui/Desktop/Apps/File Manager/components/DiffWindow.tsx
Normal file
75
src/ui/Desktop/Apps/File Manager/components/DiffWindow.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DraggableWindow } from './DraggableWindow';
|
||||||
|
import { DiffViewer } from './DiffViewer';
|
||||||
|
import { useWindowManager } from './WindowManager';
|
||||||
|
import type { FileItem, SSHHost } from '../../../../types/index.js';
|
||||||
|
|
||||||
|
interface DiffWindowProps {
|
||||||
|
windowId: string;
|
||||||
|
file1: FileItem;
|
||||||
|
file2: FileItem;
|
||||||
|
sshSessionId: string;
|
||||||
|
sshHost: SSHHost;
|
||||||
|
initialX?: number;
|
||||||
|
initialY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiffWindow({
|
||||||
|
windowId,
|
||||||
|
file1,
|
||||||
|
file2,
|
||||||
|
sshSessionId,
|
||||||
|
sshHost,
|
||||||
|
initialX = 150,
|
||||||
|
initialY = 100
|
||||||
|
}: DiffWindowProps) {
|
||||||
|
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } = useWindowManager();
|
||||||
|
|
||||||
|
const currentWindow = windows.find(w => w.id === windowId);
|
||||||
|
|
||||||
|
// 窗口操作处理
|
||||||
|
const handleClose = () => {
|
||||||
|
closeWindow(windowId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMinimize = () => {
|
||||||
|
minimizeWindow(windowId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaximize = () => {
|
||||||
|
maximizeWindow(windowId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
focusWindow(windowId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!currentWindow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraggableWindow
|
||||||
|
title={`文件对比: ${file1.name} ↔ ${file2.name}`}
|
||||||
|
initialX={initialX}
|
||||||
|
initialY={initialY}
|
||||||
|
initialWidth={1200}
|
||||||
|
initialHeight={700}
|
||||||
|
minWidth={800}
|
||||||
|
minHeight={500}
|
||||||
|
onClose={handleClose}
|
||||||
|
onMinimize={handleMinimize}
|
||||||
|
onMaximize={handleMaximize}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
isMaximized={currentWindow.isMaximized}
|
||||||
|
zIndex={currentWindow.zIndex}
|
||||||
|
>
|
||||||
|
<DiffViewer
|
||||||
|
file1={file1}
|
||||||
|
file2={file2}
|
||||||
|
sshSessionId={sshSessionId}
|
||||||
|
sshHost={sshHost}
|
||||||
|
/>
|
||||||
|
</DraggableWindow>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user