From 004e1adcf56b6a150a6d0d31043b345b0a8f7cde Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sat, 6 Dec 2025 22:32:57 +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 --- ...itDivider.tsx => TerminalSplitDivider.tsx} | 13 +- .../components/Terminal/TerminalSplitNode.tsx | 191 ++++++++++++++++++ .../components/Terminal/TerminalSplitView.tsx | 190 ++--------------- .../src/components/Terminal/useSplitView.ts | 74 ++++++- .../List/components/SSHWindowManager.tsx | 63 +++++- 5 files changed, 344 insertions(+), 187 deletions(-) rename frontend/src/components/Terminal/{SplitDivider.tsx => TerminalSplitDivider.tsx} (86%) create mode 100644 frontend/src/components/Terminal/TerminalSplitNode.tsx diff --git a/frontend/src/components/Terminal/SplitDivider.tsx b/frontend/src/components/Terminal/TerminalSplitDivider.tsx similarity index 86% rename from frontend/src/components/Terminal/SplitDivider.tsx rename to frontend/src/components/Terminal/TerminalSplitDivider.tsx index 9a97b966..d77a5339 100644 --- a/frontend/src/components/Terminal/SplitDivider.tsx +++ b/frontend/src/components/Terminal/TerminalSplitDivider.tsx @@ -1,16 +1,21 @@ /** - * 分屏分隔条组件 - * 可拖动调整分屏大小 + * Terminal 分屏分隔条 + * 可拖动调整两个分屏之间的大小比例 */ import React, { useRef, useState, useEffect } from 'react'; import type { LayoutOrientation } from './types'; -interface SplitDividerProps { +interface TerminalSplitDividerProps { orientation: LayoutOrientation; onResize: (delta: number) => void; } -export const SplitDivider: React.FC = ({ orientation, onResize }) => { +/** + * 分屏分隔条组件 + * - 水平分割:垂直拖动条,左右调整 + * - 垂直分割:水平拖动条,上下调整 + */ +export const TerminalSplitDivider: React.FC = ({ orientation, onResize }) => { const [isDragging, setIsDragging] = useState(false); const startPosRef = useRef(0); diff --git a/frontend/src/components/Terminal/TerminalSplitNode.tsx b/frontend/src/components/Terminal/TerminalSplitNode.tsx new file mode 100644 index 00000000..837e72e5 --- /dev/null +++ b/frontend/src/components/Terminal/TerminalSplitNode.tsx @@ -0,0 +1,191 @@ +/** + * Terminal 分屏节点渲染器 + * 负责递归渲染分屏树的每个节点(Group 或 Container) + */ +import React from 'react'; +import { Terminal } from './Terminal'; +import { TerminalSplitDivider } from './TerminalSplitDivider'; +import { X, Plus } from 'lucide-react'; +import type { SplitNode, TerminalTab, TerminalConnectionConfig, TerminalAuditConfig, TerminalToolbarConfig } from './types'; + +/** + * 分屏节点渲染器 Props + */ +export interface TerminalSplitNodeProps { + node: SplitNode; + activeGroupId: string; + onTabClick: (groupId: string, tabId: string) => void; + onTabClose: (groupId: string, tabId: string) => void; + onNewTab: (groupId?: string) => void; + onSplitUp: () => void; + onSplitDown: () => void; + onSplitLeft: () => void; + onSplitRight: () => void; + onFocus: (groupId: string) => void; + onResize: (nodeId: string, delta: number) => void; + getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig; + getAuditConfig: () => TerminalAuditConfig; + getToolbarConfig: () => TerminalToolbarConfig; +} + +/** + * 递归渲染分屏节点 + * - Group 节点:渲染 Tab 栏和 Terminal + * - Container 节点:递归渲染子节点,插入分隔条 + */ +const TerminalSplitNodeComponent: React.FC = ({ + node, + activeGroupId, + onTabClick, + onTabClose, + onNewTab, + onSplitUp, + onSplitDown, + onSplitLeft, + onSplitRight, + onFocus, + onResize, + getConnectionConfig, + getAuditConfig, + getToolbarConfig, +}) => { + // 渲染 Group 节点(包含 Tabs 和 Terminal) + if (node.type === 'group') { + const isActive = node.id === activeGroupId; + const connectionConfigs: Record = {}; + node.tabs.forEach(tab => { + connectionConfigs[tab.id] = getConnectionConfig(tab); + }); + const auditConfig = getAuditConfig(); + const toolbarConfig = getToolbarConfig(); + + return ( +
{ + console.log(`[Group Click] Activating group: ${node.id}`); + onFocus(node.id); + }} + > + {/* Tab栏 */} +
+
+ {node.tabs.map((tab) => ( +
{ + e.stopPropagation(); + console.log(`[Tab Click] groupId: ${node.id}, tabId: ${tab.id}`); + onTabClick(node.id, tab.id); + }} + > + + {tab.serverName} + + +
+ ))} + +
+
+ + {/* 终端内容 */} +
+ {node.tabs.map((tab) => ( +
+ +
+ ))} +
+
+ ); + } + + // 渲染 Container 节点(递归渲染子节点) + const isHorizontal = node.orientation === 'horizontal'; + + return ( +
+ {node.children.map((child, index) => ( + +
+ {/* 递归渲染子节点 */} + +
+ + {/* 分隔条 - 不在最后一个节点之后显示 */} + {index < node.children.length - 1 && ( + onResize(child.id, delta)} + /> + )} +
+ ))} +
+ ); +}; + +/** + * 使用 React.memo 优化性能 + * 只在 props 变化时才重新渲染 + */ +export const TerminalSplitNode = React.memo(TerminalSplitNodeComponent); diff --git a/frontend/src/components/Terminal/TerminalSplitView.tsx b/frontend/src/components/Terminal/TerminalSplitView.tsx index 278c792c..8a785054 100644 --- a/frontend/src/components/Terminal/TerminalSplitView.tsx +++ b/frontend/src/components/Terminal/TerminalSplitView.tsx @@ -1,13 +1,11 @@ /** - * Terminal 分屏组件 - * 支持树形嵌套布局,类似 VS Code 的编辑器分屏系统 + * Terminal 分屏视图 - 主组件 + * 管理分屏状态、键盘快捷键,并委托给 TerminalSplitNode 进行递归渲染 */ import React, { useEffect } from 'react'; -import { Terminal } from './Terminal'; -import { SplitDivider } from './SplitDivider'; -import { X, Plus } from 'lucide-react'; import { useSplitView } from './useSplitView'; -import type { SplitNode, TerminalTab, TerminalConnectionConfig, TerminalAuditConfig, TerminalToolbarConfig } from './types'; +import { TerminalSplitNode } from './TerminalSplitNode'; +import type { TerminalTab, TerminalConnectionConfig, TerminalAuditConfig, TerminalToolbarConfig } from './types'; // 对外暴露的主组件Props export interface TerminalSplitViewProps { @@ -15,179 +13,23 @@ export interface TerminalSplitViewProps { getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig; getAuditConfig: () => TerminalAuditConfig; getToolbarConfig: () => TerminalToolbarConfig; + onCloseAllReady?: (closeAllFn: () => void) => void; // 暴露closeAll函数给外部 } -// 内部递归组件Props -interface SplitNodeRendererProps { - node: SplitNode; - activeGroupId: string; - onTabClick: (groupId: string, tabId: string) => void; - onTabClose: (groupId: string, tabId: string) => void; - onNewTab: () => void; - onSplitUp: () => void; - onSplitDown: () => void; - onSplitLeft: () => void; - onSplitRight: () => void; - onFocus: (groupId: string) => void; - onResize: (nodeId: string, delta: number) => void; - getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig; - getAuditConfig: () => TerminalAuditConfig; - getToolbarConfig: () => TerminalToolbarConfig; -} - -const SplitNodeRendererComponent: React.FC = ({ - node, - activeGroupId, - onTabClick, - onTabClose, - onNewTab, - onSplitUp, - onSplitDown, - onSplitLeft, - onSplitRight, - onFocus, - onResize, - getConnectionConfig, - getAuditConfig, - getToolbarConfig, -}) => { - // 渲染编辑器组(叶子节点) - if (node.type === 'group') { - const isActive = node.id === activeGroupId; - const connectionConfigs: Record = {}; - node.tabs.forEach(tab => { - connectionConfigs[tab.id] = getConnectionConfig(tab); - }); - const auditConfig = getAuditConfig(); - const toolbarConfig = getToolbarConfig(); - - return ( -
- {/* Tab栏 */} -
-
- {node.tabs.map((tab) => ( -
onTabClick(node.id, tab.id)} - > - - {tab.serverName} - - -
- ))} - -
-
- - {/* 终端内容 */} -
- {node.tabs.map((tab) => ( -
- -
- ))} -
-
- ); - } - - // 渲染容器(内部节点)- 递归渲染子节点 - const isHorizontal = node.orientation === 'horizontal'; - - return ( -
- {node.children.map((child, index) => ( - -
- -
- - {/* 分隔条 - 不在最后一个节点之后显示 */} - {index < node.children.length - 1 && ( - onResize(child.id, delta)} - /> - )} -
- ))} -
- ); -}; - -// 使用 React.memo 优化,避免不必要的重新渲染 -const SplitNodeRenderer = React.memo(SplitNodeRendererComponent); - /** * Terminal 分屏视图 - 主组件 - * 管理分屏状态并渲染分屏树 + * 职责: + * 1. 管理分屏状态(通过 useSplitView hook) + * 2. 处理键盘快捷键 + * 3. 暴露 closeAll 方法供外部调用 + * 4. 委托给 TerminalSplitNode 进行递归渲染 */ export const TerminalSplitView: React.FC = ({ initialTab, getConnectionConfig, getAuditConfig, getToolbarConfig, + onCloseAllReady, }) => { const { layout, @@ -199,10 +41,18 @@ export const TerminalSplitView: React.FC = ({ splitInGroup, switchTab, closeTab, + closeAll, resizeGroups, setActiveGroupId, } = useSplitView({ initialTab }); + // 暴露closeAll方法给外部 + useEffect(() => { + if (onCloseAllReady) { + onCloseAllReady(closeAll); + } + }, [onCloseAllReady, closeAll]); + // 快捷键支持 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -235,7 +85,7 @@ export const TerminalSplitView: React.FC = ({ return (
- { }; // 在组中拆分(创建新标签页) - const splitInGroup = useCallback(() => { - const activeGroup = getActiveGroup(); - if (!activeGroup) return; + const splitInGroup = useCallback((groupId?: string) => { + // 如果提供了groupId就用它,否则用activeGroupId + const targetGroupId = groupId || activeGroupId; + console.log(`[splitInGroup] Target groupId: ${targetGroupId}`); + + const targetGroup = findGroup(layout.root, targetGroupId); + if (!targetGroup) { + console.log('[splitInGroup] Target group not found'); + return; + } - const activeTab = activeGroup.tabs.find(t => t.id === activeGroup.activeTabId); - if (!activeTab) return; + console.log(`[splitInGroup] Target group: ${targetGroup.id}, tabs count: ${targetGroup.tabs.length}`); + + const activeTab = targetGroup.tabs.find(t => t.id === targetGroup.activeTabId); + if (!activeTab) { + console.log('[splitInGroup] No active tab in target group'); + return; + } + + console.log(`[splitInGroup] Creating new tab based on: ${activeTab.serverName}`); const newTab: TerminalTab = { id: `tab-${Date.now()}`, @@ -217,19 +231,32 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => { }; setLayout(prev => ({ - root: updateNode(prev.root, activeGroup.id, { - ...activeGroup, - tabs: [...activeGroup.tabs.map(t => ({ ...t, isActive: false })), newTab], + root: updateNode(prev.root, targetGroup.id, { + ...targetGroup, + tabs: [...targetGroup.tabs.map(t => ({ ...t, isActive: false })), newTab], activeTabId: newTab.id, }), })); - }, [activeGroupId, getActiveGroup]); + + // 更新activeGroupId + setActiveGroupId(targetGroupId); + + console.log(`[splitInGroup] New tab created: ${newTab.id} in group: ${targetGroup.id}`); + }, [activeGroupId, findGroup, layout.root]); // 切换标签页 const switchTab = useCallback((groupId: string, tabId: string) => { + console.log(`[switchTab] Switching to group: ${groupId}, tab: ${tabId}`); + + // 先设置activeGroupId,确保后续操作使用正确的组 + setActiveGroupId(groupId); + setLayout(prev => { const group = findGroup(prev.root, groupId); - if (!group) return prev; + if (!group) { + console.log(`[switchTab] Group not found: ${groupId}`); + return prev; + } return { root: updateNode(prev.root, groupId, { @@ -239,7 +266,6 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => { }), }; }); - setActiveGroupId(groupId); }, [findGroup]); // 关闭标签页 @@ -368,6 +394,31 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => { }); }, [findParent]); + // 关闭所有Tab(窗口关闭时调用) + const closeAll = useCallback(() => { + console.log('[useSplitView] 关闭所有Tab,断开所有连接'); + const manager = TerminalInstanceManager.getInstance(); + + // 收集所有Tab ID + const allTabIds: string[] = []; + const collectTabIds = (node: SplitNode) => { + if (node.type === 'group') { + allTabIds.push(...node.tabs.map(t => t.id)); + } else if (node.type === 'container') { + node.children.forEach(collectTabIds); + } + }; + collectTabIds(layout.root); + + // 销毁所有Terminal实例 + allTabIds.forEach(tabId => { + manager.destroy(tabId); + console.log(`[useSplitView] 已销毁Terminal实例: ${tabId}`); + }); + + console.log(`[useSplitView] 共关闭 ${allTabIds.length} 个Tab`); + }, [layout.root]); + return { layout, activeGroupId, @@ -378,6 +429,7 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => { splitInGroup, switchTab, closeTab, + closeAll, resizeGroups, setActiveGroupId, }; diff --git a/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx b/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx index 8ac593ae..30f40b99 100644 --- a/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx +++ b/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx @@ -2,7 +2,7 @@ * SSH 窗口管理器 * 使用 TerminalSplitView 支持分屏功能 */ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { TerminalWindowManager, TerminalSplitView, @@ -13,6 +13,63 @@ import { } from '@/components/Terminal'; import type { ServerResponse } from '../types'; +/** + * SSH Terminal 分屏视图包装器 + * 负责注册优雅关闭方法到window对象 + */ +interface SSHTerminalSplitViewWrapperProps { + windowId: string; + initialTab: TerminalTab; + getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig; + getAuditConfig: () => TerminalAuditConfig; + getToolbarConfig: () => TerminalToolbarConfig; + onCloseReady: () => void; +} + +const SSHTerminalSplitViewWrapper: React.FC = ({ + windowId, + initialTab, + getConnectionConfig, + getAuditConfig, + getToolbarConfig, + onCloseReady, +}) => { + const closeAllRef = useRef<(() => void) | null>(null); + + // 注册优雅关闭方法到window对象 + useEffect(() => { + const closeMethodName = `__closeSSH_${windowId}`; + (window as any)[closeMethodName] = () => { + console.log(`[SSHTerminalSplitViewWrapper] 调用优雅关闭方法: ${windowId}`); + // 先关闭所有连接 + if (closeAllRef.current) { + closeAllRef.current(); + } + // 再通知窗口可以关闭 + onCloseReady(); + }; + console.log(`[SSHTerminalSplitViewWrapper] 注册优雅关闭方法: ${closeMethodName}`); + + return () => { + delete (window as any)[closeMethodName]; + console.log(`[SSHTerminalSplitViewWrapper] 注销优雅关闭方法: ${closeMethodName}`); + }; + }, [windowId, onCloseReady]); + + return ( + { + // 保存closeAll函数到ref + closeAllRef.current = closeAllFn; + }} + /> + ); +}; + interface SSHWindowManagerProps { onOpenWindow?: (windowId: string) => void; } @@ -65,11 +122,13 @@ export const SSHWindowManager: React.FC = ({ onOpenWindow }); return ( - ); }}