重写ssh前端组件,通用化
This commit is contained in:
parent
1fd72bf007
commit
004e1adcf5
@ -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<SplitDividerProps> = ({ orientation, onResize }) => {
|
||||
/**
|
||||
* 分屏分隔条组件
|
||||
* - 水平分割:垂直拖动条,左右调整
|
||||
* - 垂直分割:水平拖动条,上下调整
|
||||
*/
|
||||
export const TerminalSplitDivider: React.FC<TerminalSplitDividerProps> = ({ orientation, onResize }) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const startPosRef = useRef(0);
|
||||
|
||||
191
frontend/src/components/Terminal/TerminalSplitNode.tsx
Normal file
191
frontend/src/components/Terminal/TerminalSplitNode.tsx
Normal file
@ -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<TerminalSplitNodeProps> = ({
|
||||
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<string, TerminalConnectionConfig> = {};
|
||||
node.tabs.forEach(tab => {
|
||||
connectionConfigs[tab.id] = getConnectionConfig(tab);
|
||||
});
|
||||
const auditConfig = getAuditConfig();
|
||||
const toolbarConfig = getToolbarConfig();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full flex flex-col bg-white dark:bg-gray-900"
|
||||
onClick={() => {
|
||||
console.log(`[Group Click] Activating group: ${node.id}`);
|
||||
onFocus(node.id);
|
||||
}}
|
||||
>
|
||||
{/* Tab栏 */}
|
||||
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center px-2 py-1 gap-1 overflow-x-auto">
|
||||
{node.tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-t cursor-pointer transition-colors ${
|
||||
tab.isActive
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-b-2 border-blue-500'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`[Tab Click] groupId: ${node.id}, tabId: ${tab.id}`);
|
||||
onTabClick(node.id, tab.id);
|
||||
}}
|
||||
>
|
||||
<span className="text-xs truncate max-w-[150px]" title={tab.title}>
|
||||
{tab.serverName}
|
||||
</span>
|
||||
<button
|
||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-0.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTabClose(node.id, tab.id);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="flex items-center justify-center w-8 h-8 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`[+ Button Click] Creating tab in group: ${node.id}`);
|
||||
onNewTab(node.id); // 直接传递groupId
|
||||
}}
|
||||
title="新建终端 (Cmd+T)"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 终端内容 */}
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
{node.tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className="absolute inset-0"
|
||||
style={{ display: tab.isActive ? 'block' : 'none' }}
|
||||
>
|
||||
<Terminal
|
||||
id={tab.id}
|
||||
connection={connectionConfigs[tab.id]}
|
||||
audit={auditConfig}
|
||||
toolbar={toolbarConfig}
|
||||
isActive={tab.isActive}
|
||||
onSplitUp={onSplitUp}
|
||||
onSplitDown={onSplitDown}
|
||||
onSplitLeft={onSplitLeft}
|
||||
onSplitRight={onSplitRight}
|
||||
onSplitInGroup={onNewTab}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染 Container 节点(递归渲染子节点)
|
||||
const isHorizontal = node.orientation === 'horizontal';
|
||||
|
||||
return (
|
||||
<div className={`h-full w-full flex ${isHorizontal ? 'flex-row' : 'flex-col'}`}>
|
||||
{node.children.map((child, index) => (
|
||||
<React.Fragment key={child.id}>
|
||||
<div
|
||||
className="flex-shrink-0 flex-grow-0"
|
||||
style={{
|
||||
[isHorizontal ? 'width' : 'height']: `${child.size}%`,
|
||||
}}
|
||||
>
|
||||
{/* 递归渲染子节点 */}
|
||||
<TerminalSplitNodeComponent
|
||||
node={child}
|
||||
activeGroupId={activeGroupId}
|
||||
onTabClick={onTabClick}
|
||||
onTabClose={onTabClose}
|
||||
onNewTab={onNewTab}
|
||||
onSplitUp={onSplitUp}
|
||||
onSplitDown={onSplitDown}
|
||||
onSplitLeft={onSplitLeft}
|
||||
onSplitRight={onSplitRight}
|
||||
onFocus={onFocus}
|
||||
onResize={onResize}
|
||||
getConnectionConfig={getConnectionConfig}
|
||||
getAuditConfig={getAuditConfig}
|
||||
getToolbarConfig={getToolbarConfig}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分隔条 - 不在最后一个节点之后显示 */}
|
||||
{index < node.children.length - 1 && (
|
||||
<TerminalSplitDivider
|
||||
orientation={node.orientation}
|
||||
onResize={(delta) => onResize(child.id, delta)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用 React.memo 优化性能
|
||||
* 只在 props 变化时才重新渲染
|
||||
*/
|
||||
export const TerminalSplitNode = React.memo(TerminalSplitNodeComponent);
|
||||
@ -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<SplitNodeRendererProps> = ({
|
||||
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<string, TerminalConnectionConfig> = {};
|
||||
node.tabs.forEach(tab => {
|
||||
connectionConfigs[tab.id] = getConnectionConfig(tab);
|
||||
});
|
||||
const auditConfig = getAuditConfig();
|
||||
const toolbarConfig = getToolbarConfig();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col bg-white dark:bg-gray-900">
|
||||
{/* Tab栏 */}
|
||||
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center px-2 py-1 gap-1 overflow-x-auto">
|
||||
{node.tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-t cursor-pointer transition-colors ${
|
||||
tab.isActive
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-b-2 border-blue-500'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
|
||||
}`}
|
||||
onClick={() => onTabClick(node.id, tab.id)}
|
||||
>
|
||||
<span className="text-xs truncate max-w-[150px]" title={tab.title}>
|
||||
{tab.serverName}
|
||||
</span>
|
||||
<button
|
||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-0.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTabClose(node.id, tab.id);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="flex items-center justify-center w-8 h-8 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNewTab();
|
||||
}}
|
||||
title="新建终端 (Cmd+T)"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 终端内容 */}
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
{node.tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className="absolute inset-0"
|
||||
style={{ display: tab.isActive ? 'block' : 'none' }}
|
||||
>
|
||||
<Terminal
|
||||
id={tab.id}
|
||||
connection={connectionConfigs[tab.id]}
|
||||
audit={auditConfig}
|
||||
toolbar={toolbarConfig}
|
||||
isActive={tab.isActive}
|
||||
onSplitUp={onSplitUp}
|
||||
onSplitDown={onSplitDown}
|
||||
onSplitLeft={onSplitLeft}
|
||||
onSplitRight={onSplitRight}
|
||||
onSplitInGroup={onNewTab}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染容器(内部节点)- 递归渲染子节点
|
||||
const isHorizontal = node.orientation === 'horizontal';
|
||||
|
||||
return (
|
||||
<div className={`h-full w-full flex ${isHorizontal ? 'flex-row' : 'flex-col'}`}>
|
||||
{node.children.map((child, index) => (
|
||||
<React.Fragment key={child.id}>
|
||||
<div
|
||||
key={child.id}
|
||||
className="flex-shrink-0 flex-grow-0"
|
||||
style={{
|
||||
[isHorizontal ? 'width' : 'height']: `${child.size}%`,
|
||||
}}
|
||||
>
|
||||
<SplitNodeRendererComponent
|
||||
key={child.id}
|
||||
node={child}
|
||||
activeGroupId={activeGroupId}
|
||||
onTabClick={onTabClick}
|
||||
onTabClose={onTabClose}
|
||||
onNewTab={onNewTab}
|
||||
onSplitUp={onSplitUp}
|
||||
onSplitDown={onSplitDown}
|
||||
onSplitLeft={onSplitLeft}
|
||||
onSplitRight={onSplitRight}
|
||||
onFocus={onFocus}
|
||||
onResize={onResize}
|
||||
getConnectionConfig={getConnectionConfig}
|
||||
getAuditConfig={getAuditConfig}
|
||||
getToolbarConfig={getToolbarConfig}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分隔条 - 不在最后一个节点之后显示 */}
|
||||
{index < node.children.length - 1 && (
|
||||
<SplitDivider
|
||||
orientation={node.orientation}
|
||||
onResize={(delta) => onResize(child.id, delta)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 使用 React.memo 优化,避免不必要的重新渲染
|
||||
const SplitNodeRenderer = React.memo(SplitNodeRendererComponent);
|
||||
|
||||
/**
|
||||
* Terminal 分屏视图 - 主组件
|
||||
* 管理分屏状态并渲染分屏树
|
||||
* 职责:
|
||||
* 1. 管理分屏状态(通过 useSplitView hook)
|
||||
* 2. 处理键盘快捷键
|
||||
* 3. 暴露 closeAll 方法供外部调用
|
||||
* 4. 委托给 TerminalSplitNode 进行递归渲染
|
||||
*/
|
||||
export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
||||
initialTab,
|
||||
getConnectionConfig,
|
||||
getAuditConfig,
|
||||
getToolbarConfig,
|
||||
onCloseAllReady,
|
||||
}) => {
|
||||
const {
|
||||
layout,
|
||||
@ -199,10 +41,18 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
||||
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<TerminalSplitViewProps> = ({
|
||||
|
||||
return (
|
||||
<div className="terminal-split-container h-full w-full">
|
||||
<SplitNodeRenderer
|
||||
<TerminalSplitNode
|
||||
node={layout.root}
|
||||
activeGroupId={activeGroupId}
|
||||
onTabClick={switchTab}
|
||||
|
||||
@ -201,12 +201,26 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => {
|
||||
};
|
||||
|
||||
// 在组中拆分(创建新标签页)
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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<SSHTerminalSplitViewWrapperProps> = ({
|
||||
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 (
|
||||
<TerminalSplitView
|
||||
initialTab={initialTab}
|
||||
getConnectionConfig={getConnectionConfig}
|
||||
getAuditConfig={getAuditConfig}
|
||||
getToolbarConfig={getToolbarConfig}
|
||||
onCloseAllReady={(closeAllFn) => {
|
||||
// 保存closeAll函数到ref
|
||||
closeAllRef.current = closeAllFn;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface SSHWindowManagerProps {
|
||||
onOpenWindow?: (windowId: string) => void;
|
||||
}
|
||||
@ -65,11 +122,13 @@ export const SSHWindowManager: React.FC<SSHWindowManagerProps> = ({ onOpenWindow
|
||||
});
|
||||
|
||||
return (
|
||||
<TerminalSplitView
|
||||
<SSHTerminalSplitViewWrapper
|
||||
windowId={windowId}
|
||||
initialTab={initialTab}
|
||||
getConnectionConfig={getConnectionConfig}
|
||||
getAuditConfig={getAuditConfig}
|
||||
getToolbarConfig={getToolbarConfig}
|
||||
onCloseReady={onCloseReady}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user