重写ssh前端组件,通用化
This commit is contained in:
parent
1fd72bf007
commit
004e1adcf5
@ -1,16 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* 分屏分隔条组件
|
* Terminal 分屏分隔条
|
||||||
* 可拖动调整分屏大小
|
* 可拖动调整两个分屏之间的大小比例
|
||||||
*/
|
*/
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import type { LayoutOrientation } from './types';
|
import type { LayoutOrientation } from './types';
|
||||||
|
|
||||||
interface SplitDividerProps {
|
interface TerminalSplitDividerProps {
|
||||||
orientation: LayoutOrientation;
|
orientation: LayoutOrientation;
|
||||||
onResize: (delta: number) => void;
|
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 [isDragging, setIsDragging] = useState(false);
|
||||||
const startPosRef = useRef(0);
|
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 分屏组件
|
* Terminal 分屏视图 - 主组件
|
||||||
* 支持树形嵌套布局,类似 VS Code 的编辑器分屏系统
|
* 管理分屏状态、键盘快捷键,并委托给 TerminalSplitNode 进行递归渲染
|
||||||
*/
|
*/
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Terminal } from './Terminal';
|
|
||||||
import { SplitDivider } from './SplitDivider';
|
|
||||||
import { X, Plus } from 'lucide-react';
|
|
||||||
import { useSplitView } from './useSplitView';
|
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
|
// 对外暴露的主组件Props
|
||||||
export interface TerminalSplitViewProps {
|
export interface TerminalSplitViewProps {
|
||||||
@ -15,179 +13,23 @@ export interface TerminalSplitViewProps {
|
|||||||
getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig;
|
getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig;
|
||||||
getAuditConfig: () => TerminalAuditConfig;
|
getAuditConfig: () => TerminalAuditConfig;
|
||||||
getToolbarConfig: () => TerminalToolbarConfig;
|
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 分屏视图 - 主组件
|
* Terminal 分屏视图 - 主组件
|
||||||
* 管理分屏状态并渲染分屏树
|
* 职责:
|
||||||
|
* 1. 管理分屏状态(通过 useSplitView hook)
|
||||||
|
* 2. 处理键盘快捷键
|
||||||
|
* 3. 暴露 closeAll 方法供外部调用
|
||||||
|
* 4. 委托给 TerminalSplitNode 进行递归渲染
|
||||||
*/
|
*/
|
||||||
export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
||||||
initialTab,
|
initialTab,
|
||||||
getConnectionConfig,
|
getConnectionConfig,
|
||||||
getAuditConfig,
|
getAuditConfig,
|
||||||
getToolbarConfig,
|
getToolbarConfig,
|
||||||
|
onCloseAllReady,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
layout,
|
layout,
|
||||||
@ -199,10 +41,18 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
|||||||
splitInGroup,
|
splitInGroup,
|
||||||
switchTab,
|
switchTab,
|
||||||
closeTab,
|
closeTab,
|
||||||
|
closeAll,
|
||||||
resizeGroups,
|
resizeGroups,
|
||||||
setActiveGroupId,
|
setActiveGroupId,
|
||||||
} = useSplitView({ initialTab });
|
} = useSplitView({ initialTab });
|
||||||
|
|
||||||
|
// 暴露closeAll方法给外部
|
||||||
|
useEffect(() => {
|
||||||
|
if (onCloseAllReady) {
|
||||||
|
onCloseAllReady(closeAll);
|
||||||
|
}
|
||||||
|
}, [onCloseAllReady, closeAll]);
|
||||||
|
|
||||||
// 快捷键支持
|
// 快捷键支持
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@ -235,7 +85,7 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="terminal-split-container h-full w-full">
|
<div className="terminal-split-container h-full w-full">
|
||||||
<SplitNodeRenderer
|
<TerminalSplitNode
|
||||||
node={layout.root}
|
node={layout.root}
|
||||||
activeGroupId={activeGroupId}
|
activeGroupId={activeGroupId}
|
||||||
onTabClick={switchTab}
|
onTabClick={switchTab}
|
||||||
|
|||||||
@ -201,12 +201,26 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 在组中拆分(创建新标签页)
|
// 在组中拆分(创建新标签页)
|
||||||
const splitInGroup = useCallback(() => {
|
const splitInGroup = useCallback((groupId?: string) => {
|
||||||
const activeGroup = getActiveGroup();
|
// 如果提供了groupId就用它,否则用activeGroupId
|
||||||
if (!activeGroup) return;
|
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);
|
console.log(`[splitInGroup] Target group: ${targetGroup.id}, tabs count: ${targetGroup.tabs.length}`);
|
||||||
if (!activeTab) return;
|
|
||||||
|
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 = {
|
const newTab: TerminalTab = {
|
||||||
id: `tab-${Date.now()}`,
|
id: `tab-${Date.now()}`,
|
||||||
@ -217,19 +231,32 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setLayout(prev => ({
|
setLayout(prev => ({
|
||||||
root: updateNode(prev.root, activeGroup.id, {
|
root: updateNode(prev.root, targetGroup.id, {
|
||||||
...activeGroup,
|
...targetGroup,
|
||||||
tabs: [...activeGroup.tabs.map(t => ({ ...t, isActive: false })), newTab],
|
tabs: [...targetGroup.tabs.map(t => ({ ...t, isActive: false })), newTab],
|
||||||
activeTabId: newTab.id,
|
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) => {
|
const switchTab = useCallback((groupId: string, tabId: string) => {
|
||||||
|
console.log(`[switchTab] Switching to group: ${groupId}, tab: ${tabId}`);
|
||||||
|
|
||||||
|
// 先设置activeGroupId,确保后续操作使用正确的组
|
||||||
|
setActiveGroupId(groupId);
|
||||||
|
|
||||||
setLayout(prev => {
|
setLayout(prev => {
|
||||||
const group = findGroup(prev.root, groupId);
|
const group = findGroup(prev.root, groupId);
|
||||||
if (!group) return prev;
|
if (!group) {
|
||||||
|
console.log(`[switchTab] Group not found: ${groupId}`);
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: updateNode(prev.root, groupId, {
|
root: updateNode(prev.root, groupId, {
|
||||||
@ -239,7 +266,6 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setActiveGroupId(groupId);
|
|
||||||
}, [findGroup]);
|
}, [findGroup]);
|
||||||
|
|
||||||
// 关闭标签页
|
// 关闭标签页
|
||||||
@ -368,6 +394,31 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => {
|
|||||||
});
|
});
|
||||||
}, [findParent]);
|
}, [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 {
|
return {
|
||||||
layout,
|
layout,
|
||||||
activeGroupId,
|
activeGroupId,
|
||||||
@ -378,6 +429,7 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => {
|
|||||||
splitInGroup,
|
splitInGroup,
|
||||||
switchTab,
|
switchTab,
|
||||||
closeTab,
|
closeTab,
|
||||||
|
closeAll,
|
||||||
resizeGroups,
|
resizeGroups,
|
||||||
setActiveGroupId,
|
setActiveGroupId,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* SSH 窗口管理器
|
* SSH 窗口管理器
|
||||||
* 使用 TerminalSplitView 支持分屏功能
|
* 使用 TerminalSplitView 支持分屏功能
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
TerminalWindowManager,
|
TerminalWindowManager,
|
||||||
TerminalSplitView,
|
TerminalSplitView,
|
||||||
@ -13,6 +13,63 @@ import {
|
|||||||
} from '@/components/Terminal';
|
} from '@/components/Terminal';
|
||||||
import type { ServerResponse } from '../types';
|
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 {
|
interface SSHWindowManagerProps {
|
||||||
onOpenWindow?: (windowId: string) => void;
|
onOpenWindow?: (windowId: string) => void;
|
||||||
}
|
}
|
||||||
@ -65,11 +122,13 @@ export const SSHWindowManager: React.FC<SSHWindowManagerProps> = ({ onOpenWindow
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TerminalSplitView
|
<SSHTerminalSplitViewWrapper
|
||||||
|
windowId={windowId}
|
||||||
initialTab={initialTab}
|
initialTab={initialTab}
|
||||||
getConnectionConfig={getConnectionConfig}
|
getConnectionConfig={getConnectionConfig}
|
||||||
getAuditConfig={getAuditConfig}
|
getAuditConfig={getAuditConfig}
|
||||||
getToolbarConfig={getToolbarConfig}
|
getToolbarConfig={getToolbarConfig}
|
||||||
|
onCloseReady={onCloseReady}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user