From 88ad7332a39d7ba346c86011e770a016783fa30b Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sat, 3 May 2025 19:43:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BAssh=E6=A0=87=E7=AD=BE=E6=A0=8F?= =?UTF-8?q?=E5=92=8C=E6=96=87=E4=BB=B6=E7=BC=96=E8=BE=91=E5=99=A8=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E6=A0=8F=E6=B7=BB=E5=8A=A0=E5=8F=B3=E9=94=AE=E8=8F=9C?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/FileEditorContainer.vue | 11 +- .../src/components/FileEditorOverlay.vue | 82 +++++++-- .../src/components/FileEditorTabs.vue | 114 ++++++++++++- .../src/components/LayoutRenderer.vue | 13 +- .../src/components/TabBarContextMenu.vue | 103 ++++++++++++ .../src/components/TerminalTabBar.vue | 156 +++++++++++++++--- packages/frontend/src/locales/en-US.json | 8 + packages/frontend/src/locales/zh-CN.json | 8 + .../frontend/src/stores/fileEditor.store.ts | 60 ++++++- packages/frontend/src/stores/session.store.ts | 44 ++++- packages/frontend/src/views/WorkspaceView.vue | 68 +++++++- 11 files changed, 620 insertions(+), 47 deletions(-) create mode 100644 packages/frontend/src/components/TabBarContextMenu.vue diff --git a/packages/frontend/src/components/FileEditorContainer.vue b/packages/frontend/src/components/FileEditorContainer.vue index 46fa210..4e15e57 100644 --- a/packages/frontend/src/components/FileEditorContainer.vue +++ b/packages/frontend/src/components/FileEditorContainer.vue @@ -34,6 +34,10 @@ const emit = defineEmits<{ (e: 'request-save', tabId: string): void; // 发送保存请求,携带 tabId (e: 'update:content', payload: { tabId: string; content: string }): void; // 用于 v-model 同步 (e: 'change-encoding', payload: { tabId: string; encoding: string }): void; // +++ 新增:编码更改事件 +++ + // +++ 新增:传递右键菜单关闭事件 +++ + (e: 'close-other-tabs', tabId: string): void; + (e: 'close-tabs-to-right', tabId: string): void; + (e: 'close-tabs-to-left', tabId: string): void; }>(); @@ -228,8 +232,11 @@ const handleKeyDown = (event: KeyboardEvent) => { diff --git a/packages/frontend/src/components/FileEditorOverlay.vue b/packages/frontend/src/components/FileEditorOverlay.vue index 9cffce9..6c20c43 100644 --- a/packages/frontend/src/components/FileEditorOverlay.vue +++ b/packages/frontend/src/components/FileEditorOverlay.vue @@ -23,7 +23,7 @@ const { popupTrigger, popupFileInfo, // 包含 sessionId 和 filePath activeTabId: globalActiveTabIdRef, // 获取全局 activeTabId - tabs: globalTabsRef, // 获取全局 tabs Map + // tabs: globalTabsRef, // 不再使用 storeToRefs 获取 tabs } = storeToRefs(fileEditorStore); // 设置 Store (用于判断模式) @@ -32,18 +32,26 @@ const { showPopupFileEditorBoolean, shareFileEditorTabsBoolean } = storeToRefs(s // --- 从 Store 获取方法 --- // 全局 Store Actions (用于共享模式) const { - saveFile: saveGlobalFile, - closeTab: closeGlobalTab, - setActiveTab: setGlobalActiveTab, - updateFileContent: updateGlobalFileContent, + saveFile: saveGlobalFile, + closeTab: closeGlobalTab, + setActiveTab: setGlobalActiveTab, + updateFileContent: updateGlobalFileContent, + // + 添加右键菜单操作 actions + closeOtherTabs, // 修正:移除 Global 后缀 + closeTabsToTheRight, // 修正:移除 Global 后缀 + closeTabsToTheLeft, // 修正:移除 Global 后缀 } = fileEditorStore; // 会话 Store Actions (用于非共享模式) const { - saveFileInSession, - closeEditorTabInSession, - setActiveEditorTabInSession, - updateFileContentInSession, + saveFileInSession, + closeEditorTabInSession, + setActiveEditorTabInSession, + updateFileContentInSession, + // + 添加右键菜单操作 actions + closeOtherTabsInSession, + closeTabsToTheRightInSession, + closeTabsToTheLeftInSession, } = sessionStore; // --- 移除本地文件状态 --- @@ -89,10 +97,12 @@ const currentSession = computed(() => { // 获取当前模式下的标签页列表 const orderedTabs = computed(() => { + // 直接访问 store.tabs if (shareFileEditorTabsBoolean.value) { - return Array.from(globalTabsRef.value.values()); // 全局 Store + return Array.from(fileEditorStore.tabs.values()); // 直接访问 store } else { - return currentSession.value?.editorTabs.value ?? []; // 会话 Store + // 非共享模式保持不变,因为它依赖 sessionStore + return currentSession.value?.editorTabs.value ?? []; } }); @@ -110,10 +120,11 @@ const activeTab = computed((): FileTab | null => { const currentId = activeTabId.value; if (!currentId) return null; + // 直接访问 store.tabs if (shareFileEditorTabsBoolean.value) { - return globalTabsRef.value.get(currentId) ?? null; // 全局 Store + return fileEditorStore.tabs.get(currentId) ?? null; // 直接访问 store } else { - // 在会话的 editorTabs 数组中查找 + // 非共享模式保持不变 return currentSession.value?.editorTabs.value.find(tab => tab.id === currentId) ?? null; } }); @@ -197,6 +208,48 @@ const handleCloseTab = (tabId: string) => { } }; +// +++ 处理右键菜单事件 +++ +const handleCloseOtherTabs = (targetTabId: string) => { + console.log(`[FileEditorOverlay] handleCloseOtherTabs called for target: ${targetTabId}`); // Add log + if (shareFileEditorTabsBoolean.value) { + closeOtherTabs(targetTabId); // 修正:调用正确的 action 名称 + } else { + const sessionId = popupFileInfo.value?.sessionId; + if (sessionId) { + closeOtherTabsInSession(sessionId, targetTabId); // 会话 Store + } else { + console.error("[FileEditorOverlay] 无法关闭其他标签页:非共享模式下缺少 sessionId。"); + } + } +}; + +const handleCloseRightTabs = (targetTabId: string) => { + console.log(`[FileEditorOverlay] handleCloseRightTabs called for target: ${targetTabId}`); // Add log + if (shareFileEditorTabsBoolean.value) { + closeTabsToTheRight(targetTabId); // 修正:调用正确的 action 名称 + } else { + const sessionId = popupFileInfo.value?.sessionId; + if (sessionId) { + closeTabsToTheRightInSession(sessionId, targetTabId); // 会话 Store + } else { + console.error("[FileEditorOverlay] 无法关闭右侧标签页:非共享模式下缺少 sessionId。"); + } + } +}; + +const handleCloseLeftTabs = (targetTabId: string) => { + console.log(`[FileEditorOverlay] handleCloseLeftTabs called for target: ${targetTabId}`); // Add log + if (shareFileEditorTabsBoolean.value) { + closeTabsToTheLeft(targetTabId); // 修正:调用正确的 action 名称 + } else { + const sessionId = popupFileInfo.value?.sessionId; + if (sessionId) { + closeTabsToTheLeftInSession(sessionId, targetTabId); // 会话 Store + } else { + console.error("[FileEditorOverlay] 无法关闭左侧标签页:非共享模式下缺少 sessionId。"); + } + } +}; // 关闭弹窗 (保持不变) const handleCloseContainer = () => { @@ -291,6 +344,9 @@ onBeforeUnmount(() => { :active-tab-id="activeTabId" @activate-tab="handleActivateTab" @close-tab="handleCloseTab" + @close-other-tabs="handleCloseOtherTabs" + @close-tabs-to-right="handleCloseRightTabs" + @close-tabs-to-left="handleCloseLeftTabs" /> diff --git a/packages/frontend/src/components/FileEditorTabs.vue b/packages/frontend/src/components/FileEditorTabs.vue index c6dda2e..83143b8 100644 --- a/packages/frontend/src/components/FileEditorTabs.vue +++ b/packages/frontend/src/components/FileEditorTabs.vue @@ -1,9 +1,10 @@ diff --git a/packages/frontend/src/components/LayoutRenderer.vue b/packages/frontend/src/components/LayoutRenderer.vue index 18772b6..2bcabcd 100644 --- a/packages/frontend/src/components/LayoutRenderer.vue +++ b/packages/frontend/src/components/LayoutRenderer.vue @@ -66,9 +66,13 @@ const emit = defineEmits({ 'close-search': null, // () 'clear-terminal': null, // () +++ 添加 clear-terminal 事件 +++ 'change-encoding': null, // +++ 添加 change-encoding 事件 +++ + // +++ 添加文件编辑器标签页关闭事件 +++ + 'close-other-tabs': null, // (tabId: string) + 'close-tabs-to-right': null, // (tabId: string) + 'close-tabs-to-left': null, // (tabId: string) // --- 移除 RDP 事件 --- }); - + // --- Setup --- const layoutStore = useLayoutStore(); const sessionStore = useSessionStore(); @@ -206,6 +210,10 @@ const componentProps = computed(() => { onRequestSave: (tabId: string) => emit('saveEditorTab', tabId), // +++ 添加:转发 change-encoding 事件 +++ onChangeEncoding: (payload: { tabId: string; encoding: string }) => emit('change-encoding', payload), + // +++ 添加:转发其他关闭事件 +++ + onCloseOtherTabs: (tabId: string) => emit('close-other-tabs', tabId), + onCloseTabsToRight: (tabId: string) => emit('close-tabs-to-right', tabId), + onCloseTabsToLeft: (tabId: string) => emit('close-tabs-to-left', tabId), }; case 'commandBar': // CommandInputBar 需要转发 send-command 事件 @@ -514,6 +522,9 @@ onMounted(() => { @close-search="emit('close-search')" @clear-terminal="() => emit('clear-terminal')" @change-encoding="emit('change-encoding', $event)" + @close-other-tabs="emit('close-other-tabs', $event)" + @close-tabs-to-right="emit('close-tabs-to-right', $event)" + @close-tabs-to-left="emit('close-tabs-to-left', $event)" class="flex-grow overflow-auto" /> diff --git a/packages/frontend/src/components/TabBarContextMenu.vue b/packages/frontend/src/components/TabBarContextMenu.vue new file mode 100644 index 0000000..271bef1 --- /dev/null +++ b/packages/frontend/src/components/TabBarContextMenu.vue @@ -0,0 +1,103 @@ + + + + + \ No newline at end of file diff --git a/packages/frontend/src/components/TerminalTabBar.vue b/packages/frontend/src/components/TerminalTabBar.vue index 5bbdc7b..dd1bac9 100644 --- a/packages/frontend/src/components/TerminalTabBar.vue +++ b/packages/frontend/src/components/TerminalTabBar.vue @@ -1,44 +1,49 @@ - - diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index e5ce733..35cb911 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -1016,6 +1016,14 @@ "terminalTabBar": { "selectServerTitle": "Select server to connect" }, + "tabs": { + "contextMenu": { + "close": "Close Tab", + "closeOthers": "Close Other Tabs", + "closeRight": "Close Tabs to the Right", + "closeLeft": "Close Tabs to the Left" + } + }, "sshKeys": { "selector": { "selectPlaceholder": "Select an SSH key...", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 2352fec..9bf4137 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -1019,6 +1019,14 @@ "terminalTabBar": { "selectServerTitle": "选择要连接的服务器" }, + "tabs": { + "contextMenu": { + "close": "关闭标签页", + "closeOthers": "关闭其他标签页", + "closeRight": "关闭右侧标签页", + "closeLeft": "关闭左侧标签页" + } + }, "sshKeys": { "selector": { "selectPlaceholder": "选择一个 SSH 密钥...", diff --git a/packages/frontend/src/stores/fileEditor.store.ts b/packages/frontend/src/stores/fileEditor.store.ts index c339fa6..10565b2 100644 --- a/packages/frontend/src/stores/fileEditor.store.ts +++ b/packages/frontend/src/stores/fileEditor.store.ts @@ -413,8 +413,61 @@ export const useFileEditorStore = defineStore('fileEditor', () => { // setEditorVisibility('closed'); // 移除:容器可见性由外部控制 }; - // 设置当前激活的标签页 - const setActiveTab = (tabId: string) => { + // +++ 新增:关闭其他标签页 +++ + const closeOtherTabs = (targetTabId: string) => { + console.log(`[文件编辑器 Store] closeOtherTabs: Action called. Current keys in tabs map:`, Array.from(tabs.value.keys())); // ++ Log current keys at start + if (!tabs.value.has(targetTabId)) { + console.warn(`[文件编辑器 Store] closeOtherTabs: 目标 ID ${targetTabId} 在 Map 中不存在。`); // Updated warning + return; + } + console.log(`[文件编辑器 Store] closeOtherTabs: 开始关闭除 ${targetTabId} 之外的所有标签页...`); + const tabsToClose = Array.from(tabs.value.keys()).filter(id => id !== targetTabId); + console.log(`[文件编辑器 Store] closeOtherTabs: 将要关闭的标签页 IDs:`, tabsToClose); // + Log IDs to close + tabsToClose.forEach(id => { + console.log(`[文件编辑器 Store] closeOtherTabs: 正在调用 closeTab 关闭 ${id}`); // + Log loop iteration + closeTab(id); + }); + }; + + // +++ 新增:关闭右侧标签页 +++ + const closeTabsToTheRight = (targetTabId: string) => { + const tabsArray = Array.from(tabs.value.values()); + const targetIndex = tabsArray.findIndex(tab => tab.id === targetTabId); + console.log(`[文件编辑器 Store] closeTabsToTheRight: Action called. Current keys in tabs map:`, Array.from(tabs.value.keys())); // ++ Log current keys at start + if (targetIndex === -1) { + console.warn(`[文件编辑器 Store] closeTabsToTheRight: 目标 ID ${targetTabId} 未找到索引。`); + return; + } + console.log(`[文件编辑器 Store] closeTabsToTheRight: 开始关闭 ${targetTabId} (索引 ${targetIndex}) 右侧的所有标签页...`); + const tabsToClose = tabsArray.slice(targetIndex + 1).map(tab => tab.id); + console.log(`[文件编辑器 Store] closeTabsToTheRight: 将要关闭的标签页 IDs:`, tabsToClose); // + Log IDs to close + tabsToClose.forEach(id => { + console.log(`[文件编辑器 Store] closeTabsToTheRight: 正在调用 closeTab 关闭 ${id}`); // + Log loop iteration + closeTab(id); + }); + }; + + // +++ 新增:关闭左侧标签页 +++ + const closeTabsToTheLeft = (targetTabId: string) => { + const tabsArray = Array.from(tabs.value.values()); + const targetIndex = tabsArray.findIndex(tab => tab.id === targetTabId); + console.log(`[文件编辑器 Store] closeTabsToTheLeft: Action called. Current keys in tabs map:`, Array.from(tabs.value.keys())); // ++ Log current keys at start + if (targetIndex === -1) { + console.warn(`[文件编辑器 Store] closeTabsToTheLeft: 目标 ID ${targetTabId} 未找到索引。`); + return; + } + console.log(`[文件编辑器 Store] closeTabsToTheLeft: 开始关闭 ${targetTabId} (索引 ${targetIndex}) 左侧的所有标签页...`); + const tabsToClose = tabsArray.slice(0, targetIndex).map(tab => tab.id); + console.log(`[文件编辑器 Store] closeTabsToTheLeft: 将要关闭的标签页 IDs:`, tabsToClose); // + Log IDs to close + tabsToClose.forEach(id => { + console.log(`[文件编辑器 Store] closeTabsToTheLeft: 正在调用 closeTab 关闭 ${id}`); // + Log loop iteration + closeTab(id); + }); + }; + + + // 设置当前激活的标签页 + const setActiveTab = (tabId: string) => { if (tabs.value.has(tabId)) { activeTabId.value = tabId; console.log(`[文件编辑器 Store] 激活标签页: ${tabId}`); @@ -556,6 +609,9 @@ export const useFileEditorStore = defineStore('fileEditor', () => { openFile, saveFile, closeTab, + closeOtherTabs, // +++ 暴露新 action +++ + closeTabsToTheRight, // +++ 暴露新 action +++ + closeTabsToTheLeft, // +++ 暴露新 action +++ closeAllTabs, setActiveTab, updateFileContent, // 暴露新的更新方法 diff --git a/packages/frontend/src/stores/session.store.ts b/packages/frontend/src/stores/session.store.ts index 4f9e4e0..faf0024 100644 --- a/packages/frontend/src/stores/session.store.ts +++ b/packages/frontend/src/stores/session.store.ts @@ -495,7 +495,7 @@ export const useSessionStore = defineStore('session', () => { // 创建新标签页 (使用简化后的 FileTab 接口) // --- 修复:初始化 rawContentBase64 --- const newTab: FileTab = { - id: generateSessionId(), // 使用独立 ID + id: `${sessionId}:${fileInfo.fullPath}`, // <-- 修复:使用 sessionId:filePath 作为 ID sessionId: sessionId, filePath: fileInfo.fullPath, filename: fileInfo.name, @@ -635,6 +635,45 @@ export const useSessionStore = defineStore('session', () => { } }; + // +++ 新增:关闭指定会话中的其他编辑器标签页 +++ + const closeOtherTabsInSession = (sessionId: string, targetTabId: string) => { + const session = sessions.value.get(sessionId); + if (!session) return; + const targetIndex = session.editorTabs.value.findIndex(tab => tab.id === targetTabId); + if (targetIndex === -1) return; + console.log(`[SessionStore ${sessionId}] 关闭除 ${targetTabId} 之外的所有标签页...`); + const tabsToClose = session.editorTabs.value + .filter(tab => tab.id !== targetTabId) + .map(tab => tab.id); + tabsToClose.forEach(id => closeEditorTabInSession(sessionId, id)); // 复用单个关闭逻辑 + }; + + // +++ 新增:关闭指定会话中右侧的编辑器标签页 +++ + const closeTabsToTheRightInSession = (sessionId: string, targetTabId: string) => { + const session = sessions.value.get(sessionId); + if (!session) return; + const targetIndex = session.editorTabs.value.findIndex(tab => tab.id === targetTabId); + if (targetIndex === -1) return; + console.log(`[SessionStore ${sessionId}] 关闭 ${targetTabId} 右侧的所有标签页...`); + const tabsToClose = session.editorTabs.value + .slice(targetIndex + 1) + .map(tab => tab.id); + tabsToClose.forEach(id => closeEditorTabInSession(sessionId, id)); + }; + + // +++ 新增:关闭指定会话中左侧的编辑器标签页 +++ + const closeTabsToTheLeftInSession = (sessionId: string, targetTabId: string) => { + const session = sessions.value.get(sessionId); + if (!session) return; + const targetIndex = session.editorTabs.value.findIndex(tab => tab.id === targetTabId); + if (targetIndex === -1) return; + console.log(`[SessionStore ${sessionId}] 关闭 ${targetTabId} 左侧的所有标签页...`); + const tabsToClose = session.editorTabs.value + .slice(0, targetIndex) + .map(tab => tab.id); + tabsToClose.forEach(id => closeEditorTabInSession(sessionId, id)); + }; + /** * 在指定会话中更改文件编码并重新解码 @@ -812,6 +851,9 @@ export const useSessionStore = defineStore('session', () => { updateFileContentInSession, // 导出更新内容 Action saveFileInSession, // 导出保存文件 Action changeEncodingInSession, // 导出更改编码 Action + closeOtherTabsInSession, // +++ 导出新 action +++ + closeTabsToTheRightInSession, // +++ 导出新 action +++ + closeTabsToTheLeftInSession, // +++ 导出新 action +++ // --- RDP Modal Actions --- openRdpModal, // 导出打开 RDP 模态框 Action closeRdpModal, // 导出关闭 RDP 模态框 Action diff --git a/packages/frontend/src/views/WorkspaceView.vue b/packages/frontend/src/views/WorkspaceView.vue index 564bbb1..63b3f94 100644 --- a/packages/frontend/src/views/WorkspaceView.vue +++ b/packages/frontend/src/views/WorkspaceView.vue @@ -12,7 +12,7 @@ import LayoutConfigurator from '../components/LayoutConfigurator.vue'; // *** import RemoteDesktopModal from '../components/RemoteDesktopModal.vue'; // +++ 导入 RDP 模态框 +++ import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 session store import { useSettingsStore } from '../stores/settings.store'; -import { useFileEditorStore } from '../stores/fileEditor.store'; +import { useFileEditorStore, type FileTab } from '../stores/fileEditor.store'; // + Import FileTab type // import { useLayoutStore } from '../stores/layout.store'; // 重复导入,移除 import { useCommandHistoryStore } from '../stores/commandHistory.store'; // import type { ConnectionInfo } from '../stores/connections.store'; // 重复导入,移除 @@ -37,7 +37,7 @@ const { layoutTree } = storeToRefs(layoutStore); // 只获取布局树 // --- 计算属性 (用于动态绑定编辑器 Props) --- // 这些计算属性现在需要传递给 LayoutRenderer -const editorTabs = computed(() => { +const editorTabs = computed((): FileTab[] => { // Ensure return type is FileTab[] if (shareFileEditorTabsBoolean.value) { return globalEditorTabs.value; } else { @@ -292,7 +292,7 @@ const handleCloseSearch = () => { console.warn(`[WorkspaceView] Cannot clear search, no active session manager.`); } }; - + // +++ 新增:处理清空终端事件 +++ const handleClearTerminal = () => { const currentSession = activeSession.value; @@ -309,7 +309,7 @@ const handleClearTerminal = () => { console.warn(`[WorkspaceView] Cannot clear terminal for session ${currentSession.sessionId}, terminal manager, instance, or clear method not available.`); } }; - + // Removed computed properties for search results, will pass manager directly // --- 编辑器操作处理 (用于 FileEditorContainer) --- const handleCloseEditorTab = (tabId: string) => { @@ -407,6 +407,58 @@ const handleCloseEditorTab = (tabId: string) => { // RDP 事件处理方法已被移除 + // --- 标签页关闭操作处理 --- + + const handleCloseOtherSessions = (targetSessionId: string) => { + const sessionsToClose = sessionTabsWithStatus.value + .filter(tab => tab.sessionId !== targetSessionId) + .map(tab => tab.sessionId); + sessionsToClose.forEach(id => sessionStore.closeSession(id)); + }; + + const handleCloseSessionsToRight = (targetSessionId: string) => { + const targetIndex = sessionTabsWithStatus.value.findIndex(tab => tab.sessionId === targetSessionId); + if (targetIndex === -1) return; + const sessionsToClose = sessionTabsWithStatus.value + .slice(targetIndex + 1) + .map(tab => tab.sessionId); + sessionsToClose.forEach(id => sessionStore.closeSession(id)); + }; + + const handleCloseSessionsToLeft = (targetSessionId: string) => { + const targetIndex = sessionTabsWithStatus.value.findIndex(tab => tab.sessionId === targetSessionId); + if (targetIndex === -1) return; + const sessionsToClose = sessionTabsWithStatus.value + .slice(0, targetIndex) + .map(tab => tab.sessionId); + sessionsToClose.forEach(id => sessionStore.closeSession(id)); + }; + + const handleCloseOtherEditorTabs = (targetTabId: string) => { + const tabsToClose = editorTabs.value + .filter(tab => tab.id !== targetTabId) + .map(tab => tab.id); + tabsToClose.forEach(id => handleCloseEditorTab(id)); // Reuse existing close logic + }; + + const handleCloseEditorTabsToRight = (targetTabId: string) => { + const targetIndex = editorTabs.value.findIndex(tab => tab.id === targetTabId); + if (targetIndex === -1) return; + const tabsToClose = editorTabs.value + .slice(targetIndex + 1) + .map(tab => tab.id); + tabsToClose.forEach(id => handleCloseEditorTab(id)); + }; + + const handleCloseEditorTabsToLeft = (targetTabId: string) => { + const targetIndex = editorTabs.value.findIndex(tab => tab.id === targetTabId); + if (targetIndex === -1) return; + const tabsToClose = editorTabs.value + .slice(0, targetIndex) + .map(tab => tab.id); + tabsToClose.forEach(id => handleCloseEditorTab(id)); + }; +