diff --git a/packages/frontend/src/components/FileEditorOverlay.vue b/packages/frontend/src/components/FileEditorOverlay.vue index b3b005f..f724e05 100644 --- a/packages/frontend/src/components/FileEditorOverlay.vue +++ b/packages/frontend/src/components/FileEditorOverlay.vue @@ -3,39 +3,63 @@ import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; import MonacoEditor from './MonacoEditor.vue'; -// import FileEditorTabs from './FileEditorTabs.vue'; // 不再需要标签栏 -import { useFileEditorStore } from '../stores/fileEditor.store'; +import FileEditorTabs from './FileEditorTabs.vue'; +import { useFileEditorStore, type FileTab } from '../stores/fileEditor.store'; // 导入 FileTab 类型 import { useSettingsStore } from '../stores/settings.store'; import { useSessionStore } from '../stores/session.store'; // 导入 Session Store -import type { EditorFileContent, SaveStatus } from '../types/sftp.types'; // 导入类型 -import { getLanguageFromFilename, getFilenameFromPath } from '../stores/fileEditor.store'; // 导入辅助函数 +import type { EditorFileContent, SaveStatus } from '../types/sftp.types'; +import { getLanguageFromFilename, getFilenameFromPath } from '../stores/fileEditor.store'; const { t } = useI18n(); const fileEditorStore = useFileEditorStore(); const settingsStore = useSettingsStore(); -const sessionStore = useSessionStore(); // 导入 Session Store -// const { t } = useI18n(); // 移除重复声明 +const sessionStore = useSessionStore(); // 实例化 Session Store // --- 本地状态控制弹窗显示 --- const isVisible = ref(false); // --- 从 Store 获取状态 --- -const { popupTrigger, popupFileInfo } = storeToRefs(fileEditorStore); // 监听触发器和文件信息 -const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); +// 全局 Store (用于共享模式和触发器) +const { + popupTrigger, + popupFileInfo, // 包含 sessionId 和 filePath + activeTabId: globalActiveTabIdRef, // 获取全局 activeTabId + tabs: globalTabsRef, // 获取全局 tabs Map +} = storeToRefs(fileEditorStore); -// --- 本地状态管理弹窗内的文件 --- -const popupFilePath = ref(null); -const popupSessionId = ref(null); -const popupFilename = ref(''); -const popupContent = ref(''); -const popupOriginalContent = ref(''); -const popupLanguage = ref('plaintext'); -const popupIsLoading = ref(false); -const popupLoadingError = ref(null); -const popupIsSaving = ref(false); -const popupSaveStatus = ref('idle'); -const popupSaveError = ref(null); -const popupIsModified = computed(() => popupContent.value !== popupOriginalContent.value); +// 设置 Store (用于判断模式) +const { showPopupFileEditorBoolean, shareFileEditorTabsBoolean } = storeToRefs(settingsStore); + +// --- 从 Store 获取方法 --- +// 全局 Store Actions (用于共享模式) +const { + saveFile: saveGlobalFile, + closeTab: closeGlobalTab, + setActiveTab: setGlobalActiveTab, + updateFileContent: updateGlobalFileContent, +} = fileEditorStore; + +// 会话 Store Actions (用于非共享模式) +const { + saveFileInSession, + closeEditorTabInSession, + setActiveEditorTabInSession, + updateFileContentInSession, +} = sessionStore; + +// --- 移除本地文件状态 --- +// const popupFilePath = ref(null); +// const popupSessionId = ref(null); +// const popupFilename = ref(''); +// const popupContent = ref(''); +// const popupOriginalContent = ref(''); +// const popupLanguage = ref('plaintext'); +// const popupIsLoading = ref(false); +// const popupLoadingError = ref(null); +// const popupIsSaving = ref(false); +// const popupSaveStatus = ref('idle'); +// const popupSaveError = ref(null); +// const popupIsModified = computed(() => popupContent.value !== popupOriginalContent.value); // --- 弹窗尺寸和拖拽状态 --- const popupWidthPx = ref(window.innerWidth * 0.75); // 初始宽度 75vw (像素) @@ -53,73 +77,135 @@ const popupStyle = computed(() => ({ width: `${popupWidthPx.value}px`, height: `${popupHeightPx.value}px`, })); -// 不再需要基于 activeTab 的计算属性 -// --- 事件处理 --- -// 保存弹窗中的文件 -const handlePopupSave = async () => { - if (!popupFilePath.value || !popupSessionId.value || popupIsSaving.value || popupIsLoading.value) { - console.warn('[FileEditorOverlay] 保存条件不满足,无法保存。'); - return; +// --- 动态计算属性 (根据模式选择数据源) --- + +// 获取当前弹窗关联的会话 (仅非共享模式需要) +const currentSession = computed(() => { + if (shareFileEditorTabsBoolean.value || !popupFileInfo.value?.sessionId) { + return null; } + return sessionStore.sessions.get(popupFileInfo.value.sessionId) ?? null; +}); - const session = sessionStore.sessions.get(popupSessionId.value); - if (!session || !session.wsManager.isConnected.value || !session.wsManager.isSftpReady.value) { - console.error(`[FileEditorOverlay] 保存失败:会话 ${popupSessionId.value} 无效或未连接/SFTP 未就绪。`); - popupSaveStatus.value = 'error'; - popupSaveError.value = t('fileManager.errors.sessionInvalidOrNotReady'); - setTimeout(() => { popupSaveStatus.value = 'idle'; popupSaveError.value = null; }, 5000); - return; +// 获取当前模式下的标签页列表 +const orderedTabs = computed(() => { + if (shareFileEditorTabsBoolean.value) { + return Array.from(globalTabsRef.value.values()); // 全局 Store + } else { + return currentSession.value?.editorTabs.value ?? []; // 会话 Store } +}); - const sftpManager = session.sftpManager; - const contentToSave = popupContent.value; +// 获取当前模式下的活动标签页 ID +const activeTabId = computed(() => { + if (shareFileEditorTabsBoolean.value) { + return globalActiveTabIdRef.value; // 全局 Store + } else { + return currentSession.value?.activeEditorTabId.value ?? null; // 会话 Store + } +}); - console.log(`[FileEditorOverlay] 开始保存文件: ${popupFilePath.value}`); - popupIsSaving.value = true; - popupSaveStatus.value = 'saving'; - popupSaveError.value = null; +// 获取当前模式下的活动标签页对象 +const activeTab = computed((): FileTab | null => { + const currentId = activeTabId.value; + if (!currentId) return null; - try { - await sftpManager.writeFile(popupFilePath.value, contentToSave); - console.log(`[FileEditorOverlay] 文件 ${popupFilePath.value} 保存成功。`); - popupIsSaving.value = false; - popupSaveStatus.value = 'success'; - popupSaveError.value = null; - popupOriginalContent.value = contentToSave; // 更新原始内容 - // popupIsModified 会自动变为 false + if (shareFileEditorTabsBoolean.value) { + return globalTabsRef.value.get(currentId) ?? null; // 全局 Store + } else { + // 在会话的 editorTabs 数组中查找 + return currentSession.value?.editorTabs.value.find(tab => tab.id === currentId) ?? null; + } +}); - setTimeout(() => { if (popupSaveStatus.value === 'success') popupSaveStatus.value = 'idle'; }, 2000); +// Monaco 编辑器内容绑定 (根据模式调用不同 action) +const activeEditorContent = computed({ + get: () => activeTab.value?.content ?? '', + set: (value) => { + const currentActiveTab = activeTab.value; // 缓存当前活动标签 + if (!currentActiveTab) return; - } catch (err: any) { - console.error(`[FileEditorOverlay] 保存文件 ${popupFilePath.value} 失败:`, err); - popupIsSaving.value = false; - popupSaveStatus.value = 'error'; - popupSaveError.value = `${t('fileManager.errors.saveFailed')}: ${err.message || err}`; - setTimeout(() => { if (popupSaveStatus.value === 'error') popupSaveStatus.value = 'idle'; popupSaveError.value = null; }, 5000); + if (shareFileEditorTabsBoolean.value) { + updateGlobalFileContent(currentActiveTab.id, value); // 全局 Store + } else { + // 非共享模式需要 sessionId + const sessionId = popupFileInfo.value?.sessionId; + if (sessionId) { + updateFileContentInSession(sessionId, currentActiveTab.id, value); // 会话 Store + } else { + console.error("[FileEditorOverlay] 无法更新内容:非共享模式下缺少 sessionId。"); + } + } + }, +}); + + +// --- 从 activeTab 派生的计算属性 (保持不变,因为 activeTab 已动态化) --- +const currentTabIsLoading = computed(() => activeTab.value?.isLoading ?? false); +const currentTabLoadingError = computed(() => activeTab.value?.loadingError ?? null); +const currentTabIsSaving = computed(() => activeTab.value?.isSaving ?? false); +const currentTabSaveStatus = computed(() => activeTab.value?.saveStatus ?? 'idle'); +const currentTabSaveError = computed(() => activeTab.value?.saveError ?? null); +const currentTabLanguage = computed(() => activeTab.value?.language ?? 'plaintext'); +const currentTabFilePath = computed(() => activeTab.value?.filePath ?? ''); +const currentTabIsModified = computed(() => activeTab.value?.isModified ?? false); + +// --- 事件处理 (根据模式调用不同 action) --- + +// 保存当前激活的标签页 +const handleSaveRequest = () => { + const currentActiveTab = activeTab.value; + if (!currentActiveTab) return; + + if (shareFileEditorTabsBoolean.value) { + saveGlobalFile(currentActiveTab.id); // 全局 Store + } else { + const sessionId = popupFileInfo.value?.sessionId; + if (sessionId) { + saveFileInSession(sessionId, currentActiveTab.id); // 会话 Store + } else { + console.error("[FileEditorOverlay] 无法保存:非共享模式下缺少 sessionId。"); + } } }; -// 关闭弹窗并重置状态 -const handleCloseContainer = () => { - if (popupIsModified.value) { - if (!confirm(`文件 ${popupFilename.value} 已修改但未保存。确定要关闭吗?`)) { - return; // 用户取消关闭 +// 激活标签页 +const handleActivateTab = (tabId: string) => { + if (shareFileEditorTabsBoolean.value) { + setGlobalActiveTab(tabId); // 全局 Store + } else { + const sessionId = popupFileInfo.value?.sessionId; + if (sessionId) { + setActiveEditorTabInSession(sessionId, tabId); // 会话 Store + } else { + console.error("[FileEditorOverlay] 无法激活标签页:非共享模式下缺少 sessionId。"); } } +}; + +// 关闭标签页 +const handleCloseTab = (tabId: string) => { + if (shareFileEditorTabsBoolean.value) { + closeGlobalTab(tabId); // 全局 Store + } else { + const sessionId = popupFileInfo.value?.sessionId; + if (sessionId) { + closeEditorTabInSession(sessionId, tabId); // 会话 Store + } else { + console.error("[FileEditorOverlay] 无法关闭标签页:非共享模式下缺少 sessionId。"); + } + } +}; + + +// 关闭弹窗 (保持不变) +const handleCloseContainer = () => { + // 关闭前不再检查本地修改状态,因为没有本地状态了 + // 如果需要检查 store 中 activeTab 的修改状态,可以在这里添加逻辑 + // if (activeTab.value?.isModified) { ... } isVisible.value = false; - // 重置本地状态 - popupFilePath.value = null; - popupSessionId.value = null; - popupFilename.value = ''; - popupContent.value = ''; - popupOriginalContent.value = ''; - popupLanguage.value = 'plaintext'; - popupIsLoading.value = false; - popupLoadingError.value = null; - popupIsSaving.value = false; - popupSaveStatus.value = 'idle'; - popupSaveError.value = null; + // 不需要重置本地状态了 }; // 最小化编辑器容器 (如果需要实现) @@ -158,73 +244,32 @@ const stopResize = () => { } }; -// 监听 popupTrigger 的变化来加载文件并显示弹窗 -watch(popupTrigger, async () => { +// 监听 popupTrigger 的变化来显示弹窗 +watch(popupTrigger, () => { if (!showPopupFileEditorBoolean.value || !popupFileInfo.value) { console.log('[FileEditorOverlay] Popup trigger changed, but overlay is disabled or file info is missing.'); - isVisible.value = false; // 确保在不应显示时隐藏 + isVisible.value = false; return; } const { filePath, sessionId } = popupFileInfo.value; - console.log(`[FileEditorOverlay] Triggered for file: ${filePath}, session: ${sessionId}`); + console.log(`[FileEditorOverlay] Triggered for file: ${filePath} in session: ${sessionId}`); - // 设置状态并显示弹窗 - popupFilePath.value = filePath; - popupSessionId.value = sessionId; - popupFilename.value = getFilenameFromPath(filePath); - popupLanguage.value = getLanguageFromFilename(filePath); - popupIsLoading.value = true; - popupLoadingError.value = null; - popupContent.value = ''; // 清空旧内容 - popupOriginalContent.value = ''; - popupIsSaving.value = false; - popupSaveStatus.value = 'idle'; - popupSaveError.value = null; - isVisible.value = true; // 显示弹窗 + // 仅显示弹窗,激活逻辑由各自 store 的 openFile/openFileInSession 处理 + // 或者由 handleActivateTab 处理用户点击 + isVisible.value = true; - // 获取 SFTP 管理器并加载文件 - const session = sessionStore.sessions.get(sessionId); - if (!session || !session.sftpManager) { - console.error(`[FileEditorOverlay] Cannot find SFTP manager for session ${sessionId}`); - popupLoadingError.value = t('fileManager.errors.sftpManagerNotFound'); - popupIsLoading.value = false; - return; - } - const sftpManager = session.sftpManager; + // --- 确保激活状态正确 (重要) --- + // 在非共享模式下,FileManager 调用 openFileInSession 时会设置会话内的 activeTabId。 + // 在共享模式下,FileManager 调用 openFile 时会设置全局的 activeTabId。 + // 这里不再需要强制调用 setActiveTab,因为触发弹窗时,对应的 store 应该已经处理了激活。 + // 如果发现激活不正确,问题可能在 FileManager 或对应的 store action 中。 - try { - const fileData = await sftpManager.readFile(filePath); - console.log(`[FileEditorOverlay] File ${filePath} read successfully. Encoding: ${fileData.encoding}`); + // 检查激活状态 (调试用) + // nextTick(() => { // 确保在 DOM 更新后检查 + // console.log(`[FileEditorOverlay] Popup shown. Current activeTabId: ${activeTabId.value}, Active Tab Object:`, activeTab.value); + // }); - let decodedContent = ''; - if (fileData.encoding === 'base64') { - try { - const binaryString = atob(fileData.content); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } - const decoder = new TextDecoder('utf-8'); - decodedContent = decoder.decode(bytes); - } catch (decodeError) { - console.error(`[FileEditorOverlay] Base64/UTF-8 decode error for ${filePath}:`, decodeError); - popupLoadingError.value = t('fileManager.errors.fileDecodeError'); - decodedContent = `// ${popupLoadingError.value}\n// Original Base64:\n${fileData.content}`; - } - } else { - decodedContent = fileData.content; - if (decodedContent.includes('\uFFFD')) { - console.warn(`[FileEditorOverlay] File ${filePath} might not be UTF-8.`); - } - } - popupContent.value = decodedContent; - popupOriginalContent.value = decodedContent; - } catch (err: any) { - console.error(`[FileEditorOverlay] Failed to read file ${filePath}:`, err); - popupLoadingError.value = `${t('fileManager.errors.readFileFailed')}: ${err.message || err}`; - popupContent.value = `// ${popupLoadingError.value}`; - } finally { - popupIsLoading.value = false; - } }); @@ -241,38 +286,51 @@ onBeforeUnmount(() => {
- + + - -
+ +
- {{ t('fileManager.editingFile') }}: {{ popupFilename }} - * + {{ t('fileManager.editingFile') }}: {{ currentTabFilePath }} + *
- {{ t('fileManager.saving') }}... - ✅ {{ t('fileManager.saveSuccess') }} - ❌ {{ t('fileManager.saveError') }}: {{ popupSaveError }} -
+ +
+ {{ t('fileManager.noOpenFile') }} + +
- +
-
{{ t('fileManager.loadingFile') }}
-
{{ popupLoadingError }}
+
{{ t('fileManager.loadingFile') }}
+
{{ currentTabLoadingError }}
+ +
{{ t('fileManager.selectFileToEdit') }}