deploy-ease-platform/frontend/src/components/Terminal/useSplitView.ts
2025-12-06 23:10:37 +08:00

450 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Terminal 分屏管理 Hook
* 支持树形嵌套布局,参考 VS Code 的分屏逻辑
*/
import { useState, useCallback } from 'react';
import type { EditorGroup, TerminalTab, SplitDirection, SplitLayout, SplitNode, SplitContainer, LayoutOrientation } from './types';
import { TerminalInstanceManager } from './core/TerminalInstanceManager';
interface UseSplitViewOptions {
initialTab: TerminalTab;
onWindowClose?: () => void; // 最后一个Tab关闭时的回调
}
export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions) => {
const [layout, setLayout] = useState<SplitLayout>(() => ({
root: {
type: 'group',
id: 'group-1',
tabs: [initialTab],
activeTabId: initialTab.id,
size: 100,
} as EditorGroup,
}));
const [activeGroupId, setActiveGroupId] = useState('group-1');
// 在树中查找组
const findGroup = useCallback((node: SplitNode, groupId: string): EditorGroup | null => {
if (node.type === 'group') {
return node.id === groupId ? node : null;
}
for (const child of node.children) {
const found = findGroup(child, groupId);
if (found) return found;
}
return null;
}, []);
// 查找父容器和节点索引
const findParent = useCallback((
node: SplitNode,
targetId: string,
parent: SplitContainer | null = null,
index: number = 0
): { parent: SplitContainer | null; index: number; node: SplitNode } | null => {
if (node.id === targetId) {
return { parent, index, node };
}
if (node.type === 'container') {
for (let i = 0; i < node.children.length; i++) {
const result = findParent(node.children[i], targetId, node, i);
if (result) return result;
}
}
return null;
}, []);
// 获取当前激活的组
const getActiveGroup = useCallback(() => {
return findGroup(layout.root, activeGroupId);
}, [layout.root, activeGroupId, findGroup]);
// 向指定方向拆分
// 辅助函数:安全地更新节点的 size保持其他属性引用不变
const updateNodeSize = (node: SplitNode, newSize: number): SplitNode => {
if (node.size === newSize) {
return node; // size 没变,返回原对象
}
// size 变了,创建新对象,但保持其他属性引用
if (node.type === 'group') {
return {
...node,
size: newSize,
// tabs 数组引用保持不变
};
} else {
return {
...node,
size: newSize,
// children 数组引用保持不变
};
}
};
const splitToDirection = useCallback((direction: SplitDirection) => {
const activeGroup = getActiveGroup();
if (!activeGroup) return;
const activeTab = activeGroup.tabs.find(t => t.id === activeGroup.activeTabId);
if (!activeTab) return;
const newGroupId = `group-${Date.now()}`;
const newTab: TerminalTab = {
id: `tab-${Date.now()}`,
title: activeTab.title,
serverId: activeTab.serverId,
serverName: activeTab.serverName,
isActive: true,
};
const newGroup: EditorGroup = {
type: 'group',
id: newGroupId,
tabs: [newTab],
activeTabId: newTab.id,
size: 50,
};
setLayout(prev => {
const result = findParent(prev.root, activeGroup.id);
if (!result) return prev;
const { parent, index, node } = result;
const isHorizontalSplit = direction === 'left' || direction === 'right';
const newOrientation: LayoutOrientation = isHorizontalSplit ? 'horizontal' : 'vertical';
// 如果是根节点
if (!parent) {
const updatedNode = updateNodeSize(node, 50);
const container: SplitContainer = {
type: 'container',
id: `container-${Date.now()}`,
orientation: newOrientation,
children: direction === 'right' || direction === 'down'
? [updatedNode, newGroup]
: [newGroup, updatedNode],
size: 100,
};
return { root: container };
}
// 父容器方向一致:直接插入
if (parent.orientation === newOrientation) {
const newChildren = [...parent.children];
const insertIndex = direction === 'right' || direction === 'down' ? index + 1 : index;
newChildren.splice(insertIndex, 0, newGroup);
// 重新分配大小
const sizePerChild = 100 / newChildren.length;
const resizedChildren = newChildren.map(child => updateNodeSize(child, sizePerChild));
return {
root: updateNode(prev.root, parent.id, {
...parent,
children: resizedChildren,
}),
};
}
// 父容器方向不一致:创建新容器包裹当前节点和新节点
const updatedNode = updateNodeSize(node, 50);
const newContainer: SplitContainer = {
type: 'container',
id: `container-${Date.now()}`,
orientation: newOrientation,
children: direction === 'right' || direction === 'down'
? [updatedNode, newGroup]
: [newGroup, updatedNode],
size: node.size,
};
const newChildren = [...parent.children];
newChildren[index] = newContainer;
return {
root: updateNode(prev.root, parent.id, {
...parent,
children: newChildren,
}),
};
});
setActiveGroupId(newGroupId);
}, [activeGroupId, getActiveGroup, findParent]);
// 更新节点(递归)- 优化:只在真正需要更新时才创建新对象
const updateNode = (node: SplitNode, targetId: string, newNode: SplitNode): SplitNode => {
if (node.id === targetId) {
return newNode;
}
if (node.type === 'container') {
// 递归更新子节点
const newChildren = node.children.map(child => updateNode(child, targetId, newNode));
// 检查是否真的有子节点被更新了
const hasChanged = newChildren.some((child, index) => child !== node.children[index]);
// 如果没有变化,返回原节点(保持引用不变)
if (!hasChanged) {
return node;
}
// 有变化才创建新对象
return {
...node,
children: newChildren,
};
}
return node;
};
// 在组中拆分(创建新标签页)
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;
}
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()}`,
title: activeTab.title,
serverId: activeTab.serverId,
serverName: activeTab.serverName,
isActive: true,
};
setLayout(prev => ({
root: updateNode(prev.root, targetGroup.id, {
...targetGroup,
tabs: [...targetGroup.tabs.map(t => ({ ...t, isActive: false })), newTab],
activeTabId: newTab.id,
}),
}));
// 更新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) {
console.log(`[switchTab] Group not found: ${groupId}`);
return prev;
}
return {
root: updateNode(prev.root, groupId, {
...group,
tabs: group.tabs.map(t => ({ ...t, isActive: t.id === tabId })),
activeTabId: tabId,
}),
};
});
}, [findGroup]);
// 关闭标签页
const closeTab = useCallback((groupId: string, tabId: string) => {
// 销毁 Terminal 实例
const manager = TerminalInstanceManager.getInstance();
manager.destroy(tabId);
console.log(`[useSplitView] 销毁 Terminal 实例: ${tabId}`);
setLayout(prev => {
const group = findGroup(prev.root, groupId);
if (!group) return prev;
if (group.tabs.length === 1) {
// 关闭整个组
return closeGroup(prev, groupId);
}
const newTabs = group.tabs.filter(t => t.id !== tabId);
const newActiveTabId = group.activeTabId === tabId ? newTabs[0].id : group.activeTabId;
// 更新 isActive 状态
const updatedTabs = newTabs.map(t => ({
...t,
isActive: t.id === newActiveTabId,
}));
return {
root: updateNode(prev.root, groupId, {
...group,
tabs: updatedTabs,
activeTabId: newActiveTabId,
}),
};
});
}, [findGroup]);
// 关闭组
const closeGroup = (prev: SplitLayout, groupId: string): SplitLayout => {
// 销毁组内所有 Terminal 实例
const group = findGroup(prev.root, groupId);
if (group) {
const manager = TerminalInstanceManager.getInstance();
group.tabs.forEach(tab => {
manager.destroy(tab.id);
console.log(`[useSplitView] 关闭组,销毁 Terminal 实例: ${tab.id}`);
});
}
const result = findParent(prev.root, groupId);
if (!result) return prev;
const { parent } = result;
if (!parent) {
// 根节点 且 只有一个Tab关闭整个窗口
if (group && group.tabs.length === 1) {
console.log('[useSplitView] 最后一个Tab关闭整个窗口');
onWindowClose?.();
}
return prev;
}
const newChildren = parent.children.filter(c => c.id !== groupId);
if (newChildren.length === 0) return prev;
if (newChildren.length === 1) {
// 只剩一个子节点,提升它
const child = newChildren[0];
const grandParentResult = findParent(prev.root, parent.id);
if (!grandParentResult || !grandParentResult.parent) {
// 父容器是根节点,提升子节点为新根
return { root: { ...child, size: 100 } };
}
// 用子节点替换父容器
const grandParent = grandParentResult.parent;
const newGrandChildren = grandParent.children.map(c =>
c.id === parent.id ? { ...child, size: parent.size } : c
);
return {
root: updateNode(prev.root, grandParent.id, {
...grandParent,
children: newGrandChildren,
}),
};
}
// 重新分配大小
const sizePerChild = 100 / newChildren.length;
newChildren.forEach(child => { child.size = sizePerChild; });
return {
root: updateNode(prev.root, parent.id, {
...parent,
children: newChildren,
}),
};
};
// 调整分屏大小
const resizeGroups = useCallback((nodeId: string, delta: number, containerSize?: number) => {
setLayout(prev => {
const result = findParent(prev.root, nodeId);
if (!result || !result.parent) return prev;
const { parent, index } = result;
if (index >= parent.children.length - 1) return prev;
const current = parent.children[index];
const next = parent.children[index + 1];
const totalSize = current.size + next.size;
// 将像素delta转换为百分比delta
// delta是像素值需要转换为百分比(delta / containerSize) * totalSize
const deltaPercent = containerSize ? (delta / containerSize) * totalSize : delta;
// 计算最小尺寸百分比520px / 容器尺寸 * 100
// 如果没有容器尺寸使用默认20%作为兜底
const minSizePercent = containerSize ? Math.min(50, (520 / containerSize) * 100) : 20;
const newCurrentSize = Math.max(minSizePercent, Math.min(totalSize - minSizePercent, current.size + deltaPercent));
const newNextSize = totalSize - newCurrentSize;
const newChildren = parent.children.map((child, i) => {
if (i === index) return { ...child, size: newCurrentSize };
if (i === index + 1) return { ...child, size: newNextSize };
return child;
});
return {
root: updateNode(prev.root, parent.id, {
...parent,
children: newChildren,
}),
};
});
}, [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,
splitUp: () => splitToDirection('up'),
splitDown: () => splitToDirection('down'),
splitLeft: () => splitToDirection('left'),
splitRight: () => splitToDirection('right'),
splitInGroup,
switchTab,
closeTab,
closeAll,
resizeGroups,
setActiveGroupId,
};
};