diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index 6c839bb..c898101 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -7,6 +7,7 @@ import { useSettingsStore } from './stores/settings.store'; import { useAppearanceStore } from './stores/appearance.store'; import { useLayoutStore } from './stores/layout.store'; import { useFocusSwitcherStore } from './stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++ +import { useSessionStore } from './stores/session.store'; // +++ 导入 Session Store +++ import { storeToRefs } from 'pinia'; // 导入通知显示组件 import UINotificationDisplay from './components/UINotificationDisplay.vue'; @@ -16,6 +17,8 @@ import FileEditorOverlay from './components/FileEditorOverlay.vue'; import StyleCustomizer from './components/StyleCustomizer.vue'; // +++ 导入焦点切换配置器组件 +++ import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue'; +// +++ 导入 RDP 模态框组件 +++ +import RemoteDesktopModal from './components/RemoteDesktopModal.vue'; const { t } = useI18n(); const authStore = useAuthStore(); @@ -23,11 +26,13 @@ const settingsStore = useSettingsStore(); const appearanceStore = useAppearanceStore(); const layoutStore = useLayoutStore(); const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++ +const sessionStore = useSessionStore(); // +++ 实例化 Session Store +++ const { isAuthenticated } = storeToRefs(authStore); const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); const { isStyleCustomizerVisible } = storeToRefs(appearanceStore); const { isLayoutVisible, isHeaderVisible } = storeToRefs(layoutStore); // 添加 isHeaderVisible const { isConfiguratorVisible: isFocusSwitcherVisible } = storeToRefs(focusSwitcherStore); +const { isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // +++ 获取 RDP 状态 +++ const route = useRoute(); const navRef = ref(null); @@ -293,6 +298,13 @@ const isElementVisibleAndFocusable = (element: HTMLElement): boolean => { @close="focusSwitcherStore.toggleConfigurator(false)" /> + + + diff --git a/packages/frontend/src/components/LayoutRenderer.vue b/packages/frontend/src/components/LayoutRenderer.vue index 535b81c..12740cd 100644 --- a/packages/frontend/src/components/LayoutRenderer.vue +++ b/packages/frontend/src/components/LayoutRenderer.vue @@ -163,9 +163,11 @@ const componentProps = computed(() => { if (!currentActiveSession) return {}; // 传递 instanceId (使用布局节点的 ID), sessionId, dbConnectionId // 移除 sftpManager 和 wsDeps + // +++ 提供 instanceId 的备用值 +++ + const instanceId = props.layoutNode.id || `fm-main-${props.activeSessionId ?? 'unknown'}`; return { sessionId: props.activeSessionId ?? '', // 确保 sessionId 不为 null - instanceId: props.layoutNode.id, // 使用布局节点 ID 作为实例 ID + instanceId: instanceId, // 使用计算出的 instanceId (包含备用值) dbConnectionId: currentActiveSession.connectionId, // sftpManager: currentActiveSession.sftpManager, // 移除 sftpManager,因为它现在由 FileManager 内部管理 wsDeps: { // 恢复 wsDeps diff --git a/packages/frontend/src/components/RemoteDesktopModal.vue b/packages/frontend/src/components/RemoteDesktopModal.vue index 05f2439..ecc4eef 100644 --- a/packages/frontend/src/components/RemoteDesktopModal.vue +++ b/packages/frontend/src/components/RemoteDesktopModal.vue @@ -40,7 +40,8 @@ const MIN_MODAL_HEIGHT = 768; // Dynamically construct WebSocket URL based on environment let backendBaseUrl: string; -const LOCAL_BACKEND_URL = 'ws://localhost:18112' +// const LOCAL_BACKEND_URL = 'ws://localhost:18112' +const LOCAL_BACKEND_URL = 'ws://localhost:8081' // Determine WebSocket URL based on hostname if (window.location.hostname === 'localhost') { diff --git a/packages/frontend/src/components/TerminalTabBar.vue b/packages/frontend/src/components/TerminalTabBar.vue index 37a4220..cc26124 100644 --- a/packages/frontend/src/components/TerminalTabBar.vue +++ b/packages/frontend/src/components/TerminalTabBar.vue @@ -5,6 +5,7 @@ import { useRoute } from 'vue-router'; // 导入 useRoute import { storeToRefs } from 'pinia'; // 导入 storeToRefs import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue'; // 导入连接列表组件 import { useSessionStore } from '../stores/session.store'; // 导入 session store +import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store'; // +++ 导入 connections store 和类型 +++ import { useLayoutStore, type PaneName } from '../stores/layout.store'; // 导入布局 store 和类型 // 导入会话状态类型 import type { SessionTabInfoWithStatus } from '../stores/session.store'; // 导入更新后的类型 @@ -14,6 +15,7 @@ import type { SessionTabInfoWithStatus } from '../stores/session.store'; // 导 // --- Setup --- const { t } = useI18n(); // 初始化 i18n const layoutStore = useLayoutStore(); // 初始化布局 store +const connectionsStore = useConnectionsStore(); // +++ 获取 connections store 实例 +++ const { isHeaderVisible } = storeToRefs(layoutStore); // 从 layout store 获取主导航栏可见状态 const route = useRoute(); // 获取路由实例 @@ -61,8 +63,13 @@ const togglePopup = () => { // 处理从弹出列表中选择连接的事件 const handlePopupConnect = (connectionId: number) => { console.log(`[TabBar] Popup connect request for ID: ${connectionId}`); - // 使用 sessionStore 的方法来处理连接请求(它现在总是新建标签) - sessionStore.handleConnectRequest(connectionId); + // +++ 修复:传递 ConnectionInfo 而不是 ID +++ + const connectionInfo = connectionsStore.connections.find(c => c.id === connectionId); + if (connectionInfo) { + sessionStore.handleConnectRequest(connectionInfo); + } else { + console.error(`[TabBar] handlePopupConnect: 未找到 ID 为 ${connectionId} 的连接信息。`); + } showConnectionListPopup.value = false; // 关闭弹出窗口 }; diff --git a/packages/frontend/src/stores/session.store.ts b/packages/frontend/src/stores/session.store.ts index 7c7cf7d..cb62f97 100644 --- a/packages/frontend/src/stores/session.store.ts +++ b/packages/frontend/src/stores/session.store.ts @@ -1,6 +1,7 @@ import { ref, computed, shallowRef, type Ref } from 'vue'; // 导入 shallowRef import { defineStore } from 'pinia'; import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; // +++ 导入 useRouter +++ import { useConnectionsStore, type ConnectionInfo } from './connections.store'; // 导入文件编辑器相关的类型 import type { FileTab, FileInfo } from './fileEditor.store'; // 导入 FileTab 和 FileInfo @@ -86,12 +87,17 @@ export const useSessionStore = defineStore('session', () => { // --- 依赖 --- const { t } = useI18n(); const connectionsStore = useConnectionsStore(); + const router = useRouter(); // +++ 获取 router 实例 +++ // --- State --- // 使用 shallowRef 避免深度响应性问题,保留管理器实例内部的响应性 const sessions = shallowRef>(new Map()); const activeSessionId = ref(null); + // --- RDP Modal State --- + const isRdpModalOpen = ref(false); + const rdpConnectionInfo = ref(null); + // --- Getters --- const sessionTabs = computed(() => { return Array.from(sessions.value.values()).map(session => ({ @@ -364,46 +370,58 @@ export const useSessionStore = defineStore('session', () => { /** * 处理连接列表的左键点击 - * 优先级 1: 如果点击的是当前活动标签且断开,则重连该标签。 - * 优先级 2: 其他所有情况(非活动标签、活动且已连接标签、新连接),总是打开新标签。 + * 处理来自 UI 的连接请求(例如点击连接列表或仪表盘)。 + * - 如果是 RDP 连接,直接打开 RDP 模态框。 + * - 如果是非 RDP 连接: + * - 如果点击的是当前活动且断开的会话,则尝试重连。 + * - 否则,打开一个新的会话标签页并导航到 Workspace。 */ - const handleConnectRequest = (connectionId: number | string) => { - const connIdStr = String(connectionId); - console.log(`[SessionStore] handleConnectRequest called for ID: ${connIdStr}`); + const handleConnectRequest = (connection: ConnectionInfo) => { + console.log(`[SessionStore] handleConnectRequest called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`); - let activeAndDisconnected = false; // 标记是否满足最高优先级条件 + if (connection.type === 'RDP') { + // RDP: 直接打开模态框 + openRdpModal(connection); + } else { + // 非 RDP (e.g., SSH): 处理会话和导航 + const connIdStr = String(connection.id); + let activeAndDisconnected = false; - // 检查是否点击了当前活动且断开的会话 - if (activeSessionId.value) { - const currentActiveSession = sessions.value.get(activeSessionId.value); - if (currentActiveSession && currentActiveSession.connectionId === connIdStr) { - const currentStatus = currentActiveSession.wsManager.connectionStatus.value; - console.log(`[SessionStore] 点击的是当前活动会话 ${activeSessionId.value},状态: ${currentStatus}`); - if (currentStatus === 'disconnected' || currentStatus === 'error') { - activeAndDisconnected = true; - // 满足最高优先级:重连当前活动会话 - console.log(`[SessionStore] 活动会话 ${activeSessionId.value} 已断开或出错,尝试重连...`); - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - // --- 修改:根据环境确定 WebSocket 主机 --- - let wsHost = window.location.hostname; - if (window.location.hostname === 'localhost') { - wsHost = 'localhost:3001'; // 本地调试时硬编码 - console.log('[SessionStore handleConnectRequest] Using hardcoded localhost:3001 for WebSocket reconnect.'); - } else { - console.log(`[SessionStore handleConnectRequest] Using hostname: ${wsHost}`); + // 检查是否点击了当前活动且断开的会话 + if (activeSessionId.value) { + const currentActiveSession = sessions.value.get(activeSessionId.value); + if (currentActiveSession && currentActiveSession.connectionId === connIdStr) { + const currentStatus = currentActiveSession.wsManager.connectionStatus.value; + console.log(`[SessionStore] 点击的是当前活动会话 ${activeSessionId.value},状态: ${currentStatus}`); + if (currentStatus === 'disconnected' || currentStatus === 'error') { + activeAndDisconnected = true; + // 满足最高优先级:重连当前活动会话 + console.log(`[SessionStore] 活动会话 ${activeSessionId.value} 已断开或出错,尝试重连...`); + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + let wsHost = window.location.hostname; + if (window.location.hostname === 'localhost') { + wsHost = 'localhost:3001'; + console.log('[SessionStore handleConnectRequest] Using hardcoded localhost:3001 for WebSocket reconnect.'); + } else { + console.log(`[SessionStore handleConnectRequest] Using hostname: ${wsHost}`); + } + const wsUrl = `${protocol}//${wsHost}/ws/`; + console.log(`[SessionStore handleConnectRequest] Generated WebSocket URL for reconnect: ${wsUrl}`); + currentActiveSession.wsManager.connect(wsUrl); + // 重连后,确保激活该会话(如果它不是活动会话)并导航 + activateSession(activeSessionId.value); // 确保激活 + router.push({ name: 'Workspace' }); // +++ 添加跳转 +++ } - // --- 结束修改 --- - const wsUrl = `${protocol}//${wsHost}/ws/`; // 使用 wsHost 并添加 /ws/ 路径 - console.log(`[SessionStore handleConnectRequest] Generated WebSocket URL for reconnect: ${wsUrl}`); // 添加日志 - currentActiveSession.wsManager.connect(wsUrl); } } - } - // 如果不满足最高优先级条件,则总是打开新会话 - if (!activeAndDisconnected) { - console.log(`[SessionStore] 不满足重连条件或点击了其他连接,将打开新会话 for ID: ${connIdStr}`); - openNewSession(connIdStr); + // 如果不满足重连条件,则总是打开新会话并导航 + if (!activeAndDisconnected) { + console.log(`[SessionStore] 不满足重连条件或点击了其他连接,将打开新会话 for ID: ${connIdStr}`); + openNewSession(connIdStr); // openNewSession 会自动激活新会话 + // 导航到 Workspace,让 WorkspaceView 处理新激活的会话 + router.push({ name: 'Workspace' }); // +++ 添加跳转 +++ + } } }; @@ -634,10 +652,26 @@ export const useSessionStore = defineStore('session', () => { } }; + // --- RDP Modal Actions --- + const openRdpModal = (connection: ConnectionInfo) => { + console.log(`[SessionStore] Opening RDP modal for connection: ${connection.name} (ID: ${connection.id})`); + rdpConnectionInfo.value = connection; + isRdpModalOpen.value = true; + }; + + const closeRdpModal = () => { + console.log('[SessionStore] Closing RDP modal.'); + isRdpModalOpen.value = false; + rdpConnectionInfo.value = null; // 清除连接信息 + }; + + return { // State sessions, activeSessionId, + isRdpModalOpen, // 导出 RDP 模态框状态 + rdpConnectionInfo, // 导出 RDP 连接信息 // Getters sessionTabs, sessionTabsWithStatus, // 导出新的 getter @@ -657,5 +691,8 @@ export const useSessionStore = defineStore('session', () => { setActiveEditorTabInSession, updateFileContentInSession, // 导出更新内容 Action saveFileInSession, // 导出保存文件 Action + // --- RDP Modal Actions --- + openRdpModal, // 导出打开 RDP 模态框 Action + closeRdpModal, // 导出关闭 RDP 模态框 Action }; }); diff --git a/packages/frontend/src/views/DashboardView.vue b/packages/frontend/src/views/DashboardView.vue index 6f1211f..c6f14a8 100644 --- a/packages/frontend/src/views/DashboardView.vue +++ b/packages/frontend/src/views/DashboardView.vue @@ -84,11 +84,9 @@ onMounted(async () => { // --- 方法 --- // 修改函数签名,接受 ConnectionInfo 类型 const connectTo = (connection: ConnectionInfo) => { - console.log(`[Dashboard] connectTo called for ID: ${connection.id}`); - // 调用 session store 处理连接请求 - sessionStore.handleConnectRequest(connection.id); - // 跳转到工作区 - router.push({ name: 'Workspace' }); + console.log(`[Dashboard] connectTo called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`); + // 将连接处理逻辑委托给 sessionStore + sessionStore.handleConnectRequest(connection); }; diff --git a/packages/frontend/src/views/WorkspaceView.vue b/packages/frontend/src/views/WorkspaceView.vue index fa7f29a..f276b44 100644 --- a/packages/frontend/src/views/WorkspaceView.vue +++ b/packages/frontend/src/views/WorkspaceView.vue @@ -3,6 +3,7 @@ import { onMounted, onBeforeUnmount, computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; import { useLayoutStore } from '../stores/layout.store'; // *** 重新导入 layoutStore *** +import { useConnectionsStore } from '../stores/connections.store'; // +++ 导入 connectionsStore +++ // 移除不再直接使用的组件导入 import AddConnectionFormComponent from '../components/AddConnectionForm.vue'; import TerminalTabBar from '../components/TerminalTabBar.vue'; @@ -24,6 +25,7 @@ const settingsStore = useSettingsStore(); const fileEditorStore = useFileEditorStore(); const layoutStore = useLayoutStore(); // *** 确保 layoutStore 实例存在 *** const commandHistoryStore = useCommandHistoryStore(); +const connectionsStore = useConnectionsStore(); // +++ 获取 connectionsStore 实例 +++ const { isHeaderVisible } = storeToRefs(layoutStore); // *** 获取 isHeaderVisible 状态 *** // --- 从 Store 获取响应式状态和 Getters --- @@ -162,7 +164,13 @@ onBeforeUnmount(() => { if (terminalManager.terminalInstance?.value) { terminalManager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`); } - sessionStore.handleConnectRequest(currentSession.connectionId); + // +++ 修复:传递 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; } @@ -195,7 +203,13 @@ onBeforeUnmount(() => { } else { console.warn(`[WorkspaceView] 无法写入重连提示,terminalInstance 不可用。`); } - sessionStore.handleConnectRequest(session.connectionId); + // +++ 修复:传递 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); } @@ -342,7 +356,13 @@ const handleCloseEditorTab = (tabId: string) => { // --- 连接列表操作处理 (用于 WorkspaceConnectionList) --- const handleConnectRequest = (id: number) => { console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`); - sessionStore.handleConnectRequest(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}`);