feat: v0.3.0 版本 - SSH终端、SFTP、端口转发功能实现

核心功能:
- SSH终端: 密码/密钥认证、XTerm.js终端、多主题配色、IP高亮
- SFTP管理: 双栏文件管理、上传下载、传输队列
- 端口转发: 本地/远程转发、SOCKS5动态代理、规则管理

架构改进:
- 拆分大型组件 (SFTPView/TabBar/Sidebar)
- 添加统一日志和错误处理
- 修复React严格模式下的终端黑屏问题

UI更新:
- 新增端口转发视图
- 更新UI预览文档
- 左侧栏右键菜单支持SFTP快捷打开
This commit is contained in:
Logan.Li committed 2026-04-06 01:20:47 +08:00
1 parent cf1c054381
commit 484d850abd
105 files changed
+25330 -64

No files matched your search

+276
View File
@@ -0,0 +1,276 @@
/**
* SFTPView - SFTP 文件传输主组件 (简化版)
*
* 使用子组件拆分后的简化版本
*/
import { useState, useCallback } from 'react'
import { useSFTP } from '../hooks'
import { FileListPanel } from './sftp/FileListPanel'
import { TransferPanel } from './sftp/TransferPanel'
import { Toolbar } from './sftp/Toolbar'
import { ConnectionPanel } from './sftp/ConnectionPanel'
import { ContextMenu, type MenuItem } from './sftp/ContextMenu'
import { sortFiles } from './sftp/utils'
import type { FileItem, SortField, SortOrder } from '../../shared/types/sftp.types'
interface SFTPViewProps {
tabId: string
isActive?: boolean
}
export default function SFTPView({ tabId, isActive: _isActive = true }: SFTPViewProps) {
const {
sessions,
localPath,
localFiles,
localSelected,
setLocalSelected,
localHistory,
localHistoryIndex,
navigateLocal,
navigateLocalBack,
navigateLocalForward,
loadLocalDirectory,
remotePath,
remoteFiles,
remoteSelected,
setRemoteSelected,
remoteHistory,
remoteHistoryIndex,
navigateRemote,
navigateRemoteBack,
navigateRemoteForward,
loadRemoteDirectory,
isConnected,
isConnecting,
connect,
transferQueue,
uploadFiles,
downloadFiles,
error,
} = useSFTP({ tabId })
// UI State
const [sortField, setSortField] = useState<SortField>('name')
const [sortOrder, setSortOrder] = useState<SortOrder>('asc')
const [isTransferPanelOpen, setIsTransferPanelOpen] = useState(true)
const [showConnectionSelector, setShowConnectionSelector] = useState(false)
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; items: MenuItem[] } | null>(null)
// Sort handler
const handleSort = useCallback((field: SortField) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field)
setSortOrder('asc')
}
}, [sortField, sortOrder])
// Local selection
const toggleLocalSelect = useCallback((name: string) => {
setLocalSelected((prev: Set<string>) => {
const next = new Set(prev)
if (next.has(name)) next.delete(name)
else next.add(name)
return next
})
}, [setLocalSelected])
// Remote selection
const toggleRemoteSelect = useCallback((name: string) => {
setRemoteSelected((prev: Set<string>) => {
const next = new Set(prev)
if (next.has(name)) next.delete(name)
else next.add(name)
return next
})
}, [setRemoteSelected])
// Local context menu
const showLocalContextMenu = useCallback((e: React.MouseEvent, file: FileItem) => {
e.preventDefault()
const items: MenuItem[] = [
{
icon: '📂',
label: '打开',
action: () => file.type === 'directory' && navigateLocal(file.path),
},
{
icon: '📤',
label: '上传',
action: () => {},
},
]
setContextMenu({ x: e.clientX, y: e.clientY, items })
}, [navigateLocal])
// Remote context menu
const showRemoteContextMenu = useCallback((e: React.MouseEvent, file: FileItem) => {
e.preventDefault()
const items: MenuItem[] = [
{
icon: '📂',
label: '打开',
action: () => file.type === 'directory' && navigateRemote(file.path),
},
{
icon: '📥',
label: '下载',
action: () => {
setRemoteSelected(new Set([file.name]))
downloadFiles()
},
},
{
icon: '🗑️',
label: '删除',
danger: true,
action: () => {},
},
]
setContextMenu({ x: e.clientX, y: e.clientY, items })
}, [navigateRemote, downloadFiles, setRemoteSelected])
// Navigate up
const navigateLocalUp = useCallback(() => {
const parent = localPath.split('/').slice(0, -1).join('/') || '/'
navigateLocal(parent)
}, [localPath, navigateLocal])
const navigateRemoteUp = useCallback(() => {
const parent = remotePath.split('/').slice(0, -1).join('/') || '/'
navigateRemote(parent)
}, [remotePath, navigateRemote])
// Handle file open
const handleLocalOpen = useCallback((file: FileItem) => {
if (file.type === 'directory') {
navigateLocal(file.path)
}
}, [navigateLocal])
const handleRemoteOpen = useCallback((file: FileItem) => {
if (file.type === 'directory') {
navigateRemote(file.path)
}
}, [navigateRemote])
// Handle upload
const handleUpload = useCallback(() => {
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
input.onchange = (e) => {
uploadFiles((e.target as HTMLInputElement).files)
}
input.click()
}, [uploadFiles])
// Handle new folder
const handleNewFolder = useCallback(() => {
const name = prompt('输入文件夹名称:')
if (name && window.electronAPI?.localMkdir) {
window.electronAPI.localMkdir(`${localPath}/${name}`).then(() => {
loadLocalDirectory(localPath)
})
}
}, [localPath, loadLocalDirectory])
return (
<div className="h-full flex flex-col bg-dark-900">
{/* Connection Status */}
<ConnectionPanel
isConnected={isConnected}
isConnecting={isConnecting}
error={error}
sessions={sessions}
showSelector={showConnectionSelector}
onConnect={(sessionId) => sessionId && connect(sessionId)}
onShowSelector={() => setShowConnectionSelector(true)}
onCloseSelector={() => setShowConnectionSelector(false)}
/>
{/* Toolbar */}
<Toolbar
isConnected={isConnected}
onUpload={handleUpload}
onDownload={downloadFiles}
onRefresh={() => {
loadLocalDirectory(localPath)
if (isConnected) loadRemoteDirectory(remotePath)
}}
onNewFolder={handleNewFolder}
/>
{/* File Panels */}
<div className="flex-1 flex gap-4 p-4 min-h-0">
{/* Local Panel */}
<FileListPanel
title="本地"
isLocal={true}
path={localPath}
files={sortFiles(localFiles, sortField, sortOrder)}
selected={localSelected}
history={localHistory}
historyIndex={localHistoryIndex}
sortField={sortField}
sortOrder={sortOrder}
onNavigate={navigateLocal}
onNavigateBack={navigateLocalBack}
onNavigateForward={navigateLocalForward}
onNavigateUp={navigateLocalUp}
onSelect={toggleLocalSelect}
onOpen={handleLocalOpen}
onContextMenu={showLocalContextMenu}
onSort={handleSort}
contextMenu={contextMenu}
onCloseContextMenu={() => setContextMenu(null)}
/>
{/* Remote Panel */}
<FileListPanel
title="远程"
isLocal={false}
path={remotePath}
files={sortFiles(remoteFiles, sortField, sortOrder)}
selected={remoteSelected}
history={remoteHistory}
historyIndex={remoteHistoryIndex}
sortField={sortField}
sortOrder={sortOrder}
onNavigate={navigateRemote}
onNavigateBack={navigateRemoteBack}
onNavigateForward={navigateRemoteForward}
onNavigateUp={navigateRemoteUp}
onSelect={toggleRemoteSelect}
onOpen={handleRemoteOpen}
onContextMenu={showRemoteContextMenu}
onSort={handleSort}
contextMenu={contextMenu}
onCloseContextMenu={() => setContextMenu(null)}
/>
</div>
{/* Transfer Panel */}
<TransferPanel
tasks={transferQueue}
isOpen={isTransferPanelOpen}
onToggle={() => setIsTransferPanelOpen(!isTransferPanelOpen)}
onCancel={() => {}}
onRemove={() => {}}
/>
{/* Global Context Menu */}
{contextMenu && (
<ContextMenu
x={contextMenu.x}
y={contextMenu.y}
items={contextMenu.items}
onClose={() => setContextMenu(null)}
/>
)}
</div>
)
}