diff --git a/package-lock.json b/package-lock.json index 9a32be58..720db82a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.6.0", "dependencies": { "@hookform/resolvers": "^5.1.1", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", @@ -2950,6 +2951,29 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "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": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5311,6 +5335,13 @@ "dev": true, "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": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -12234,6 +12265,16 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "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": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -14891,6 +14932,12 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index fd4ee4fb..9fa0bed4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@hookform/resolvers": "^5.1.1", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx index 31d6b3e9..0689569f 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx @@ -5,6 +5,7 @@ import { useFileSelection } from "./hooks/useFileSelection"; import { useDragAndDrop } from "./hooks/useDragAndDrop"; import { WindowManager, useWindowManager } from "./components/WindowManager"; import { FileWindow } from "./components/FileWindow"; +import { DiffWindow } from "./components/DiffWindow"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; @@ -1021,64 +1022,37 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { return; } - // 在新窗口中打开两个文件进行对比 + // 使用专用的DiffWindow进行文件对比 console.log('Opening diff comparison:', file1.name, 'vs', file2.name); - // 计算第一个窗口位置 - const offsetX1 = 100; - const offsetY1 = 100; + // 计算窗口位置 + const offsetX = 100; + const offsetY = 80; - // 计算第二个窗口位置(偏移) - const offsetX2 = 450; - const offsetY2 = 120; - - // 创建第一个文件窗口 - const windowId1 = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const createWindowComponent1 = (windowId: string) => ( - ( + ); openWindow({ - id: windowId1, - type: 'file', - title: `${file1.name} (对比文件1)`, + id: windowId, + type: 'diff', + title: `文件对比: ${file1.name} ↔ ${file2.name}`, isMaximized: false, - component: createWindowComponent1, + component: createWindowComponent, zIndex: Date.now() }); - // 稍后打开第二个文件窗口 - setTimeout(() => { - const windowId2 = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const createWindowComponent2 = (windowId: string) => ( - - ); - - openWindow({ - id: windowId2, - type: 'file', - title: `${file2.name} (对比文件2)`, - isMaximized: false, - component: createWindowComponent2, - zIndex: Date.now() + 1 - }); - }, 200); - - toast.success(`正在打开文件对比: ${file1.name} 与 ${file2.name}`); + toast.success(`正在对比文件: ${file1.name} 与 ${file2.name}`); } // 过滤文件并添加新建的临时项目 diff --git a/src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx b/src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx new file mode 100644 index 00000000..7b7ecd27 --- /dev/null +++ b/src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx @@ -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(''); + const [content2, setContent2] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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 = { + '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 ( +
+
+
+

正在加载文件对比...

+
+
+ ); + } + + if (error) { + return ( +
+
+ +

{error}

+ +
+
+ ); + } + + return ( +
+ {/* 工具栏 */} +
+
+
+
+ 对比: + {file1.name} + + {file2.name} +
+
+ +
+ {/* 视图切换 */} + + + {/* 行号切换 */} + + + {/* 下载按钮 */} + + + + + {/* 刷新按钮 */} + +
+
+
+ + {/* Diff编辑器 */} +
+ +
+
+

初始化编辑器...

+
+
+ } + /> +
+ + ); +} \ No newline at end of file diff --git a/src/ui/Desktop/Apps/File Manager/components/DiffWindow.tsx b/src/ui/Desktop/Apps/File Manager/components/DiffWindow.tsx new file mode 100644 index 00000000..87569572 --- /dev/null +++ b/src/ui/Desktop/Apps/File Manager/components/DiffWindow.tsx @@ -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 ( + + + + ); +} \ No newline at end of file