refactor: 优化代码结构
This commit is contained in:
@@ -1,871 +1,138 @@
|
||||
import { ref, computed, shallowRef, type Ref } from 'vue'; // 导入 shallowRef
|
||||
// packages/frontend/src/stores/session.store.ts
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router'; // +++ 导入 useRouter +++
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useConnectionsStore, type ConnectionInfo } from './connections.store';
|
||||
// 导入文件编辑器相关的类型
|
||||
import type { FileTab, FileInfo } from './fileEditor.store'; // 导入 FileTab 和 FileInfo (确保 FileTab 已在 fileEditor.store.ts 中简化)
|
||||
import type { SftpReadFileSuccessPayload } from '../types/sftp.types'; // 导入更新后的类型
|
||||
// --- 修复:添加 iconv-lite 和 buffer 导入 ---
|
||||
import * as iconv from '@vscode/iconv-lite-umd';
|
||||
import { Buffer } from 'buffer/';
|
||||
|
||||
// 导入管理器工厂函数 (用于创建实例)
|
||||
// 导入 WsConnectionStatus 类型
|
||||
import { createWebSocketConnectionManager, type WsConnectionStatus } from '../composables/useWebSocketConnection';
|
||||
import { createSftpActionsManager, type WebSocketDependencies } from '../composables/useSftpActions';
|
||||
import { createSshTerminalManager, type SshTerminalDependencies } from '../composables/useSshTerminal';
|
||||
import { createStatusMonitorManager, type StatusMonitorDependencies } from '../composables/useStatusMonitor';
|
||||
import { createDockerManager, type DockerManagerInstance, type DockerManagerDependencies } from '../composables/useDockerManager'; // +++ Import Docker Manager +++
|
||||
// 从新模块导入状态
|
||||
import {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
isRdpModalOpen,
|
||||
rdpConnectionInfo,
|
||||
isVncModalOpen,
|
||||
vncConnectionInfo,
|
||||
} from './session/state';
|
||||
|
||||
// --- 辅助函数 ---
|
||||
function generateSessionId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
}
|
||||
// 从新模块导入 Getters
|
||||
import {
|
||||
sessionTabs,
|
||||
sessionTabsWithStatus,
|
||||
activeSession,
|
||||
} from './session/getters';
|
||||
|
||||
// 辅助函数:根据文件名获取语言 (从 fileEditor.store 迁移过来)
|
||||
const getLanguageFromFilename = (filename: string): string => {
|
||||
const extension = filename.split('.').pop()?.toLowerCase();
|
||||
switch (extension) {
|
||||
case 'js': return 'javascript';
|
||||
case 'ts': return 'typescript';
|
||||
case 'json': return 'json';
|
||||
case 'html': return 'html';
|
||||
case 'css': return 'css';
|
||||
case 'scss': return 'scss';
|
||||
case 'less': return 'less';
|
||||
case 'py': return 'python';
|
||||
case 'java': return 'java';
|
||||
case 'c': return 'c';
|
||||
case 'cpp': return 'cpp';
|
||||
case 'cs': return 'csharp';
|
||||
case 'go': return 'go';
|
||||
case 'php': return 'php';
|
||||
case 'rb': return 'ruby';
|
||||
case 'rs': return 'rust';
|
||||
case 'sql': return 'sql';
|
||||
case 'sh': return 'shell';
|
||||
case 'yaml': case 'yml': return 'yaml';
|
||||
case 'md': return 'markdown';
|
||||
case 'xml': return 'xml';
|
||||
case 'ini': return 'ini';
|
||||
case 'bat': return 'bat';
|
||||
case 'dockerfile': return 'dockerfile';
|
||||
default: return 'plaintext';
|
||||
}
|
||||
};
|
||||
// 从新模块导入 Actions
|
||||
import * as sessionActions from './session/actions/sessionActions';
|
||||
import * as editorActions from './session/actions/editorActions';
|
||||
import * as sftpManagerActions from './session/actions/sftpManagerActions';
|
||||
import * as modalActions from './session/actions/modalActions';
|
||||
import * as commandInputActions from './session/actions/commandInputActions';
|
||||
|
||||
// --- 修复:添加 decodeRawContent 辅助函数 ---
|
||||
const decodeRawContent = (rawContentBase64: string, encoding: string): string => {
|
||||
try {
|
||||
const buffer = Buffer.from(rawContentBase64, 'base64');
|
||||
const normalizedEncoding = encoding.toLowerCase().replace(/[^a-z0-9]/g, ''); // Normalize encoding name
|
||||
|
||||
// 优先使用 TextDecoder 处理标准编码
|
||||
if (['utf8', 'utf16le', 'utf16be'].includes(normalizedEncoding)) {
|
||||
const decoder = new TextDecoder(encoding); // Use original encoding name for TextDecoder
|
||||
return decoder.decode(buffer);
|
||||
}
|
||||
// 使用 iconv-lite 处理其他编码
|
||||
else if (iconv.encodingExists(normalizedEncoding)) {
|
||||
return iconv.decode(buffer, normalizedEncoding);
|
||||
}
|
||||
// 如果 iconv-lite 也不支持,回退到 UTF-8 并警告
|
||||
else {
|
||||
console.warn(`[SessionStore decodeRawContent] Unsupported encoding "${encoding}" requested. Falling back to UTF-8.`);
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
return decoder.decode(buffer);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[SessionStore decodeRawContent] Error decoding content with encoding "${encoding}":`, error);
|
||||
return `// Error decoding content: ${error.message}`; // 返回错误信息
|
||||
}
|
||||
};
|
||||
// --- 结束修复 ---
|
||||
|
||||
// --- 类型定义 (导出以便其他模块使用) ---
|
||||
export type WsManagerInstance = ReturnType<typeof createWebSocketConnectionManager>;
|
||||
export type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
|
||||
export type SshTerminalInstance = ReturnType<typeof createSshTerminalManager>;
|
||||
export type StatusMonitorInstance = ReturnType<typeof createStatusMonitorManager>;
|
||||
// Removed conflicting local declaration of DockerManagerInstance, it's imported above.
|
||||
|
||||
export interface SessionState {
|
||||
sessionId: string;
|
||||
connectionId: string; // 数据库中的连接 ID
|
||||
connectionName: string; // 用于显示
|
||||
wsManager: WsManagerInstance;
|
||||
// sftpManager: SftpManagerInstance; // 移除单个实例
|
||||
sftpManagers: Map<string, SftpManagerInstance>; // 使用 Map 管理多个实例
|
||||
terminalManager: SshTerminalInstance;
|
||||
statusMonitorManager: StatusMonitorInstance;
|
||||
dockerManager: DockerManagerInstance; // +++ Add Docker Manager Instance +++
|
||||
// currentSftpPath: Ref<string>; // 移除,由每个 sftpManager 内部管理
|
||||
// --- 新增:独立编辑器状态 ---
|
||||
editorTabs: Ref<FileTab[]>; // 编辑器标签页列表
|
||||
activeEditorTabId: Ref<string | null>; // 当前活动的编辑器标签页 ID
|
||||
// --- 新增:命令输入框内容 ---
|
||||
commandInputContent: Ref<string>; // 当前会话的命令输入框内容
|
||||
}
|
||||
|
||||
// 为标签栏定义包含状态的类型
|
||||
export interface SessionTabInfoWithStatus {
|
||||
sessionId: string;
|
||||
connectionName: string;
|
||||
status: WsConnectionStatus; // 添加状态字段
|
||||
}
|
||||
// 导入需要的类型 (例如 FileInfo 可能会在参数中使用)
|
||||
import type { FileInfo } from './fileEditor.store';
|
||||
// SftpManagerInstance 类型主要在 action 文件内部使用,但如果 store 直接暴露它,则需要导入
|
||||
// import type { SftpManagerInstance } from './session/types';
|
||||
|
||||
|
||||
export const useSessionStore = defineStore('session', () => {
|
||||
// --- 依赖 ---
|
||||
const { t } = useI18n();
|
||||
const connectionsStore = useConnectionsStore();
|
||||
const router = useRouter(); // +++ 获取 router 实例 +++
|
||||
const router = useRouter();
|
||||
|
||||
// --- 移除 decodeBase64Content 辅助方法 ---
|
||||
// --- 包装 Actions 以注入依赖 ---
|
||||
|
||||
// Modal Actions (这些可能被其他 actions 依赖,所以先定义)
|
||||
const openRdpModal = (connection: ConnectionInfo) => modalActions.openRdpModal(connection);
|
||||
const closeRdpModal = () => modalActions.closeRdpModal();
|
||||
const openVncModal = (connection: ConnectionInfo) => modalActions.openVncModal(connection);
|
||||
const closeVncModal = () => modalActions.closeVncModal();
|
||||
|
||||
// --- State ---
|
||||
// 使用 shallowRef 避免深度响应性问题,保留管理器实例内部的响应性
|
||||
const sessions = shallowRef<Map<string, SessionState>>(new Map());
|
||||
const activeSessionId = ref<string | null>(null);
|
||||
|
||||
// --- RDP Modal State ---
|
||||
const isRdpModalOpen = ref(false);
|
||||
const rdpConnectionInfo = ref<ConnectionInfo | null>(null);
|
||||
|
||||
// --- VNC Modal State ---
|
||||
const isVncModalOpen = ref(false);
|
||||
const vncConnectionInfo = ref<ConnectionInfo | null>(null);
|
||||
|
||||
// --- Getters ---
|
||||
const sessionTabs = computed(() => {
|
||||
return Array.from(sessions.value.values()).map(session => ({
|
||||
sessionId: session.sessionId,
|
||||
connectionName: session.connectionName,
|
||||
}));
|
||||
});
|
||||
|
||||
// 新增:包含状态的标签页信息
|
||||
const sessionTabsWithStatus = computed((): SessionTabInfoWithStatus[] => {
|
||||
return Array.from(sessions.value.values()).map(session => ({
|
||||
sessionId: session.sessionId,
|
||||
connectionName: session.connectionName,
|
||||
status: session.wsManager.connectionStatus.value, // 从 wsManager 获取状态
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
const activeSession = computed((): SessionState | null => {
|
||||
if (!activeSessionId.value) return null;
|
||||
return sessions.value.get(activeSessionId.value) || null;
|
||||
});
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
/**
|
||||
* 根据连接 ID 查找连接信息
|
||||
*/
|
||||
const findConnectionInfo = (connectionId: number | string): ConnectionInfo | undefined => {
|
||||
return connectionsStore.connections.find(c => c.id === Number(connectionId));
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开一个新的会话标签页
|
||||
*/
|
||||
const openNewSession = (connectionId: number | string) => {
|
||||
console.log(`[SessionStore] 请求打开新会话: ${connectionId}`);
|
||||
const connInfo = findConnectionInfo(connectionId);
|
||||
if (!connInfo) {
|
||||
console.error(`[SessionStore] 无法打开新会话:找不到 ID 为 ${connectionId} 的连接信息。`);
|
||||
// TODO: 向用户显示错误
|
||||
return;
|
||||
}
|
||||
|
||||
const newSessionId = generateSessionId();
|
||||
const dbConnId = String(connInfo.id);
|
||||
|
||||
// 1. 创建管理器实例 (从 WorkspaceView 迁移)
|
||||
const wsManager = createWebSocketConnectionManager(newSessionId, dbConnId, t);
|
||||
// const currentSftpPath = ref<string>('.'); // 移除单个 SFTP 路径状态
|
||||
// const wsDeps: WebSocketDependencies = { ... }; // wsDeps 将在 getOrCreateSftpManager 中创建
|
||||
// const sftpManager = createSftpActionsManager(newSessionId, currentSftpPath, wsDeps, t); // 移除单个 sftpManager 创建
|
||||
const sshTerminalDeps: SshTerminalDependencies = {
|
||||
sendMessage: wsManager.sendMessage,
|
||||
onMessage: wsManager.onMessage,
|
||||
isConnected: wsManager.isConnected,
|
||||
};
|
||||
const terminalManager = createSshTerminalManager(newSessionId, sshTerminalDeps, t);
|
||||
const statusMonitorDeps: StatusMonitorDependencies = {
|
||||
onMessage: wsManager.onMessage,
|
||||
isConnected: wsManager.isConnected,
|
||||
};
|
||||
const statusMonitorManager = createStatusMonitorManager(newSessionId, statusMonitorDeps);
|
||||
// +++ Create Docker Manager Dependencies +++
|
||||
const dockerManagerDeps: DockerManagerDependencies = {
|
||||
sendMessage: wsManager.sendMessage,
|
||||
onMessage: wsManager.onMessage,
|
||||
isConnected: wsManager.isConnected,
|
||||
};
|
||||
// +++ Create Docker Manager Instance +++
|
||||
const dockerManager = createDockerManager(newSessionId, dockerManagerDeps, { t });
|
||||
|
||||
// 2. 创建 SessionState 对象
|
||||
const newSession: SessionState = {
|
||||
sessionId: newSessionId,
|
||||
connectionId: dbConnId,
|
||||
connectionName: connInfo.name || connInfo.host,
|
||||
wsManager: wsManager,
|
||||
// sftpManager: sftpManager, // 移除
|
||||
sftpManagers: new Map<string, SftpManagerInstance>(), // 初始化 Map
|
||||
terminalManager: terminalManager,
|
||||
statusMonitorManager: statusMonitorManager,
|
||||
dockerManager: dockerManager, // +++ Add Docker Manager to Session State +++
|
||||
// currentSftpPath: currentSftpPath, // 移除
|
||||
// --- 初始化编辑器状态 ---
|
||||
editorTabs: ref([]), // 初始化为空数组
|
||||
activeEditorTabId: ref(null), // 初始化为 null
|
||||
// --- 初始化命令输入框内容 ---
|
||||
commandInputContent: ref(''), // 初始化为空字符串
|
||||
};
|
||||
|
||||
// 3. 添加到 Map 并激活 (需要创建 Map 的新实例以触发 shallowRef 更新)
|
||||
const newSessionsMap = new Map(sessions.value);
|
||||
newSessionsMap.set(newSessionId, newSession);
|
||||
sessions.value = newSessionsMap; // 触发 shallowRef 更新
|
||||
activeSessionId.value = newSessionId;
|
||||
console.log(`[SessionStore] 已创建新会话实例: ${newSessionId} for connection ${dbConnId}`);
|
||||
|
||||
// 4. 启动 WebSocket 连接
|
||||
// 根据当前页面协议动态生成 WebSocket URL,并移除硬编码的端口
|
||||
// 假设 WebSocket 服务与 Web 服务在同一主机和端口上提供(通过反向代理)
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// 使用 window.location.host,它包含主机名和端口(如果非默认)
|
||||
const wsHostAndPort = window.location.host;
|
||||
// 假设 WebSocket 路径固定为 /ws/
|
||||
const wsUrl = `${protocol}//${wsHostAndPort}/ws/`;
|
||||
console.log(`[SessionStore] Generated WebSocket URL: ${wsUrl}`); // 添加日志记录生成的 URL
|
||||
wsManager.connect(wsUrl);
|
||||
console.log(`[SessionStore] 已为会话 ${newSessionId} 启动 WebSocket 连接。`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 激活指定 ID 的会话标签页
|
||||
*/
|
||||
const activateSession = (sessionId: string) => {
|
||||
if (sessions.value.has(sessionId)) {
|
||||
if (activeSessionId.value !== sessionId) {
|
||||
activeSessionId.value = sessionId;
|
||||
console.log(`[SessionStore] 已激活会话: ${sessionId}`);
|
||||
// TODO: 可能需要 nextTick 来聚焦终端?
|
||||
} else {
|
||||
console.log(`[SessionStore] 会话 ${sessionId} 已经是活动状态。`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[SessionStore] 尝试激活不存在的会话 ID: ${sessionId}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新指定会话中编辑器标签页的内容
|
||||
*/
|
||||
const updateFileContentInSession = (sessionId: string, tabId: string, newContent: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[SessionStore] 尝试在不存在的会话 ${sessionId} 中更新标签页 ${tabId} 内容`);
|
||||
return;
|
||||
}
|
||||
const tab = session.editorTabs.value.find(t => t.id === tabId);
|
||||
if (tab && !tab.isLoading) {
|
||||
tab.content = newContent;
|
||||
// 检查是否修改
|
||||
tab.isModified = tab.content !== tab.originalContent;
|
||||
// 当用户编辑时,重置保存状态
|
||||
if (tab.saveStatus === 'success' || tab.saveStatus === 'error') {
|
||||
tab.saveStatus = 'idle';
|
||||
tab.saveError = null;
|
||||
}
|
||||
} else if (tab) {
|
||||
console.warn(`[SessionStore] 尝试更新正在加载的标签页 ${tabId} 的内容`);
|
||||
} else {
|
||||
console.warn(`[SessionStore] 尝试更新会话 ${sessionId} 中不存在的标签页 ${tabId} 的内容`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存指定会话中的编辑器标签页
|
||||
*/
|
||||
const saveFileInSession = async (sessionId: string, tabId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[SessionStore] 尝试在不存在的会话 ${sessionId} 中保存标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
const tab = session.editorTabs.value.find(t => t.id === tabId);
|
||||
if (!tab) {
|
||||
console.warn(`[SessionStore] 尝试保存在会话 ${sessionId} 中不存在的标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab.isSaving || tab.isLoading || tab.loadingError || !tab.isModified) {
|
||||
console.warn(`[SessionStore] 保存条件不满足 for ${tab.filePath} (会话 ${sessionId}),无法保存。`, { tab });
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查会话连接状态
|
||||
if (!session.wsManager.isConnected.value || !session.wsManager.isSftpReady.value) {
|
||||
console.error(`[SessionStore] 保存失败:会话 ${sessionId} 无效或未连接/SFTP 未就绪。`);
|
||||
tab.saveStatus = 'error';
|
||||
tab.saveError = t('fileManager.errors.sessionInvalidOrNotReady');
|
||||
setTimeout(() => { if (tab.saveStatus === 'error') { tab.saveStatus = 'idle'; tab.saveError = null; } }, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取默认的 sftpManager 实例来执行保存操作
|
||||
const sftpManager = getOrCreateSftpManager(sessionId, 'primary');
|
||||
if (!sftpManager) {
|
||||
console.error(`[SessionStore] 保存失败:无法获取会话 ${sessionId} 的 primary sftpManager。`);
|
||||
tab.saveStatus = 'error';
|
||||
tab.saveError = t('fileManager.errors.sftpManagerNotFound');
|
||||
setTimeout(() => { if (tab.saveStatus === 'error') { tab.saveStatus = 'idle'; tab.saveError = null; } }, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[SessionStore] 开始保存文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tab.id}) using primary sftpManager`);
|
||||
tab.isSaving = true;
|
||||
tab.saveStatus = 'saving';
|
||||
tab.saveError = null;
|
||||
|
||||
const contentToSave = tab.content;
|
||||
const encodingToUse = tab.selectedEncoding; // 获取选定的编码
|
||||
|
||||
try {
|
||||
// --- 修改:传递 selectedEncoding 给 writeFile ---
|
||||
await sftpManager.writeFile(tab.filePath, contentToSave, encodingToUse);
|
||||
console.log(`[SessionStore] 文件 ${tab.filePath} (会话 ${sessionId}) 使用编码 ${encodingToUse} 保存成功。`);
|
||||
tab.isSaving = false;
|
||||
tab.saveStatus = 'success';
|
||||
tab.saveError = null;
|
||||
tab.originalContent = contentToSave; // 更新原始内容
|
||||
tab.isModified = false; // 重置修改状态
|
||||
setTimeout(() => { if (tab.saveStatus === 'success') { tab.saveStatus = 'idle'; } }, 2000);
|
||||
} catch (err: any) {
|
||||
console.error(`[SessionStore] 保存文件 ${tab.filePath} (会话 ${sessionId}) 失败:`, err);
|
||||
tab.isSaving = false;
|
||||
tab.saveStatus = 'error';
|
||||
tab.saveError = `${t('fileManager.errors.saveFailed')}: ${err.message || err}`;
|
||||
setTimeout(() => { if (tab.saveStatus === 'error') { tab.saveStatus = 'idle'; tab.saveError = null; } }, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 关闭指定 ID 的会话标签页
|
||||
*/
|
||||
const closeSession = (sessionId: string) => {
|
||||
console.log(`[SessionStore] 请求关闭会话 ID: ${sessionId}`);
|
||||
const sessionToClose = sessions.value.get(sessionId);
|
||||
if (!sessionToClose) {
|
||||
console.warn(`[SessionStore] 尝试关闭不存在的会话 ID: ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 调用实例上的清理和断开方法 (从 WorkspaceView 迁移)
|
||||
sessionToClose.wsManager.disconnect();
|
||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 wsManager.disconnect()`);
|
||||
// 清理该会话下的所有 sftpManager 实例
|
||||
sessionToClose.sftpManagers.forEach((manager, instanceId) => {
|
||||
manager.cleanup();
|
||||
console.log(`[SessionStore] 已为会话 ${sessionId} 的 sftpManager (实例 ${instanceId}) 调用 cleanup()`);
|
||||
// Session Actions
|
||||
const openNewSession = (connectionId: number | string) =>
|
||||
sessionActions.openNewSession(connectionId, { connectionsStore, t });
|
||||
const activateSession = (sessionId: string) => sessionActions.activateSession(sessionId);
|
||||
const closeSession = (sessionId: string) => sessionActions.closeSession(sessionId);
|
||||
const handleConnectRequest = (connection: ConnectionInfo) =>
|
||||
sessionActions.handleConnectRequest(connection, {
|
||||
connectionsStore,
|
||||
router,
|
||||
openRdpModalAction: openRdpModal, // 传递包装后的 action
|
||||
openVncModalAction: openVncModal, // 传递包装后的 action
|
||||
t,
|
||||
});
|
||||
sessionToClose.sftpManagers.clear();
|
||||
// sessionToClose.sftpManager.cleanup(); // 移除旧的调用
|
||||
// console.log(`[SessionStore] 已为会话 ${sessionId} 调用 sftpManager.cleanup()`); // 移除旧的日志
|
||||
sessionToClose.terminalManager.cleanup();
|
||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 terminalManager.cleanup()`);
|
||||
sessionToClose.statusMonitorManager.cleanup();
|
||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 statusMonitorManager.cleanup()`);
|
||||
sessionToClose.dockerManager.cleanup(); // +++ Call Docker Manager cleanup +++
|
||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 dockerManager.cleanup()`); // +++ Add log +++
|
||||
// TODO: 清理编辑器相关资源?例如提示保存未保存的文件
|
||||
const handleOpenNewSession = (connectionId: number | string) =>
|
||||
sessionActions.handleOpenNewSession(connectionId, { connectionsStore, t });
|
||||
const cleanupAllSessions = () => sessionActions.cleanupAllSessions();
|
||||
|
||||
// 2. 从 Map 中移除会话 (需要创建 Map 的新实例以触发 shallowRef 更新)
|
||||
const newSessionsMap = new Map(sessions.value);
|
||||
newSessionsMap.delete(sessionId);
|
||||
sessions.value = newSessionsMap; // 触发 shallowRef 更新
|
||||
console.log(`[SessionStore] 已从 Map 中移除会话: ${sessionId}`);
|
||||
// SFTP Manager Actions
|
||||
const getOrCreateSftpManager = (sessionId: string, instanceId: string) =>
|
||||
sftpManagerActions.getOrCreateSftpManager(sessionId, instanceId, { t });
|
||||
const removeSftpManager = (sessionId: string, instanceId: string) =>
|
||||
sftpManagerActions.removeSftpManager(sessionId, instanceId);
|
||||
|
||||
// 3. 切换活动标签页
|
||||
if (activeSessionId.value === sessionId) {
|
||||
const remainingSessions = Array.from(sessions.value.keys());
|
||||
const nextActiveId = remainingSessions.length > 0 ? remainingSessions[remainingSessions.length - 1] : null;
|
||||
activeSessionId.value = nextActiveId;
|
||||
console.log(`[SessionStore] 关闭活动会话后,切换到: ${nextActiveId}`);
|
||||
}
|
||||
};
|
||||
// Editor Actions
|
||||
const openFileInSession = (sessionId: string, fileInfo: FileInfo) =>
|
||||
editorActions.openFileInSession(sessionId, fileInfo, { getOrCreateSftpManager, t });
|
||||
const closeEditorTabInSession = (sessionId: string, tabId: string) =>
|
||||
editorActions.closeEditorTabInSession(sessionId, tabId);
|
||||
const setActiveEditorTabInSession = (sessionId: string, tabId: string) =>
|
||||
editorActions.setActiveEditorTabInSession(sessionId, tabId);
|
||||
const updateFileContentInSession = (sessionId: string, tabId: string, newContent: string) =>
|
||||
editorActions.updateFileContentInSession(sessionId, tabId, newContent);
|
||||
const saveFileInSession = (sessionId: string, tabId: string) =>
|
||||
editorActions.saveFileInSession(sessionId, tabId, { getOrCreateSftpManager, t });
|
||||
const changeEncodingInSession = (sessionId: string, tabId: string, newEncoding: string) =>
|
||||
editorActions.changeEncodingInSession(sessionId, tabId, newEncoding);
|
||||
const closeOtherTabsInSession = (sessionId: string, targetTabId: string) =>
|
||||
editorActions.closeOtherTabsInSession(sessionId, targetTabId);
|
||||
const closeTabsToTheRightInSession = (sessionId: string, targetTabId: string) =>
|
||||
editorActions.closeTabsToTheRightInSession(sessionId, targetTabId);
|
||||
const closeTabsToTheLeftInSession = (sessionId: string, targetTabId: string) =>
|
||||
editorActions.closeTabsToTheLeftInSession(sessionId, targetTabId);
|
||||
|
||||
/**
|
||||
* 处理连接列表的左键点击
|
||||
* 处理来自 UI 的连接请求(例如点击连接列表或仪表盘)。
|
||||
* - 如果是 RDP 连接,直接打开 RDP 模态框。
|
||||
* - 如果是非 RDP 连接:
|
||||
* - 如果点击的是当前活动且断开的会话,则尝试重连。
|
||||
* - 否则,打开一个新的会话标签页并导航到 Workspace。
|
||||
*/
|
||||
const handleConnectRequest = (connection: ConnectionInfo) => {
|
||||
// console.log(`[SessionStore] handleConnectRequest called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`); // 保留原始日志或移除
|
||||
|
||||
if (connection.type === 'RDP') {
|
||||
// RDP: 直接打开 RDP 模态框
|
||||
openRdpModal(connection);
|
||||
} else if (connection.type === 'VNC') {
|
||||
// VNC: 直接打开 VNC 模态框
|
||||
openVncModal(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:';
|
||||
// 使用 window.location.host
|
||||
const wsHostAndPort = window.location.host;
|
||||
const wsUrl = `${protocol}//${wsHostAndPort}/ws/`;
|
||||
console.log(`[SessionStore handleConnectRequest] Generated WebSocket URL for reconnect: ${wsUrl}`);
|
||||
currentActiveSession.wsManager.connect(wsUrl);
|
||||
// 重连后,确保激活该会话(如果它不是活动会话)并导航
|
||||
activateSession(activeSessionId.value); // 确保激活
|
||||
router.push({ name: 'Workspace' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不满足重连条件,则总是打开新会话并导航
|
||||
if (!activeAndDisconnected) {
|
||||
console.log(`[SessionStore] 不满足重连条件或点击了其他连接,将打开新会话 for ID: ${connIdStr}`);
|
||||
openNewSession(connIdStr); // openNewSession 会自动激活新会话
|
||||
// 导航到 Workspace,让 WorkspaceView 处理新激活的会话
|
||||
router.push({ name: 'Workspace' }); // +++ 添加跳转 +++
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理连接列表的中键点击(总是打开新会话)
|
||||
*/
|
||||
const handleOpenNewSession = (connectionId: number | string) => {
|
||||
console.log(`[SessionStore] handleOpenNewSession called for ID: ${connectionId}`);
|
||||
openNewSession(connectionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理所有会话(例如在应用卸载时)
|
||||
*/
|
||||
const cleanupAllSessions = () => {
|
||||
console.log('[SessionStore] 清理所有会话...');
|
||||
sessions.value.forEach((session, sessionId) => {
|
||||
closeSession(sessionId); // 调用单个会话的关闭逻辑
|
||||
});
|
||||
sessions.value.clear();
|
||||
activeSessionId.value = null;
|
||||
};
|
||||
|
||||
// --- 新增:编辑器相关 Actions ---
|
||||
|
||||
/**
|
||||
* 在指定会话中打开文件
|
||||
*/
|
||||
const openFileInSession = (sessionId: string, fileInfo: FileInfo) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[SessionStore] 尝试在不存在的会话 ${sessionId} 中打开文件`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查标签页是否已存在 (使用 filePath)
|
||||
const existingTab = session.editorTabs.value.find(tab => tab.filePath === fileInfo.fullPath);
|
||||
if (existingTab) {
|
||||
// 如果标签页已存在,则激活它
|
||||
session.activeEditorTabId.value = existingTab.id;
|
||||
console.log(`[SessionStore] 会话 ${sessionId} 中已存在文件 ${fileInfo.fullPath} 的标签页,已激活: ${existingTab.id}`);
|
||||
} else {
|
||||
// 创建新标签页
|
||||
// 创建新标签页 (使用简化后的 FileTab 接口)
|
||||
// --- 修复:初始化 rawContentBase64 ---
|
||||
const newTab: FileTab = {
|
||||
id: `${sessionId}:${fileInfo.fullPath}`, // <-- 修复:使用 sessionId:filePath 作为 ID
|
||||
sessionId: sessionId,
|
||||
filePath: fileInfo.fullPath,
|
||||
filename: fileInfo.name,
|
||||
content: '', // 将由后端填充
|
||||
originalContent: '', // 将由后端填充
|
||||
rawContentBase64: null, // 初始化为 null
|
||||
language: getLanguageFromFilename(fileInfo.name),
|
||||
selectedEncoding: 'utf-8', // 初始默认,将由后端更新
|
||||
isLoading: true,
|
||||
loadingError: null,
|
||||
isSaving: false,
|
||||
saveStatus: 'idle',
|
||||
saveError: null,
|
||||
isModified: false,
|
||||
};
|
||||
// session.editorTabs.value.push(newTab); // 移除重复的 push
|
||||
session.editorTabs.value.push(newTab);
|
||||
session.activeEditorTabId.value = newTab.id;
|
||||
console.log(`[SessionStore] 已在会话 ${sessionId} 中为文件 ${fileInfo.fullPath} 创建新标签页: ${newTab.id}`);
|
||||
|
||||
// --- 新增:异步加载文件内容 ---
|
||||
// --- 修改:异步加载文件内容 (处理后端解码后的内容) ---
|
||||
const loadContent = async () => {
|
||||
const tabIndex = session.editorTabs.value.findIndex(t => t.id === newTab.id);
|
||||
if (tabIndex === -1) return; // Tab might have been closed quickly
|
||||
|
||||
// 更新加载状态 (使用 splice 保证响应性)
|
||||
const loadingTab = { ...session.editorTabs.value[tabIndex], isLoading: true, loadingError: null };
|
||||
session.editorTabs.value.splice(tabIndex, 1, loadingTab);
|
||||
|
||||
try {
|
||||
// 获取默认的 sftpManager 实例来执行读取操作
|
||||
const sftpManager = getOrCreateSftpManager(sessionId, 'primary');
|
||||
if (!sftpManager) {
|
||||
throw new Error(t('fileManager.errors.sftpManagerNotFound'));
|
||||
}
|
||||
console.log(`[SessionStore ${sessionId}] 使用 primary sftpManager 读取文件 ${fileInfo.fullPath}`);
|
||||
|
||||
// 调用 sftpManager.readFile (不带编码参数,让后端自动检测)
|
||||
// 后端现在返回 { content: string, encodingUsed: string }
|
||||
const fileData: SftpReadFileSuccessPayload = await sftpManager.readFile(fileInfo.fullPath);
|
||||
console.log(`[SessionStore ${sessionId}] 文件 ${fileInfo.fullPath} 读取成功。后端使用编码: ${fileData.encodingUsed}`);
|
||||
|
||||
// 更新标签页状态 (使用 splice 保证响应性)
|
||||
const currentTabState = session.editorTabs.value.find(t => t.id === newTab.id); // 获取最新状态
|
||||
if (!currentTabState) return; // 可能在请求期间关闭了
|
||||
|
||||
// --- 修复:使用 decodeRawContent 解码并存储原始数据 ---
|
||||
const initialContent = decodeRawContent(fileData.rawContentBase64, fileData.encodingUsed);
|
||||
const updatedTab: FileTab = {
|
||||
...currentTabState,
|
||||
content: initialContent,
|
||||
originalContent: initialContent, // 初始原始内容
|
||||
rawContentBase64: fileData.rawContentBase64, // 存储原始 Base64 数据
|
||||
selectedEncoding: fileData.encodingUsed, // 存储后端实际使用的编码
|
||||
isLoading: false,
|
||||
isModified: false,
|
||||
loadingError: null,
|
||||
};
|
||||
const finalTabIndex = session.editorTabs.value.findIndex(t => t.id === newTab.id); // 重新获取索引
|
||||
if (finalTabIndex !== -1) {
|
||||
session.editorTabs.value.splice(finalTabIndex, 1, updatedTab);
|
||||
console.log(`[SessionStore ${sessionId}] 文件 ${fileInfo.fullPath} 内容已加载并设置到标签页 ${newTab.id}。`);
|
||||
} else {
|
||||
console.warn(`[SessionStore ${sessionId}] 尝试更新标签页 ${newTab.id} 时未找到索引。`);
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(`[SessionStore ${sessionId}] 读取文件 ${fileInfo.fullPath} 失败:`, err);
|
||||
// 更新错误状态 (使用 splice 保证响应性)
|
||||
const errorTabIndex = session.editorTabs.value.findIndex(t => t.id === newTab.id);
|
||||
if (errorTabIndex !== -1) {
|
||||
const errorTab = {
|
||||
...session.editorTabs.value[errorTabIndex],
|
||||
isLoading: false,
|
||||
loadingError: `${t('fileManager.errors.readFileFailed')}: ${err.message || err}`,
|
||||
content: `// 加载错误: ${err.message || err}`
|
||||
};
|
||||
session.editorTabs.value.splice(errorTabIndex, 1, errorTab);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadContent(); // 启动内容加载
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭指定会话中的编辑器标签页
|
||||
*/
|
||||
const closeEditorTabInSession = (sessionId: string, tabId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[SessionStore] 尝试在不存在的会话 ${sessionId} 中关闭标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = session.editorTabs.value.findIndex(tab => tab.id === tabId);
|
||||
if (tabIndex === -1) {
|
||||
console.warn(`[SessionStore] 尝试关闭会话 ${sessionId} 中不存在的标签页 ID: ${tabId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 检查 isDirty 状态,提示保存?
|
||||
|
||||
session.editorTabs.value.splice(tabIndex, 1);
|
||||
console.log(`[SessionStore] 已从会话 ${sessionId} 中移除标签页: ${tabId}`);
|
||||
|
||||
// 如果关闭的是当前活动标签页,则切换到前一个或 null
|
||||
if (session.activeEditorTabId.value === tabId) {
|
||||
const remainingTabs = session.editorTabs.value;
|
||||
const nextActiveTabId = remainingTabs.length > 0
|
||||
? remainingTabs[Math.max(0, tabIndex - 1)].id // 尝试激活前一个,或第一个
|
||||
: null;
|
||||
session.activeEditorTabId.value = nextActiveTabId;
|
||||
console.log(`[SessionStore] 会话 ${sessionId} 关闭活动标签页后,切换到: ${nextActiveTabId}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 激活指定会话中的编辑器标签页
|
||||
*/
|
||||
const setActiveEditorTabInSession = (sessionId: string, tabId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[SessionStore] 尝试在不存在的会话 ${sessionId} 中激活标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.editorTabs.value.some(tab => tab.id === tabId)) {
|
||||
if (session.activeEditorTabId.value !== tabId) {
|
||||
session.activeEditorTabId.value = tabId;
|
||||
console.log(`[SessionStore] 已在会话 ${sessionId} 中激活标签页: ${tabId}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[SessionStore] 尝试激活会话 ${sessionId} 中不存在的标签页 ID: ${tabId}`);
|
||||
}
|
||||
};
|
||||
|
||||
// +++ 新增:关闭指定会话中的其他编辑器标签页 +++
|
||||
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));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 在指定会话中更改文件编码并重新解码
|
||||
*/
|
||||
// --- 修改:更改文件编码(通过请求后端重新读取) ---
|
||||
const changeEncodingInSession = (sessionId: string, tabId: string, newEncoding: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.warn(`[SessionStore] 尝试更改不存在的会话 ${sessionId} 中标签页 ${tabId} 的编码。`);
|
||||
return;
|
||||
}
|
||||
const tabIndex = session.editorTabs.value.findIndex(t => t.id === tabId);
|
||||
if (tabIndex === -1) {
|
||||
console.warn(`[SessionStore] 尝试更改会话 ${sessionId} 中不存在的标签页 ${tabId} 的编码。`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = session.editorTabs.value[tabIndex];
|
||||
|
||||
if (!tab.rawContentBase64) {
|
||||
console.error(`[SessionStore] 无法更改编码:会话 ${sessionId} 标签页 ${tabId} 没有原始文件数据。`);
|
||||
// 更新错误状态
|
||||
const errorTab = { ...tab, isLoading: false, loadingError: '缺少原始文件数据,无法更改编码' };
|
||||
session.editorTabs.value.splice(tabIndex, 1, errorTab);
|
||||
return;
|
||||
}
|
||||
if (tab.selectedEncoding === newEncoding) {
|
||||
console.log(`[SessionStore] 会话 ${sessionId} 标签页 ${tabId} 编码已经是 ${newEncoding},无需更改。`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[SessionStore] 使用新编码 "${newEncoding}" 在前端重新解码文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tabId})`);
|
||||
|
||||
try {
|
||||
// 使用新编码解码存储的原始数据
|
||||
const newContent = decodeRawContent(tab.rawContentBase64, newEncoding);
|
||||
|
||||
// 更新标签页状态 (使用 splice 保证响应性)
|
||||
const updatedTab: FileTab = {
|
||||
...tab,
|
||||
content: newContent,
|
||||
selectedEncoding: newEncoding, // 更新选择的编码
|
||||
isLoading: false, // 解码完成 (移除加载状态)
|
||||
loadingError: null,
|
||||
// isModified 状态保持不变
|
||||
};
|
||||
session.editorTabs.value.splice(tabIndex, 1, updatedTab);
|
||||
console.log(`[SessionStore] 文件 ${tab.filePath} (会话 ${sessionId}) 使用新编码 "${newEncoding}" 解码完成。`);
|
||||
|
||||
} catch (err: any) { // catch 应该在 decodeRawContent 内部处理了,但以防万一
|
||||
console.error(`[SessionStore] 使用编码 "${newEncoding}" 在前端解码文件 ${tab.filePath} (会话 ${sessionId}) 失败:`, err);
|
||||
const errorMsg = `前端解码失败 (编码: ${newEncoding}): ${err.message || err}`;
|
||||
// 更新错误状态 (使用 splice)
|
||||
const errorTab: FileTab = {
|
||||
...tab,
|
||||
isLoading: false,
|
||||
loadingError: errorMsg,
|
||||
};
|
||||
session.editorTabs.value.splice(tabIndex, 1, errorTab);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 获取或创建指定会话和实例 ID 的 SFTP 管理器。
|
||||
* @param sessionId 会话 ID
|
||||
* @param instanceId 文件管理器实例 ID (e.g., 'sidebar', 'panel-xyz')
|
||||
* @returns SftpManagerInstance 或 null (如果会话不存在)
|
||||
*/
|
||||
const getOrCreateSftpManager = (sessionId: string, instanceId: string): SftpManagerInstance | null => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[SessionStore] 尝试为不存在的会话 ${sessionId} 获取 SFTP 管理器`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let manager = session.sftpManagers.get(instanceId);
|
||||
if (!manager) {
|
||||
console.log(`[SessionStore] 为会话 ${sessionId} 创建新的 SFTP 管理器实例: ${instanceId}`);
|
||||
const currentSftpPath = ref<string>('.'); // 每个实例有自己的路径
|
||||
const wsDeps: WebSocketDependencies = {
|
||||
sendMessage: session.wsManager.sendMessage,
|
||||
onMessage: session.wsManager.onMessage,
|
||||
isConnected: session.wsManager.isConnected,
|
||||
isSftpReady: session.wsManager.isSftpReady,
|
||||
};
|
||||
manager = createSftpActionsManager(sessionId, currentSftpPath, wsDeps, t);
|
||||
session.sftpManagers.set(instanceId, manager);
|
||||
}
|
||||
return manager;
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除并清理指定会话和实例 ID 的 SFTP 管理器。
|
||||
* @param sessionId 会话 ID
|
||||
* @param instanceId 文件管理器实例 ID
|
||||
*/
|
||||
const removeSftpManager = (sessionId: string, instanceId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (session) {
|
||||
const manager = session.sftpManagers.get(instanceId);
|
||||
if (manager) {
|
||||
manager.cleanup();
|
||||
session.sftpManagers.delete(instanceId);
|
||||
console.log(`[SessionStore] 已移除并清理会话 ${sessionId} 的 SFTP 管理器实例: ${instanceId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- 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; // 清除连接信息
|
||||
};
|
||||
|
||||
// --- VNC Modal Actions ---
|
||||
const openVncModal = (connection: ConnectionInfo) => {
|
||||
// console.log(`[SessionStore] Opening VNC modal for connection: ${connection.name} (ID: ${connection.id})`); // 保留原始日志或移除
|
||||
vncConnectionInfo.value = connection;
|
||||
isVncModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeVncModal = () => {
|
||||
// console.log('[SessionStore] Closing VNC modal.'); // 保留原始日志或移除
|
||||
isVncModalOpen.value = false;
|
||||
vncConnectionInfo.value = null; // 清除连接信息
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新指定会话的命令输入框内容
|
||||
*/
|
||||
const updateSessionCommandInput = (sessionId: string, content: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (session) {
|
||||
session.commandInputContent.value = content;
|
||||
} else {
|
||||
console.warn(`[SessionStore] 尝试更新不存在的会话 ${sessionId} 的命令输入内容`);
|
||||
}
|
||||
};
|
||||
// Command Input Actions
|
||||
const updateSessionCommandInput = (sessionId: string, content: string) =>
|
||||
commandInputActions.updateSessionCommandInput(sessionId, content);
|
||||
|
||||
|
||||
return {
|
||||
// State
|
||||
// State (直接从 state 模块导出,Pinia 会处理)
|
||||
sessions,
|
||||
activeSessionId,
|
||||
isRdpModalOpen, // 导出 RDP 模态框状态
|
||||
rdpConnectionInfo, // 导出 RDP 连接信息
|
||||
isVncModalOpen, // 导出 VNC 模态框状态
|
||||
vncConnectionInfo, // 导出 VNC 连接信息
|
||||
// Getters
|
||||
isRdpModalOpen,
|
||||
rdpConnectionInfo,
|
||||
isVncModalOpen,
|
||||
vncConnectionInfo,
|
||||
|
||||
// Getters (直接从 getters 模块导出)
|
||||
sessionTabs,
|
||||
sessionTabsWithStatus, // 导出新的 getter
|
||||
sessionTabsWithStatus,
|
||||
activeSession,
|
||||
// Actions
|
||||
|
||||
// Wrapped Actions
|
||||
openNewSession,
|
||||
activateSession,
|
||||
closeSession,
|
||||
handleConnectRequest,
|
||||
handleOpenNewSession,
|
||||
cleanupAllSessions,
|
||||
getOrCreateSftpManager, // 导出新的 Action
|
||||
removeSftpManager, // 导出新的 Action
|
||||
// --- 新增:导出编辑器相关 Actions ---
|
||||
getOrCreateSftpManager,
|
||||
removeSftpManager,
|
||||
openFileInSession,
|
||||
closeEditorTabInSession,
|
||||
setActiveEditorTabInSession,
|
||||
updateFileContentInSession, // 导出更新内容 Action
|
||||
saveFileInSession, // 导出保存文件 Action
|
||||
changeEncodingInSession, // 导出更改编码 Action
|
||||
closeOtherTabsInSession, // +++ 导出新 action +++
|
||||
closeTabsToTheRightInSession, // +++ 导出新 action +++
|
||||
closeTabsToTheLeftInSession, // +++ 导出新 action +++
|
||||
// --- RDP Modal Actions ---
|
||||
openRdpModal, // 导出打开 RDP 模态框 Action
|
||||
closeRdpModal, // 导出关闭 RDP 模态框 Action
|
||||
openVncModal, // 导出打开 VNC 模态框 Action
|
||||
closeVncModal, // 导出关闭 VNC 模态框 Action
|
||||
// --- 命令输入框 Action ---
|
||||
updateSessionCommandInput, // 导出更新命令输入 Action
|
||||
updateFileContentInSession,
|
||||
saveFileInSession,
|
||||
changeEncodingInSession,
|
||||
closeOtherTabsInSession,
|
||||
closeTabsToTheRightInSession,
|
||||
closeTabsToTheLeftInSession,
|
||||
openRdpModal,
|
||||
closeRdpModal,
|
||||
openVncModal,
|
||||
closeVncModal,
|
||||
updateSessionCommandInput,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// packages/frontend/src/stores/session/actions/commandInputActions.ts
|
||||
|
||||
import { sessions } from '../state';
|
||||
|
||||
/**
|
||||
* 更新指定会话的命令输入框内容
|
||||
*/
|
||||
export const updateSessionCommandInput = (sessionId: string, content: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (session) {
|
||||
session.commandInputContent.value = content;
|
||||
} else {
|
||||
console.warn(`[CommandInputActions] 尝试更新不存在的会话 ${sessionId} 的命令输入内容`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,310 @@
|
||||
// packages/frontend/src/stores/session/actions/editorActions.ts
|
||||
|
||||
import { sessions } from '../state';
|
||||
import { getLanguageFromFilename, decodeRawContent } from '../utils';
|
||||
import type { FileTab, SftpManagerInstance } from '../types';
|
||||
import type { FileInfo } from '../../fileEditor.store'; // 路径: packages/frontend/src/stores/fileEditor.store.ts
|
||||
import type { SftpReadFileSuccessPayload } from '../../../types/sftp.types'; // 路径: packages/frontend/src/types/sftp.types.ts
|
||||
import type { useI18n } from 'vue-i18n';
|
||||
|
||||
// --- Editor Actions ---
|
||||
export const openFileInSession = (
|
||||
sessionId: string,
|
||||
fileInfo: FileInfo,
|
||||
dependencies: {
|
||||
getOrCreateSftpManager: (sessionId: string, instanceId: string) => SftpManagerInstance | null;
|
||||
t: ReturnType<typeof useI18n>['t'];
|
||||
}
|
||||
) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[EditorActions] 尝试在不存在的会话 ${sessionId} 中打开文件`);
|
||||
return;
|
||||
}
|
||||
const { getOrCreateSftpManager, t } = dependencies;
|
||||
|
||||
const existingTab = session.editorTabs.value.find(tab => tab.filePath === fileInfo.fullPath);
|
||||
if (existingTab) {
|
||||
session.activeEditorTabId.value = existingTab.id;
|
||||
console.log(`[EditorActions] 会话 ${sessionId} 中已存在文件 ${fileInfo.fullPath} 的标签页,已激活: ${existingTab.id}`);
|
||||
} else {
|
||||
const newTabId = `${sessionId}:${fileInfo.fullPath}`; // 保证唯一性
|
||||
const newTab: FileTab = {
|
||||
id: newTabId,
|
||||
sessionId: sessionId,
|
||||
filePath: fileInfo.fullPath,
|
||||
filename: fileInfo.name,
|
||||
content: '',
|
||||
originalContent: '',
|
||||
rawContentBase64: null,
|
||||
language: getLanguageFromFilename(fileInfo.name),
|
||||
selectedEncoding: 'utf-8',
|
||||
isLoading: true,
|
||||
loadingError: null,
|
||||
isSaving: false,
|
||||
saveStatus: 'idle',
|
||||
saveError: null,
|
||||
isModified: false,
|
||||
};
|
||||
session.editorTabs.value.push(newTab);
|
||||
session.activeEditorTabId.value = newTab.id;
|
||||
console.log(`[EditorActions] 已在会话 ${sessionId} 中为文件 ${fileInfo.fullPath} 创建新标签页: ${newTab.id}`);
|
||||
|
||||
const loadContent = async () => {
|
||||
const tabRef = session.editorTabs.value.find(t => t.id === newTab.id);
|
||||
if (!tabRef) return;
|
||||
|
||||
// 使用 tabRef 更新,确保操作的是响应式对象内的属性
|
||||
tabRef.isLoading = true;
|
||||
tabRef.loadingError = null;
|
||||
|
||||
try {
|
||||
const sftpManager = getOrCreateSftpManager(sessionId, 'primary-editor'); // 使用特定实例 ID
|
||||
if (!sftpManager) {
|
||||
throw new Error(t('fileManager.errors.sftpManagerNotFound'));
|
||||
}
|
||||
console.log(`[EditorActions ${sessionId}] 使用 primary-editor sftpManager 读取文件 ${fileInfo.fullPath}`);
|
||||
|
||||
const fileData: SftpReadFileSuccessPayload = await sftpManager.readFile(fileInfo.fullPath);
|
||||
console.log(`[EditorActions ${sessionId}] 文件 ${fileInfo.fullPath} 读取成功。后端使用编码: ${fileData.encodingUsed}`);
|
||||
|
||||
// 再次查找 tab,因为它可能在异步操作期间被关闭
|
||||
const currentTabState = session.editorTabs.value.find(t => t.id === newTab.id);
|
||||
if (!currentTabState) return;
|
||||
|
||||
const initialContent = decodeRawContent(fileData.rawContentBase64, fileData.encodingUsed);
|
||||
currentTabState.content = initialContent;
|
||||
currentTabState.originalContent = initialContent;
|
||||
currentTabState.rawContentBase64 = fileData.rawContentBase64;
|
||||
currentTabState.selectedEncoding = fileData.encodingUsed;
|
||||
currentTabState.isLoading = false;
|
||||
currentTabState.isModified = false;
|
||||
currentTabState.loadingError = null;
|
||||
console.log(`[EditorActions ${sessionId}] 文件 ${fileInfo.fullPath} 内容已加载并设置到标签页 ${newTab.id}。`);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(`[EditorActions ${sessionId}] 读取文件 ${fileInfo.fullPath} 失败:`, err);
|
||||
const errorTabRef = session.editorTabs.value.find(t => t.id === newTab.id);
|
||||
if (errorTabRef) {
|
||||
errorTabRef.isLoading = false;
|
||||
errorTabRef.loadingError = `${t('fileManager.errors.readFileFailed')}: ${err.message || err}`;
|
||||
errorTabRef.content = `// 加载错误: ${err.message || err}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
loadContent();
|
||||
}
|
||||
};
|
||||
|
||||
export const closeEditorTabInSession = (sessionId: string, tabId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[EditorActions] 尝试在不存在的会话 ${sessionId} 中关闭标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = session.editorTabs.value.findIndex(tab => tab.id === tabId);
|
||||
if (tabIndex === -1) {
|
||||
console.warn(`[EditorActions] 尝试关闭会话 ${sessionId} 中不存在的标签页 ID: ${tabId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 检查 isDirty 状态,提示保存?
|
||||
|
||||
session.editorTabs.value.splice(tabIndex, 1);
|
||||
console.log(`[EditorActions] 已从会话 ${sessionId} 中移除标签页: ${tabId}`);
|
||||
|
||||
if (session.activeEditorTabId.value === tabId) {
|
||||
const remainingTabs = session.editorTabs.value;
|
||||
const nextActiveTabId = remainingTabs.length > 0
|
||||
? remainingTabs[Math.max(0, tabIndex > 0 ? tabIndex -1 : 0 )].id // 激活前一个或第一个
|
||||
: null;
|
||||
session.activeEditorTabId.value = nextActiveTabId;
|
||||
console.log(`[EditorActions] 会话 ${sessionId} 关闭活动标签页后,切换到: ${nextActiveTabId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const setActiveEditorTabInSession = (sessionId: string, tabId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[EditorActions] 尝试在不存在的会话 ${sessionId} 中激活标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.editorTabs.value.some(tab => tab.id === tabId)) {
|
||||
if (session.activeEditorTabId.value !== tabId) {
|
||||
session.activeEditorTabId.value = tabId;
|
||||
console.log(`[EditorActions] 已在会话 ${sessionId} 中激活标签页: ${tabId}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[EditorActions] 尝试激活会话 ${sessionId} 中不存在的标签页 ID: ${tabId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFileContentInSession = (sessionId: string, tabId: string, newContent: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[EditorActions] 尝试在不存在的会话 ${sessionId} 中更新标签页 ${tabId} 内容`);
|
||||
return;
|
||||
}
|
||||
const tab = session.editorTabs.value.find(t => t.id === tabId);
|
||||
if (tab && !tab.isLoading) {
|
||||
tab.content = newContent;
|
||||
tab.isModified = tab.content !== tab.originalContent;
|
||||
if (tab.saveStatus === 'success' || tab.saveStatus === 'error') {
|
||||
tab.saveStatus = 'idle';
|
||||
tab.saveError = null;
|
||||
}
|
||||
} else if (tab) {
|
||||
console.warn(`[EditorActions] 尝试更新正在加载的标签页 ${tabId} 的内容`);
|
||||
} else {
|
||||
console.warn(`[EditorActions] 尝试更新会话 ${sessionId} 中不存在的标签页 ${tabId} 的内容`);
|
||||
}
|
||||
};
|
||||
|
||||
export const saveFileInSession = async (
|
||||
sessionId: string,
|
||||
tabId: string,
|
||||
dependencies: {
|
||||
getOrCreateSftpManager: (sessionId: string, instanceId: string) => SftpManagerInstance | null;
|
||||
t: ReturnType<typeof useI18n>['t'];
|
||||
}
|
||||
) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[EditorActions] 尝试在不存在的会话 ${sessionId} 中保存标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
const tab = session.editorTabs.value.find(t => t.id === tabId);
|
||||
if (!tab) {
|
||||
console.warn(`[EditorActions] 尝试保存在会话 ${sessionId} 中不存在的标签页 ${tabId}`);
|
||||
return;
|
||||
}
|
||||
const { getOrCreateSftpManager, t } = dependencies;
|
||||
|
||||
if (tab.isSaving || tab.isLoading || tab.loadingError || !tab.isModified) {
|
||||
console.warn(`[EditorActions] 保存条件不满足 for ${tab.filePath} (会话 ${sessionId}),无法保存。`, { tab });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.wsManager.isConnected.value || !session.wsManager.isSftpReady.value) {
|
||||
console.error(`[EditorActions] 保存失败:会话 ${sessionId} 无效或未连接/SFTP 未就绪。`);
|
||||
tab.saveStatus = 'error';
|
||||
tab.saveError = t('fileManager.errors.sessionInvalidOrNotReady');
|
||||
setTimeout(() => { if (tab.saveStatus === 'error') { tab.saveStatus = 'idle'; tab.saveError = null; } }, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
const sftpManager = getOrCreateSftpManager(sessionId, 'primary-editor');
|
||||
if (!sftpManager) {
|
||||
console.error(`[EditorActions] 保存失败:无法获取会话 ${sessionId} 的 primary-editor sftpManager。`);
|
||||
tab.saveStatus = 'error';
|
||||
tab.saveError = t('fileManager.errors.sftpManagerNotFound');
|
||||
setTimeout(() => { if (tab.saveStatus === 'error') { tab.saveStatus = 'idle'; tab.saveError = null; } }, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[EditorActions] 开始保存文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tab.id}) using primary-editor sftpManager`);
|
||||
tab.isSaving = true;
|
||||
tab.saveStatus = 'saving';
|
||||
tab.saveError = null;
|
||||
|
||||
const contentToSave = tab.content;
|
||||
const encodingToUse = tab.selectedEncoding;
|
||||
|
||||
try {
|
||||
await sftpManager.writeFile(tab.filePath, contentToSave, encodingToUse);
|
||||
console.log(`[EditorActions] 文件 ${tab.filePath} (会话 ${sessionId}) 使用编码 ${encodingToUse} 保存成功。`);
|
||||
tab.isSaving = false;
|
||||
tab.saveStatus = 'success';
|
||||
tab.saveError = null;
|
||||
tab.originalContent = contentToSave;
|
||||
tab.isModified = false;
|
||||
setTimeout(() => { if (tab.saveStatus === 'success') { tab.saveStatus = 'idle'; } }, 2000);
|
||||
} catch (err: any) {
|
||||
console.error(`[EditorActions] 保存文件 ${tab.filePath} (会话 ${sessionId}) 失败:`, err);
|
||||
tab.isSaving = false;
|
||||
tab.saveStatus = 'error';
|
||||
tab.saveError = `${t('fileManager.errors.saveFailed')}: ${err.message || err}`;
|
||||
setTimeout(() => { if (tab.saveStatus === 'error') { tab.saveStatus = 'idle'; tab.saveError = null; } }, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
export const changeEncodingInSession = (sessionId: string, tabId: string, newEncoding: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.warn(`[EditorActions] 尝试更改不存在的会话 ${sessionId} 中标签页 ${tabId} 的编码。`);
|
||||
return;
|
||||
}
|
||||
const tab = session.editorTabs.value.find(t => t.id === tabId);
|
||||
if (!tab) {
|
||||
console.warn(`[EditorActions] 尝试更改会话 ${sessionId} 中不存在的标签页 ${tabId} 的编码。`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tab.rawContentBase64) {
|
||||
console.error(`[EditorActions] 无法更改编码:会话 ${sessionId} 标签页 ${tabId} 没有原始文件数据。`);
|
||||
tab.isLoading = false; // 应该已经是 false,但确保
|
||||
tab.loadingError = '缺少原始文件数据,无法更改编码';
|
||||
return;
|
||||
}
|
||||
if (tab.selectedEncoding === newEncoding) {
|
||||
console.log(`[EditorActions] 会话 ${sessionId} 标签页 ${tabId} 编码已经是 ${newEncoding},无需更改。`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[EditorActions] 使用新编码 "${newEncoding}" 在前端重新解码文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tabId})`);
|
||||
|
||||
try {
|
||||
const newContent = decodeRawContent(tab.rawContentBase64, newEncoding);
|
||||
tab.content = newContent;
|
||||
tab.selectedEncoding = newEncoding;
|
||||
// tab.isModified 状态取决于新内容是否与 originalContent 不同,或者用户可能希望将更改编码视为“修改”
|
||||
// 这里我们假设仅更改编码预览不直接标记为 isModified,除非内容实际变化
|
||||
// 如果 newContent === tab.originalContent,isModified 可以保持不变或设为 false
|
||||
// 如果 newContent !== tab.originalContent,isModified 应该为 true
|
||||
// 为了简单起见,这里不改变 isModified,由后续的 content 比较来决定
|
||||
tab.loadingError = null; // 清除可能存在的旧错误
|
||||
console.log(`[EditorActions] 文件 ${tab.filePath} (会话 ${sessionId}) 使用新编码 "${newEncoding}" 解码完成。`);
|
||||
} catch (err: any) {
|
||||
console.error(`[EditorActions] 使用编码 "${newEncoding}" 在前端解码文件 ${tab.filePath} (会话 ${sessionId}) 失败:`, err);
|
||||
tab.loadingError = `前端解码失败 (编码: ${newEncoding}): ${err.message || err}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const closeOtherTabsInSession = (sessionId: string, targetTabId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) return;
|
||||
const targetTab = session.editorTabs.value.find(tab => tab.id === targetTabId);
|
||||
if (!targetTab) return;
|
||||
|
||||
console.log(`[EditorActions ${sessionId}] 关闭除 ${targetTabId} 之外的所有标签页...`);
|
||||
const tabsToClose = session.editorTabs.value.filter(tab => tab.id !== targetTabId);
|
||||
// 为了避免在迭代中修改数组导致的问题,先收集 ID
|
||||
const idsToClose = tabsToClose.map(t => t.id);
|
||||
idsToClose.forEach(id => closeEditorTabInSession(sessionId, id));
|
||||
};
|
||||
|
||||
export 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(`[EditorActions ${sessionId}] 关闭 ${targetTabId} 右侧的所有标签页...`);
|
||||
const tabsToClose = session.editorTabs.value.slice(targetIndex + 1);
|
||||
const idsToClose = tabsToClose.map(t => t.id);
|
||||
idsToClose.forEach(id => closeEditorTabInSession(sessionId, id));
|
||||
};
|
||||
|
||||
export 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(`[EditorActions ${sessionId}] 关闭 ${targetTabId} 左侧的所有标签页...`);
|
||||
const tabsToClose = session.editorTabs.value.slice(0, targetIndex);
|
||||
const idsToClose = tabsToClose.map(t => t.id);
|
||||
idsToClose.forEach(id => closeEditorTabInSession(sessionId, id));
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// packages/frontend/src/stores/session/actions/modalActions.ts
|
||||
|
||||
import { isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } from '../state';
|
||||
import type { ConnectionInfo } from '../../connections.store'; // 路径: packages/frontend/src/stores/connections.store.ts
|
||||
|
||||
// --- RDP Modal Actions ---
|
||||
export const openRdpModal = (connection: ConnectionInfo) => {
|
||||
// console.log(`[ModalActions] Opening RDP modal for connection: ${connection.name} (ID: ${connection.id})`);
|
||||
rdpConnectionInfo.value = connection;
|
||||
isRdpModalOpen.value = true;
|
||||
};
|
||||
|
||||
export const closeRdpModal = () => {
|
||||
// console.log('[ModalActions] Closing RDP modal.');
|
||||
isRdpModalOpen.value = false;
|
||||
rdpConnectionInfo.value = null; // 清除连接信息
|
||||
};
|
||||
|
||||
// --- VNC Modal Actions ---
|
||||
export const openVncModal = (connection: ConnectionInfo) => {
|
||||
// console.log(`[ModalActions] Opening VNC modal for connection: ${connection.name} (ID: ${connection.id})`);
|
||||
vncConnectionInfo.value = connection;
|
||||
isVncModalOpen.value = true;
|
||||
};
|
||||
|
||||
export const closeVncModal = () => {
|
||||
// console.log('[ModalActions] Closing VNC modal.');
|
||||
isVncModalOpen.value = false;
|
||||
vncConnectionInfo.value = null; // 清除连接信息
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
// packages/frontend/src/stores/session/actions/sessionActions.ts
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useConnectionsStore, type ConnectionInfo } from '../../connections.store'; // 路径: packages/frontend/src/stores/connections.store.ts
|
||||
import { sessions, activeSessionId } from '../state';
|
||||
import { generateSessionId } from '../utils';
|
||||
import type { SessionState, SshTerminalInstance, StatusMonitorInstance, DockerManagerInstance, SftpManagerInstance } from '../types';
|
||||
|
||||
// Composables for manager creation - 路径相对于此文件
|
||||
import { createWebSocketConnectionManager } from '../../../composables/useWebSocketConnection';
|
||||
import { createSshTerminalManager, type SshTerminalDependencies } from '../../../composables/useSshTerminal';
|
||||
import { createStatusMonitorManager, type StatusMonitorDependencies } from '../../../composables/useStatusMonitor';
|
||||
import { createDockerManager, type DockerManagerDependencies } from '../../../composables/useDockerManager';
|
||||
// getOrCreateSftpManager 将在 sftpManagerActions.ts 中定义,并在主 store 中协调
|
||||
|
||||
// --- 辅助函数 (特定于此模块的 actions) ---
|
||||
const findConnectionInfo = (connectionId: number | string, connectionsStore: ReturnType<typeof useConnectionsStore>): ConnectionInfo | undefined => {
|
||||
return connectionsStore.connections.find(c => c.id === Number(connectionId));
|
||||
};
|
||||
|
||||
// --- Actions ---
|
||||
export const openNewSession = (
|
||||
connectionId: number | string,
|
||||
dependencies: {
|
||||
connectionsStore: ReturnType<typeof useConnectionsStore>;
|
||||
t: ReturnType<typeof useI18n>['t'];
|
||||
}
|
||||
) => {
|
||||
const { connectionsStore, t } = dependencies;
|
||||
console.log(`[SessionActions] 请求打开新会话: ${connectionId}`);
|
||||
const connInfo = findConnectionInfo(connectionId, connectionsStore);
|
||||
if (!connInfo) {
|
||||
console.error(`[SessionActions] 无法打开新会话:找不到 ID 为 ${connectionId} 的连接信息。`);
|
||||
// TODO: 向用户显示错误
|
||||
return;
|
||||
}
|
||||
|
||||
const newSessionId = generateSessionId();
|
||||
const dbConnId = String(connInfo.id);
|
||||
|
||||
// 1. 创建管理器实例
|
||||
const wsManager = createWebSocketConnectionManager(newSessionId, dbConnId, t);
|
||||
const sshTerminalDeps: SshTerminalDependencies = {
|
||||
sendMessage: wsManager.sendMessage,
|
||||
onMessage: wsManager.onMessage,
|
||||
isConnected: wsManager.isConnected,
|
||||
};
|
||||
const terminalManager = createSshTerminalManager(newSessionId, sshTerminalDeps, t);
|
||||
const statusMonitorDeps: StatusMonitorDependencies = {
|
||||
onMessage: wsManager.onMessage,
|
||||
isConnected: wsManager.isConnected,
|
||||
};
|
||||
const statusMonitorManager = createStatusMonitorManager(newSessionId, statusMonitorDeps);
|
||||
const dockerManagerDeps: DockerManagerDependencies = {
|
||||
sendMessage: wsManager.sendMessage,
|
||||
onMessage: wsManager.onMessage,
|
||||
isConnected: wsManager.isConnected,
|
||||
};
|
||||
const dockerManager = createDockerManager(newSessionId, dockerManagerDeps, { t });
|
||||
|
||||
// 2. 创建 SessionState 对象
|
||||
const newSession: SessionState = {
|
||||
sessionId: newSessionId,
|
||||
connectionId: dbConnId,
|
||||
connectionName: connInfo.name || connInfo.host,
|
||||
wsManager: wsManager,
|
||||
sftpManagers: new Map<string, SftpManagerInstance>(), // 初始化 Map
|
||||
terminalManager: terminalManager,
|
||||
statusMonitorManager: statusMonitorManager,
|
||||
dockerManager: dockerManager,
|
||||
editorTabs: ref([]),
|
||||
activeEditorTabId: ref(null),
|
||||
commandInputContent: ref(''),
|
||||
};
|
||||
|
||||
// 3. 添加到 Map 并激活
|
||||
const newSessionsMap = new Map(sessions.value);
|
||||
newSessionsMap.set(newSessionId, newSession);
|
||||
sessions.value = newSessionsMap;
|
||||
activeSessionId.value = newSessionId;
|
||||
console.log(`[SessionActions] 已创建新会话实例: ${newSessionId} for connection ${dbConnId}`);
|
||||
|
||||
// 4. 启动 WebSocket 连接
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHostAndPort = window.location.host;
|
||||
const wsUrl = `${protocol}//${wsHostAndPort}/ws/`;
|
||||
console.log(`[SessionActions] Generated WebSocket URL: ${wsUrl}`);
|
||||
wsManager.connect(wsUrl);
|
||||
console.log(`[SessionActions] 已为会话 ${newSessionId} 启动 WebSocket 连接。`);
|
||||
};
|
||||
|
||||
export const activateSession = (sessionId: string) => {
|
||||
if (sessions.value.has(sessionId)) {
|
||||
if (activeSessionId.value !== sessionId) {
|
||||
activeSessionId.value = sessionId;
|
||||
console.log(`[SessionActions] 已激活会话: ${sessionId}`);
|
||||
} else {
|
||||
console.log(`[SessionActions] 会话 ${sessionId} 已经是活动状态。`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[SessionActions] 尝试激活不存在的会话 ID: ${sessionId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const closeSession = (sessionId: string) => {
|
||||
console.log(`[SessionActions] 请求关闭会话 ID: ${sessionId}`);
|
||||
const sessionToClose = sessions.value.get(sessionId);
|
||||
if (!sessionToClose) {
|
||||
console.warn(`[SessionActions] 尝试关闭不存在的会话 ID: ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 调用实例上的清理和断开方法
|
||||
sessionToClose.wsManager.disconnect();
|
||||
console.log(`[SessionActions] 已为会话 ${sessionId} 调用 wsManager.disconnect()`);
|
||||
sessionToClose.sftpManagers.forEach((manager, instanceId) => {
|
||||
manager.cleanup();
|
||||
console.log(`[SessionActions] 已为会话 ${sessionId} 的 sftpManager (实例 ${instanceId}) 调用 cleanup()`);
|
||||
});
|
||||
sessionToClose.sftpManagers.clear();
|
||||
sessionToClose.terminalManager.cleanup();
|
||||
console.log(`[SessionActions] 已为会话 ${sessionId} 调用 terminalManager.cleanup()`);
|
||||
sessionToClose.statusMonitorManager.cleanup();
|
||||
console.log(`[SessionActions] 已为会话 ${sessionId} 调用 statusMonitorManager.cleanup()`);
|
||||
sessionToClose.dockerManager.cleanup();
|
||||
console.log(`[SessionActions] 已为会话 ${sessionId} 调用 dockerManager.cleanup()`);
|
||||
|
||||
// 2. 从 Map 中移除会话
|
||||
const newSessionsMap = new Map(sessions.value);
|
||||
newSessionsMap.delete(sessionId);
|
||||
sessions.value = newSessionsMap;
|
||||
console.log(`[SessionActions] 已从 Map 中移除会话: ${sessionId}`);
|
||||
|
||||
// 3. 切换活动标签页
|
||||
if (activeSessionId.value === sessionId) {
|
||||
const remainingSessions = Array.from(sessions.value.keys());
|
||||
const nextActiveId = remainingSessions.length > 0 ? remainingSessions[remainingSessions.length - 1] : null;
|
||||
activeSessionId.value = nextActiveId;
|
||||
console.log(`[SessionActions] 关闭活动会话后,切换到: ${nextActiveId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleConnectRequest = (
|
||||
connection: ConnectionInfo,
|
||||
dependencies: {
|
||||
connectionsStore: ReturnType<typeof useConnectionsStore>;
|
||||
router: ReturnType<typeof useRouter>;
|
||||
openRdpModalAction: (connection: ConnectionInfo) => void; // 来自 modalActions
|
||||
openVncModalAction: (connection: ConnectionInfo) => void; // 来自 modalActions
|
||||
t: ReturnType<typeof useI18n>['t'];
|
||||
}
|
||||
) => {
|
||||
const { connectionsStore, router, openRdpModalAction, openVncModalAction, t } = dependencies;
|
||||
|
||||
if (connection.type === 'RDP') {
|
||||
openRdpModalAction(connection);
|
||||
} else if (connection.type === 'VNC') {
|
||||
openVncModalAction(connection);
|
||||
} else {
|
||||
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(`[SessionActions] 点击的是当前活动会话 ${activeSessionId.value},状态: ${currentStatus}`);
|
||||
if (currentStatus === 'disconnected' || currentStatus === 'error') {
|
||||
activeAndDisconnected = true;
|
||||
console.log(`[SessionActions] 活动会话 ${activeSessionId.value} 已断开或出错,尝试重连...`);
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHostAndPort = window.location.host;
|
||||
const wsUrl = `${protocol}//${wsHostAndPort}/ws/`;
|
||||
console.log(`[SessionActions handleConnectRequest] Generated WebSocket URL for reconnect: ${wsUrl}`);
|
||||
currentActiveSession.wsManager.connect(wsUrl);
|
||||
activateSession(activeSessionId.value);
|
||||
router.push({ name: 'Workspace' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeAndDisconnected) {
|
||||
console.log(`[SessionActions] 不满足重连条件或点击了其他连接,将打开新会话 for ID: ${connIdStr}`);
|
||||
openNewSession(connIdStr, { connectionsStore, t });
|
||||
router.push({ name: 'Workspace' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const handleOpenNewSession = (
|
||||
connectionId: number | string,
|
||||
dependencies: {
|
||||
connectionsStore: ReturnType<typeof useConnectionsStore>;
|
||||
t: ReturnType<typeof useI18n>['t'];
|
||||
}
|
||||
) => {
|
||||
console.log(`[SessionActions] handleOpenNewSession called for ID: ${connectionId}`);
|
||||
openNewSession(connectionId, dependencies);
|
||||
};
|
||||
|
||||
export const cleanupAllSessions = () => {
|
||||
console.log('[SessionActions] 清理所有会话...');
|
||||
sessions.value.forEach((_session, sessionId) => {
|
||||
closeSession(sessionId);
|
||||
});
|
||||
// sessions.value.clear(); // closeSession 内部会逐个删除,这里不需要重复clear,但确认Map为空
|
||||
if (sessions.value.size > 0) { // 以防万一
|
||||
const newSessionsMap = new Map(sessions.value);
|
||||
newSessionsMap.clear();
|
||||
sessions.value = newSessionsMap;
|
||||
}
|
||||
activeSessionId.value = null;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
// packages/frontend/src/stores/session/actions/sftpManagerActions.ts
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { sessions } from '../state';
|
||||
import type { SftpManagerInstance } from '../types';
|
||||
import { createSftpActionsManager, type WebSocketDependencies } from '../../../composables/useSftpActions'; // 路径: packages/frontend/src/composables/useSftpActions.ts
|
||||
import type { useI18n } from 'vue-i18n';
|
||||
|
||||
export const getOrCreateSftpManager = (
|
||||
sessionId: string,
|
||||
instanceId: string,
|
||||
dependencies: {
|
||||
t: ReturnType<typeof useI18n>['t'];
|
||||
}
|
||||
): SftpManagerInstance | null => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.error(`[SftpManagerActions] 尝试为不存在的会话 ${sessionId} 获取 SFTP 管理器`);
|
||||
return null;
|
||||
}
|
||||
const { t } = dependencies;
|
||||
|
||||
let manager = session.sftpManagers.get(instanceId);
|
||||
if (!manager) {
|
||||
console.log(`[SftpManagerActions] 为会话 ${sessionId} 创建新的 SFTP 管理器实例: ${instanceId}`);
|
||||
const currentSftpPath = ref<string>('.'); // 每个实例有自己的路径
|
||||
const wsDeps: WebSocketDependencies = {
|
||||
sendMessage: session.wsManager.sendMessage,
|
||||
onMessage: session.wsManager.onMessage,
|
||||
isConnected: session.wsManager.isConnected,
|
||||
isSftpReady: session.wsManager.isSftpReady,
|
||||
};
|
||||
manager = createSftpActionsManager(sessionId, currentSftpPath, wsDeps, t);
|
||||
session.sftpManagers.set(instanceId, manager);
|
||||
}
|
||||
return manager;
|
||||
};
|
||||
|
||||
export const removeSftpManager = (sessionId: string, instanceId: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (session) {
|
||||
const manager = session.sftpManagers.get(instanceId);
|
||||
if (manager) {
|
||||
manager.cleanup();
|
||||
session.sftpManagers.delete(instanceId);
|
||||
console.log(`[SftpManagerActions] 已移除并清理会话 ${sessionId} 的 SFTP 管理器实例: ${instanceId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// packages/frontend/src/stores/session/getters.ts
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { sessions, activeSessionId } from './state';
|
||||
import type { SessionState, SessionTabInfoWithStatus } from './types';
|
||||
|
||||
export const sessionTabs = computed(() => {
|
||||
return Array.from(sessions.value.values()).map(session => ({
|
||||
sessionId: session.sessionId,
|
||||
connectionName: session.connectionName,
|
||||
}));
|
||||
});
|
||||
|
||||
// 包含状态的标签页信息
|
||||
export const sessionTabsWithStatus = computed((): SessionTabInfoWithStatus[] => {
|
||||
return Array.from(sessions.value.values()).map(session => ({
|
||||
sessionId: session.sessionId,
|
||||
connectionName: session.connectionName,
|
||||
status: session.wsManager.connectionStatus.value, // 从 wsManager 获取状态
|
||||
}));
|
||||
});
|
||||
|
||||
export const activeSession = computed((): SessionState | null => {
|
||||
if (!activeSessionId.value) return null;
|
||||
return sessions.value.get(activeSessionId.value) || null;
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// packages/frontend/src/stores/session/state.ts
|
||||
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import type { SessionState } from './types';
|
||||
// 修正导入路径
|
||||
import type { ConnectionInfo } from '../connections.store'; // 路径: packages/frontend/src/stores/connections.store.ts
|
||||
|
||||
// 使用 shallowRef 避免深度响应性问题,保留管理器实例内部的响应性
|
||||
export const sessions = shallowRef<Map<string, SessionState>>(new Map());
|
||||
export const activeSessionId = ref<string | null>(null);
|
||||
|
||||
// --- RDP Modal State ---
|
||||
export const isRdpModalOpen = ref(false);
|
||||
export const rdpConnectionInfo = ref<ConnectionInfo | null>(null);
|
||||
|
||||
// --- VNC Modal State ---
|
||||
export const isVncModalOpen = ref(false);
|
||||
export const vncConnectionInfo = ref<ConnectionInfo | null>(null);
|
||||
@@ -0,0 +1,51 @@
|
||||
// packages/frontend/src/stores/session/types.ts
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
import type { FileTab as OriginalFileTab } from '../fileEditor.store'; // 路径: packages/frontend/src/stores/fileEditor.store.ts
|
||||
import type { WsConnectionStatus } from '../../composables/useWebSocketConnection'; // 路径: packages/frontend/src/composables/useWebSocketConnection.ts
|
||||
|
||||
// 从源模块导入 DockerManagerInstance 类型并使用别名
|
||||
import type { DockerManagerInstance as OriginalDockerManagerInstance } from '../../composables/useDockerManager'; // 路径: packages/frontend/src/composables/useDockerManager.ts
|
||||
|
||||
// 导入工厂函数仅用于通过 ReturnType 推导实例类型
|
||||
// 这些导入仅用于类型推断,不在运行时使用
|
||||
import type { createWebSocketConnectionManager } from '../../composables/useWebSocketConnection';
|
||||
import type { createSftpActionsManager } from '../../composables/useSftpActions';
|
||||
import type { createSshTerminalManager } from '../../composables/useSshTerminal';
|
||||
import type { createStatusMonitorManager } from '../../composables/useStatusMonitor';
|
||||
|
||||
|
||||
// 使用 ReturnType 定义其他管理器实例类型
|
||||
export type WsManagerInstance = ReturnType<typeof createWebSocketConnectionManager>;
|
||||
export type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
|
||||
export type SshTerminalInstance = ReturnType<typeof createSshTerminalManager>;
|
||||
export type StatusMonitorInstance = ReturnType<typeof createStatusMonitorManager>;
|
||||
|
||||
// 为 DockerManagerInstance 创建一个本地类型别名,并导出它
|
||||
export type DockerManagerInstance = OriginalDockerManagerInstance;
|
||||
|
||||
// 重新导出 FileTab 类型,使其可用于其他模块
|
||||
export type FileTab = OriginalFileTab;
|
||||
|
||||
export interface SessionState {
|
||||
sessionId: string;
|
||||
connectionId: string; // 数据库中的连接 ID
|
||||
connectionName: string; // 用于显示
|
||||
wsManager: WsManagerInstance;
|
||||
sftpManagers: Map<string, SftpManagerInstance>; // 使用 Map 管理多个实例
|
||||
terminalManager: SshTerminalInstance;
|
||||
statusMonitorManager: StatusMonitorInstance;
|
||||
dockerManager: DockerManagerInstance; // 现在应该可以找到 DockerManagerInstance
|
||||
// --- 新增:独立编辑器状态 ---
|
||||
editorTabs: Ref<FileTab[]>; // 编辑器标签页列表
|
||||
activeEditorTabId: Ref<string | null>; // 当前活动的编辑器标签页 ID
|
||||
// --- 新增:命令输入框内容 ---
|
||||
commandInputContent: Ref<string>; // 当前会话的命令输入框内容
|
||||
}
|
||||
|
||||
// 为标签栏定义包含状态的类型
|
||||
export interface SessionTabInfoWithStatus {
|
||||
sessionId: string;
|
||||
connectionName: string;
|
||||
status: WsConnectionStatus; // 添加状态字段
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// packages/frontend/src/stores/session/utils.ts
|
||||
|
||||
import * as iconv from '@vscode/iconv-lite-umd';
|
||||
import { Buffer } from 'buffer/';
|
||||
|
||||
/**
|
||||
* 生成唯一的会话 ID。
|
||||
* @returns {string} 新的会话 ID。
|
||||
*/
|
||||
export function generateSessionId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名获取 Monaco Editor 对应的语言标识符。
|
||||
* @param {string} filename - 文件名。
|
||||
* @returns {string} 语言标识符,默认为 'plaintext'。
|
||||
*/
|
||||
export const getLanguageFromFilename = (filename: string): string => {
|
||||
const extension = filename.split('.').pop()?.toLowerCase();
|
||||
switch (extension) {
|
||||
case 'js': return 'javascript';
|
||||
case 'ts': return 'typescript';
|
||||
case 'json': return 'json';
|
||||
case 'html': return 'html';
|
||||
case 'css': return 'css';
|
||||
case 'scss': return 'scss';
|
||||
case 'less': return 'less';
|
||||
case 'py': return 'python';
|
||||
case 'java': return 'java';
|
||||
case 'c': return 'c';
|
||||
case 'cpp': return 'cpp';
|
||||
case 'cs': return 'csharp';
|
||||
case 'go': return 'go';
|
||||
case 'php': return 'php';
|
||||
case 'rb': return 'ruby';
|
||||
case 'rs': return 'rust';
|
||||
case 'sql': return 'sql';
|
||||
case 'sh': return 'shell';
|
||||
case 'yaml': case 'yml': return 'yaml';
|
||||
case 'md': return 'markdown';
|
||||
case 'xml': return 'xml';
|
||||
case 'ini': return 'ini';
|
||||
case 'bat': return 'bat';
|
||||
case 'dockerfile': return 'dockerfile';
|
||||
default: return 'plaintext';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用指定的编码解码 Base64 编码的原始文件内容。
|
||||
* @param {string} rawContentBase64 - Base64 编码的原始文件内容。
|
||||
* @param {string} encoding - 用于解码的字符编码。
|
||||
* @returns {string} 解码后的文件内容。如果解码失败,则返回错误信息。
|
||||
*/
|
||||
export const decodeRawContent = (rawContentBase64: string, encoding: string): string => {
|
||||
try {
|
||||
const buffer = Buffer.from(rawContentBase64, 'base64');
|
||||
const normalizedEncoding = encoding.toLowerCase().replace(/[^a-z0-9]/g, ''); // 标准化编码名称
|
||||
|
||||
// 优先使用 TextDecoder 处理标准编码
|
||||
if (['utf8', 'utf16le', 'utf16be'].includes(normalizedEncoding)) {
|
||||
const decoder = new TextDecoder(encoding); // TextDecoder 使用原始编码名称
|
||||
return decoder.decode(buffer);
|
||||
}
|
||||
// 使用 iconv-lite 处理其他编码
|
||||
else if (iconv.encodingExists(normalizedEncoding)) {
|
||||
return iconv.decode(buffer, normalizedEncoding);
|
||||
}
|
||||
// 如果 iconv-lite 也不支持,回退到 UTF-8 并警告
|
||||
else {
|
||||
console.warn(`[SessionUtils decodeRawContent] Unsupported encoding "${encoding}" requested. Falling back to UTF-8.`);
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
return decoder.decode(buffer);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[SessionUtils decodeRawContent] Error decoding content with encoding "${encoding}":`, error);
|
||||
return `// Error decoding content: ${error.message}`; // 返回错误信息
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user