/** * 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'; import { getMinSplitSize, getMinSplitPercent } from './splitUtils'; interface UseSplitViewOptions { initialTab: TerminalTab; onWindowClose?: () => void; // 最后一个Tab关闭时的回调 } export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions) => { const [layout, setLayout] = useState(() => ({ 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; // 计算最小尺寸百分比:根据方向和容器尺寸计算 let minSizePercent = totalSize * 0.2; // 默认20% if (containerSize && parent.type === 'container') { const minPixels = getMinSplitSize(containerSize, parent.orientation); minSizePercent = getMinSplitPercent(minPixels, containerSize, totalSize); } 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, }; };