From 4d4ffabe05b8bdbea4a42c4749a85981a394c185 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sun, 7 Dec 2025 18:26:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99ssh=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=80=9A=E7=94=A8=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FileManager/ServerFilePanel.tsx | 73 +++++++---- .../components/FileManager/UploadPanel.tsx | 115 ++++++++++++++++-- frontend/src/components/Terminal/Terminal.tsx | 16 ++- .../src/components/Terminal/index.module.less | 4 + .../strategies/SSHConnectionStrategy.ts | 21 +++- 5 files changed, 193 insertions(+), 36 deletions(-) diff --git a/frontend/src/components/FileManager/ServerFilePanel.tsx b/frontend/src/components/FileManager/ServerFilePanel.tsx index bfe300fa..97d6d814 100644 --- a/frontend/src/components/FileManager/ServerFilePanel.tsx +++ b/frontend/src/components/FileManager/ServerFilePanel.tsx @@ -15,7 +15,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { ChevronRight, Folder, File, Home, ArrowLeft, Plus, Trash2, RefreshCw, Link } from 'lucide-react'; +import { ChevronRight, Folder, File, Home, ArrowLeft, Plus, Trash2, RefreshCw, Link, Copy, FileText } from 'lucide-react'; import { browseDirectory, createDirectory, removeFile, pathUtils, type RemoteFileInfo } from '@/services/fileService'; import { message } from 'antd'; import type { ServerFilePanelProps } from './types'; @@ -224,22 +224,20 @@ export const ServerFilePanel: React.FC = ({ {/* 文件列表 */}
- {/* 表头 */} - {!loading && files.length > 0 && ( -
-
名称
-
大小
-
权限
-
所有者
-
修改时间
-
-
- )} + {/* 表头 - 固定显示 */} +
+
名称
+
大小
+
权限
+
所有者
+
修改时间
+
+
{loading ? (
- {/* 骨架屏 - 模拟文件列表 */} + {/* 骨架屏 - 仅模拟数据行 */} {Array.from({ length: 8 }).map((_, i) => (
@@ -248,6 +246,7 @@ export const ServerFilePanel: React.FC = ({ +
))}
@@ -306,15 +305,45 @@ export const ServerFilePanel: React.FC = ({ })} - {/* 删除按钮 */} - + {/* 操作按钮 */} +
+ {/* 复制文件名按钮 */} + + {/* 复制路径按钮 */} + + {/* 删除按钮 */} + +
))}
diff --git a/frontend/src/components/FileManager/UploadPanel.tsx b/frontend/src/components/FileManager/UploadPanel.tsx index 1a87d1cf..1351dd20 100644 --- a/frontend/src/components/FileManager/UploadPanel.tsx +++ b/frontend/src/components/FileManager/UploadPanel.tsx @@ -4,6 +4,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; +import { Input } from '@/components/ui/input'; import { AlertDialog, AlertDialogAction, @@ -39,6 +40,7 @@ export const UploadPanel: React.FC = ({ const [isDragging, setIsDragging] = useState(false); const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const [pendingOverwriteItem, setPendingOverwriteItem] = useState(null); + const [renameFileName, setRenameFileName] = useState(''); // 重命名的新文件名 const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [pendingDeleteId, setPendingDeleteId] = useState(null); const [clearConfirmOpen, setClearConfirmOpen] = useState(false); @@ -96,10 +98,17 @@ export const UploadPanel: React.FC = ({ // 如果正在上传,先取消任务 if (item?.status === 'uploading' && item.taskId) { try { - await cancelUploadTask(serverId, item.taskId); - message.success('上传已取消'); - } catch (error) { + const cancelled = await cancelUploadTask(serverId, item.taskId); + if (cancelled) { + message.success('上传已取消'); + } else { + message.error('取消上传失败:任务未能成功取消'); + return; // 取消失败,不移除文件 + } + } catch (error: any) { console.error('取消上传失败:', error); + message.error(error.response?.data?.message || '取消上传失败'); + return; // 出错,不移除文件 } } @@ -129,6 +138,37 @@ export const UploadPanel: React.FC = ({ setPendingDeleteId(null); }; + // 生成重命名文件名(自动添加(1), (2), (3)等) + const generateRenamedFileName = async (originalName: string): Promise => { + try { + const files = await browseDirectory(serverId, currentPath); + const existingNames = new Set(files.map(f => f.name)); + + // 解析文件名和扩展名 + const lastDotIndex = originalName.lastIndexOf('.'); + const baseName = lastDotIndex > 0 ? originalName.substring(0, lastDotIndex) : originalName; + const extension = lastDotIndex > 0 ? originalName.substring(lastDotIndex) : ''; + + // 尝试生成不冲突的文件名 + let counter = 1; + let newName = `${baseName}(${counter})${extension}`; + + while (existingNames.has(newName) && counter < 100) { + counter++; + newName = `${baseName}(${counter})${extension}`; + } + + return newName; + } catch (error) { + console.error('生成重命名文件名失败:', error); + // 如果失败,默认返回(1) + const lastDotIndex = originalName.lastIndexOf('.'); + const baseName = lastDotIndex > 0 ? originalName.substring(0, lastDotIndex) : originalName; + const extension = lastDotIndex > 0 ? originalName.substring(lastDotIndex) : ''; + return `${baseName}(1)${extension}`; + } + }; + // 上传单个文件(异步上传+轮询) const uploadSingleFile = async (item: UploadFileItem, overwrite: boolean = false, skipCheck: boolean = false) => { const remotePath = pathUtils.join(currentPath, item.file.name); @@ -141,7 +181,9 @@ export const UploadPanel: React.FC = ({ const fileExists = files.some(f => f.name === item.file.name && !f.isDirectory); if (fileExists) { - // 文件已存在,弹出确认对话框 + // 文件已存在,生成建议的重命名文件名 + const suggestedName = await generateRenamedFileName(item.file.name); + setRenameFileName(suggestedName); setPendingOverwriteItem(item); setConfirmDialogOpen(true); return; // 暂停上传,等待用户决定 @@ -196,6 +238,37 @@ export const UploadPanel: React.FC = ({ // 以覆盖模式重新上传,跳过检查 await uploadSingleFile(pendingOverwriteItem, true, true); setPendingOverwriteItem(null); + setRenameFileName(''); + } + }; + + // 处理用户重命名上传 + const handleRename = async () => { + setConfirmDialogOpen(false); + if (pendingOverwriteItem && renameFileName.trim()) { + // 创建新的文件项,使用重命名后的文件名 + const renamedFile = new File([pendingOverwriteItem.file], renameFileName, { + type: pendingOverwriteItem.file.type, + }); + + const renamedItem: UploadFileItem = { + ...pendingOverwriteItem, + file: renamedFile, + }; + + // 更新文件列表中的文件名 + setUploadFiles(prev => + prev.map(f => + f.id === pendingOverwriteItem.id + ? renamedItem + : f + ) + ); + + // 使用新文件名上传,跳过检查 + await uploadSingleFile(renamedItem, false, true); + setPendingOverwriteItem(null); + setRenameFileName(''); } }; @@ -207,11 +280,12 @@ export const UploadPanel: React.FC = ({ setUploadFiles(prev => prev.map(f => f.id === pendingOverwriteItem.id - ? { ...f, status: 'error', errorMessage: '用户取消覆盖' } + ? { ...f, status: 'error', errorMessage: '用户取消上传' } : f ) ); setPendingOverwriteItem(null); + setRenameFileName(''); } }; @@ -447,7 +521,7 @@ export const UploadPanel: React.FC = ({
- + {formatFileSize(item.file.size)} {item.status === 'uploading' && ( @@ -482,14 +556,39 @@ export const UploadPanel: React.FC = ({ 文件已存在 - - 文件 {pendingOverwriteItem?.file.name} 已存在,是否覆盖? + +
+ 文件 {pendingOverwriteItem?.file.name} 已存在 +
+
+ + setRenameFileName(e.target.value)} + placeholder="输入新文件名" + className="w-full" + onKeyDown={(e) => { + if (e.key === 'Enter' && renameFileName.trim()) { + handleRename(); + } + }} + /> +
取消 + 覆盖 diff --git a/frontend/src/components/Terminal/Terminal.tsx b/frontend/src/components/Terminal/Terminal.tsx index 9013fcf9..b80045e2 100644 --- a/frontend/src/components/Terminal/Terminal.tsx +++ b/frontend/src/components/Terminal/Terminal.tsx @@ -10,7 +10,7 @@ import { TerminalToolbar } from './TerminalToolbar'; import { FileManager } from '@/components/FileManager'; import type { TerminalProps, TerminalToolbarConfig } from './types'; import { TERMINAL_THEMES, getThemeByName } from './themes'; -import { Loader2, XCircle, ChevronUp, ChevronDown, X } from 'lucide-react'; +import { Loader2, XCircle, ChevronUp, ChevronDown, X, WifiOff } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { message } from 'antd'; import { TerminalInstanceManager } from './core/TerminalInstanceManager'; @@ -425,6 +425,20 @@ export const Terminal: React.FC = ({
)} + {/* 已断开连接 */} + {connectionStatus === 'disconnected' && ( +
+ +

已断开连接

+ {errorMessage && ( +

{errorMessage}

+ )} + +
+ )} + {/* 错误 */} {connectionStatus === 'error' && (
diff --git a/frontend/src/components/Terminal/index.module.less b/frontend/src/components/Terminal/index.module.less index 5dd0d44a..8529af07 100644 --- a/frontend/src/components/Terminal/index.module.less +++ b/frontend/src/components/Terminal/index.module.less @@ -137,6 +137,10 @@ color: #fb923c; } +.disconnected { + color: #94a3b8; +} + .error { color: #ef4444; } diff --git a/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts b/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts index f06f3dc4..e38fea4e 100644 --- a/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts +++ b/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts @@ -55,10 +55,12 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy { try { const ws = new WebSocket(wsUrl); this.ws = ws; + let isResolved = false; ws.onopen = () => { - this.notifyStatusChange('connected'); + this.notifyStatusChange('connected'); this.reconnectAttempts = 0; + isResolved = true; resolve(); }; @@ -68,18 +70,27 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy { ws.onerror = (error) => { console.error('[SSHConnectionStrategy] WebSocket error:', error); - this.notifyStatusChange('error'); - this.notifyError('WebSocket 连接错误'); - reject(error); + // WebSocket错误时不立即设置状态,等待onclose处理 }; ws.onclose = () => { - + console.log('[SSHConnectionStrategy] WebSocket closed, current status:', this.status); + + const wasConnecting = this.status === 'connecting'; + // 只有在已连接状态下关闭才尝试重连 if (this.status === 'connected' && this.config.autoReconnect) { this.handleReconnect(); } else { + // 其他情况统一显示为"已断开连接" this.notifyStatusChange('disconnected'); + if (wasConnecting) { + // 初次连接失败 + this.notifyError('无法连接到服务器'); + if (!isResolved) { + reject(new Error('无法连接到服务器')); + } + } } }; } catch (error) {