refactor: 引入 workspaceEvents 并迁移核心组件事件处理

This commit is contained in:
Baobhan Sith
2025-05-09 13:45:20 +08:00
parent 70b2d92d8a
commit fdf5c18dfb
12 changed files with 231 additions and 236 deletions
+3 -2
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@simplewebauthn/browser": "^9.0.1",
"@tailwindcss/vite": "^4.1.4",
"@vscode/iconv-lite-umd": "^0.7.0",
"@vueuse/core": "^13.1.0",
@@ -21,6 +22,7 @@
"date-fns": "^4.1.0",
"guacamole-common-js": "^1.5.0",
"iconv-lite": "^0.6.3",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.2",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
@@ -33,8 +35,7 @@
"vue3-recaptcha2": "^1.8.0",
"vuedraggable": "^4.1.0",
"xterm": "^5.3.0",
"xterm-addon-web-links": "^0.9.0",
"@simplewebauthn/browser": "^9.0.1"
"xterm-addon-web-links": "^0.9.0"
},
"devDependencies": {
"@types/node": "^20",
@@ -8,19 +8,14 @@ import { useSettingsStore } from '../stores/settings.store';
import { useQuickCommandsStore } from '../stores/quickCommands.store';
import { useCommandHistoryStore } from '../stores/commandHistory.store';
import QuickCommandsModal from './QuickCommandsModal.vue'; // +++ Import the modal component +++
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents'; // +++ 新增导入 +++
// Disable attribute inheritance as this component has multiple root nodes (div + modal)
defineOptions({ inheritAttrs: false });
const emit = defineEmits([
'send-command',
'search',
'find-next',
'find-previous',
'close-search',
'clear-terminal',
'toggle-virtual-keyboard' // +++ Add new emit +++
]);
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
const emit = defineEmits(['toggle-virtual-keyboard']);
const { t } = useI18n();
const focusSwitcherStore = useFocusSwitcherStore();
const settingsStore = useSettingsStore();
@@ -72,7 +67,7 @@ const currentSessionCommandInput = computed({
const sendCommand = () => {
const command = currentSessionCommandInput.value; // 使用计算属性获取值
console.log(`[CommandInputBar] Sending command: ${command || '<Enter>'} `);
emit('send-command', command);
emitWorkspaceEvent('terminal:sendCommand', { command });
// 清空 store 中的值
if (activeSessionId.value) {
updateSessionCommandInput(activeSessionId.value, '');
@@ -83,7 +78,7 @@ const toggleSearch = () => {
isSearching.value = !isSearching.value;
if (!isSearching.value) {
searchTerm.value = ''; // 关闭搜索时清空
emit('close-search'); // 通知父组件关闭搜索
emitWorkspaceEvent('search:close'); // 通知父组件关闭搜索
} else {
// 可以在这里聚焦搜索输入框
// nextTick(() => searchInputRef.value?.focus());
@@ -91,16 +86,16 @@ const toggleSearch = () => {
};
const performSearch = () => {
emit('search', searchTerm.value);
emitWorkspaceEvent('search:start', { term: searchTerm.value });
// 实际的计数更新逻辑应该由父组件通过 props 或事件传递回来
};
const findNext = () => {
emit('find-next');
emitWorkspaceEvent('search:findNext');
};
const findPrevious = () => {
emit('find-previous');
emitWorkspaceEvent('search:findPrevious');
};
// 监听搜索词变化,执行搜索
@@ -151,7 +146,7 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => {
if (selectedCommand !== undefined) {
event.preventDefault();
console.log(`[CommandInputBar] Enter detected with selection. Sending selected command: ${selectedCommand}`);
emit('send-command', selectedCommand); // 发送选中命令 (移除多余的 \n)
emitWorkspaceEvent('terminal:sendCommand', { command: selectedCommand }); // 发送选中命令
if (activeSessionId.value) {
updateSessionCommandInput(activeSessionId.value, ''); // 清空输入框
}
@@ -190,7 +185,7 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => {
// Handle Ctrl+C when input is empty
event.preventDefault();
console.log('[CommandInputBar] Ctrl+C detected with empty input. Sending SIGINT.');
emit('send-command', '\x03'); // Send ETX character (Ctrl+C)
emitWorkspaceEvent('terminal:sendCommand', { command: '\x03' }); // Send ETX character (Ctrl+C)
} else if (!event.altKey && event.key === 'Enter') {
// Handle regular Enter key press - send current input (empty or not)
event.preventDefault(); // Prevent default if needed, e.g., form submission
@@ -288,7 +283,7 @@ const closeQuickCommandsModal = () => {
// +++ Handler for command execution from the modal +++
const handleQuickCommandExecute = (command: string) => {
console.log(`[CommandInputBar] Executing quick command: ${command}`);
emit('send-command', command); // Emit the command to the parent
emitWorkspaceEvent('terminal:sendCommand', { command }); // Emit the command to the parent
closeQuickCommandsModal(); // Close the modal after selection
};
</script>
@@ -298,7 +293,7 @@ const handleQuickCommandExecute = (command: string) => {
<div class="flex-grow flex items-center bg-transparent relative gap-1 px-2"> <!-- Added px-2 here -->
<!-- Clear Terminal Button -->
<button
@click="emit('clear-terminal')"
@click="emitWorkspaceEvent('terminal:clear')"
class="flex-shrink-0 flex items-center justify-center w-8 h-8 border border-border/50 rounded-lg text-text-secondary transition-colors duration-200 hover:bg-border hover:text-foreground"
:title="t('commandInputBar.clearTerminal', '清空终端')"
>
@@ -10,8 +10,10 @@ import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++
import { useSessionStore } from '../stores/session.store'; // +++ 导入会话 Store +++
import { useSettingsStore } from '../stores/settings.store'; // +++ 导入设置 Store +++
import { storeToRefs } from 'pinia'; // +++ 导入 storeToRefs +++
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents'; // +++ 新增导入 +++
const { t } = useI18n();
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
const sessionStore = useSessionStore(); // +++ 实例化会话 Store +++
const settingsStore = useSettingsStore(); // +++ 实例化设置 Store +++
@@ -33,18 +35,7 @@ const props = defineProps({
},
});
// --- Emits ---
const emit = defineEmits<{
(e: 'activate-tab', tabId: string): void;
(e: 'close-tab', tabId: string): void;
(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;
}>();
// --- 计算属性,用于模板绑定 ---
@@ -112,7 +103,7 @@ watch(localEditorContent, (newContent) => {
if (activeTab.value && newContent !== activeTab.value.content) {
// console.log(`[EditorContainer] Emitting update:content for tab ${activeTab.value.id}`);
// 只有当内容实际改变时才发出事件
emit('update:content', { tabId: activeTab.value.id, content: newContent });
emitWorkspaceEvent('editor:updateContent', { tabId: activeTab.value.id, content: newContent });
// 注意:isModified 状态应该由 Store 根据 content 和 originalContent 计算
}
});
@@ -199,7 +190,7 @@ const encodingOptions = ref([
// --- 事件处理 ---
const handleSaveRequest = () => {
if (activeTab.value) {
emit('request-save', activeTab.value.id); // 发出保存请求事件
emitWorkspaceEvent('editor:saveTab', { tabId: activeTab.value.id }); // 发出保存请求事件
}
};
@@ -209,7 +200,7 @@ const handleEncodingChange = (event: Event) => {
const newEncoding = target.value;
if (activeTab.value && newEncoding && newEncoding !== currentSelectedEncoding.value) {
console.log(`[EditorContainer] Encoding changed to ${newEncoding} for tab ${activeTab.value.id}`);
emit('change-encoding', { tabId: activeTab.value.id, encoding: newEncoding });
emitWorkspaceEvent('editor:changeEncoding', { tabId: activeTab.value.id, encoding: newEncoding });
}
};
@@ -278,7 +269,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
const nextTabId = props.tabs[nextIndex]?.id;
if (nextTabId) {
emit('activate-tab', nextTabId);
emitWorkspaceEvent('editor:activateTab', { tabId: nextTabId });
}
}
};
@@ -292,11 +283,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)"
@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)"
@activate-tab="(tabId: string) => emitWorkspaceEvent('editor:activateTab', { tabId })"
@close-tab="(tabId: string) => emitWorkspaceEvent('editor:closeTab', { tabId })"
@close-other-tabs="(tabId: string) => emitWorkspaceEvent('editor:closeOtherTabs', { tabId })"
@close-tabs-to-right="(tabId: string) => emitWorkspaceEvent('editor:closeTabsToRight', { tabId })"
@close-tabs-to-left="(tabId: string) => emitWorkspaceEvent('editor:closeTabsToLeft', { tabId })"
/>
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
@@ -11,7 +11,7 @@ import { useFileEditorStore } from '../stores/fileEditor.store'; // <-- Import F
import { useSettingsStore } from '../stores/settings.store'; // +++ Import SettingsStore +++
import { useSidebarResize } from '../composables/useSidebarResize'; // +++ Import useSidebarResize +++
import { storeToRefs } from 'pinia';
import { defineEmits } from 'vue';
// import { defineEmits } from 'vue'; // --- 移除 ---
// --- Props ---
const props = defineProps({
@@ -44,39 +44,9 @@ const props = defineProps({
type: Boolean,
default: false,
},
// Removed terminalManager prop definition
});
// --- Emits ---
// *** 新增:声明所有需要转发的事件 (使用对象语法) ***
const emit = defineEmits({
'sendCommand': null, // (command: string) - No validation needed here for now
'terminalInput': null, // (payload: { sessionId: string; data: string })
'terminalResize': null, // (payload: { sessionId: string; dims: { cols: number; rows: number } })
'closeEditorTab': null, // (tabId: string)
'activateEditorTab': null, // (tabId: string)
'updateEditorContent': null, // (payload: { tabId: string; content: string })
'saveEditorTab': null, // (tabId: string)
'connect-request': null, // (id: number)
'open-new-session': null, // (id: number)
'request-add-connection': null, // ()
'request-edit-connection': null, // (conn: any)
// *** 修正:更新 terminal-ready 事件的 payload 类型 ***
'terminal-ready': (payload: { sessionId: string; terminal: any }) => // 使用 any 简化类型检查,或导入 Terminal
typeof payload === 'object' && typeof payload.sessionId === 'string' && typeof payload.terminal === 'object',
// *** 新增:声明搜索相关事件 ***
'search': null, // (searchTerm: string)
'find-next': null, // ()
'find-previous': null, // ()
'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();
@@ -160,17 +130,8 @@ const componentProps = computed(() => {
return {
sessionId: props.activeSessionId ?? '', // 如果 activeSessionId 为 null,则传递空字符串
isActive: true,
// *** 添加日志并修正事件处理 ***
onReady: (payload: { sessionId: string; terminal: any }) => {
console.log(`[LayoutRenderer ${props.activeSessionId}] 收到内部 Terminal 的 'ready' 事件:`, payload); // 添加日志
emit('terminal-ready', payload); // 直接转发收到的 payload
},
onData: (data: string) => emit('terminalInput', { sessionId: props.activeSessionId ?? '', data }), // 包装成 payload,确保 sessionId 不为 null
onResize: (dims: { cols: number; rows: number }) => emit('terminalResize', { sessionId: props.activeSessionId ?? '', dims }), // 包装成 payload,确保 sessionId 不为 null
// --- 移除事件转发 ---
};
// --- 添加日志:确认 onReady 是否在 props 中 ---
console.log(`[LayoutRenderer ${props.activeSessionId}] Terminal componentProps 计算完成,包含 onReady。`);
// -----------------------------------------
case 'fileManager':
// 仅当有活动会话时才返回实际 props,否则返回空对象
if (!currentActiveSession) return {};
@@ -208,50 +169,26 @@ const componentProps = computed(() => {
activeTabId: props.activeEditorTabId, // 从 WorkspaceView 传入
sessionId: props.activeSessionId,
class: 'pane-content',
// 绑定内部处理器以转发事件 (恢复正确的编辑器事件)
onCloseTab: (tabId: string) => emit('closeEditorTab', tabId),
onActivateTab: (tabId: string) => emit('activateEditorTab', tabId),
'onUpdate:content': (payload: { tabId: string; content: string }) => emit('updateEditorContent', payload), // 注意事件名
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 事件
// searchResultCount 和 currentSearchResultIndex 将在模板中直接从 terminalManager 绑定
return {
class: 'pane-content',
onSendCommand: (command: string) => emit('sendCommand', command),
// 转发搜索事件
onSearch: (term: string) => emit('search', term),
onFindNext: () => emit('find-next'),
onFindPrevious: () => emit('find-previous'),
onCloseSearch: () => emit('close-search'),
onClearTerminal: () => emit('clear-terminal'), // --- 移除日志 ---
// --- 移除事件转发 ---
};
case 'connections':
// WorkspaceConnectionList 需要转发 connect-request 等事件
return {
class: 'pane-content',
// 绑定内部处理器以转发事件 (除了 request-add-connection)
onConnectRequest: (id: number) => emit('connect-request', id),
onOpenNewSession: (id: number) => emit('open-new-session', id),
// onRequestAddConnection: () => { ... }, // 移除,将在模板中处理
onRequestEditConnection: (conn: any) => emit('request-edit-connection', conn),
// --- 移除事件转发 ---
};
case 'commandHistory':
case 'quickCommands':
// 这两个视图需要转发 execute-command 事件
return {
class: 'flex flex-col flex-grow h-full overflow-auto', // 移除 pane-content,保留填充类
onExecuteCommand: (command: string) => emit('sendCommand', command), // 复用 sendCommand 事件
// --- 移除事件转发 ---
};
case 'dockerManager':
// DockerManager 可能不需要 session 信息,但需要转发事件
// DockerManager 可能不需要 session 信息
return {
class: 'flex-grow h-full overflow-hidden', // <-- 修改:添加 flex-grow 和 h-full,并保留 overflow-hidden
// 假设 DockerManager 会发出 'docker-command' 事件
@@ -277,30 +214,12 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | '
tabs: editorTabsFromStore.value, // Access .value for refs from storeToRefs
activeTabId: activeEditorTabIdFromStore.value, // Access .value
sessionId: props.activeSessionId,
// Event forwarding
onCloseTab: (tabId: string) => emit('closeEditorTab', tabId),
onActivateTab: (tabId: string) => emit('activateEditorTab', tabId),
'onUpdate:content': (payload: { tabId: string; content: string }) => emit('updateEditorContent', payload),
onRequestSave: (tabId: string) => emit('saveEditorTab', tabId),
// --- 移除事件转发 ---
};
case 'connections':
return {
...baseProps,
// Event forwarding
onConnectRequest: (id: number) => emit('connect-request', id),
onOpenNewSession: (id: number) => {
console.log(`[LayoutRenderer Sidebar] Forwarding 'open-new-session' for ID: ${id}`);
emit('open-new-session', id);
},
onRequestEditConnection: (conn: any) => {
console.log(`[LayoutRenderer Sidebar] Forwarding 'request-edit-connection'`);
emit('request-edit-connection', conn);
},
// Forward 'request-add-connection' from sidebar context
onRequestAddConnection: () => {
console.log(`[LayoutRenderer Sidebar] Forwarding 'request-add-connection'`);
emit('request-add-connection');
},
// --- 移除事件转发 ---
};
case 'fileManager':
// Only provide props if there's an active session
@@ -508,27 +427,6 @@ onMounted(() => {
:active-session-id="activeSessionId"
:editor-tabs="editorTabs"
:active-editor-tab-id="activeEditorTabId"
@send-command="emit('sendCommand', $event)"
@terminal-input="emit('terminalInput', $event)"
@terminal-resize="emit('terminalResize', $event)"
@terminal-ready="emit('terminal-ready', $event)"
@close-editor-tab="emit('closeEditorTab', $event)"
@activate-editor-tab="emit('activateEditorTab', $event)"
@update-editor-content="emit('updateEditorContent', $event)"
@save-editor-tab="emit('saveEditorTab', $event)"
@connect-request="emit('connect-request', $event)"
@open-new-session="emit('open-new-session', $event)"
@request-add-connection="() => emit('request-add-connection')"
@request-edit-connection="emit('request-edit-connection', $event)"
@search="emit('search', $event)"
@find-next="emit('find-next')"
@find-previous="emit('find-previous')"
@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>
@@ -597,7 +495,6 @@ onMounted(() => {
v-if="layoutNode.component === 'connections'"
:is="currentMainComponent"
v-bind="componentProps"
@request-add-connection="() => emit('request-add-connection')"
class="flex-grow overflow-auto"
/>
<component
+17 -19
View File
@@ -1,13 +1,14 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import { Terminal, ITerminalAddon, IDisposable } from 'xterm';
import { useAppearanceStore } from '../stores/appearance.store';
import { useSettingsStore } from '../stores/settings.store';
import { storeToRefs } from 'pinia';
import { Terminal, ITerminalAddon, IDisposable } from 'xterm';
import { useAppearanceStore } from '../stores/appearance.store';
import { useSettingsStore } from '../stores/settings.store';
import { storeToRefs } from 'pinia';
import { FitAddon } from '@xterm/addon-fit'; // Updated import path
import { WebLinksAddon } from 'xterm-addon-web-links';
import { SearchAddon, type ISearchOptions } from '@xterm/addon-search';
import 'xterm/css/xterm.css';
import { SearchAddon, type ISearchOptions } from '@xterm/addon-search';
import 'xterm/css/xterm.css';
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents'; // +++ 新增导入 +++
// 定义 props 和 emits
@@ -18,12 +19,9 @@ const props = defineProps<{
options?: object; // xterm 的配置选项
}>();
const emit = defineEmits<{
(e: 'data', data: string): void; // 用户输入事件
(e: 'resize', dimensions: { cols: number; rows: number }): void; // 终端大小调整事件
// *** 更新 ready 事件 payload,包含 searchAddon ***
(e: 'ready', payload: { sessionId: string; terminal: Terminal; searchAddon: SearchAddon | null }): void;
}>();
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
const terminalRef = ref<HTMLElement | null>(null); // 终端容器的引用
let terminal: Terminal | null = null;
@@ -70,7 +68,7 @@ const debouncedEmitResize = debounce((term: Terminal) => {
if (term && props.isActive) { // 仅当标签仍处于活动状态时才发送防抖后的 resize
const dimensions = { cols: term.cols, rows: term.rows };
console.log(`[Terminal ${props.sessionId}] Debounced resize emit (from ResizeObserver):`, dimensions);
emit('resize', dimensions);
emitWorkspaceEvent('terminal:resize', { sessionId: props.sessionId, dims: dimensions });
// *** 新增:尝试在发送 resize 后强制刷新终端显示 ***
try {
term.refresh(0, term.rows - 1); // Refresh entire viewport
@@ -92,7 +90,7 @@ const fitAndEmitResizeNow = (term: Terminal) => {
fitAddon?.fit();
const dimensions = { cols: term.cols, rows: term.rows };
console.log(`[Terminal ${props.sessionId}] Immediate resize emit:`, dimensions);
emit('resize', dimensions);
emitWorkspaceEvent('terminal:resize', { sessionId: props.sessionId, dims: dimensions });
// *** 恢复:仅使用 nextTick 触发 window resize ***
// 使用 nextTick 确保 fit() 的效果已反映,再触发 resize
@@ -162,11 +160,11 @@ onMounted(() => {
// 适应容器大小
fitAddon.fit();
emit('resize', { cols: terminal.cols, rows: terminal.rows }); // 触发初始 resize 事件
emitWorkspaceEvent('terminal:resize', { sessionId: props.sessionId, dims: { cols: terminal.cols, rows: terminal.rows } }); // 触发初始 resize 事件
// 监听用户输入
terminal.onData((data) => {
emit('data', data);
emitWorkspaceEvent('terminal:input', { sessionId: props.sessionId, data });
});
// 监听终端大小变化 (通过 ResizeObserver) - 主要处理浏览器窗口大小变化等
@@ -244,7 +242,7 @@ onMounted(() => {
// 触发 ready 事件,传递 sessionId, terminal 和 searchAddon 实例
if (terminal) {
emit('ready', { sessionId: props.sessionId, terminal: terminal, searchAddon: searchAddon });
emitWorkspaceEvent('terminal:ready', { sessionId: props.sessionId, terminal: terminal, searchAddon: searchAddon });
}
// --- 监听并处理选中即复制 ---
@@ -359,7 +357,7 @@ onMounted(() => {
const text = await navigator.clipboard.readText();
if (text) {
// 将粘贴的文本发送到后端,模拟用户输入
emit('data', text);
emitWorkspaceEvent('terminal:input', { sessionId: props.sessionId, data: text });
console.log('[Terminal] Pasted via Ctrl+Shift+V');
}
} catch (err) {
@@ -380,7 +378,7 @@ onMounted(() => {
const text = await navigator.clipboard.readText();
if (text && terminal) {
// 将粘贴的文本发送到后端
emit('data', text);
emitWorkspaceEvent('terminal:input', { sessionId: props.sessionId, data: text });
console.log('[Terminal] Pasted via Right Click');
}
} catch (err) {
@@ -9,11 +9,13 @@ import TabBarContextMenu from './TabBarContextMenu.vue';
import { useSessionStore } from '../stores/session.store';
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
import { useLayoutStore, type PaneName } from '../stores/layout.store';
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents'; // +++ 新增导入 +++
import type { SessionTabInfoWithStatus } from '../stores/session.store';
const { t } = useI18n(); // 初始化 i18n
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
const layoutStore = useLayoutStore(); // 初始化布局 store
const connectionsStore = useConnectionsStore();
const { isHeaderVisible } = storeToRefs(layoutStore); // 从 layout store 获取主导航栏可见状态
@@ -37,30 +39,21 @@ const props = defineProps({
},
});
// 定义事件 (使用对象语法修复类型)
// 定义事件 (保留 update:sessions 用于 v-model)
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;
(e: 'update:sessions', newSessions: SessionTabInfoWithStatus[]): void; // + Add event for reordering
(e: 'update:sessions', newSessions: SessionTabInfoWithStatus[]): void;
}>();
const activateSession = (sessionId: string) => {
if (sessionId !== props.activeSessionId) {
emit('activate-session', sessionId);
emitWorkspaceEvent('session:activate', { sessionId });
}
};
const closeSession = (event: MouseEvent, sessionId: string) => {
event.stopPropagation(); // 阻止事件冒泡到标签点击事件
emit('close-session', sessionId);
emitWorkspaceEvent('session:close', { sessionId });
};
// --- 本地状态 ---
@@ -109,15 +102,15 @@ const handlePopupConnect = (connectionId: number) => {
const handleRequestAddFromPopup = () => {
console.log('[TabBar] Received request-add-connection from popup component.');
showConnectionListPopup.value = false; // 关闭弹窗
emit('request-add-connection-from-popup'); // 向上发出事件
emitWorkspaceEvent('connection:requestAdd'); // 向上发出事件
};
// 新增:处理从弹窗内部发出的编辑连接请求
const handleRequestEditFromPopup = (connection: any) => { // 假设 WorkspaceConnectionList 传递了连接对象
const handleRequestEditFromPopup = (connection: ConnectionInfo) => { // 假设 WorkspaceConnectionList 传递了连接对象
console.log('[TabBar] Received request-edit-connection from popup component for connection:', connection);
showConnectionListPopup.value = false; // 关闭弹窗
// 向上发出事件,并携带连接信息
emit('request-edit-connection-from-popup', connection);
emitWorkspaceEvent('connection:requestEdit', { connectionInfo: connection });
};
// --- 移除 handleRequestRdpFromPopup 方法 ---
@@ -166,17 +159,17 @@ const handleContextMenuAction = (payload: { action: string; targetId: string | n
switch (action) {
case 'close':
emit('close-session', targetId);
emitWorkspaceEvent('session:close', { sessionId: targetId });
break;
case 'close-others':
emit('close-other-sessions', targetId);
emitWorkspaceEvent('session:closeOthers', { targetSessionId: targetId });
break;
case 'close-right':
emit('close-sessions-to-right', targetId);
emitWorkspaceEvent('session:closeToRight', { targetSessionId: targetId });
break;
case 'close-left':
// 注意:关闭左侧通常不包括当前标签本身
emit('close-sessions-to-left', targetId);
emitWorkspaceEvent('session:closeToLeft', { targetSessionId: targetId });
break;
default:
console.warn(`[TabBar] Unknown context menu action: ${action}`);
@@ -214,7 +207,7 @@ const contextMenuItems = computed(() => {
// 新增:处理打开布局配置器的事件
const openLayoutConfigurator = () => {
console.log('[TabBar] Emitting open-layout-configurator event');
emit('open-layout-configurator'); // 发出事件
emitWorkspaceEvent('ui:openLayoutConfigurator'); // 发出事件
};
// --- Header Visibility Logic ---
@@ -10,15 +10,11 @@ import { useSessionStore } from '../stores/session.store';
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // +++ 修正导入大小写 +++
import { useSettingsStore } from '../stores/settings.store'; // 新增:导入设置 store
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents'; // +++ 新增导入 +++
// 定义事件
const emit = defineEmits([
'connect-request', // 左键单击 - 请求激活或替换当前标签
// 'open-new-session', // 中键单击 - 请求在新标签中打开 (已移除)
'request-add-connection', // 右键菜单 - 添加
'request-edit-connection' // 右键菜单 - 编辑
]);
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
const { t } = useI18n();
// const router = useRouter(); // 不再需要
@@ -307,7 +303,7 @@ const handleConnect = (connectionId: number, event?: MouseEvent | KeyboardEvent)
closeContextMenu(); // 关闭右键菜单
// 统一发出 connect-request 事件,让 sessionStore.handleConnectRequest 处理模态框和会话
emit('connect-request', connectionId);
emitWorkspaceEvent('connection:connect', { connectionId });
};
// --- 移除 closeRdpModal 方法 ---
@@ -346,11 +342,11 @@ const handleMenuAction = (action: 'add' | 'edit' | 'delete' | 'clone') => { //
if (action === 'add') {
console.log('[WorkspaceConnectionList] handleMenuAction called with action: add. Emitting request-add-connection...'); // 添加日志
// router.push('/connections/add'); // 改为触发事件
emit('request-add-connection');
emitWorkspaceEvent('connection:requestAdd');
} else if (conn) {
if (action === 'edit') {
// router.push(`/connections/edit/${conn.id}`); // 改为触发事件
emit('request-edit-connection', conn); // 传递整个连接对象
emitWorkspaceEvent('connection:requestEdit', { connectionInfo: conn }); // 传递整个连接对象
} else if (action === 'delete') {
if (confirm(t('connections.prompts.confirmDelete', { name: conn.name || conn.host }))) {
connectionsStore.deleteConnection(conn.id);
@@ -0,0 +1,74 @@
// packages/frontend/src/composables/workspaceEvents.ts
import mitt from 'mitt';
import type { ConnectionInfo } from '../stores/connections.store';
import type { Terminal as XtermTerminal } from 'xterm';
// 定义事件载荷类型
export type WorkspaceEventPayloads = {
// Terminal Events
'terminal:input': { sessionId: string; data: string };
'terminal:resize': { sessionId: string; dims: { cols: number; rows: number } };
'terminal:ready': { sessionId: string; terminal: XtermTerminal; searchAddon: any };
'terminal:sendCommand': { command: string; sessionId?: string }; // sessionId 可选,用于指定目标,默认为 active
'terminal:clear': void; // sessionId 可选,默认为 active
// Editor Events
'editor:closeTab': { tabId: string };
'editor:activateTab': { tabId: string };
'editor:updateContent': { tabId: string; content: string };
'editor:saveTab': { tabId: string };
'editor:changeEncoding': { tabId: string; encoding: string };
'editor:closeOtherTabs': { tabId: string };
'editor:closeTabsToRight': { tabId: string };
'editor:closeTabsToLeft': { tabId: string };
// Connection Events
'connection:connect': { connectionId: number }; // 来自 WorkspaceConnectionList 或其他地方
'connection:openNewSession': { connectionId: number }; // 来自 WorkspaceConnectionList
'connection:requestAdd': void; // 来自 WorkspaceConnectionList 或 TerminalTabBar
'connection:requestEdit': { connectionInfo: ConnectionInfo }; // 来自 WorkspaceConnectionList 或 TerminalTabBar
// Search Events (主要由 CommandInputBar 或 PaneTitleBar 发出)
'search:start': { term: string; sessionId?: string }; // sessionId 可选,用于指定搜索目标终端
'search:findNext': void;
'search:findPrevious': void;
'search:close': void;
// Session Management Events (主要由 TerminalTabBar 发出)
'session:activate': { sessionId: string };
'session:close': { sessionId: string };
'session:closeOthers': { targetSessionId: string };
'session:closeToRight': { targetSessionId: string };
'session:closeToLeft': { targetSessionId: string };
// UI Interaction Events
'ui:openLayoutConfigurator': void;
// 'ui:toggleVirtualKeyboard': void; // 如果决定迁移 CommandInputBar 的这个事件
};
// 创建 mitt 事件发射器实例
export const workspaceEmitter = mitt<WorkspaceEventPayloads>();
/**
* Composable to get the workspace event emitter function.
* @returns The emit function from the mitt instance.
*/
export function useWorkspaceEventEmitter() {
return workspaceEmitter.emit;
}
/**
* Composable to get the workspace event subscriber function.
* @returns The 'on' function from the mitt instance for subscribing to events.
*/
export function useWorkspaceEventSubscriber() {
return workspaceEmitter.on;
}
/**
* Composable to get the workspace event unsubscriber function.
* @returns The 'off' function from the mitt instance for unsubscribing from events.
*/
export function useWorkspaceEventOff() {
return workspaceEmitter.off;
}
@@ -68,13 +68,14 @@ import { storeToRefs } from 'pinia'; // Import storeToRefs
import { useCommandHistoryStore, CommandHistoryEntryFE } from '../stores/commandHistory.store';
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
import { useI18n } from 'vue-i18n';
import PaneTitleBar from '../components/PaneTitleBar.vue'; //
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ Store +++
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents'; // +++ +++
const commandHistoryStore = useCommandHistoryStore();
const uiNotificationsStore = useUiNotificationsStore();
const { t } = useI18n();
const focusSwitcherStore = useFocusSwitcherStore(); // +++ Store +++
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ +++
const hoveredItemId = ref<number | null>(null);
// const selectedIndex = ref<number>(-1); // REMOVED: Use store's selectedIndex
const historyListRef = ref<HTMLUListElement | null>(null); // Ref for the history list UL
@@ -88,11 +89,6 @@ const filteredHistory = computed(() => commandHistoryStore.filteredHistory);
const isLoading = computed(() => commandHistoryStore.isLoading);
const { selectedIndex: storeSelectedIndex } = storeToRefs(commandHistoryStore); // Get selectedIndex reactively
// --- ---
//
const emit = defineEmits<{
(e: 'execute-command', command: string): void; //
}>();
// --- ---
onMounted(() => {
@@ -207,7 +203,7 @@ const deleteSingleCommand = (id: number) => {
// ()
const executeCommand = (command: string) => {
emit('execute-command', command);
emitWorkspaceEvent('terminal:sendCommand', { command });
// Optionally reset selection after execution
// selectedIndex.value = -1; // REMOVED: Store handles index
};
@@ -166,6 +166,7 @@ import { useI18n } from 'vue-i18n';
import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; //
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ Store +++
import { useSettingsStore } from '../stores/settings.store'; // store
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents'; // +++ +++
const quickCommandsStore = useQuickCommandsStore();
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate the new tag store +++
@@ -173,6 +174,7 @@ const uiNotificationsStore = useUiNotificationsStore(); // 如果需要显示通
const { t } = useI18n();
const focusSwitcherStore = useFocusSwitcherStore(); // +++ Store +++
const settingsStore = useSettingsStore(); // store
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ +++
const hoveredItemId = ref<number | null>(null);
const isFormVisible = ref(false);
@@ -213,10 +215,7 @@ const isCommandSelected = (commandId: number): boolean => {
return flatVisibleCommands.value[storeSelectedIndex.value].id === commandId;
};
// --- ---
const emit = defineEmits<{
(e: 'execute-command', command: string): void; // WorkspaceView
}>();
// --- ---
onMounted(async () => { // Make onMounted async
@@ -399,7 +398,7 @@ const executeCommand = (command: QuickCommandFE) => {
// 1. 使 ()
quickCommandsStore.incrementUsage(command.id);
// 2.
emit('execute-command', command.command);
emitWorkspaceEvent('terminal:sendCommand', { command: command.command });
// Optionally reset selection after execution
// selectedIndex.value = -1; // REMOVED: Store handles index
};
+75 -21
View File
@@ -20,6 +20,11 @@ import { useFileEditorStore, type FileTab } from '../stores/fileEditor.store';
import { useCommandHistoryStore } from '../stores/commandHistory.store';
import type { Terminal as XtermTerminal } from 'xterm'; // --- ---
import type { ISearchOptions } from '@xterm/addon-search';
import {
useWorkspaceEventSubscriber,
useWorkspaceEventOff,
type WorkspaceEventPayloads
} from '../composables/workspaceEvents'; // +++ +++
// --- Setup ---
const { t } = useI18n();
@@ -112,6 +117,40 @@ onMounted(() => {
//
window.addEventListener('keydown', handleGlobalKeyDown);
// (layoutStore )
// +++ +++
subscribeToWorkspaceEvents('terminal:sendCommand', (payload) => handleSendCommand(payload.command));
subscribeToWorkspaceEvents('terminal:input', handleTerminalInput);
subscribeToWorkspaceEvents('terminal:resize', handleTerminalResize);
subscribeToWorkspaceEvents('terminal:ready', handleTerminalReady);
subscribeToWorkspaceEvents('terminal:clear', handleClearTerminal);
subscribeToWorkspaceEvents('editor:closeTab', (payload) => handleCloseEditorTab(payload.tabId));
subscribeToWorkspaceEvents('editor:activateTab', (payload) => handleActivateEditorTab(payload.tabId));
subscribeToWorkspaceEvents('editor:updateContent', handleUpdateEditorContent);
subscribeToWorkspaceEvents('editor:saveTab', (payload) => handleSaveEditorTab(payload.tabId));
subscribeToWorkspaceEvents('editor:changeEncoding', handleChangeEncoding);
subscribeToWorkspaceEvents('editor:closeOtherTabs', (payload) => handleCloseOtherEditorTabs(payload.tabId));
subscribeToWorkspaceEvents('editor:closeTabsToRight', (payload) => handleCloseEditorTabsToRight(payload.tabId));
subscribeToWorkspaceEvents('editor:closeTabsToLeft', (payload) => handleCloseEditorTabsToLeft(payload.tabId));
subscribeToWorkspaceEvents('connection:connect', (payload) => handleConnectRequest(payload.connectionId));
subscribeToWorkspaceEvents('connection:openNewSession', (payload) => handleOpenNewSession(payload.connectionId));
subscribeToWorkspaceEvents('connection:requestAdd', handleRequestAddConnection);
subscribeToWorkspaceEvents('connection:requestEdit', (payload) => handleRequestEditConnection(payload.connectionInfo));
subscribeToWorkspaceEvents('search:start', (payload) => handleSearch(payload.term));
subscribeToWorkspaceEvents('search:findNext', handleFindNext);
subscribeToWorkspaceEvents('search:findPrevious', handleFindPrevious);
subscribeToWorkspaceEvents('search:close', handleCloseSearch);
// TerminalTabBar
subscribeToWorkspaceEvents('session:activate', (payload) => sessionStore.activateSession(payload.sessionId));
subscribeToWorkspaceEvents('session:close', (payload) => sessionStore.closeSession(payload.sessionId));
subscribeToWorkspaceEvents('session:closeOthers', (payload) => handleCloseOtherSessions(payload.targetSessionId));
subscribeToWorkspaceEvents('session:closeToRight', (payload) => handleCloseSessionsToRight(payload.targetSessionId));
subscribeToWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
subscribeToWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
});
onBeforeUnmount(() => {
@@ -119,8 +158,44 @@ onBeforeUnmount(() => {
//
window.removeEventListener('keydown', handleGlobalKeyDown);
sessionStore.cleanupAllSessions();
// +++ +++
unsubscribeFromWorkspaceEvents('terminal:sendCommand', (payload) => handleSendCommand(payload.command));
unsubscribeFromWorkspaceEvents('terminal:input', handleTerminalInput);
unsubscribeFromWorkspaceEvents('terminal:resize', handleTerminalResize);
unsubscribeFromWorkspaceEvents('terminal:ready', handleTerminalReady);
unsubscribeFromWorkspaceEvents('terminal:clear', handleClearTerminal);
unsubscribeFromWorkspaceEvents('editor:closeTab', (payload) => handleCloseEditorTab(payload.tabId));
unsubscribeFromWorkspaceEvents('editor:activateTab', (payload) => handleActivateEditorTab(payload.tabId));
unsubscribeFromWorkspaceEvents('editor:updateContent', handleUpdateEditorContent);
unsubscribeFromWorkspaceEvents('editor:saveTab', (payload) => handleSaveEditorTab(payload.tabId));
unsubscribeFromWorkspaceEvents('editor:changeEncoding', handleChangeEncoding);
unsubscribeFromWorkspaceEvents('editor:closeOtherTabs', (payload) => handleCloseOtherEditorTabs(payload.tabId));
unsubscribeFromWorkspaceEvents('editor:closeTabsToRight', (payload) => handleCloseEditorTabsToRight(payload.tabId));
unsubscribeFromWorkspaceEvents('editor:closeTabsToLeft', (payload) => handleCloseEditorTabsToLeft(payload.tabId));
unsubscribeFromWorkspaceEvents('connection:connect', (payload) => handleConnectRequest(payload.connectionId));
unsubscribeFromWorkspaceEvents('connection:openNewSession', (payload) => handleOpenNewSession(payload.connectionId));
unsubscribeFromWorkspaceEvents('connection:requestAdd', handleRequestAddConnection);
unsubscribeFromWorkspaceEvents('connection:requestEdit', (payload) => handleRequestEditConnection(payload.connectionInfo));
unsubscribeFromWorkspaceEvents('search:start', (payload) => handleSearch(payload.term));
unsubscribeFromWorkspaceEvents('search:findNext', handleFindNext);
unsubscribeFromWorkspaceEvents('search:findPrevious', handleFindPrevious);
unsubscribeFromWorkspaceEvents('search:close', handleCloseSearch);
unsubscribeFromWorkspaceEvents('session:activate', (payload) => sessionStore.activateSession(payload.sessionId));
unsubscribeFromWorkspaceEvents('session:close', (payload) => sessionStore.closeSession(payload.sessionId));
unsubscribeFromWorkspaceEvents('session:closeOthers', (payload) => handleCloseOtherSessions(payload.targetSessionId));
unsubscribeFromWorkspaceEvents('session:closeToRight', (payload) => handleCloseSessionsToRight(payload.targetSessionId));
unsubscribeFromWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
unsubscribeFromWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
});
const subscribeToWorkspaceEvents = useWorkspaceEventSubscriber(); // +++ +++
const unsubscribeFromWorkspaceEvents = useWorkspaceEventOff();
// --- ( UI ) ---
const handleRequestAddConnection = () => {
console.log('[WorkspaceView] handleRequestAddConnection 被调用!'); //
@@ -571,27 +646,6 @@ const toggleVirtualKeyboard = () => {
class="layout-renderer-wrapper"
:editor-tabs="editorTabs"
:active-editor-tab-id="activeEditorTabId"
@send-command="handleSendCommand"
@terminal-input="handleTerminalInput"
@terminal-resize="handleTerminalResize"
@terminal-ready="handleTerminalReady"
@close-editor-tab="handleCloseEditorTab"
@activate-editor-tab="handleActivateEditorTab"
@update-editor-content="handleUpdateEditorContent"
@save-editor-tab="handleSaveEditorTab"
@connect-request="handleConnectRequest"
@open-new-session="handleOpenNewSession"
@request-add-connection="handleRequestAddConnection"
@request-edit-connection="handleRequestEditConnection"
@search="handleSearch"
@find-next="handleFindNext"
@find-previous="handleFindPrevious"
@close-search="handleCloseSearch"
@clear-terminal="handleClearTerminal"
@change-encoding="handleChangeEncoding"
@close-other-tabs="handleCloseOtherEditorTabs"
@close-tabs-to-right="handleCloseEditorTabsToRight"
@close-tabs-to-left="handleCloseEditorTabsToLeft"
></LayoutRenderer>
<div v-else class="pane-placeholder">
{{ t('layout.loading', '加载布局中...') }}