feat(v1.1): Toast通知系统 + 自定义对话框 + 终端搜索 + SFTP真实实现 + 标签持久化
任务7: Toast通知系统 — ToastProvider+4类型(success/error/warning/info)+右上角堆叠动画 任务8: 替换alert/confirm/prompt — ConfirmDialog+PromptDialog+useToast替换7处原生对话框 任务9: 终端搜索 — xterm SearchAddon Ctrl+F搜索栏(上一个/下一个/关闭) 任务10: SFTP上传/删除 — useSFTP.ts upload/delete方法实现 任务11: 会话恢复 — tabStore持久化到localStorage,启动恢复tabs 14 files changed, 1218 insertions(+), 241 deletions(-)
This commit is contained in:
1 parent
53274e2291
commit
d344900118
14 files changed
+1224
-247
No files matched your search
+165
-58
@@ -75,6 +75,9 @@ export interface UseSFTPReturn {
|
||||
uploadFiles: (files: FileList | null) => Promise<void>
|
||||
downloadFiles: () => Promise<void>
|
||||
|
||||
// 远程文件操作
|
||||
deleteRemote: (files: FileItem[]) => Promise<boolean>
|
||||
|
||||
// 排序
|
||||
sortField: SortField
|
||||
sortOrder: SortOrder
|
||||
@@ -372,89 +375,190 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn {
|
||||
setRemoteFiles([])
|
||||
}, [connectionId, sftpId, getConnection])
|
||||
|
||||
// Upload files
|
||||
// Upload files (real implementation via IPC)
|
||||
const uploadFiles = useCallback(async (files: FileList | null) => {
|
||||
if (!files || !sftpId) return
|
||||
|
||||
if (!files || files.length === 0 || !sftpId || !window.electronAPI) {
|
||||
logWarning('uploadFiles: missing files, sftpId, or electronAPI')
|
||||
return
|
||||
}
|
||||
|
||||
const newTasks: TransferTask[] = Array.from(files).map(file => ({
|
||||
id: `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'upload',
|
||||
type: 'upload' as const,
|
||||
fileName: file.name,
|
||||
filePath: `${localPath}/${file.name}`,
|
||||
status: 'pending',
|
||||
status: 'pending' as const,
|
||||
progress: 0,
|
||||
total: file.size,
|
||||
transferred: 0,
|
||||
}))
|
||||
|
||||
setTransferQueue(prev => [...prev, ...newTasks])
|
||||
|
||||
// Process uploads
|
||||
for (const task of newTasks) {
|
||||
try {
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'transferring' } : t
|
||||
))
|
||||
|
||||
// Note: Actual upload implementation would go here
|
||||
// This is a simplified version
|
||||
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'completed', transferred: t.total, progress: 100 } : t
|
||||
))
|
||||
} catch (err) {
|
||||
logger.error('Upload failed', err)
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'failed', error: 'Upload failed' } : t
|
||||
))
|
||||
}
|
||||
}
|
||||
}, [sftpId, localPath, remotePath])
|
||||
|
||||
// Download files
|
||||
setTransferQueue(prev => [...prev, ...newTasks])
|
||||
|
||||
// Set up progress listener scoped to this batch
|
||||
const progressHandler = (_event: any, _sftpId: string, progress: { transferred: number; total: number; percentage: number }) => {
|
||||
// Update the first task that is currently transferring
|
||||
setTransferQueue(prev => {
|
||||
const idx = prev.findIndex(t => t.type === 'upload' && t.status === 'transferring')
|
||||
if (idx === -1) return prev
|
||||
const t = prev[idx]
|
||||
return [
|
||||
...prev.slice(0, idx),
|
||||
{ ...t, transferred: progress.transferred, total: progress.total, progress: progress.percentage },
|
||||
...prev.slice(idx + 1),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
window.electronAPI.onSFTPProgress(progressHandler)
|
||||
|
||||
try {
|
||||
for (const task of newTasks) {
|
||||
const remoteFilePath = `${remotePath}/${task.fileName}`.replace(/\/+/g, '/')
|
||||
logInfo('Uploading', `${task.filePath} -> ${remoteFilePath}`)
|
||||
|
||||
// Mark as transferring
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'transferring', startTime: Date.now() } : t
|
||||
))
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.sftpUpload(sftpId, task.filePath, remoteFilePath)
|
||||
|
||||
if (result.success) {
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'completed', transferred: t.total, progress: 100 } : t
|
||||
))
|
||||
logSuccess(`Upload complete: ${task.fileName}`)
|
||||
} else {
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'failed', error: result.error || 'Upload failed' } : t
|
||||
))
|
||||
logError(`Upload failed: ${task.fileName} - ${result.error}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error('Upload failed', err)
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'failed', error: err?.message || 'Upload failed' } : t
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh remote directory after all uploads
|
||||
loadRemoteDirectory(remotePath)
|
||||
} finally {
|
||||
window.electronAPI.removeListener?.('sftp:progress', progressHandler)
|
||||
}
|
||||
}, [sftpId, localPath, remotePath, loadRemoteDirectory])
|
||||
|
||||
// Download files (real implementation via IPC)
|
||||
const downloadFiles = useCallback(async () => {
|
||||
if (!sftpId || remoteSelected.size === 0) return
|
||||
|
||||
if (!sftpId || remoteSelected.size === 0 || !window.electronAPI) return
|
||||
|
||||
const filesToDownload = remoteFiles.filter(f => remoteSelected.has(f.name))
|
||||
|
||||
|
||||
const newTasks: TransferTask[] = filesToDownload.map(file => ({
|
||||
id: `download-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'download',
|
||||
type: 'download' as const,
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
status: 'pending',
|
||||
status: 'pending' as const,
|
||||
progress: 0,
|
||||
total: file.size,
|
||||
transferred: 0,
|
||||
}))
|
||||
|
||||
|
||||
setTransferQueue(prev => [...prev, ...newTasks])
|
||||
|
||||
// Process downloads
|
||||
for (const task of newTasks) {
|
||||
try {
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'transferring' } : t
|
||||
|
||||
// Set up progress listener
|
||||
const progressHandler = (_event: any, _sftpId: string, progress: { transferred: number; total: number; percentage: number }) => {
|
||||
setTransferQueue(prev => {
|
||||
const idx = prev.findIndex(t => t.type === 'download' && t.status === 'transferring')
|
||||
if (idx === -1) return prev
|
||||
const t = prev[idx]
|
||||
return [
|
||||
...prev.slice(0, idx),
|
||||
{ ...t, transferred: progress.transferred, total: progress.total, progress: progress.percentage },
|
||||
...prev.slice(idx + 1),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
window.electronAPI.onSFTPProgress(progressHandler)
|
||||
|
||||
try {
|
||||
for (const task of newTasks) {
|
||||
const localFilePath = `${localPath}/${task.fileName}`
|
||||
logInfo('Downloading', `${task.filePath} -> ${localFilePath}`)
|
||||
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'transferring', startTime: Date.now() } : t
|
||||
))
|
||||
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.sftpDownload(sftpId, task.filePath, `${localPath}/${task.fileName}`)
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.sftpDownload(sftpId, task.filePath, localFilePath)
|
||||
|
||||
if (result.success) {
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'completed', transferred: t.total, progress: 100 } : t
|
||||
))
|
||||
logSuccess(`Download complete: ${task.fileName}`)
|
||||
} else {
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'failed', error: result.error || 'Download failed' } : t
|
||||
))
|
||||
logError(`Download failed: ${task.fileName} - ${result.error}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error('Download failed', err)
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'failed', error: err?.message || 'Download failed' } : t
|
||||
))
|
||||
}
|
||||
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'completed', transferred: t.total, progress: 100 } : t
|
||||
))
|
||||
} catch (err) {
|
||||
logger.error('Download failed', err)
|
||||
setTransferQueue(prev => prev.map(t =>
|
||||
t.id === task.id ? { ...t, status: 'failed', error: 'Download failed' } : t
|
||||
))
|
||||
}
|
||||
|
||||
// Refresh local directory after all downloads
|
||||
loadLocalDirectory(localPath)
|
||||
} finally {
|
||||
window.electronAPI.removeListener?.('sftp:progress', progressHandler)
|
||||
}
|
||||
}, [sftpId, remoteFiles, remoteSelected, localPath, loadLocalDirectory])
|
||||
|
||||
// Delete remote files/directories
|
||||
const deleteRemote = useCallback(async (files: FileItem[]): Promise<boolean> => {
|
||||
if (!sftpId || files.length === 0 || !window.electronAPI) {
|
||||
logWarning('deleteRemote: missing sftpId, files, or electronAPI')
|
||||
return false
|
||||
}
|
||||
|
||||
let allSuccess = true
|
||||
|
||||
for (const file of files) {
|
||||
logInfo('Deleting remote', `${file.path} (isDir: ${file.type === 'directory'})`)
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.sftpDelete(sftpId, file.path, file.type === 'directory')
|
||||
|
||||
if (result.success) {
|
||||
logSuccess(`Deleted: ${file.name}`)
|
||||
} else {
|
||||
logError(`Delete failed: ${file.name} - ${result.error}`)
|
||||
allSuccess = false
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error('Delete failed', err)
|
||||
logError(`Delete error: ${file.name} - ${err?.message}`)
|
||||
allSuccess = false
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh local directory
|
||||
loadLocalDirectory(localPath)
|
||||
}, [sftpId, remoteFiles, remoteSelected, localPath, loadLocalDirectory])
|
||||
|
||||
// Refresh remote directory after deletion
|
||||
if (allSuccess || files.length > 0) {
|
||||
loadRemoteDirectory(remotePath)
|
||||
}
|
||||
|
||||
return allSuccess
|
||||
}, [sftpId, remotePath, loadRemoteDirectory])
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
@@ -499,6 +603,9 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn {
|
||||
uploadFiles,
|
||||
downloadFiles,
|
||||
|
||||
// Remote file operations
|
||||
deleteRemote,
|
||||
|
||||
sortField,
|
||||
sortOrder,
|
||||
setSortField,
|
||||
|
||||
Reference in new issue
Block a user