feat: 为ssh标签栏和文件编辑器标签栏添加右键菜单
This commit is contained in:
@@ -34,6 +34,10 @@ const emit = defineEmits<{
|
|||||||
(e: 'request-save', tabId: string): void; // 发送保存请求,携带 tabId
|
(e: 'request-save', tabId: string): void; // 发送保存请求,携带 tabId
|
||||||
(e: 'update:content', payload: { tabId: string; content: string }): void; // 用于 v-model 同步
|
(e: 'update:content', payload: { tabId: string; content: string }): void; // 用于 v-model 同步
|
||||||
(e: 'change-encoding', payload: { tabId: string; encoding: string }): void; // +++ 新增:编码更改事件 +++
|
(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) => {
|
|||||||
<FileEditorTabs
|
<FileEditorTabs
|
||||||
:tabs="orderedTabs"
|
:tabs="orderedTabs"
|
||||||
:active-tab-id="props.activeTabId"
|
:active-tab-id="props.activeTabId"
|
||||||
@activate-tab="(tabId: string) => emit('activate-tab', tabId)"
|
@activate-tab="(tabId: string) => emit('activate-tab', tabId)"
|
||||||
@close-tab="(tabId: string) => emit('close-tab', tabId)"
|
@close-tab="(tabId: string) => emit('close-tab', tabId)"
|
||||||
|
@close-other-tabs="(tabId: string) => emit('close-other-tabs', tabId)"
|
||||||
|
@close-tabs-to-right="(tabId: string) => emit('close-tabs-to-right', tabId)"
|
||||||
|
@close-tabs-to-left="(tabId: string) => emit('close-tabs-to-left', tabId)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
|
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const {
|
|||||||
popupTrigger,
|
popupTrigger,
|
||||||
popupFileInfo, // 包含 sessionId 和 filePath
|
popupFileInfo, // 包含 sessionId 和 filePath
|
||||||
activeTabId: globalActiveTabIdRef, // 获取全局 activeTabId
|
activeTabId: globalActiveTabIdRef, // 获取全局 activeTabId
|
||||||
tabs: globalTabsRef, // 获取全局 tabs Map
|
// tabs: globalTabsRef, // 不再使用 storeToRefs 获取 tabs
|
||||||
} = storeToRefs(fileEditorStore);
|
} = storeToRefs(fileEditorStore);
|
||||||
|
|
||||||
// 设置 Store (用于判断模式)
|
// 设置 Store (用于判断模式)
|
||||||
@@ -32,18 +32,26 @@ const { showPopupFileEditorBoolean, shareFileEditorTabsBoolean } = storeToRefs(s
|
|||||||
// --- 从 Store 获取方法 ---
|
// --- 从 Store 获取方法 ---
|
||||||
// 全局 Store Actions (用于共享模式)
|
// 全局 Store Actions (用于共享模式)
|
||||||
const {
|
const {
|
||||||
saveFile: saveGlobalFile,
|
saveFile: saveGlobalFile,
|
||||||
closeTab: closeGlobalTab,
|
closeTab: closeGlobalTab,
|
||||||
setActiveTab: setGlobalActiveTab,
|
setActiveTab: setGlobalActiveTab,
|
||||||
updateFileContent: updateGlobalFileContent,
|
updateFileContent: updateGlobalFileContent,
|
||||||
|
// + 添加右键菜单操作 actions
|
||||||
|
closeOtherTabs, // 修正:移除 Global 后缀
|
||||||
|
closeTabsToTheRight, // 修正:移除 Global 后缀
|
||||||
|
closeTabsToTheLeft, // 修正:移除 Global 后缀
|
||||||
} = fileEditorStore;
|
} = fileEditorStore;
|
||||||
|
|
||||||
// 会话 Store Actions (用于非共享模式)
|
// 会话 Store Actions (用于非共享模式)
|
||||||
const {
|
const {
|
||||||
saveFileInSession,
|
saveFileInSession,
|
||||||
closeEditorTabInSession,
|
closeEditorTabInSession,
|
||||||
setActiveEditorTabInSession,
|
setActiveEditorTabInSession,
|
||||||
updateFileContentInSession,
|
updateFileContentInSession,
|
||||||
|
// + 添加右键菜单操作 actions
|
||||||
|
closeOtherTabsInSession,
|
||||||
|
closeTabsToTheRightInSession,
|
||||||
|
closeTabsToTheLeftInSession,
|
||||||
} = sessionStore;
|
} = sessionStore;
|
||||||
|
|
||||||
// --- 移除本地文件状态 ---
|
// --- 移除本地文件状态 ---
|
||||||
@@ -89,10 +97,12 @@ const currentSession = computed(() => {
|
|||||||
|
|
||||||
// 获取当前模式下的标签页列表
|
// 获取当前模式下的标签页列表
|
||||||
const orderedTabs = computed(() => {
|
const orderedTabs = computed(() => {
|
||||||
|
// 直接访问 store.tabs
|
||||||
if (shareFileEditorTabsBoolean.value) {
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
return Array.from(globalTabsRef.value.values()); // 全局 Store
|
return Array.from(fileEditorStore.tabs.values()); // 直接访问 store
|
||||||
} else {
|
} 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;
|
const currentId = activeTabId.value;
|
||||||
if (!currentId) return null;
|
if (!currentId) return null;
|
||||||
|
|
||||||
|
// 直接访问 store.tabs
|
||||||
if (shareFileEditorTabsBoolean.value) {
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
return globalTabsRef.value.get(currentId) ?? null; // 全局 Store
|
return fileEditorStore.tabs.get(currentId) ?? null; // 直接访问 store
|
||||||
} else {
|
} else {
|
||||||
// 在会话的 editorTabs 数组中查找
|
// 非共享模式保持不变
|
||||||
return currentSession.value?.editorTabs.value.find(tab => tab.id === currentId) ?? null;
|
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 = () => {
|
const handleCloseContainer = () => {
|
||||||
@@ -291,6 +344,9 @@ onBeforeUnmount(() => {
|
|||||||
:active-tab-id="activeTabId"
|
:active-tab-id="activeTabId"
|
||||||
@activate-tab="handleActivateTab"
|
@activate-tab="handleActivateTab"
|
||||||
@close-tab="handleCloseTab"
|
@close-tab="handleCloseTab"
|
||||||
|
@close-other-tabs="handleCloseOtherTabs"
|
||||||
|
@close-tabs-to-right="handleCloseRightTabs"
|
||||||
|
@close-tabs-to-left="handleCloseLeftTabs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 编辑器头部 (使用动态计算属性) -->
|
<!-- 编辑器头部 (使用动态计算属性) -->
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PropType } from 'vue';
|
import { ref, computed, type PropType, onBeforeUnmount } from 'vue'; // + ref, computed, onBeforeUnmount
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import type { FileTab } from '../stores/fileEditor.store';
|
import type { FileTab } from '../stores/fileEditor.store';
|
||||||
|
import TabBarContextMenu from './TabBarContextMenu.vue'; // + Import context menu
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
tabs: {
|
tabs: {
|
||||||
type: Array as PropType<FileTab[]>,
|
type: Array as PropType<FileTab[]>,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -17,10 +18,20 @@ defineProps({
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'activate-tab', tabId: string): void;
|
(e: 'activate-tab', tabId: string): void;
|
||||||
(e: 'close-tab', tabId: string): void;
|
(e: 'close-tab', tabId: 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;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// +++ 右键菜单状态 +++
|
||||||
|
const contextMenuVisible = ref(false);
|
||||||
|
const contextMenuPosition = ref({ x: 0, y: 0 });
|
||||||
|
const contextTargetTabId = ref<string | null>(null); // Keep for logic inside this component if needed elsewhere
|
||||||
|
const menuTargetId = ref<string | null>(null); // + Ref specifically for passing to the menu prop
|
||||||
|
|
||||||
const handleActivate = (tabId: string) => {
|
const handleActivate = (tabId: string) => {
|
||||||
emit('activate-tab', tabId);
|
emit('activate-tab', tabId);
|
||||||
};
|
};
|
||||||
@@ -29,6 +40,94 @@ const handleClose = (event: MouseEvent, tabId: string) => {
|
|||||||
event.stopPropagation(); // 防止触发 activateTab
|
event.stopPropagation(); // 防止触发 activateTab
|
||||||
emit('close-tab', tabId);
|
emit('close-tab', tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// +++ 右键菜单方法 +++
|
||||||
|
const showContextMenu = (event: MouseEvent, tabId: string) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
console.log(`[FileTabs] showContextMenu called with tabId: ${tabId}`); // ++ Log the received tabId
|
||||||
|
contextTargetTabId.value = tabId; // Still set the original ref if needed elsewhere
|
||||||
|
menuTargetId.value = tabId; // + Set the dedicated ref for the prop
|
||||||
|
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
|
||||||
|
contextMenuVisible.value = true;
|
||||||
|
// 添加全局监听器以关闭菜单
|
||||||
|
document.addEventListener('click', closeContextMenuOnClickOutside, { capture: true, once: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
contextMenuVisible.value = false;
|
||||||
|
contextTargetTabId.value = null; // Clear original ref if needed
|
||||||
|
// menuTargetId.value = null; // -- REMOVE THIS LINE -- Let the value persist until next show
|
||||||
|
// 移除监听器(如果它仍然存在)
|
||||||
|
document.removeEventListener('click', closeContextMenuOnClickOutside, { capture: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用于全局点击监听器的函数
|
||||||
|
const closeContextMenuOnClickOutside = (event: MouseEvent) => {
|
||||||
|
closeContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// + Update function signature to receive payload
|
||||||
|
const handleContextMenuAction = (payload: { action: string; targetId: string | number | null }) => {
|
||||||
|
const { action, targetId } = payload;
|
||||||
|
console.log(`[FileTabs] handleContextMenuAction received payload:`, JSON.stringify(payload)); // + Log received payload
|
||||||
|
// const targetId = contextTargetTabId.value; // No longer needed
|
||||||
|
if (!targetId || typeof targetId !== 'string') { // Ensure targetId is a string (tab ID)
|
||||||
|
console.warn('[FileTabs] handleContextMenuAction called but targetId is null or not a string.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[FileTabs] Context menu action '${action}' requested for tab ID: ${targetId}`); // Keep original log
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'close':
|
||||||
|
emit('close-tab', targetId);
|
||||||
|
break;
|
||||||
|
case 'close-others':
|
||||||
|
emit('close-other-tabs', targetId);
|
||||||
|
break;
|
||||||
|
case 'close-right':
|
||||||
|
emit('close-tabs-to-right', targetId);
|
||||||
|
break;
|
||||||
|
case 'close-left':
|
||||||
|
emit('close-tabs-to-left', targetId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`[FileTabs] Unknown context menu action: ${action}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算右键菜单项
|
||||||
|
const contextMenuItems = computed(() => {
|
||||||
|
const items = [];
|
||||||
|
const targetId = contextTargetTabId.value;
|
||||||
|
if (!targetId) return [];
|
||||||
|
|
||||||
|
const currentIndex = props.tabs.findIndex(t => t.id === targetId);
|
||||||
|
const totalTabs = props.tabs.length;
|
||||||
|
|
||||||
|
items.push({ label: 'tabs.contextMenu.close', action: 'close' });
|
||||||
|
|
||||||
|
if (totalTabs > 1) {
|
||||||
|
items.push({ label: 'tabs.contextMenu.closeOthers', action: 'close-others' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < totalTabs - 1) {
|
||||||
|
items.push({ label: 'tabs.contextMenu.closeRight', action: 'close-right' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
items.push({ label: 'tabs.contextMenu.closeLeft', action: 'close-left' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
// +++ 组件卸载前移除全局监听器 +++
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', closeContextMenuOnClickOutside, { capture: true });
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -36,9 +135,11 @@ const handleClose = (event: MouseEvent, tabId: string) => {
|
|||||||
<div
|
<div
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
|
:data-tab-id-debug="tab.id"
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: tab.id === activeTabId }"
|
:class="{ active: tab.id === activeTabId }"
|
||||||
@click="handleActivate(tab.id)"
|
@click="handleActivate(tab.id)"
|
||||||
|
@contextmenu.prevent="(event) => { console.log(`[FileTabs Template Debug] Context menu for tab.id: ${tab.id}`); showContextMenu(event, tab.id); }"
|
||||||
:title="tab.filePath"
|
:title="tab.filePath"
|
||||||
>
|
>
|
||||||
<span class="tab-filename">{{ tab.filename }}</span>
|
<span class="tab-filename">{{ tab.filename }}</span>
|
||||||
@@ -54,6 +155,15 @@ const handleClose = (event: MouseEvent, tabId: string) => {
|
|||||||
<div v-if="tabs.length === 0" class="no-tabs-placeholder">
|
<div v-if="tabs.length === 0" class="no-tabs-placeholder">
|
||||||
<!-- 可以留空或添加提示 -->
|
<!-- 可以留空或添加提示 -->
|
||||||
</div>
|
</div>
|
||||||
|
<!-- +++ Context Menu Instance +++ -->
|
||||||
|
<TabBarContextMenu
|
||||||
|
:visible="contextMenuVisible"
|
||||||
|
:position="contextMenuPosition"
|
||||||
|
:items="contextMenuItems"
|
||||||
|
:target-id="menuTargetId"
|
||||||
|
@menu-action="handleContextMenuAction"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -66,9 +66,13 @@ const emit = defineEmits({
|
|||||||
'close-search': null, // ()
|
'close-search': null, // ()
|
||||||
'clear-terminal': null, // () +++ 添加 clear-terminal 事件 +++
|
'clear-terminal': null, // () +++ 添加 clear-terminal 事件 +++
|
||||||
'change-encoding': null, // +++ 添加 change-encoding 事件 +++
|
'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 事件 ---
|
// --- 移除 RDP 事件 ---
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
@@ -206,6 +210,10 @@ const componentProps = computed(() => {
|
|||||||
onRequestSave: (tabId: string) => emit('saveEditorTab', tabId),
|
onRequestSave: (tabId: string) => emit('saveEditorTab', tabId),
|
||||||
// +++ 添加:转发 change-encoding 事件 +++
|
// +++ 添加:转发 change-encoding 事件 +++
|
||||||
onChangeEncoding: (payload: { tabId: string; encoding: string }) => emit('change-encoding', payload),
|
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':
|
case 'commandBar':
|
||||||
// CommandInputBar 需要转发 send-command 事件
|
// CommandInputBar 需要转发 send-command 事件
|
||||||
@@ -514,6 +522,9 @@ onMounted(() => {
|
|||||||
@close-search="emit('close-search')"
|
@close-search="emit('close-search')"
|
||||||
@clear-terminal="() => emit('clear-terminal')"
|
@clear-terminal="() => emit('clear-terminal')"
|
||||||
@change-encoding="emit('change-encoding', $event)"
|
@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"
|
class="flex-grow overflow-auto"
|
||||||
/>
|
/>
|
||||||
</pane>
|
</pane>
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, PropType } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
label: string;
|
||||||
|
action: string;
|
||||||
|
disabled?: boolean; // 可选:是否禁用
|
||||||
|
isSeparator?: boolean; // 可选:是否是分隔线
|
||||||
|
isDanger?: boolean; // 可选:是否是危险操作 (例如红色文本)
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: Object as PropType<{ x: number; y: number }>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<MenuItem[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// + Add targetId prop
|
||||||
|
targetId: {
|
||||||
|
type: [String, Number, null] as PropType<string | number | null>,
|
||||||
|
default: null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
// + Update signature to include targetId
|
||||||
|
(e: 'menu-action', payload: { action: string; targetId: string | number | null }): void;
|
||||||
|
(e: 'close'): void; // 请求关闭菜单
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const menuStyle = computed(() => ({
|
||||||
|
top: `${props.position.y}px`,
|
||||||
|
left: `${props.position.x}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleAction = (item: MenuItem) => {
|
||||||
|
console.log(`[ContextMenu] handleAction called for item:`, JSON.stringify(item)); // + Log item
|
||||||
|
if (!item.disabled && !item.isSeparator) {
|
||||||
|
console.log(`[ContextMenu] Inside handleAction, props.targetId is:`, props.targetId); // ++ Log prop value before emit
|
||||||
|
const payload = { action: item.action, targetId: props.targetId };
|
||||||
|
console.log(`[ContextMenu] Emitting menu-action with payload:`, JSON.stringify(payload)); // + Log emit payload
|
||||||
|
emit('menu-action', payload);
|
||||||
|
emit('close'); // 点击后自动关闭
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击菜单外部时,也应该关闭,这通常在父组件中处理 document click listener
|
||||||
|
// 但这里也添加一个遮罩层点击关闭
|
||||||
|
const handleOverlayClick = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="fixed inset-0 z-40"
|
||||||
|
@click.self="handleOverlayClick"
|
||||||
|
@contextmenu.prevent
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="fixed bg-background border border-border/50 shadow-xl rounded-lg py-1.5 z-50 min-w-[180px]"
|
||||||
|
:style="menuStyle"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<ul class="list-none p-0 m-0">
|
||||||
|
<template v-for="(item, index) in items" :key="index">
|
||||||
|
<li v-if="item.isSeparator" class="border-t border-border/50 my-1 mx-1"></li>
|
||||||
|
<li
|
||||||
|
v-else
|
||||||
|
class="group px-4 py-1.5 flex items-center text-sm transition-colors duration-150 rounded-md mx-1"
|
||||||
|
:class="[
|
||||||
|
item.disabled
|
||||||
|
? 'text-text-secondary opacity-50 cursor-not-allowed'
|
||||||
|
: item.isDanger
|
||||||
|
? 'text-error hover:bg-error/10 cursor-pointer'
|
||||||
|
: 'text-foreground hover:bg-primary/10 hover:text-primary cursor-pointer',
|
||||||
|
]"
|
||||||
|
@click="handleAction(item)"
|
||||||
|
>
|
||||||
|
<!-- 移除了图标 -->
|
||||||
|
<span>{{ t(item.label, item.label) }}</span> <!-- 使用 i18n -->
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 可以添加一些额外的样式,如果需要的话 */
|
||||||
|
</style>
|
||||||
@@ -1,44 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, PropType, onMounted, watch } from 'vue';
|
import { ref, computed, PropType, onMounted, onBeforeUnmount, watch } from 'vue'; // + onBeforeUnmount
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue';
|
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue';
|
||||||
|
import TabBarContextMenu from './TabBarContextMenu.vue'; // + Import context menu
|
||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
|
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
|
||||||
import { useLayoutStore, type PaneName } from '../stores/layout.store';
|
import { useLayoutStore, type PaneName } from '../stores/layout.store';
|
||||||
|
|
||||||
import type { SessionTabInfoWithStatus } from '../stores/session.store';
|
import type { SessionTabInfoWithStatus } from '../stores/session.store';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const { t } = useI18n(); // 初始化 i18n
|
const { t } = useI18n(); // 初始化 i18n
|
||||||
const layoutStore = useLayoutStore(); // 初始化布局 store
|
const layoutStore = useLayoutStore(); // 初始化布局 store
|
||||||
const connectionsStore = useConnectionsStore();
|
const connectionsStore = useConnectionsStore();
|
||||||
const { isHeaderVisible } = storeToRefs(layoutStore); // 从 layout store 获取主导航栏可见状态
|
const { isHeaderVisible } = storeToRefs(layoutStore); // 从 layout store 获取主导航栏可见状态
|
||||||
const route = useRoute(); // 获取路由实例
|
const route = useRoute(); // 获取路由实例
|
||||||
|
|
||||||
// 定义 Props
|
// 定义 Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
sessions: {
|
sessions: {
|
||||||
type: Array as PropType<SessionTabInfoWithStatus[]>,
|
type: Array as PropType<SessionTabInfoWithStatus[]>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
activeSessionId: {
|
activeSessionId: {
|
||||||
type: String as PropType<string | null>,
|
type: String as PropType<string | null>,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件 (使用对象语法修复类型)
|
||||||
const emit = defineEmits([
|
const emit = defineEmits<{
|
||||||
'activate-session',
|
(e: 'activate-session', sessionId: string): void;
|
||||||
'close-session',
|
(e: 'close-session', sessionId: string): void;
|
||||||
'open-layout-configurator',
|
(e: 'open-layout-configurator'): void;
|
||||||
'request-add-connection-from-popup',
|
(e: 'request-add-connection-from-popup'): void;
|
||||||
'request-edit-connection-from-popup'
|
(e: 'request-edit-connection-from-popup', connection: any): void; // 保持 any 或使用 ConnectionInfo
|
||||||
]);
|
// + 新增右键菜单事件
|
||||||
|
(e: 'close-other-sessions', sessionId: string): void;
|
||||||
|
(e: 'close-sessions-to-right', sessionId: string): void;
|
||||||
|
(e: 'close-sessions-to-left', sessionId: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const activateSession = (sessionId: string) => {
|
const activateSession = (sessionId: string) => {
|
||||||
if (sessionId !== props.activeSessionId) {
|
if (sessionId !== props.activeSessionId) {
|
||||||
@@ -55,6 +60,12 @@ const closeSession = (event: MouseEvent, sessionId: string) => {
|
|||||||
const sessionStore = useSessionStore(); // Session store 保持不变
|
const sessionStore = useSessionStore(); // Session store 保持不变
|
||||||
const showConnectionListPopup = ref(false); // 连接列表弹出状态
|
const showConnectionListPopup = ref(false); // 连接列表弹出状态
|
||||||
|
|
||||||
|
// +++ 右键菜单状态 +++
|
||||||
|
const contextMenuVisible = ref(false);
|
||||||
|
const contextMenuPosition = ref({ x: 0, y: 0 });
|
||||||
|
const contextTargetSessionId = ref<string | null>(null); // Keep for logic inside this component if needed elsewhere
|
||||||
|
const menuTargetId = ref<string | null>(null); // + Ref specifically for passing to the menu prop
|
||||||
|
|
||||||
const togglePopup = () => {
|
const togglePopup = () => {
|
||||||
showConnectionListPopup.value = !showConnectionListPopup.value;
|
showConnectionListPopup.value = !showConnectionListPopup.value;
|
||||||
};
|
};
|
||||||
@@ -98,6 +109,94 @@ const handleRequestEditFromPopup = (connection: any) => { // 假设 WorkspaceCon
|
|||||||
// --- 移除 handleRequestRdpFromPopup 方法 ---
|
// --- 移除 handleRequestRdpFromPopup 方法 ---
|
||||||
// const handleRequestRdpFromPopup = (connection: ConnectionInfo) => { ... };
|
// const handleRequestRdpFromPopup = (connection: ConnectionInfo) => { ... };
|
||||||
|
|
||||||
|
// +++ 右键菜单方法 +++
|
||||||
|
const showContextMenu = (event: MouseEvent, sessionId: string) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
contextTargetSessionId.value = sessionId; // Still set the original ref if needed elsewhere
|
||||||
|
menuTargetId.value = sessionId; // + Set the dedicated ref for the prop
|
||||||
|
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
|
||||||
|
contextMenuVisible.value = true;
|
||||||
|
// 添加全局监听器以关闭菜单
|
||||||
|
document.addEventListener('click', closeContextMenuOnClickOutside, { capture: true, once: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
contextMenuVisible.value = false;
|
||||||
|
contextTargetSessionId.value = null; // Clear original ref if needed
|
||||||
|
// menuTargetId.value = null; // -- REMOVE THIS LINE -- Let the value persist until next show
|
||||||
|
// 移除监听器(如果它仍然存在)
|
||||||
|
document.removeEventListener('click', closeContextMenuOnClickOutside, { capture: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用于全局点击监听器的函数
|
||||||
|
const closeContextMenuOnClickOutside = (event: MouseEvent) => {
|
||||||
|
// 检查点击是否发生在菜单内部,如果是,则不关闭
|
||||||
|
// 这个检查在 TabBarContextMenu 组件内部通过 @click.stop 完成了
|
||||||
|
// 所以这里可以直接关闭
|
||||||
|
closeContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// + Update function signature to receive payload
|
||||||
|
const handleContextMenuAction = (payload: { action: string; targetId: string | number | null }) => {
|
||||||
|
const { action, targetId } = payload;
|
||||||
|
console.log(`[TabBar] handleContextMenuAction received payload:`, JSON.stringify(payload)); // + Log received payload
|
||||||
|
// const targetId = contextTargetSessionId.value; // No longer needed
|
||||||
|
if (!targetId || typeof targetId !== 'string') { // Ensure targetId is a string (session ID)
|
||||||
|
console.warn('[TabBar] handleContextMenuAction called but targetId is null or not a string.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[TabBar] Context menu action '${action}' requested for session ID: ${targetId}`); // Keep original log
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'close':
|
||||||
|
emit('close-session', targetId);
|
||||||
|
break;
|
||||||
|
case 'close-others':
|
||||||
|
emit('close-other-sessions', targetId);
|
||||||
|
break;
|
||||||
|
case 'close-right':
|
||||||
|
emit('close-sessions-to-right', targetId);
|
||||||
|
break;
|
||||||
|
case 'close-left':
|
||||||
|
// 注意:关闭左侧通常不包括当前标签本身
|
||||||
|
emit('close-sessions-to-left', targetId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`[TabBar] Unknown context menu action: ${action}`);
|
||||||
|
}
|
||||||
|
// closeContextMenu(); // TabBarContextMenu 内部点击后会触发 close 事件
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算右键菜单项
|
||||||
|
const contextMenuItems = computed(() => {
|
||||||
|
const items = [];
|
||||||
|
const targetId = contextTargetSessionId.value;
|
||||||
|
if (!targetId) return [];
|
||||||
|
|
||||||
|
const currentIndex = props.sessions.findIndex(s => s.sessionId === targetId);
|
||||||
|
const totalTabs = props.sessions.length;
|
||||||
|
|
||||||
|
items.push({ label: 'tabs.contextMenu.close', action: 'close' }); // 使用 i18n key
|
||||||
|
|
||||||
|
if (totalTabs > 1) {
|
||||||
|
items.push({ label: 'tabs.contextMenu.closeOthers', action: 'close-others' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < totalTabs - 1) {
|
||||||
|
items.push({ label: 'tabs.contextMenu.closeRight', action: 'close-right' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
items.push({ label: 'tabs.contextMenu.closeLeft', action: 'close-left' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// 新增:处理打开布局配置器的事件
|
// 新增:处理打开布局配置器的事件
|
||||||
const openLayoutConfigurator = () => {
|
const openLayoutConfigurator = () => {
|
||||||
console.log('[TabBar] Emitting open-layout-configurator event');
|
console.log('[TabBar] Emitting open-layout-configurator event');
|
||||||
@@ -125,6 +224,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// +++ 组件卸载前移除全局监听器 +++
|
||||||
|
// onBeforeUnmount is imported now
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', closeContextMenuOnClickOutside, { capture: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// 切换主导航栏可见性 (只在 workspace 路由下生效)
|
// 切换主导航栏可见性 (只在 workspace 路由下生效)
|
||||||
const toggleHeader = () => {
|
const toggleHeader = () => {
|
||||||
if (isWorkspaceRoute.value) {
|
if (isWorkspaceRoute.value) {
|
||||||
@@ -153,7 +259,7 @@ const toggleButtonTitle = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex bg-header border border-border rounded-t-md mx-2 mt-2 overflow-hidden h-10">
|
<div class="flex bg-header border border-border rounded-t-md mx-2 mt-2 overflow-hidden h-10">
|
||||||
<div class="flex items-center overflow-x-auto flex-shrink min-w-0">
|
<div class="flex items-center overflow-x-auto flex-shrink min-w-0">
|
||||||
<ul class="flex list-none p-0 m-0 h-full flex-shrink-0">
|
<ul class="flex list-none p-0 m-0 h-full flex-shrink-0">
|
||||||
<li
|
<li
|
||||||
@@ -162,6 +268,7 @@ const toggleButtonTitle = computed(() => {
|
|||||||
:class="['flex items-center px-3 h-full cursor-pointer border-r border-border transition-colors duration-150 relative group',
|
:class="['flex items-center px-3 h-full cursor-pointer border-r border-border transition-colors duration-150 relative group',
|
||||||
session.sessionId === activeSessionId ? 'bg-background text-foreground' : 'bg-header text-text-secondary hover:bg-border']"
|
session.sessionId === activeSessionId ? 'bg-background text-foreground' : 'bg-header text-text-secondary hover:bg-border']"
|
||||||
@click="activateSession(session.sessionId)"
|
@click="activateSession(session.sessionId)"
|
||||||
|
@contextmenu.prevent="showContextMenu($event, session.sessionId)"
|
||||||
:title="session.connectionName"
|
:title="session.connectionName"
|
||||||
>
|
>
|
||||||
<!-- Status dot -->
|
<!-- Status dot -->
|
||||||
@@ -220,7 +327,14 @@ const toggleButtonTitle = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- +++ Context Menu Instance (Ensure it's present) +++ -->
|
||||||
|
<TabBarContextMenu
|
||||||
|
:visible="contextMenuVisible"
|
||||||
|
:position="contextMenuPosition"
|
||||||
|
:items="contextMenuItems"
|
||||||
|
:target-id="menuTargetId"
|
||||||
|
@menu-action="handleContextMenuAction"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1016,6 +1016,14 @@
|
|||||||
"terminalTabBar": {
|
"terminalTabBar": {
|
||||||
"selectServerTitle": "Select server to connect"
|
"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": {
|
"sshKeys": {
|
||||||
"selector": {
|
"selector": {
|
||||||
"selectPlaceholder": "Select an SSH key...",
|
"selectPlaceholder": "Select an SSH key...",
|
||||||
|
|||||||
@@ -1019,6 +1019,14 @@
|
|||||||
"terminalTabBar": {
|
"terminalTabBar": {
|
||||||
"selectServerTitle": "选择要连接的服务器"
|
"selectServerTitle": "选择要连接的服务器"
|
||||||
},
|
},
|
||||||
|
"tabs": {
|
||||||
|
"contextMenu": {
|
||||||
|
"close": "关闭标签页",
|
||||||
|
"closeOthers": "关闭其他标签页",
|
||||||
|
"closeRight": "关闭右侧标签页",
|
||||||
|
"closeLeft": "关闭左侧标签页"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sshKeys": {
|
"sshKeys": {
|
||||||
"selector": {
|
"selector": {
|
||||||
"selectPlaceholder": "选择一个 SSH 密钥...",
|
"selectPlaceholder": "选择一个 SSH 密钥...",
|
||||||
|
|||||||
@@ -413,8 +413,61 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
// setEditorVisibility('closed'); // 移除:容器可见性由外部控制
|
// 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)) {
|
if (tabs.value.has(tabId)) {
|
||||||
activeTabId.value = tabId;
|
activeTabId.value = tabId;
|
||||||
console.log(`[文件编辑器 Store] 激活标签页: ${tabId}`);
|
console.log(`[文件编辑器 Store] 激活标签页: ${tabId}`);
|
||||||
@@ -556,6 +609,9 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
openFile,
|
openFile,
|
||||||
saveFile,
|
saveFile,
|
||||||
closeTab,
|
closeTab,
|
||||||
|
closeOtherTabs, // +++ 暴露新 action +++
|
||||||
|
closeTabsToTheRight, // +++ 暴露新 action +++
|
||||||
|
closeTabsToTheLeft, // +++ 暴露新 action +++
|
||||||
closeAllTabs,
|
closeAllTabs,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
updateFileContent, // 暴露新的更新方法
|
updateFileContent, // 暴露新的更新方法
|
||||||
|
|||||||
@@ -495,7 +495,7 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
// 创建新标签页 (使用简化后的 FileTab 接口)
|
// 创建新标签页 (使用简化后的 FileTab 接口)
|
||||||
// --- 修复:初始化 rawContentBase64 ---
|
// --- 修复:初始化 rawContentBase64 ---
|
||||||
const newTab: FileTab = {
|
const newTab: FileTab = {
|
||||||
id: generateSessionId(), // 使用独立 ID
|
id: `${sessionId}:${fileInfo.fullPath}`, // <-- 修复:使用 sessionId:filePath 作为 ID
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
filePath: fileInfo.fullPath,
|
filePath: fileInfo.fullPath,
|
||||||
filename: fileInfo.name,
|
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
|
updateFileContentInSession, // 导出更新内容 Action
|
||||||
saveFileInSession, // 导出保存文件 Action
|
saveFileInSession, // 导出保存文件 Action
|
||||||
changeEncodingInSession, // 导出更改编码 Action
|
changeEncodingInSession, // 导出更改编码 Action
|
||||||
|
closeOtherTabsInSession, // +++ 导出新 action +++
|
||||||
|
closeTabsToTheRightInSession, // +++ 导出新 action +++
|
||||||
|
closeTabsToTheLeftInSession, // +++ 导出新 action +++
|
||||||
// --- RDP Modal Actions ---
|
// --- RDP Modal Actions ---
|
||||||
openRdpModal, // 导出打开 RDP 模态框 Action
|
openRdpModal, // 导出打开 RDP 模态框 Action
|
||||||
closeRdpModal, // 导出关闭 RDP 模态框 Action
|
closeRdpModal, // 导出关闭 RDP 模态框 Action
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import LayoutConfigurator from '../components/LayoutConfigurator.vue'; // ***
|
|||||||
import RemoteDesktopModal from '../components/RemoteDesktopModal.vue'; // +++ 导入 RDP 模态框 +++
|
import RemoteDesktopModal from '../components/RemoteDesktopModal.vue'; // +++ 导入 RDP 模态框 +++
|
||||||
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 session store
|
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 session store
|
||||||
import { useSettingsStore } from '../stores/settings.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 { useLayoutStore } from '../stores/layout.store'; // 重复导入,移除
|
||||||
import { useCommandHistoryStore } from '../stores/commandHistory.store';
|
import { useCommandHistoryStore } from '../stores/commandHistory.store';
|
||||||
// import type { ConnectionInfo } from '../stores/connections.store'; // 重复导入,移除
|
// import type { ConnectionInfo } from '../stores/connections.store'; // 重复导入,移除
|
||||||
@@ -37,7 +37,7 @@ const { layoutTree } = storeToRefs(layoutStore); // 只获取布局树
|
|||||||
|
|
||||||
// --- 计算属性 (用于动态绑定编辑器 Props) ---
|
// --- 计算属性 (用于动态绑定编辑器 Props) ---
|
||||||
// 这些计算属性现在需要传递给 LayoutRenderer
|
// 这些计算属性现在需要传递给 LayoutRenderer
|
||||||
const editorTabs = computed(() => {
|
const editorTabs = computed((): FileTab[] => { // Ensure return type is FileTab[]
|
||||||
if (shareFileEditorTabsBoolean.value) {
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
return globalEditorTabs.value;
|
return globalEditorTabs.value;
|
||||||
} else {
|
} else {
|
||||||
@@ -292,7 +292,7 @@ const handleCloseSearch = () => {
|
|||||||
console.warn(`[WorkspaceView] Cannot clear search, no active session manager.`);
|
console.warn(`[WorkspaceView] Cannot clear search, no active session manager.`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// +++ 新增:处理清空终端事件 +++
|
// +++ 新增:处理清空终端事件 +++
|
||||||
const handleClearTerminal = () => {
|
const handleClearTerminal = () => {
|
||||||
const currentSession = activeSession.value;
|
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.`);
|
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
|
// Removed computed properties for search results, will pass manager directly
|
||||||
// --- 编辑器操作处理 (用于 FileEditorContainer) ---
|
// --- 编辑器操作处理 (用于 FileEditorContainer) ---
|
||||||
const handleCloseEditorTab = (tabId: string) => {
|
const handleCloseEditorTab = (tabId: string) => {
|
||||||
@@ -407,6 +407,58 @@ const handleCloseEditorTab = (tabId: string) => {
|
|||||||
|
|
||||||
// RDP 事件处理方法已被移除
|
// 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));
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -421,6 +473,9 @@ const handleCloseEditorTab = (tabId: string) => {
|
|||||||
@open-layout-configurator="handleOpenLayoutConfigurator"
|
@open-layout-configurator="handleOpenLayoutConfigurator"
|
||||||
@request-add-connection-from-popup="handleRequestAddConnection"
|
@request-add-connection-from-popup="handleRequestAddConnection"
|
||||||
@request-edit-connection-from-popup="handleRequestEditConnection"
|
@request-edit-connection-from-popup="handleRequestEditConnection"
|
||||||
|
@close-other-sessions="handleCloseOtherSessions"
|
||||||
|
@close-sessions-to-right="handleCloseSessionsToRight"
|
||||||
|
@close-sessions-to-left="handleCloseSessionsToLeft"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 移除 :class 绑定 -->
|
<!-- 移除 :class 绑定 -->
|
||||||
@@ -450,7 +505,10 @@ const handleCloseEditorTab = (tabId: string) => {
|
|||||||
@find-previous="handleFindPrevious"
|
@find-previous="handleFindPrevious"
|
||||||
@close-search="handleCloseSearch"
|
@close-search="handleCloseSearch"
|
||||||
@clear-terminal="handleClearTerminal"
|
@clear-terminal="handleClearTerminal"
|
||||||
@change-encoding="handleChangeEncoding"
|
@change-encoding="handleChangeEncoding"
|
||||||
|
@close-other-tabs="handleCloseOtherEditorTabs"
|
||||||
|
@close-tabs-to-right="handleCloseEditorTabsToRight"
|
||||||
|
@close-tabs-to-left="handleCloseEditorTabsToLeft"
|
||||||
></LayoutRenderer> <!-- 修正:使用单独的结束标签 -->
|
></LayoutRenderer> <!-- 修正:使用单独的结束标签 -->
|
||||||
<div v-else class="pane-placeholder"> <!-- 确保 v-else 紧随 v-if -->
|
<div v-else class="pane-placeholder"> <!-- 确保 v-else 紧随 v-if -->
|
||||||
{{ t('layout.loading', '加载布局中...') }}
|
{{ t('layout.loading', '加载布局中...') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user