diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index 62f44ec..b3e1e1f 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -61,48 +61,41 @@ const { t } = useI18n(); const route = useRoute(); // Keep for download URL generation for now const sessionStore = useSessionStore(); // 实例化 Session Store -// --- 获取此实例的 SFTP 管理器 --- -const sftpManagerInstance = sessionStore.getOrCreateSftpManager(props.sessionId, props.instanceId); +// --- 获取并存储 SFTP 管理器实例 --- +// 使用 shallowRef 存储管理器实例,以便在 sessionId 变化时切换 +const currentSftpManager = shallowRef(null); -// --- 错误处理:如果无法获取管理器 --- -if (!sftpManagerInstance) { - // 抛出错误或显示错误消息,阻止组件进一步渲染 - // 这里我们简单地抛出一个错误 - throw new Error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to get or create SFTP manager instance.`); - // 或者可以设置一个错误状态 ref,并在模板中显示错误信息 - // const managerError = ref(`Failed to get SFTP manager for instance ${props.instanceId}`); -} +const initializeSftpManager = (sessionId: string, instanceId: string) => { + const manager = sessionStore.getOrCreateSftpManager(sessionId, instanceId); + if (!manager) { + // 抛出错误或显示错误消息,阻止组件进一步渲染 + console.error(`[FileManager ${sessionId}-${instanceId}] Failed to get or create SFTP manager instance.`); + // 可以设置一个错误状态 ref 在模板中显示 + // managerError.value = `Failed to get SFTP manager for instance ${instanceId}`; + currentSftpManager.value = null; // 确保设置为 null + // 抛出错误会阻止组件渲染,可能不是最佳用户体验 + // throw new Error(`[FileManager ${sessionId}-${instanceId}] Failed to get or create SFTP manager instance.`); + } else { + currentSftpManager.value = manager; + console.log(`[FileManager ${sessionId}-${instanceId}] SFTP Manager initialized/retrieved.`); + } +}; -// --- 从获取到的管理器实例中解构状态和方法 --- -// 使用 shallowRef 包装 manager 实例本身可能不是最佳选择,因为 manager 不是响应式对象 -// 直接解构其内部的 ref 和函数 -const { - fileList, - isLoading, - // error, // Removed, handled by UI notification store - loadDirectory, - createDirectory, - createFile, - deleteItems, - renameItem, - changePermissions, - readFile, // Provided by the manager instance - writeFile, // Provided by the manager instance - joinPath, - currentPath, // 从 manager instance 获取 currentPath - // cleanup: cleanupSftpHandlers, // cleanup 由 store 在 onBeforeUnmount 中处理 -} = sftpManagerInstance; // 从获取的实例解构 +// 初始加载管理器 +initializeSftpManager(props.sessionId, props.instanceId); -// 文件上传模块 - Needs WebSocket dependencies and session context + +// --- 文件上传模块 --- +// 修改:依赖 currentSftpManager 的状态 const { uploads, startFileUpload, cancelUpload, - // cleanup: cleanupUploader, // 假设 uploader 也提供 cleanup } = useFileUploader( - currentPath, // 使用从 manager instance 解构的 currentPath - fileList, // 使用从 manager instance 解构的 fileList - props.wsDeps // 仍然传递注入的 WebSocket 依赖项 + // 传递 manager 的 currentPath 和 fileList ref + computed(() => currentSftpManager.value?.currentPath.value ?? '/'), // 提供默认值 + computed(() => currentSftpManager.value?.fileList.value ?? []), // 提供默认值 + props.wsDeps ); // 实例化其他 Stores @@ -171,11 +164,11 @@ const formatMode = (mode: number): string => { return str; }; -// --- 排序与过滤逻辑 (保持在此处,Selection 和 ContextMenu 依赖它) --- +// --- 排序与过滤逻辑 --- +// 修改:依赖 currentSftpManager.value.fileList const sortedFileList = computed(() => { - // Ensure fileList.value is used (it's reactive from the manager) - if (!fileList.value) return []; - const list = [...fileList.value]; + if (!currentSftpManager.value?.fileList.value) return []; // 检查 manager 和 fileList 是否存在 + const list = [...currentSftpManager.value.fileList.value]; // 从 manager 获取列表 const key = sortKey.value; const direction = sortDirection.value === 'asc' ? 1 : -1; @@ -227,21 +220,24 @@ const handleSort = (key: keyof FileListItem | 'type' | 'size' | 'mtime') => { // --- 列表项点击与选择逻辑 (使用 Composable) --- // 定义单击时的动作回调 (移到 Selection 实例化之前) const handleItemAction = (item: FileListItem) => { + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; + if (item.attrs.isDirectory) { - // 使用从 manager instance 解构的 isLoading - if (isLoading.value) { + // 修改:使用 currentSftpManager.value.isLoading + if (currentSftpManager.value.isLoading.value) { console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Ignoring directory click, already loading...`); return; } const newPath = item.filename === '..' - // 使用从 manager instance 解构的 currentPath 和 joinPath - ? currentPath.value.substring(0, currentPath.value.lastIndexOf('/')) || '/' - : joinPath(currentPath.value, item.filename); - // 使用从 manager instance 解构的 loadDirectory - loadDirectory(newPath); + // 修改:使用 currentSftpManager.value 的 currentPath 和 joinPath + ? currentSftpManager.value.currentPath.value.substring(0, currentSftpManager.value.currentPath.value.lastIndexOf('/')) || '/' + : currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename); + // 修改:使用 currentSftpManager.value.loadDirectory + currentSftpManager.value.loadDirectory(newPath); } else if (item.attrs.isFile) { - // 使用从 manager instance 解构的 currentPath 和 joinPath - const filePath = joinPath(currentPath.value, item.filename); + // 修改:使用 currentSftpManager.value 的 currentPath 和 joinPath + const filePath = currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename); const fileInfo: FileInfo = { name: item.filename, fullPath: filePath }; if (settingsStore.showPopupFileEditorBoolean) { @@ -251,9 +247,11 @@ const handleItemAction = (item: FileListItem) => { if (shareFileEditorTabsBoolean.value) { console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Opening file in shared mode (store handles loading): ${filePath}`); - fileEditorStore.openFile(filePath, props.sessionId); // Shared mode 关联 sessionId + // 修改:传递 instanceId 给 openFile + fileEditorStore.openFile(filePath, props.sessionId, props.instanceId); } else { - console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Opening file in independent mode (store handles loading): ${filePath}`); + // 独立模式由 sessionStore 处理,它内部应该已经知道 instanceId 或不需要它 + console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Opening file in independent mode (session store handles loading): ${filePath}`); sessionStore.openFileInSession(props.sessionId, fileInfo); // Independent mode 关联 sessionId } } @@ -274,11 +272,12 @@ const { // --- SFTP 操作处理函数 (定义在此处,供 Composable 使用) --- const handleDeleteSelectedClick = () => { - // 现在 selectedItems 来自 useFileManagerSelection - // 使用 props.wsDeps 和从 manager instance 解构的 fileList + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; + // 使用 props.wsDeps 和 currentSftpManager.value.fileList if (!props.wsDeps.isConnected.value || selectedItems.value.size === 0) return; const itemsToDelete = Array.from(selectedItems.value) - .map(filename => fileList.value.find((f: FileListItem) => f.filename === filename)) // f 已有类型 + .map(filename => currentSftpManager.value?.fileList.value.find((f: FileListItem) => f.filename === filename)) // 从 manager 获取列表 .filter((item): item is FileListItem => item !== undefined); if (itemsToDelete.length === 0) return; @@ -290,18 +289,20 @@ const handleDeleteSelectedClick = () => { : t('fileManager.prompts.confirmDeleteFile', { name: itemsToDelete[0].filename }); if (confirm(confirmMsg)) { - // 使用从 manager instance 解构的 deleteItems - deleteItems(itemsToDelete); + // 修改:使用 currentSftpManager.value.deleteItems + currentSftpManager.value?.deleteItems(itemsToDelete); selectedItems.value.clear(); } }; const handleRenameContextMenuClick = (item: FileListItem) => { // item 已有类型 if (!props.wsDeps.isConnected.value || !item) return; // 恢复使用 props.wsDeps + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; const newName = prompt(t('fileManager.prompts.enterNewName', { oldName: item.filename }), item.filename); if (newName && newName !== item.filename) { - // 使用从 manager instance 解构的 renameItem - renameItem(item, newName); + // 修改:使用 currentSftpManager.value.renameItem + currentSftpManager.value.renameItem(item, newName); } }; @@ -313,38 +314,44 @@ const handleChangePermissionsContextMenuClick = (item: FileListItem) => { // ite if (!/^[0-7]{3,4}$/.test(newModeStr)) { alert(t('fileManager.errors.invalidPermissionsFormat')); return; + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; } const newMode = parseInt(newModeStr, 8); - // 使用从 manager instance 解构的 changePermissions - changePermissions(item, newMode); + // 修改:使用 currentSftpManager.value.changePermissions + currentSftpManager.value.changePermissions(item, newMode); } }; const handleNewFolderContextMenuClick = () => { if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; const folderName = prompt(t('fileManager.prompts.enterFolderName')); if (folderName) { - // 使用从 manager instance 解构的 fileList - if (fileList.value.some((item: FileListItem) => item.filename === folderName)) { // item 已有类型 + // 修改:使用 currentSftpManager.value.fileList + if (currentSftpManager.value.fileList.value.some((item: FileListItem) => item.filename === folderName)) { alert(t('fileManager.errors.folderExists', { name: folderName })); return; } - // 使用从 manager instance 解构的 createDirectory - createDirectory(folderName); + // 修改:使用 currentSftpManager.value.createDirectory + currentSftpManager.value.createDirectory(folderName); } }; const handleNewFileContextMenuClick = () => { if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; const fileName = prompt(t('fileManager.prompts.enterFileName')); if (fileName) { - // 使用从 manager instance 解构的 fileList - if (fileList.value.some((item: FileListItem) => item.filename === fileName)) { // item 已有类型 + // 修改:使用 currentSftpManager.value.fileList + if (currentSftpManager.value.fileList.value.some((item: FileListItem) => item.filename === fileName)) { alert(t('fileManager.errors.fileExists', { name: fileName })); return; } - // 使用从 manager instance 解构的 createFile - createFile(fileName); + // 修改:使用 currentSftpManager.value.createFile + currentSftpManager.value.createFile(fileName); } }; @@ -362,11 +369,13 @@ const triggerDownload = (item: FileListItem) => { // item 已有类型 const currentConnectionId = props.dbConnectionId; if (!currentConnectionId) { console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: Missing connection ID.`); + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; alert(t('fileManager.errors.missingConnectionId')); return; } - // 使用从 manager instance 解构的 joinPath 和 currentPath - const downloadPath = joinPath(currentPath.value, item.filename); + // 修改:使用 currentSftpManager.value 的 joinPath 和 currentPath + const downloadPath = currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename); const downloadUrl = `/api/v1/sftp/download?connectionId=${currentConnectionId}&remotePath=${encodeURIComponent(downloadPath)}`; console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering download: ${downloadUrl}`); const link = document.createElement('a'); @@ -387,15 +396,17 @@ const { showContextMenu, // 使用 Composable 提供的函数 hideContextMenu, // <-- 获取 hideContextMenu 函数 } = useFileManagerContextMenu({ - selectedItems, // 传递来自 useFileManagerSelection 的 selectedItems - lastClickedIndex, // 传递来自 useFileManagerSelection 的 lastClickedIndex - fileList, // 传递从 manager instance 解构的 fileList - currentPath, // 传递从 manager instance 解构的 currentPath - isConnected: props.wsDeps.isConnected, // 仍然传递 props.wsDeps - isSftpReady: props.wsDeps.isSftpReady, // 仍然传递 props.wsDeps - t, // 传递 i18n 的 t 函数 + selectedItems, + lastClickedIndex, + // 修改:传递 manager 的 fileList 和 currentPath ref + fileList: computed(() => currentSftpManager.value?.fileList.value ?? []), + currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'), + isConnected: props.wsDeps.isConnected, + isSftpReady: props.wsDeps.isSftpReady, + t, // --- 传递回调函数 --- - onRefresh: () => loadDirectory(currentPath.value), // 使用解构的 loadDirectory 和 currentPath + // 修改:使用 currentSftpManager.value + onRefresh: () => currentSftpManager.value?.loadDirectory(currentSftpManager.value.currentPath.value), onUpload: triggerFileUpload, onDownload: triggerDownload, onDelete: handleDeleteSelectedClick, @@ -424,14 +435,17 @@ const { handleDragLeaveRow, handleDropOnRow, } = useFileManagerDragAndDrop({ - isConnected: props.wsDeps.isConnected, // 仍然传递 props.wsDeps - currentPath: currentPath, // 使用从 manager instance 解构的 currentPath - fileListContainerRef: fileListContainerRef, // 传递容器 ref - joinPath: joinPath, // 使用从 manager instance 解构的 joinPath - onFileUpload: startFileUpload, // 从 useFileUploader 获取 - onItemMove: renameItem, // 使用从 manager instance 解构的 renameItem - selectedItems: selectedItems, // 从 useFileManagerSelection 获取 - fileList: fileList, // 使用从 manager instance 解构的 fileList + isConnected: props.wsDeps.isConnected, + // 修改:传递 manager 的 currentPath 和 joinPath + currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'), + fileListContainerRef: fileListContainerRef, + joinPath: computed(() => currentSftpManager.value?.joinPath ?? ((...args: string[]) => args.join('/'))), // 提供默认 joinPath + onFileUpload: startFileUpload, + // 修改:使用 currentSftpManager.value.renameItem + onItemMove: (item, newName) => currentSftpManager.value?.renameItem(item, newName), + selectedItems: selectedItems, + // 修改:传递 manager 的 fileList ref + fileList: computed(() => currentSftpManager.value?.fileList.value ?? []), }); @@ -449,19 +463,20 @@ const { selectedIndex, // 使用 Composable 返回的 selectedIndex handleKeydown, // 使用 Composable 返回的 handleKeydown } = useFileManagerKeyboardNavigation({ - filteredFileList: filteredFileList, // 传递过滤后的列表 - currentPath: currentPath, // 使用从 manager instance 解构的 currentPath - fileListContainerRef: fileListContainerRef, // 传递容器引用 + filteredFileList: filteredFileList, + // 修改:传递 manager 的 currentPath ref + currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'), + fileListContainerRef: fileListContainerRef, // 当 Enter 键按下时,模拟鼠标单击 onEnterPress: (item) => handleItemClick(new MouseEvent('click'), item), }); // --- 重置选中索引和清空选择的 Watchers --- -// 注意:这里的 Watchers 现在需要修改 Composable 返回的 selectedIndex.value -watch(currentPath, () => { +// 修改:监听 manager 的 currentPath +watch(() => currentSftpManager.value?.currentPath.value, () => { selectedIndex.value = -1; - clearSelection(); // 清空选择 + clearSelection(); }); watch(searchQuery, () => { selectedIndex.value = -1; @@ -501,9 +516,9 @@ watchEffect((onCleanup) => { onCleanup(cleanupListeners); - // 使用 props.wsDeps 和从 manager instance 解构的 isLoading - if (props.wsDeps.isConnected.value && props.wsDeps.isSftpReady.value && !isLoading.value && !initialLoadDone.value && !isFetchingInitialPath.value) { - console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Connection ready, fetching initial path.`); + // 修改:检查 currentSftpManager 是否存在,并使用其 isLoading 状态 + if (currentSftpManager.value && props.wsDeps.isConnected.value && props.wsDeps.isSftpReady.value && !currentSftpManager.value.isLoading.value && !initialLoadDone.value && !isFetchingInitialPath.value) { + console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Connection ready for manager, fetching initial path.`); isFetchingInitialPath.value = true; // 仍然使用 props.wsDeps 中的 sendMessage 和 onMessage @@ -513,10 +528,12 @@ watchEffect((onCleanup) => { unregisterSuccess = wsOnMessage('sftp:realpath:success', (payload: any, message: WebSocketMessage) => { // message 已有类型 if (message.requestId === requestId && payload.requestedPath === requestedPath) { + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; const absolutePath = payload.absolutePath; console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 收到 '.' 的绝对路径: ${absolutePath}。开始加载目录。`); - // 使用从 manager instance 解构的 loadDirectory - loadDirectory(absolutePath); + // 修改:使用 currentSftpManager.value.loadDirectory + currentSftpManager.value.loadDirectory(absolutePath); initialLoadDone.value = true; cleanupListeners(); } @@ -563,6 +580,31 @@ watch(() => focusSwitcherStore.activateFileManagerSearchTrigger, (newValue, oldV }, { immediate: false }); // 添加 immediate: false 避免初始值为 0 时触发 +// --- 新增:监听 sessionId prop 的变化 --- +watch(() => props.sessionId, (newSessionId, oldSessionId) => { + if (newSessionId && newSessionId !== oldSessionId) { + console.log(`[FileManager ${newSessionId}-${props.instanceId}] Session ID changed from ${oldSessionId} to ${newSessionId}. Re-initializing.`); + + // 1. 重新初始化 SFTP 管理器 + initializeSftpManager(newSessionId, props.instanceId); + + // 2. 重置 UI 状态 + clearSelection(); + searchQuery.value = ''; + isSearchActive.value = false; + isEditingPath.value = false; + sortKey.value = 'filename'; // 重置排序 + sortDirection.value = 'asc'; + initialLoadDone.value = false; // 需要重新加载初始路径 + isFetchingInitialPath.value = false; // 重置获取状态 + + // 3. 触发新会话的初始路径加载 (watchEffect 会处理) + // watchEffect 会在 currentSftpManager.value 改变后重新运行 + // 并检查新 manager 的状态来决定是否加载初始路径 + } +}, { immediate: false }); // immediate: false 避免初始挂载时触发 + + // onBeforeUnmount 中 cleanupSftpHandlers 的调用已移至新的 onBeforeUnmount 逻辑中 // +++ 注册/注销自定义聚焦动作 +++ @@ -650,10 +692,10 @@ const stopResize = () => { // --- 路径编辑逻辑 --- const startPathEdit = () => { - // 使用从 manager instance 解构的 isLoading 和 props.wsDeps.isConnected - if (isLoading.value || !props.wsDeps.isConnected.value) return; - // 使用从 manager instance 解构的 currentPath 初始化编辑框 - editablePath.value = currentPath.value; + // 修改:检查 currentSftpManager 是否存在并使用其状态 + if (!currentSftpManager.value || currentSftpManager.value.isLoading.value || !props.wsDeps.isConnected.value) return; + // 修改:使用 currentSftpManager.value.currentPath 初始化编辑框 + editablePath.value = currentSftpManager.value.currentPath.value; isEditingPath.value = true; nextTick(() => { pathInputRef.value?.focus(); @@ -665,15 +707,17 @@ const handlePathInput = async (event?: Event) => { if (event && event instanceof KeyboardEvent && event.key !== 'Enter') { return; } + // 修改:检查 currentSftpManager 是否存在 + if (!currentSftpManager.value) return; const newPath = editablePath.value.trim(); isEditingPath.value = false; - // 使用从 manager instance 解构的 currentPath 比较 - if (newPath === currentPath.value || !newPath) { + // 修改:使用 currentSftpManager.value.currentPath 比较 + if (newPath === currentSftpManager.value.currentPath.value || !newPath) { return; } console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 尝试导航到新路径: ${newPath}`); - // 使用从 manager instance 解构的 loadDirectory - await loadDirectory(newPath); + // 修改:使用 currentSftpManager.value.loadDirectory + await currentSftpManager.value.loadDirectory(newPath); }; const cancelPathEdit = () => { @@ -756,8 +800,8 @@ defineExpose({ focusSearchInput });
- - {{ t('fileManager.currentPath') }}: {{ currentPath }} + + {{ t('fileManager.currentPath') }}: {{ currentSftpManager?.currentPath.value ?? '/' }}
- - - - + + + +
- - - - - - + + + + + +
@@ -874,29 +918,29 @@ defineExpose({ focusSearchInput }); - - + + - {{ t('fileManager.loading') }} + {{ t('fileManager.loading') }} - - + + - {{ t('fileManager.emptyDirectory') }} + {{ t('fileManager.emptyDirectory') }} - + - - + { diff --git a/packages/frontend/src/stores/fileEditor.store.ts b/packages/frontend/src/stores/fileEditor.store.ts index 307330a..3e967f9 100644 --- a/packages/frontend/src/stores/fileEditor.store.ts +++ b/packages/frontend/src/stores/fileEditor.store.ts @@ -107,22 +107,19 @@ export const useFileEditorStore = defineStore('fileEditor', () => { popupTrigger.value++; // 增加触发器值以通知监听者 }; - // 获取指定会话的 SFTP 管理器 (保持不变) - const getSftpManager = (sessionId: string | null) => { - if (!sessionId) return null; - const session = sessionStore.sessions.get(sessionId); - return session?.sftpManager ?? null; - }; + // 移除内部的 getSftpManager 辅助函数,将直接使用 sessionStore.getOrCreateSftpManager + // const getSftpManager = (sessionId: string | null) => { ... }; // 移除 setEditorVisibility 方法 // const setEditorVisibility = ... // 打开或切换到文件标签页 - const openFile = async (targetFilePath: string, sessionId: string) => { + // 修改:添加 instanceId 参数 + const openFile = async (targetFilePath: string, sessionId: string, instanceId: string) => { // 在共享模式下,我们仍然需要 sessionId 来构建唯一的 tabId // 并与 SFTP 管理器关联 - const tabId = `${sessionId}:${targetFilePath}`; - console.log(`[文件编辑器 Store - 共享模式] 尝试打开文件: ${targetFilePath} (会话: ${sessionId}, Tab ID: ${tabId})`); + const tabId = `${sessionId}:${targetFilePath}`; // Tab ID 仍然基于 sessionId 和 filePath 保持唯一性 + console.log(`[文件编辑器 Store - 共享模式] 尝试打开文件: ${targetFilePath} (会话: ${sessionId}, 实例: ${instanceId}, Tab ID: ${tabId})`); // 移除确保编辑器可见的逻辑 // if (editorVisibleState.value === 'closed') { @@ -167,14 +164,15 @@ export const useFileEditorStore = defineStore('fileEditor', () => { // 不再在这里触发弹窗 // popupTrigger.value++; - // 获取 SFTP 管理器 - const sftpManager = getSftpManager(sessionId); + // 获取 SFTP 管理器 - 修改:使用 sessionStore.getOrCreateSftpManager 并传入 instanceId + const sftpManager = sessionStore.getOrCreateSftpManager(sessionId, instanceId); if (!sftpManager) { - console.error(`[文件编辑器 Store] 无法找到会话 ${sessionId} 的 SFTP 管理器。`); + // 错误消息保持不变,但现在知道是哪个实例找不到管理器 + console.error(`[文件编辑器 Store] 无法找到会话 ${sessionId} (实例 ${instanceId}) 的 SFTP 管理器。`); const tabToUpdate = tabs.value.get(tabId); if (tabToUpdate) { tabToUpdate.isLoading = false; - tabToUpdate.loadingError = t('fileManager.errors.sftpManagerNotFound'); + tabToUpdate.loadingError = t('fileManager.errors.sftpManagerNotFound'); // 可以考虑添加 instanceId 到错误消息 } return; } @@ -274,9 +272,26 @@ export const useFileEditorStore = defineStore('fileEditor', () => { return; } - const sftpManager = session.sftpManager; // 直接从有效会话获取 + // 修改:从 sftpManagers Map 获取第一个可用的管理器 + const sftpManagersMap = session.sftpManagers; + if (!sftpManagersMap || sftpManagersMap.size === 0) { + console.error(`[文件编辑器 Store] 保存失败:会话 ${tab.sessionId} 没有可用的 SFTP 管理器实例。`); + tab.saveStatus = 'error'; + tab.saveError = t('fileManager.errors.sftpManagerNotFound'); // 复用错误消息 + // 添加短暂错误提示 + setTimeout(() => { + if (tab.saveStatus === 'error') { + tab.saveStatus = 'idle'; + tab.saveError = null; + } + }, 5000); + return; + } + // 获取 Map 中的第一个管理器实例 + const sftpManager = sftpManagersMap.values().next().value; - console.log(`[文件编辑器 Store] 开始保存文件: ${tab.filePath} (Tab ID: ${tab.id})`); + + console.log(`[文件编辑器 Store] 开始保存文件: ${tab.filePath} (Tab ID: ${tab.id}) 使用实例 ${sftpManager.instanceId}`); // 添加实例 ID 日志 tab.isSaving = true; tab.saveStatus = 'saving'; tab.saveError = null;