feat: 为ssh标签栏和文件编辑器标签栏添加右键菜单
This commit is contained in:
@@ -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) => {
|
||||
<FileEditorTabs
|
||||
:tabs="orderedTabs"
|
||||
:active-tab-id="props.activeTabId"
|
||||
@activate-tab="(tabId: string) => emit('activate-tab', tabId)"
|
||||
@close-tab="(tabId: string) => emit('close-tab', tabId)"
|
||||
@activate-tab="(tabId: string) => emit('activate-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. 编辑器头部 (显示当前激活标签信息) -->
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<!-- 编辑器头部 (使用动态计算属性) -->
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<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 type { FileTab } from '../stores/fileEditor.store';
|
||||
import TabBarContextMenu from './TabBarContextMenu.vue'; // + Import context menu
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
type: Array as PropType<FileTab[]>,
|
||||
required: true,
|
||||
@@ -17,10 +18,20 @@ defineProps({
|
||||
const emit = defineEmits<{
|
||||
(e: 'activate-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 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) => {
|
||||
emit('activate-tab', tabId);
|
||||
};
|
||||
@@ -29,6 +40,94 @@ const handleClose = (event: MouseEvent, tabId: string) => {
|
||||
event.stopPropagation(); // 防止触发 activateTab
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -36,9 +135,11 @@ const handleClose = (event: MouseEvent, tabId: string) => {
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:data-tab-id-debug="tab.id"
|
||||
class="tab-item"
|
||||
:class="{ active: tab.id === activeTabId }"
|
||||
@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"
|
||||
>
|
||||
<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>
|
||||
<!-- +++ Context Menu Instance +++ -->
|
||||
<TabBarContextMenu
|
||||
:visible="contextMenuVisible"
|
||||
:position="contextMenuPosition"
|
||||
:items="contextMenuItems"
|
||||
:target-id="menuTargetId"
|
||||
@menu-action="handleContextMenuAction"
|
||||
@close="closeContextMenu"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</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">
|
||||
import { ref, computed, PropType, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, computed, PropType, onMounted, onBeforeUnmount, watch } from 'vue'; // + onBeforeUnmount
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue';
|
||||
import TabBarContextMenu from './TabBarContextMenu.vue'; // + Import context menu
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.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 layoutStore = useLayoutStore(); // 初始化布局 store
|
||||
const connectionsStore = useConnectionsStore();
|
||||
const connectionsStore = useConnectionsStore();
|
||||
const { isHeaderVisible } = storeToRefs(layoutStore); // 从 layout store 获取主导航栏可见状态
|
||||
const route = useRoute(); // 获取路由实例
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({
|
||||
sessions: {
|
||||
type: Array as PropType<SessionTabInfoWithStatus[]>,
|
||||
type: Array as PropType<SessionTabInfoWithStatus[]>,
|
||||
required: true,
|
||||
},
|
||||
activeSessionId: {
|
||||
type: String as PropType<string | null>,
|
||||
required: false,
|
||||
default: null,
|
||||
type: String as PropType<string | null>,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits([
|
||||
'activate-session',
|
||||
'close-session',
|
||||
'open-layout-configurator',
|
||||
'request-add-connection-from-popup',
|
||||
'request-edit-connection-from-popup'
|
||||
]);
|
||||
// 定义事件 (使用对象语法修复类型)
|
||||
const emit = defineEmits<{
|
||||
(e: 'activate-session', sessionId: string): void;
|
||||
(e: 'close-session', sessionId: string): void;
|
||||
(e: 'open-layout-configurator'): void;
|
||||
(e: 'request-add-connection-from-popup'): void;
|
||||
(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) => {
|
||||
if (sessionId !== props.activeSessionId) {
|
||||
@@ -55,6 +60,12 @@ const closeSession = (event: MouseEvent, sessionId: string) => {
|
||||
const sessionStore = useSessionStore(); // Session store 保持不变
|
||||
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 = () => {
|
||||
showConnectionListPopup.value = !showConnectionListPopup.value;
|
||||
};
|
||||
@@ -98,6 +109,94 @@ const handleRequestEditFromPopup = (connection: any) => { // 假设 WorkspaceCon
|
||||
// --- 移除 handleRequestRdpFromPopup 方法 ---
|
||||
// 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 = () => {
|
||||
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 路由下生效)
|
||||
const toggleHeader = () => {
|
||||
if (isWorkspaceRoute.value) {
|
||||
@@ -153,7 +259,7 @@ const toggleButtonTitle = computed(() => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<ul class="flex list-none p-0 m-0 h-full flex-shrink-0">
|
||||
<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',
|
||||
session.sessionId === activeSessionId ? 'bg-background text-foreground' : 'bg-header text-text-secondary hover:bg-border']"
|
||||
@click="activateSession(session.sessionId)"
|
||||
@contextmenu.prevent="showContextMenu($event, session.sessionId)"
|
||||
:title="session.connectionName"
|
||||
>
|
||||
<!-- Status dot -->
|
||||
@@ -220,7 +327,14 @@ const toggleButtonTitle = computed(() => {
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -1019,6 +1019,14 @@
|
||||
"terminalTabBar": {
|
||||
"selectServerTitle": "选择要连接的服务器"
|
||||
},
|
||||
"tabs": {
|
||||
"contextMenu": {
|
||||
"close": "关闭标签页",
|
||||
"closeOthers": "关闭其他标签页",
|
||||
"closeRight": "关闭右侧标签页",
|
||||
"closeLeft": "关闭左侧标签页"
|
||||
}
|
||||
},
|
||||
"sshKeys": {
|
||||
"selector": {
|
||||
"selectPlaceholder": "选择一个 SSH 密钥...",
|
||||
|
||||
@@ -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, // 暴露新的更新方法
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -421,6 +473,9 @@ const handleCloseEditorTab = (tabId: string) => {
|
||||
@open-layout-configurator="handleOpenLayoutConfigurator"
|
||||
@request-add-connection-from-popup="handleRequestAddConnection"
|
||||
@request-edit-connection-from-popup="handleRequestEditConnection"
|
||||
@close-other-sessions="handleCloseOtherSessions"
|
||||
@close-sessions-to-right="handleCloseSessionsToRight"
|
||||
@close-sessions-to-left="handleCloseSessionsToLeft"
|
||||
/>
|
||||
|
||||
<!-- 移除 :class 绑定 -->
|
||||
@@ -450,7 +505,10 @@ const handleCloseEditorTab = (tabId: string) => {
|
||||
@find-previous="handleFindPrevious"
|
||||
@close-search="handleCloseSearch"
|
||||
@clear-terminal="handleClearTerminal"
|
||||
@change-encoding="handleChangeEncoding"
|
||||
@change-encoding="handleChangeEncoding"
|
||||
@close-other-tabs="handleCloseOtherEditorTabs"
|
||||
@close-tabs-to-right="handleCloseEditorTabsToRight"
|
||||
@close-tabs-to-left="handleCloseEditorTabsToLeft"
|
||||
></LayoutRenderer> <!-- 修正:使用单独的结束标签 -->
|
||||
<div v-else class="pane-placeholder"> <!-- 确保 v-else 紧随 v-if -->
|
||||
{{ t('layout.loading', '加载布局中...') }}
|
||||
|
||||
Reference in New Issue
Block a user