feat(v1.2): 体验升级 — 快捷键系统 + 标签拖拽 + SFTP拖拽上传 + 组件拆分 + 响应式

任务1: 全局快捷键管理器 — useKeyboardShortcuts hook
  Ctrl+Tab/Shift+Tab标签切换, Ctrl+1~9跳转, F11全屏, Ctrl+Shift+N新建标签

任务2: 标签拖拽排序 — tabStore.moveTab + TabBar HTML5 DnD
  不可变更新, 拖拽视觉反馈(opacity+指示线), 活跃标签底部accent-blue指示线

任务3: SFTP拖拽上传 + useSFTP调试清理
  FileListPanel拖拽覆盖层(Upload图标+虚线边框), 5个调试函数→createLogger,
  disconnect()正确调用IPC, 删除mock data

任务4: SFTP右键菜单完善 — 重命名/复制路径/属性/权限
  FilePropertiesDialog+ChmodDialog, sftpStat/sftpChmod IPC全栈实现

任务5-6: 侧边栏分组折叠动画 + 宽度可拖拽调整
  Grid 0fr→1fr折叠动画, ChevronRight旋转, resize handle 180~400px, settings记忆

任务7: 响应式布局 + 视觉优化V1~V5
  useMediaQuery hook, <800px侧边栏overlay模式, SFTP垂直堆叠, WelcomeView响应式grid

任务8: Loading/Skeleton骨架屏
  Skeleton/SkeletonLine/SkeletonCircle/SkeletonBlock + FileListSkeleton/SessionTreeSkeleton/TabContentSkeleton

任务9: 空状态设计
  EmptyState通用组件, SessionTree/FileListPanel/WelcomeView/SFTPView空状态引导

任务10: TerminalView组件拆分重构 711行→285行
  useTerminalConnection(300行) + useTerminalSearch(123行)
  + TerminalOverlay(98行) + TerminalSearchBar(74行)

33 files changed, 2682 insertions(+), 872 deletions(-)
This commit is contained in:
OpsXShell Dev committed 2026-04-18 18:35:49 +08:00
1 parent d344900118
commit caba5f4c0d
33 files changed
+2683 -873

No files matched your search

+44
View File
@@ -15,6 +15,8 @@ export const SFTP_CHANNELS = {
RENAME: 'sftp:rename',
DELETE: 'sftp:delete',
MKDIR: 'sftp:mkdir',
STAT: 'sftp:stat',
CHMOD: 'sftp:chmod',
PROGRESS: 'sftp:progress',
} as const
@@ -193,4 +195,46 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
return { success: false, error: error.message }
}
})
// SFTP stat
ipcMain.handle(SFTP_CHANNELS.STAT, async (
_event: IpcMainInvokeEvent,
sftpId: string,
filePath: string
) => {
logHeader('📋 SFTP IPC STAT')
logInfo('sftpId', sftpId)
logInfo('filePath', filePath)
try {
const stats = await sshService.sftpStat(sftpId, filePath)
logSuccess(`Stat complete: ${stats.size} bytes`)
return { success: true, stats }
} catch (error: any) {
logError(`Stat failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SFTP chmod
ipcMain.handle(SFTP_CHANNELS.CHMOD, async (
_event: IpcMainInvokeEvent,
sftpId: string,
filePath: string,
mode: number
) => {
logHeader('🔒 SFTP IPC CHMOD')
logInfo('sftpId', sftpId)
logInfo('filePath', filePath)
logInfo('mode', mode.toString(8))
try {
await sshService.sftpChmod(sftpId, filePath, mode)
logSuccess('Chmod complete')
return { success: true }
} catch (error: any) {
logError(`Chmod failed: ${error.message}`)
return { success: false, error: error.message }
}
})
}
+2
View File
@@ -25,6 +25,8 @@ const api = {
sftpRename: (sftpId: string, oldPath: string, newPath: string) => ipcRenderer.invoke('sftp:rename', sftpId, oldPath, newPath),
sftpDelete: (sftpId: string, path: string, isDirectory: boolean) => ipcRenderer.invoke('sftp:delete', sftpId, path, isDirectory),
sftpMkdir: (sftpId: string, path: string) => ipcRenderer.invoke('sftp:mkdir', sftpId, path),
sftpStat: (sftpId: string, filePath: string) => ipcRenderer.invoke('sftp:stat', sftpId, filePath),
sftpChmod: (sftpId: string, filePath: string, mode: number) => ipcRenderer.invoke('sftp:chmod', sftpId, filePath, mode),
// 端口转发
sshForwardLocal: (connectionId: string, localPort: number, remoteHost: string, remotePort: number) =>
+50
View File
@@ -480,6 +480,56 @@ export class SSHService {
})
}
async sftpStat(sftpId: string, filePath: string): Promise<{
size: number
mode: number
mtime: number
atime: number
isDirectory: boolean
isFile: boolean
isSymbolicLink: boolean
}> {
const session = this.sftps.get(sftpId)
if (!session) {
throw new Error('SFTP session not found')
}
return new Promise((resolve, reject) => {
session.sftp.stat(filePath, (err, stats) => {
if (err) {
reject(err)
return
}
resolve({
size: stats.size,
mode: stats.mode,
mtime: stats.mtime,
atime: stats.atime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
isSymbolicLink: stats.isSymbolicLink(),
})
})
})
}
async sftpChmod(sftpId: string, filePath: string, mode: number): Promise<void> {
const session = this.sftps.get(sftpId)
if (!session) {
throw new Error('SFTP session not found')
}
return new Promise((resolve, reject) => {
session.sftp.chmod(filePath, mode, (err) => {
if (err) {
reject(err)
return
}
resolve()
})
})
}
// 端口转发
// ===== 本地端口转发 =====
async forwardLocal(