Update FileEditorOverlay.vue

This commit is contained in:
Baobhan Sith
2025-04-17 09:32:32 +08:00
parent 76b740a752
commit 3b448e6fcb
@@ -3,39 +3,63 @@ import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import MonacoEditor from './MonacoEditor.vue'; import MonacoEditor from './MonacoEditor.vue';
// import FileEditorTabs from './FileEditorTabs.vue'; // 不再需要标签栏 import FileEditorTabs from './FileEditorTabs.vue';
import { useFileEditorStore } from '../stores/fileEditor.store'; import { useFileEditorStore, type FileTab } from '../stores/fileEditor.store'; // 导入 FileTab 类型
import { useSettingsStore } from '../stores/settings.store'; import { useSettingsStore } from '../stores/settings.store';
import { useSessionStore } from '../stores/session.store'; // 导入 Session Store import { useSessionStore } from '../stores/session.store'; // 导入 Session Store
import type { EditorFileContent, SaveStatus } from '../types/sftp.types'; // 导入类型 import type { EditorFileContent, SaveStatus } from '../types/sftp.types';
import { getLanguageFromFilename, getFilenameFromPath } from '../stores/fileEditor.store'; // 导入辅助函数 import { getLanguageFromFilename, getFilenameFromPath } from '../stores/fileEditor.store';
const { t } = useI18n(); const { t } = useI18n();
const fileEditorStore = useFileEditorStore(); const fileEditorStore = useFileEditorStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const sessionStore = useSessionStore(); // 导入 Session Store const sessionStore = useSessionStore(); // 实例化 Session Store
// const { t } = useI18n(); // 移除重复声明
// --- 本地状态控制弹窗显示 --- // --- 本地状态控制弹窗显示 ---
const isVisible = ref(false); const isVisible = ref(false);
// --- 从 Store 获取状态 --- // --- 从 Store 获取状态 ---
const { popupTrigger, popupFileInfo } = storeToRefs(fileEditorStore); // 监听触发器和文件信息 // 全局 Store (用于共享模式和触发器)
const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); const {
popupTrigger,
popupFileInfo, // 包含 sessionId 和 filePath
activeTabId: globalActiveTabIdRef, // 获取全局 activeTabId
tabs: globalTabsRef, // 获取全局 tabs Map
} = storeToRefs(fileEditorStore);
// --- 本地状态管理弹窗内的文件 --- // 设置 Store (用于判断模式)
const popupFilePath = ref<string | null>(null); const { showPopupFileEditorBoolean, shareFileEditorTabsBoolean } = storeToRefs(settingsStore);
const popupSessionId = ref<string | null>(null);
const popupFilename = ref<string>(''); // --- 从 Store 获取方法 ---
const popupContent = ref<string>(''); // 全局 Store Actions (用于共享模式)
const popupOriginalContent = ref<string>(''); const {
const popupLanguage = ref<string>('plaintext'); saveFile: saveGlobalFile,
const popupIsLoading = ref<boolean>(false); closeTab: closeGlobalTab,
const popupLoadingError = ref<string | null>(null); setActiveTab: setGlobalActiveTab,
const popupIsSaving = ref<boolean>(false); updateFileContent: updateGlobalFileContent,
const popupSaveStatus = ref<SaveStatus>('idle'); } = fileEditorStore;
const popupSaveError = ref<string | null>(null);
const popupIsModified = computed(() => popupContent.value !== popupOriginalContent.value); // 会话 Store Actions (用于非共享模式)
const {
saveFileInSession,
closeEditorTabInSession,
setActiveEditorTabInSession,
updateFileContentInSession,
} = sessionStore;
// --- 移除本地文件状态 ---
// const popupFilePath = ref<string | null>(null);
// const popupSessionId = ref<string | null>(null);
// const popupFilename = ref<string>('');
// const popupContent = ref<string>('');
// const popupOriginalContent = ref<string>('');
// const popupLanguage = ref<string>('plaintext');
// const popupIsLoading = ref<boolean>(false);
// const popupLoadingError = ref<string | null>(null);
// const popupIsSaving = ref<boolean>(false);
// const popupSaveStatus = ref<SaveStatus>('idle');
// const popupSaveError = ref<string | null>(null);
// const popupIsModified = computed(() => popupContent.value !== popupOriginalContent.value);
// --- 弹窗尺寸和拖拽状态 --- // --- 弹窗尺寸和拖拽状态 ---
const popupWidthPx = ref(window.innerWidth * 0.75); // 初始宽度 75vw (像素) const popupWidthPx = ref(window.innerWidth * 0.75); // 初始宽度 75vw (像素)
@@ -53,73 +77,135 @@ const popupStyle = computed(() => ({
width: `${popupWidthPx.value}px`, width: `${popupWidthPx.value}px`,
height: `${popupHeightPx.value}px`, height: `${popupHeightPx.value}px`,
})); }));
// 不再需要基于 activeTab 的计算属性
// --- 事件处理 --- // --- 动态计算属性 (根据模式选择数据源) ---
// 保存弹窗中的文件
const handlePopupSave = async () => { // 获取当前弹窗关联的会话 (仅非共享模式需要)
if (!popupFilePath.value || !popupSessionId.value || popupIsSaving.value || popupIsLoading.value) { const currentSession = computed(() => {
console.warn('[FileEditorOverlay] 保存条件不满足,无法保存。'); if (shareFileEditorTabsBoolean.value || !popupFileInfo.value?.sessionId) {
return; 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) { const orderedTabs = computed(() => {
console.error(`[FileEditorOverlay] 保存失败:会话 ${popupSessionId.value} 无效或未连接/SFTP 未就绪。`); if (shareFileEditorTabsBoolean.value) {
popupSaveStatus.value = 'error'; return Array.from(globalTabsRef.value.values()); // 全局 Store
popupSaveError.value = t('fileManager.errors.sessionInvalidOrNotReady'); } else {
setTimeout(() => { popupSaveStatus.value = 'idle'; popupSaveError.value = null; }, 5000); return currentSession.value?.editorTabs.value ?? []; // 会话 Store
return;
} }
});
const sftpManager = session.sftpManager; // 获取当前模式下的活动标签页 ID
const contentToSave = popupContent.value; 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; const activeTab = computed((): FileTab | null => {
popupSaveStatus.value = 'saving'; const currentId = activeTabId.value;
popupSaveError.value = null; if (!currentId) return null;
try { if (shareFileEditorTabsBoolean.value) {
await sftpManager.writeFile(popupFilePath.value, contentToSave); return globalTabsRef.value.get(currentId) ?? null; // 全局 Store
console.log(`[FileEditorOverlay] 文件 ${popupFilePath.value} 保存成功。`); } else {
popupIsSaving.value = false; // 在会话的 editorTabs 数组中查找
popupSaveStatus.value = 'success'; return currentSession.value?.editorTabs.value.find(tab => tab.id === currentId) ?? null;
popupSaveError.value = null; }
popupOriginalContent.value = contentToSave; // 更新原始内容 });
// popupIsModified 会自动变为 false
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) { if (shareFileEditorTabsBoolean.value) {
console.error(`[FileEditorOverlay] 保存文件 ${popupFilePath.value} 失败:`, err); updateGlobalFileContent(currentActiveTab.id, value); // 全局 Store
popupIsSaving.value = false; } else {
popupSaveStatus.value = 'error'; // 非共享模式需要 sessionId
popupSaveError.value = `${t('fileManager.errors.saveFailed')}: ${err.message || err}`; const sessionId = popupFileInfo.value?.sessionId;
setTimeout(() => { if (popupSaveStatus.value === 'error') popupSaveStatus.value = 'idle'; popupSaveError.value = null; }, 5000); 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 = () => { const handleActivateTab = (tabId: string) => {
if (popupIsModified.value) { if (shareFileEditorTabsBoolean.value) {
if (!confirm(`文件 ${popupFilename.value} 已修改但未保存。确定要关闭吗?`)) { setGlobalActiveTab(tabId); // 全局 Store
return; // 用户取消关闭 } 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; 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 的变化来加载文件并显示弹窗 // 监听 popupTrigger 的变化来显示弹窗
watch(popupTrigger, async () => { watch(popupTrigger, () => {
if (!showPopupFileEditorBoolean.value || !popupFileInfo.value) { if (!showPopupFileEditorBoolean.value || !popupFileInfo.value) {
console.log('[FileEditorOverlay] Popup trigger changed, but overlay is disabled or file info is missing.'); console.log('[FileEditorOverlay] Popup trigger changed, but overlay is disabled or file info is missing.');
isVisible.value = false; // 确保在不应显示时隐藏 isVisible.value = false;
return; return;
} }
const { filePath, sessionId } = popupFileInfo.value; const { filePath, sessionId } = popupFileInfo.value;
console.log(`[FileEditorOverlay] Triggered for file: ${filePath}, session: ${sessionId}`); console.log(`[FileEditorOverlay] Triggered for file: ${filePath} in session: ${sessionId}`);
// 设置状态并显示弹窗 // 仅显示弹窗,激活逻辑由各自 store 的 openFile/openFileInSession 处理
popupFilePath.value = filePath; // 或者由 handleActivateTab 处理用户点击
popupSessionId.value = sessionId; isVisible.value = true;
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; // 显示弹窗
// 获取 SFTP 管理器并加载文件 // --- 确保激活状态正确 (重要) ---
const session = sessionStore.sessions.get(sessionId); // 在非共享模式下,FileManager 调用 openFileInSession 时会设置会话内的 activeTabId。
if (!session || !session.sftpManager) { // 在共享模式下,FileManager 调用 openFile 时会设置全局的 activeTabId。
console.error(`[FileEditorOverlay] Cannot find SFTP manager for session ${sessionId}`); // 这里不再需要强制调用 setActiveTab,因为触发弹窗时,对应的 store 应该已经处理了激活。
popupLoadingError.value = t('fileManager.errors.sftpManagerNotFound'); // 如果发现激活不正确,问题可能在 FileManager 或对应的 store action 中。
popupIsLoading.value = false;
return;
}
const sftpManager = session.sftpManager;
try { // 检查激活状态 (调试用)
const fileData = await sftpManager.readFile(filePath); // nextTick(() => { // 确保在 DOM 更新后检查
console.log(`[FileEditorOverlay] File ${filePath} read successfully. Encoding: ${fileData.encoding}`); // 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(() => {
<!-- 编辑器弹窗/容器应用动态样式 --> <!-- 编辑器弹窗/容器应用动态样式 -->
<div class="editor-popup" :style="popupStyle"> <div class="editor-popup" :style="popupStyle">
<!-- 移除标签栏 --> <!-- 标签栏 (使用动态计算属性和事件处理器) -->
<FileEditorTabs
:tabs="orderedTabs"
:active-tab-id="activeTabId"
@activate-tab="handleActivateTab"
@close-tab="handleCloseTab"
/>
<!-- 编辑器头部 --> <!-- 编辑器头部 (使用动态计算属性) -->
<div class="editor-header"> <div v-if="activeTab" class="editor-header">
<span> <span>
{{ t('fileManager.editingFile') }}: {{ popupFilename }} {{ t('fileManager.editingFile') }}: {{ currentTabFilePath }}
<span v-if="popupIsModified" class="modified-indicator">*</span> <span v-if="currentTabIsModified" class="modified-indicator">*</span>
</span> </span>
<div class="editor-actions"> <div class="editor-actions">
<span v-if="popupSaveStatus === 'saving'" class="save-status saving">{{ t('fileManager.saving') }}...</span> <span v-if="currentTabSaveStatus === 'saving'" class="save-status saving">{{ t('fileManager.saving') }}...</span>
<span v-if="popupSaveStatus === 'success'" class="save-status success"> {{ t('fileManager.saveSuccess') }}</span> <span v-if="currentTabSaveStatus === 'success'" class="save-status success"> {{ t('fileManager.saveSuccess') }}</span>
<span v-if="popupSaveStatus === 'error'" class="save-status error"> {{ t('fileManager.saveError') }}: {{ popupSaveError }}</span> <span v-if="currentTabSaveStatus === 'error'" class="save-status error"> {{ t('fileManager.saveError') }}: {{ currentTabSaveError }}</span>
<button @click="handlePopupSave" :disabled="popupIsSaving || popupIsLoading || !!popupLoadingError || !popupFilePath" class="save-btn"> <button @click="handleSaveRequest" :disabled="currentTabIsSaving || currentTabIsLoading || !!currentTabLoadingError || !activeTab" class="save-btn">
{{ popupIsSaving ? t('fileManager.saving') : t('fileManager.actions.save') }} {{ currentTabIsSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
</button> </button>
<button @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')"></button> <button @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')"></button>
</div> </div>
</div> </div>
<!-- 如果没有活动标签页 -->
<div v-else class="editor-header editor-header-placeholder">
<span>{{ t('fileManager.noOpenFile') }}</span>
<button @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')"></button>
</div>
<!-- 编辑器内容区域 --> <!-- 编辑器内容区域 (现在基于 activeTab) -->
<div class="editor-content-area"> <div class="editor-content-area">
<div v-if="popupIsLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div> <div v-if="currentTabIsLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
<div v-else-if="popupLoadingError" class="editor-error">{{ popupLoadingError }}</div> <div v-else-if="currentTabLoadingError" class="editor-error">{{ currentTabLoadingError }}</div>
<MonacoEditor <MonacoEditor
v-else-if="popupFilePath" v-else-if="activeTab"
:key="popupFilePath" :key="activeTab.id"
v-model="popupContent" v-model="activeEditorContent"
:language="popupLanguage" :language="currentTabLanguage"
theme="vs-dark" theme="vs-dark"
class="editor-instance" class="editor-instance"
@request-save="handlePopupSave" @request-save="handleSaveRequest"
/> />
<!-- 如果容器可见但没有活动标签页 -->
<div v-else class="editor-placeholder">{{ t('fileManager.selectFileToEdit') }}</div>
</div> </div>
<!-- 添加拖拽手柄 --> <!-- 添加拖拽手柄 -->