Files
nexus-terminal/packages/frontend/src/views/WorkspaceView.vue
T

752 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, onBeforeUnmount, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core'; // +++ 引入 useBreakpoints +++
import { useLayoutStore } from '../stores/layout.store';
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
import TerminalTabBar from '../components/TerminalTabBar.vue';
import LayoutRenderer from '../components/LayoutRenderer.vue';
import LayoutConfigurator from '../components/LayoutConfigurator.vue';
import RemoteDesktopModal from '../components/RemoteDesktopModal.vue';
import Terminal from '../components/Terminal.vue'; // +++ 引入 Terminal 组件 +++
import CommandInputBar from '../components/CommandInputBar.vue'; // +++ 引入 CommandInputBar 组件 +++
import VirtualKeyboard from '../components/VirtualKeyboard.vue'; // +++ 引入 VirtualKeyboard 组件 +++
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store';
import { useSettingsStore } from '../stores/settings.store';
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';
// --- Setup ---
const { t } = useI18n();
const sessionStore = useSessionStore();
const settingsStore = useSettingsStore();
const fileEditorStore = useFileEditorStore();
const layoutStore = useLayoutStore();
const commandHistoryStore = useCommandHistoryStore();
const connectionsStore = useConnectionsStore();
const { isHeaderVisible } = storeToRefs(layoutStore);
const breakpoints = useBreakpoints(breakpointsTailwind); // +++ 初始化 Breakpoints +++
const isMobile = breakpoints.smaller('md'); // +++ 定义 isMobile (小于 md 断点) +++
// --- 从 Store 获取响应式状态和 Getters ---
const { sessionTabsWithStatus, activeSessionId, activeSession, isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // 使用 storeToRefs 获取 RDP 状态
const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore);
const { orderedTabs: globalEditorTabs, activeTabId: globalActiveEditorTabId } = storeToRefs(fileEditorStore);
const { layoutTree } = storeToRefs(layoutStore); // 只获取布局树
// --- 计算属性 (用于动态绑定编辑器 Props) ---
// 这些计算属性现在需要传递给 LayoutRenderer
const editorTabs = computed((): FileTab[] => { // Ensure return type is FileTab[]
if (shareFileEditorTabsBoolean.value) {
return globalEditorTabs.value;
} else {
return activeSession.value?.editorTabs.value ?? [];
}
});
const activeEditorTabId = computed(() => {
if (shareFileEditorTabsBoolean.value) {
return globalActiveEditorTabId.value;
} else {
return activeSession.value?.activeEditorTabId.value ?? null;
}
});
// --- UI 状态 (保持本地) ---
const showAddEditForm = ref(false);
const connectionToEdit = ref<ConnectionInfo | null>(null);
const showLayoutConfigurator = ref(false); // 控制布局配置器可见性
// 本地 RDP 状态已被移除
// --- 搜索状态 ---
const currentSearchTerm = ref(''); // 当前搜索的关键词
const mobileTerminalRef = ref<InstanceType<typeof Terminal> | null>(null); // +++ 添加 mobileTerminalRef +++
const isVirtualKeyboardVisible = ref(true); // +++ State for virtual keyboard visibility +++
// --- 新增:处理全局键盘事件 ---
const handleGlobalKeyDown = (event: KeyboardEvent) => {
// 检查是否按下了 Alt 键以及上/下箭头键
if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
event.preventDefault(); // 阻止默认行为 (例如页面滚动)
const tabs = sessionTabsWithStatus.value;
const currentId = activeSessionId.value;
if (!tabs || tabs.length <= 1 || !currentId) {
// 如果没有标签页、只有一个标签页或没有活动标签页,则不执行任何操作
return;
}
const currentIndex = tabs.findIndex(tab => tab.sessionId === currentId);
if (currentIndex === -1) {
// 如果找不到当前活动标签页 (理论上不应发生),则不执行任何操作
return;
}
let nextIndex: number;
if (event.key === 'ArrowDown') {
// Alt + 下箭头:切换到下一个标签页
nextIndex = (currentIndex + 1) % tabs.length;
} else {
// Alt + 上箭头:切换到上一个标签页
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
}
const nextSessionId = tabs[nextIndex].sessionId;
if (nextSessionId !== currentId) {
console.log(`[WorkspaceView] Alt+${event.key} detected. Switching to session: ${nextSessionId}`);
sessionStore.activateSession(nextSessionId);
}
}
};
// --- 生命周期钩子 ---
onMounted(() => {
console.log('[工作区视图] 组件已挂载。');
// 添加键盘事件监听器
window.addEventListener('keydown', handleGlobalKeyDown);
// 确保布局已初始化 (layoutStore 内部会处理)
});
onBeforeUnmount(() => {
console.log('[工作区视图] 组件即将卸载,清理所有会话...');
// 移除键盘事件监听器
window.removeEventListener('keydown', handleGlobalKeyDown);
sessionStore.cleanupAllSessions();
});
// --- 本地方法 (仅处理 UI 状态) ---
const handleRequestAddConnection = () => {
console.log('[WorkspaceView] handleRequestAddConnection 被调用!'); // 添加日志确认事件到达
connectionToEdit.value = null;
showAddEditForm.value = true;
};
const handleRequestEditConnection = (connection: ConnectionInfo) => {
connectionToEdit.value = connection;
showAddEditForm.value = true;
};
const handleFormClose = () => {
showAddEditForm.value = false;
connectionToEdit.value = null;
};
const handleConnectionAdded = () => {
console.log('[工作区视图] 连接已添加');
handleFormClose();
};
const handleConnectionUpdated = () => {
console.log('[工作区视图] 连接已更新');
handleFormClose();
};
// 处理打开和关闭布局配置器
const handleOpenLayoutConfigurator = () => {
showLayoutConfigurator.value = true;
};
const handleCloseLayoutConfigurator = () => {
showLayoutConfigurator.value = false;
};
// --- 事件处理 (传递给 LayoutRenderer 或直接使用) ---
// 处理命令发送 (用于 CommandBar, CommandHistory, QuickCommands)
const handleSendCommand = (command: string) => {
const currentSession = activeSession.value;
if (!currentSession) {
console.warn('[WorkspaceView] Cannot send command, no active session.');
return;
}
const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined);
if (terminalManager?.isSshConnected && !terminalManager.isSshConnected.value && command.trim() === '') {
console.log(`[WorkspaceView] Command bar Enter detected in disconnected session ${currentSession.sessionId}, attempting reconnect...`);
if (terminalManager.terminalInstance?.value) {
terminalManager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`);
}
// +++ 修复:传递 ConnectionInfo 而不是 ID +++
const connectionInfo = connectionsStore.connections.find(c => c.id === Number(currentSession.connectionId));
if (connectionInfo) {
sessionStore.handleConnectRequest(connectionInfo);
} else {
console.error(`[WorkspaceView] handleSendCommand: 未找到 ID 为 ${currentSession.connectionId} 的连接信息。`);
}
return;
}
if (terminalManager && typeof terminalManager.sendData === 'function') {
const commandToSend = command.trim();
console.log(`[WorkspaceView] Sending command to active session ${currentSession.sessionId}: ${commandToSend}`);
terminalManager.sendData(command + '\r');
if (commandToSend.length > 0) {
commandHistoryStore.addCommand(commandToSend);
}
} else {
console.warn(`[WorkspaceView] Cannot send command for session ${currentSession.sessionId}, terminal manager or sendData method not available.`);
}
};
// 处理终端输入 (用于 Terminal)
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-input', sessionId, data)
const handleTerminalInput = (payload: { sessionId: string; data: string }) => {
const { sessionId, data } = payload; // 解构 payload
const session = sessionStore.sessions.get(sessionId);
const manager = session?.terminalManager as (SshTerminalInstance | undefined);
if (!session || !manager) {
console.warn(`[WorkspaceView] handleTerminalInput: 未找到会话 ${sessionId} 或其 terminalManager`);
return;
}
if (data === '\r' && manager.isSshConnected && !manager.isSshConnected.value) {
console.log(`[WorkspaceView] 检测到在断开的会话 ${sessionId} 中按下回车,尝试重连...`);
if (manager.terminalInstance?.value) {
manager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`);
} else {
console.warn(`[WorkspaceView] 无法写入重连提示,terminalInstance 不可用。`);
}
// +++ 修复:传递 ConnectionInfo 而不是 ID +++
const connectionInfo = connectionsStore.connections.find(c => c.id === Number(session.connectionId));
if (connectionInfo) {
sessionStore.handleConnectRequest(connectionInfo);
} else {
console.error(`[WorkspaceView] handleTerminalInput: 未找到 ID 为 ${session.connectionId} 的连接信息。`);
}
} else {
manager.handleTerminalData(data);
}
};
// 处理终端大小调整 (用于 Terminal)
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-resize', sessionId, dims)
const handleTerminalResize = (payload: { sessionId: string; dims: { cols: number; rows: number } }) => {
console.log(`[工作区视图 ${payload.sessionId}] 收到 resize 事件:`, payload.dims);
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalResize(payload.dims);
};
// 处理终端就绪 (用于 Terminal)
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-ready', payload)
// *** 修正:更新 payload 类型以包含 searchAddon ***
const handleTerminalReady = (payload: { sessionId: string; terminal: XtermTerminal; searchAddon: any | null }) => { // --- 使用重命名的 XtermTerminal ---
console.log(`[工作区视图 ${payload.sessionId}] 收到 terminal-ready 事件。Payload:`, payload); // *** 添加 Payload 日志 ***
// *** 检查 payload 中 searchAddon 是否存在 ***
if (payload && payload.searchAddon) {
console.log(`[工作区视图 ${payload.sessionId}] Payload 包含 searchAddon 实例。`);
} else {
console.warn(`[工作区视图 ${payload.sessionId}] Payload 未包含 searchAddon 实例! Payload:`, payload);
}
// *** 修正:传递包含 terminal 和 searchAddon 的完整 payload ***
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalReady(payload);
};
// --- 搜索事件处理 ---
const handleSearch = (term: string) => { // +++ 修改 +++
currentSearchTerm.value = term;
if (!term) {
// 如果搜索词为空,清除搜索
handleCloseSearch();
return;
}
console.log(`[WorkspaceView] Received search event: "${term}"`);
// 默认向前搜索
// 触发 findNext
handleFindNext(); // 保持调用 findNext,内部会处理 isMobile
};
const handleFindNext = () => { // +++ 修改 +++
if (isMobile.value) {
if (mobileTerminalRef.value && currentSearchTerm.value) {
console.log(`[WorkspaceView Mobile] Calling findNext for term: "${currentSearchTerm.value}"`);
const found = mobileTerminalRef.value.findNext(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView Mobile] findNext returned: ${found}`);
if (!found) {
console.log(`[WorkspaceView Mobile] findNext: No more results for "${currentSearchTerm.value}"`);
}
} else {
console.warn(`[WorkspaceView Mobile] Cannot findNext, no mobile terminal ref or search term.`);
}
} else {
// --- 桌面端逻辑 ---
const manager = activeSession.value?.terminalManager;
if (manager && currentSearchTerm.value) {
console.log(`[WorkspaceView Desktop] Calling findNext for term: "${currentSearchTerm.value}"`);
const found = manager.searchNext(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView Desktop] findNext returned: ${found}`);
if (!found) {
console.log(`[WorkspaceView Desktop] findNext: No more results for "${currentSearchTerm.value}"`);
}
} else {
console.warn(`[WorkspaceView Desktop] Cannot findNext, no active session manager or search term.`);
}
}
};
const handleFindPrevious = () => { // +++ 修改 +++
if (isMobile.value) {
if (mobileTerminalRef.value && currentSearchTerm.value) {
console.log(`[WorkspaceView Mobile] Calling findPrevious for term: "${currentSearchTerm.value}"`);
const found = mobileTerminalRef.value.findPrevious(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView Mobile] findPrevious returned: ${found}`);
if (!found) {
console.log(`[WorkspaceView Mobile] findPrevious: No previous results for "${currentSearchTerm.value}"`);
}
} else {
console.warn(`[WorkspaceView Mobile] Cannot findPrevious, no mobile terminal ref or search term.`);
}
} else {
// --- 桌面端逻辑 ---
const manager = activeSession.value?.terminalManager;
if (manager && currentSearchTerm.value) {
console.log(`[WorkspaceView Desktop] Calling findPrevious for term: "${currentSearchTerm.value}"`);
const found = manager.searchPrevious(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView Desktop] findPrevious returned: ${found}`);
if (!found) {
console.log(`[WorkspaceView Desktop] findPrevious: No previous results for "${currentSearchTerm.value}"`);
}
} else {
console.warn(`[WorkspaceView Desktop] Cannot findPrevious, no active session manager or search term.`);
}
}
};
const handleCloseSearch = () => { // +++ 修改 +++
console.log(`[WorkspaceView] Received close-search event.`);
currentSearchTerm.value = ''; // 清空搜索词
if (isMobile.value) {
if (mobileTerminalRef.value) {
mobileTerminalRef.value.clearSearch();
console.log(`[WorkspaceView Mobile] Search cleared.`);
} else {
console.warn(`[WorkspaceView Mobile] Cannot clear search, no mobile terminal ref.`);
}
} else {
// --- 桌面端逻辑 ---
const manager = activeSession.value?.terminalManager;
if (manager) {
manager.clearTerminalSearch();
console.log(`[WorkspaceView Desktop] Search cleared.`);
} else {
console.warn(`[WorkspaceView Desktop] Cannot clear search, no active session manager.`);
}
}
};
// +++ 新增:处理清空终端事件 +++
const handleClearTerminal = () => { // +++ 修改 +++
const currentSession = activeSession.value;
if (!currentSession) {
console.warn('[WorkspaceView] Cannot clear terminal, no active session.');
return;
}
const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined);
// 调用 Terminal.vue 组件暴露的 clear 方法
if (isMobile.value) {
if (mobileTerminalRef.value) {
mobileTerminalRef.value.clear();
console.log(`[WorkspaceView Mobile] Terminal cleared.`);
} else {
console.warn(`[WorkspaceView Mobile] Cannot clear terminal, no mobile terminal ref.`);
}
} else {
// --- 桌面端逻辑 ---
if (terminalManager && terminalManager.terminalInstance?.value && typeof terminalManager.terminalInstance.value.clear === 'function') {
console.log(`[WorkspaceView Desktop] Clearing terminal for active session ${currentSession.sessionId}`);
terminalManager.terminalInstance.value.clear();
} else {
console.warn(`[WorkspaceView Desktop] 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) => {
const isShared = shareFileEditorTabsBoolean.value;
console.log(`[WorkspaceView] handleCloseEditorTab: ${tabId}, Shared mode: ${isShared}`);
if (isShared) {
fileEditorStore.closeTab(tabId);
} else {
const currentActiveSessionId = activeSessionId.value;
if (currentActiveSessionId) {
sessionStore.closeEditorTabInSession(currentActiveSessionId, tabId);
} else {
console.warn('[WorkspaceView] Cannot close editor tab: No active session in independent mode.');
}
}
};
const handleActivateEditorTab = (tabId: string) => {
const isShared = shareFileEditorTabsBoolean.value;
console.log(`[WorkspaceView] handleActivateEditorTab: ${tabId}, Shared mode: ${isShared}`);
if (isShared) {
fileEditorStore.setActiveTab(tabId);
} else {
const currentActiveSessionId = activeSessionId.value;
if (currentActiveSessionId) {
sessionStore.setActiveEditorTabInSession(currentActiveSessionId, tabId);
} else {
console.warn('[WorkspaceView] Cannot activate editor tab: No active session in independent mode.');
}
}
};
const handleUpdateEditorContent = (payload: { tabId: string; content: string }) => {
const isShared = shareFileEditorTabsBoolean.value;
console.log(`[WorkspaceView] handleUpdateEditorContent for tab ${payload.tabId}, Shared mode: ${isShared}`);
if (isShared) {
fileEditorStore.updateFileContent(payload.tabId, payload.content);
} else {
const currentActiveSessionId = activeSessionId.value;
if (currentActiveSessionId) {
sessionStore.updateFileContentInSession(currentActiveSessionId, payload.tabId, payload.content);
} else {
console.warn('[WorkspaceView] Cannot update editor content: No active session in independent mode.');
}
}
};
const handleSaveEditorTab = (tabId: string) => {
const isShared = shareFileEditorTabsBoolean.value;
console.log(`[WorkspaceView] handleSaveEditorTab: ${tabId}, Shared mode: ${isShared}`);
if (isShared) {
fileEditorStore.saveFile(tabId);
} else {
const currentActiveSessionId = activeSessionId.value;
if (currentActiveSessionId) {
sessionStore.saveFileInSession(currentActiveSessionId, tabId);
} else {
console.warn('[WorkspaceView] Cannot save editor tab: No active session in independent mode.');
}
}
};
// +++ 新增:处理编辑器编码更改事件 +++
const handleChangeEncoding = (payload: { tabId: string; encoding: string }) => {
const isShared = shareFileEditorTabsBoolean.value;
console.log(`[WorkspaceView] handleChangeEncoding for tab ${payload.tabId} to ${payload.encoding}, Shared mode: ${isShared}`);
if (isShared) {
fileEditorStore.changeEncoding(payload.tabId, payload.encoding);
} else {
const currentActiveSessionId = activeSessionId.value;
if (currentActiveSessionId) {
// 假设 sessionStore 有一个 changeEncodingInSession 方法
sessionStore.changeEncodingInSession(currentActiveSessionId, payload.tabId, payload.encoding);
} else {
console.warn('[WorkspaceView] Cannot change editor encoding: No active session in independent mode.');
}
}
};
// --- 连接列表操作处理 (用于 WorkspaceConnectionList) ---
const handleConnectRequest = (id: number) => {
console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`);
// +++ 修复:传递 ConnectionInfo 而不是 ID +++
const connectionInfo = connectionsStore.connections.find(c => c.id === id);
if (connectionInfo) {
sessionStore.handleConnectRequest(connectionInfo);
} else {
console.error(`[WorkspaceView] handleConnectRequest: 未找到 ID 为 ${id} 的连接信息。`);
}
};
const handleOpenNewSession = (id: number) => {
console.log(`[WorkspaceView] Received 'open-new-session' event for ID: ${id}`);
sessionStore.handleOpenNewSession(id);
};
// +++ 新增:处理虚拟键盘按键事件 +++
const handleVirtualKeyPress = (keySequence: string) => {
const currentSession = activeSession.value;
if (!currentSession) {
console.warn('[WorkspaceView] Cannot send virtual key, no active session.');
return;
}
// 在移动端模式下,我们假设 terminalManager 总是存在的(如果会话活动)
// 并且直接发送数据,因为虚拟键盘通常用于发送控制字符或特殊序列
const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined);
if (terminalManager && typeof terminalManager.sendData === 'function') {
console.log(`[WorkspaceView Mobile] Sending virtual key sequence: ${JSON.stringify(keySequence)}`);
terminalManager.sendData(keySequence);
} else {
console.warn(`[WorkspaceView Mobile] Cannot send virtual key for session ${currentSession.sessionId}, terminal manager or sendData method not available.`);
}
};
// +++ Function to toggle virtual keyboard visibility +++
const toggleVirtualKeyboard = () => {
isVirtualKeyboardVisible.value = !isVirtualKeyboardVisible.value;
};
// 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>
<!-- *** 动态 class 绑定添加 is-mobile *** -->
<div :class="['workspace-view', { 'with-header': isHeaderVisible, 'is-mobile': isMobile }]">
<!-- TerminalTabBar 始终渲染, 传递 isMobile 状态 -->
<TerminalTabBar
:sessions="sessionTabsWithStatus"
:active-session-id="activeSessionId"
:is-mobile="isMobile"
@activate-session="sessionStore.activateSession"
@close-session="sessionStore.closeSession"
@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"
/>
<!-- --- 桌面端布局 --- -->
<template v-if="!isMobile">
<div class="main-content-area">
<LayoutRenderer
v-if="layoutTree"
:is-root-renderer="true"
:layout-node="layoutTree"
:active-session-id="activeSessionId"
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', '加载布局中...') }}
</div>
</div>
</template>
<!-- --- 移动端布局 --- -->
<template v-else>
<div class="mobile-content-area">
<Terminal
v-if="activeSessionId"
ref="mobileTerminalRef"
:session-id="activeSessionId"
:is-active="true"
class="mobile-terminal"
@data="(data) => handleTerminalInput({ sessionId: activeSessionId!, data })"
@resize="(dims) => handleTerminalResize({ sessionId: activeSessionId!, dims })"
@ready="(payload) => handleTerminalReady({ ...payload, sessionId: activeSessionId! })"
/>
<div v-else class="pane-placeholder">
{{ t('workspace.noActiveSession', '没有活动的会话') }}
</div>
</div>
<CommandInputBar
class="mobile-command-bar"
:is-mobile="isMobile"
@send-command="handleSendCommand"
@search="handleSearch"
@find-next="handleFindNext"
@find-previous="handleFindPrevious"
@close-search="handleCloseSearch"
@clear-terminal="handleClearTerminal"
:is-virtual-keyboard-visible="isVirtualKeyboardVisible"
@toggle-virtual-keyboard="toggleVirtualKeyboard"
/>
<!-- +++ Use v-show for VirtualKeyboard and bind visibility +++ -->
<VirtualKeyboard
v-show="isVirtualKeyboardVisible"
class="mobile-virtual-keyboard"
@send-key="handleVirtualKeyPress"
/>
</template>
<!-- Modals 保持不变应在布局之外 -->
<AddConnectionFormComponent
v-if="showAddEditForm"
:connection-to-edit="connectionToEdit"
@close="handleFormClose"
@connection-added="handleConnectionAdded"
@connection-updated="handleConnectionUpdated"
/>
<LayoutConfigurator
:is-visible="showLayoutConfigurator"
@close="handleCloseLayoutConfigurator"
/>
<RemoteDesktopModal
v-if="isRdpModalOpen"
:connection="rdpConnectionInfo"
@close="sessionStore.closeRdpModal()"
/>
</div>
</template>
<style scoped>
.workspace-view {
display: flex;
background-color: transparent;
flex-direction: column;
height: 100dvh; /* 使用动态视口高度 */
overflow: hidden;
transition: height 0.3s ease; /* 可选:添加过渡效果 */
}
/* 当 Header 可见时,调整高度 */
.workspace-view.with-header {
/* 假设 Header 高度为 55px (根据 App.vue CSS) */
height: calc(100dvh - 55px); /* 使用动态视口高度计算 */
}
.main-content-area {
display: flex;
flex: 1;
overflow: hidden; /* Keep overflow hidden */
border: 1px solid var(--border-color, #ccc); /* Use variable for border */
border-top: none; /* Remove top border as it's handled by the tab bar */
border-radius: 0 0 5px 5px; /* Top-left, Top-right, Bottom-right, Bottom-left */
margin: var(--base-margin, 0.5rem); /* Add some margin around the content area */
margin-top: 0; /* Remove top margin if tab bar is directly above */
}
.layout-renderer-wrapper {
flex-grow: 1;
width: 100%;
height: 100%;
overflow: hidden;
}
/* 面板占位符样式 (用于加载或错误状态) */
.pane-placeholder {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
color: var(--text-color-secondary); /* Use secondary text color variable */
background-color: var(--header-bg-color); /* Use header background for slight contrast */
font-size: 0.9em;
padding: var(--base-padding); /* Use base padding variable */
}
/* --- Mobile Layout Styles --- */
.workspace-view.is-mobile {
/* Ensure flex column layout */
display: flex; /* Uncommented */
flex-direction: column; /* Uncommented */
/* Height is already handled by .workspace-view and .with-header */
}
.workspace-view.is-mobile .main-content-area {
/* Hide the desktop content area in mobile view */
display: none;
}
.mobile-content-area {
display: flex; /* Use flex for the terminal container */
flex-direction: column; /* Stack elements vertically if needed */
flex-grow: 1; /* Allow this area to take up remaining space */
overflow: hidden; /* Prevent overflow */
position: relative; /* Needed for potential absolute positioning inside */
/* Remove desktop margins/borders */
margin: 0;
border: none;
border-radius: 0;
}
.mobile-terminal {
flex-grow: 1; /* Terminal takes all available space in mobile-content-area */
width: 100%;
overflow: hidden;
}
.mobile-command-bar {
flex-shrink: 0; /* Prevent command bar from shrinking */
/* Add specific styles if needed, e.g., border-top */
border-top: 1px solid var(--border-color, #ccc);
}
.mobile-virtual-keyboard {
flex-shrink: 0; /* 防止虚拟键盘缩小 */
width: 100%; /* 确保宽度为 100% */
box-sizing: border-box; /* 边框和内边距包含在宽度内 */
/* 可以添加更多样式,例如背景色、边框等 */
}
/* Ensure modals are still displayed correctly (they are outside the main flow) */
</style>