update
This commit is contained in:
@@ -1,32 +1,77 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, type PropType, ref, watch } from 'vue'; // 添加 ref 和 watch
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
// import { storeToRefs } from 'pinia'; // 移除 storeToRefs
|
||||||
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
||||||
import FileEditorTabs from './FileEditorTabs.vue'; // 导入标签栏组件
|
import FileEditorTabs from './FileEditorTabs.vue'; // 导入标签栏组件 (路径确认无误)
|
||||||
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入新的 Store
|
// import { useFileEditorStore } from '../stores/fileEditor.store'; // 移除 Store 导入
|
||||||
|
import type { FileTab } from '../stores/fileEditor.store'; // 保留类型导入
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const fileEditorStore = useFileEditorStore();
|
|
||||||
|
|
||||||
// 从 Store 获取新的多标签状态和方法
|
// --- Props ---
|
||||||
const {
|
const props = defineProps({
|
||||||
// editorVisibleState, // 可见性由父组件 (WorkspaceView) 控制 Pane 大小决定,不再需要内部状态
|
tabs: {
|
||||||
activeTab, // 当前激活的标签页对象 (computed)
|
type: Array as PropType<FileTab[]>,
|
||||||
activeEditorContent,// 用于 v-model 绑定 (computed)
|
required: true,
|
||||||
orderedTabs, // 标签页数组 (computed)
|
},
|
||||||
} = storeToRefs(fileEditorStore);
|
activeTabId: {
|
||||||
|
type: String as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
sessionId: { // 需要 sessionId 来区分保存请求等 (虽然 tabs 里也有)
|
||||||
|
type: String as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Emits ---
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'activate-tab', tabId: string): void;
|
||||||
|
(e: 'close-tab', tabId: string): void;
|
||||||
|
(e: 'request-save', tabId: string): void; // 发送保存请求,携带 tabId
|
||||||
|
(e: 'update:content', payload: { tabId: string; content: string }): void; // 用于 v-model 同步
|
||||||
|
}>();
|
||||||
|
|
||||||
// 从 Store 获取方法
|
|
||||||
const {
|
|
||||||
saveFile, // 现在保存当前激活的标签页
|
|
||||||
closeTab, // 关闭指定标签页 (由 FileEditorTabs 调用)
|
|
||||||
setActiveTab, // 设置激活标签页 (由 FileEditorTabs 调用)
|
|
||||||
// setEditorVisibility, // 不再由此组件控制
|
|
||||||
// closeAllTabs, // 关闭所有标签页 (如果需要,可以添加按钮触发)
|
|
||||||
} = fileEditorStore;
|
|
||||||
|
|
||||||
// --- 计算属性,用于模板绑定 ---
|
// --- 计算属性,用于模板绑定 ---
|
||||||
|
const activeTab = computed((): FileTab | null => {
|
||||||
|
if (!props.activeTabId) return null;
|
||||||
|
return props.tabs.find(tab => tab.id === props.activeTabId) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monaco Editor 的 v-model 处理
|
||||||
|
const localEditorContent = ref('');
|
||||||
|
|
||||||
|
// 监听 activeTab 的变化,重置 localEditorContent
|
||||||
|
watch(activeTab, (newTab) => {
|
||||||
|
// console.log('[EditorContainer] Active tab changed, updating local content.');
|
||||||
|
localEditorContent.value = newTab?.content ?? '';
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 监听 activeTab 内容的变化 (处理异步加载完成的情况)
|
||||||
|
watch(() => activeTab.value?.content, (newContent) => {
|
||||||
|
// console.log('[EditorContainer] Active tab content changed, updating local content.');
|
||||||
|
if (localEditorContent.value !== newContent) {
|
||||||
|
localEditorContent.value = newContent ?? '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当本地编辑器内容变化时,通知父组件 (WorkspaceView)
|
||||||
|
watch(localEditorContent, (newContent) => {
|
||||||
|
// console.log('[EditorContainer] Local content changed, checking if emit needed.');
|
||||||
|
if (activeTab.value && newContent !== activeTab.value.content) {
|
||||||
|
// console.log(`[EditorContainer] Emitting update:content for tab ${activeTab.value.id}`);
|
||||||
|
// 只有当内容实际改变时才发出事件
|
||||||
|
emit('update:content', { tabId: activeTab.value.id, content: newContent });
|
||||||
|
// 注意:isModified 状态应该由 Store 根据 content 和 originalContent 计算
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// orderedTabs 直接使用 props
|
||||||
|
const orderedTabs = computed(() => props.tabs);
|
||||||
|
|
||||||
|
|
||||||
const currentTabIsLoading = computed(() => activeTab.value?.isLoading ?? false);
|
const currentTabIsLoading = computed(() => activeTab.value?.isLoading ?? false);
|
||||||
const currentTabLoadingError = computed(() => activeTab.value?.loadingError ?? null);
|
const currentTabLoadingError = computed(() => activeTab.value?.loadingError ?? null);
|
||||||
const currentTabIsSaving = computed(() => activeTab.value?.isSaving ?? false);
|
const currentTabIsSaving = computed(() => activeTab.value?.isSaving ?? false);
|
||||||
@@ -38,9 +83,8 @@ const currentTabIsModified = computed(() => activeTab.value?.isModified ?? false
|
|||||||
|
|
||||||
// --- 事件处理 ---
|
// --- 事件处理 ---
|
||||||
const handleSaveRequest = () => {
|
const handleSaveRequest = () => {
|
||||||
// saveFile() 默认保存当前激活的标签页
|
if (activeTab.value) {
|
||||||
if (activeTab.value) { // 确保有活动标签才保存
|
emit('request-save', activeTab.value.id); // 发出保存请求事件
|
||||||
saveFile();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,9 +101,9 @@ const handleSaveRequest = () => {
|
|||||||
<!-- 1. 标签栏 -->
|
<!-- 1. 标签栏 -->
|
||||||
<FileEditorTabs
|
<FileEditorTabs
|
||||||
:tabs="orderedTabs"
|
:tabs="orderedTabs"
|
||||||
:active-tab-id="activeTab?.id ?? null"
|
:active-tab-id="props.activeTabId"
|
||||||
@activate-tab="setActiveTab"
|
@activate-tab="(tabId: string) => emit('activate-tab', tabId)"
|
||||||
@close-tab="closeTab"
|
@close-tab="(tabId: string) => emit('close-tab', tabId)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
|
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
|
||||||
@@ -73,7 +117,7 @@ const handleSaveRequest = () => {
|
|||||||
<span v-if="currentTabSaveStatus === '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="currentTabSaveStatus === '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="currentTabSaveStatus === 'error'" class="save-status error">❌ {{ t('fileManager.saveError') }}: {{ currentTabSaveError }}</span>
|
<span v-if="currentTabSaveStatus === 'error'" class="save-status error">❌ {{ t('fileManager.saveError') }}: {{ currentTabSaveError }}</span>
|
||||||
<button @click="handleSaveRequest" :disabled="currentTabIsSaving || currentTabIsLoading || !!currentTabLoadingError || !activeTab" class="save-btn">
|
<button @click="handleSaveRequest" :disabled="currentTabIsSaving || currentTabIsLoading || !!currentTabLoadingError || !activeTab || !currentTabIsModified" class="save-btn">
|
||||||
{{ currentTabIsSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
{{ currentTabIsSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
||||||
</button>
|
</button>
|
||||||
<!-- 关闭/最小化按钮已移除 -->
|
<!-- 关闭/最小化按钮已移除 -->
|
||||||
@@ -92,7 +136,7 @@ const handleSaveRequest = () => {
|
|||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
v-else-if="activeTab"
|
v-else-if="activeTab"
|
||||||
:key="activeTab.id"
|
:key="activeTab.id"
|
||||||
v-model="activeEditorContent"
|
v-model="localEditorContent"
|
||||||
:language="currentTabLanguage"
|
:language="currentTabLanguage"
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
class="editor-instance"
|
class="editor-instance"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import type { EditorTab } from '../stores/fileEditor.store'; // 导入 EditorTab 类型
|
import type { FileTab } from '../stores/fileEditor.store'; // 导入 FileTab 类型
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
tabs: {
|
tabs: {
|
||||||
type: Array as PropType<EditorTab[]>,
|
type: Array as PropType<FileTab[]>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
activeTabId: {
|
activeTabId: {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watchEffect, type PropType, readonly } from 'vue'; // 恢复导入
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watchEffect, type PropType, readonly } from 'vue'; // 恢复导入
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router'; // 保留用于生成下载 URL (如果下载逻辑移动则可移除)
|
import { useRoute } from 'vue-router'; // 保留用于生成下载 URL (如果下载逻辑移动则可移除)
|
||||||
|
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||||
// 导入 SFTP Actions 工厂函数和所需的类型
|
// 导入 SFTP Actions 工厂函数和所需的类型
|
||||||
import { createSftpActionsManager, type WebSocketDependencies } from '../composables/useSftpActions';
|
import { createSftpActionsManager, type WebSocketDependencies } from '../composables/useSftpActions';
|
||||||
import { useFileUploader } from '../composables/useFileUploader';
|
import { useFileUploader } from '../composables/useFileUploader';
|
||||||
// import { useFileEditor } from '../composables/useFileEditor'; // 移除旧的 composable 导入
|
// import { useFileEditor } from '../composables/useFileEditor'; // 移除旧的 composable 导入
|
||||||
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入新的 Store
|
import { useFileEditorStore, type FileInfo } from '../stores/fileEditor.store'; // 导入新的 Store 和 FileInfo 类型
|
||||||
|
import { useSessionStore } from '../stores/session.store'; // 导入 Session Store
|
||||||
|
import { useSettingsStore } from '../stores/settings.store'; // 导入 Settings Store
|
||||||
// WebSocket composable 不再直接使用
|
// WebSocket composable 不再直接使用
|
||||||
import FileUploadPopup from './FileUploadPopup.vue';
|
import FileUploadPopup from './FileUploadPopup.vue';
|
||||||
// import FileEditorOverlay from './FileEditorOverlay.vue'; // 不再在此处渲染
|
// import FileEditorOverlay from './FileEditorOverlay.vue'; // 不再在此处渲染
|
||||||
@@ -83,8 +86,13 @@ const {
|
|||||||
props.wsDeps // 传递注入的 WebSocket 依赖项
|
props.wsDeps // 传递注入的 WebSocket 依赖项
|
||||||
);
|
);
|
||||||
|
|
||||||
// 实例化新的文件编辑器 Store
|
// 实例化 Stores
|
||||||
const fileEditorStore = useFileEditorStore();
|
const fileEditorStore = useFileEditorStore(); // 用于共享模式
|
||||||
|
const sessionStore = useSessionStore(); // 用于独立模式
|
||||||
|
const settingsStore = useSettingsStore(); // 用于获取设置
|
||||||
|
|
||||||
|
// 从 Settings Store 获取共享设置
|
||||||
|
const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore); // 使用 storeToRefs 保持响应性
|
||||||
|
|
||||||
// 文件编辑器模块 - Needs file operations from sftpManager
|
// 文件编辑器模块 - Needs file operations from sftpManager
|
||||||
// const { // 移除旧的 composable 解构
|
// const { // 移除旧的 composable 解构
|
||||||
@@ -299,8 +307,17 @@ const handleItemClick = (event: MouseEvent, item: FileListItem) => { // item 已
|
|||||||
loadDirectory(newPath); // Use loadDirectory from props
|
loadDirectory(newPath); // Use loadDirectory from props
|
||||||
} else if (item.attrs.isFile) {
|
} else if (item.attrs.isFile) {
|
||||||
const filePath = joinPath(currentPath.value, item.filename); // Use joinPath from props
|
const filePath = joinPath(currentPath.value, item.filename); // Use joinPath from props
|
||||||
// 调用全局 Store 的 openFile,并传入 sessionId
|
const fileInfo: FileInfo = { name: item.filename, fullPath: filePath };
|
||||||
|
|
||||||
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
|
// 共享模式:调用全局 fileEditorStore
|
||||||
|
console.log(`[FileManager ${props.sessionId}] Opening file in shared mode: ${filePath}`);
|
||||||
fileEditorStore.openFile(filePath, props.sessionId);
|
fileEditorStore.openFile(filePath, props.sessionId);
|
||||||
|
} else {
|
||||||
|
// 独立模式:调用 sessionStore
|
||||||
|
console.log(`[FileManager ${props.sessionId}] Opening file in independent mode: ${filePath}`);
|
||||||
|
sessionStore.openFileInSession(props.sessionId, fileInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -200,7 +200,8 @@
|
|||||||
"deleteMultiple": "Delete {count} items",
|
"deleteMultiple": "Delete {count} items",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save"
|
"save": "Save",
|
||||||
|
"closeTab": "Close Tab"
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -364,6 +365,18 @@
|
|||||||
"saveFailed": "Failed to save popup editor setting."
|
"saveFailed": "Failed to save popup editor setting."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shareEditorTabs": {
|
||||||
|
"title": "Editor Tabs",
|
||||||
|
"enableLabel": "Share editor tabs across all sessions",
|
||||||
|
"description": "If enabled, all SSH sessions will share the same set of open file editor tabs. If disabled, each session will have its own independent set of tabs.",
|
||||||
|
"saveButton": "Save Setting",
|
||||||
|
"success": {
|
||||||
|
"saved": "Editor tab sharing setting saved successfully."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"saveFailed": "Failed to save editor tab sharing setting."
|
||||||
|
}
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"title": "Language Settings",
|
"title": "Language Settings",
|
||||||
"selectLabel": "Interface Language:",
|
"selectLabel": "Interface Language:",
|
||||||
|
|||||||
@@ -202,7 +202,8 @@
|
|||||||
"deleteMultiple": "删除 {count} 个项目",
|
"deleteMultiple": "删除 {count} 个项目",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"save": "保存"
|
"save": "保存",
|
||||||
|
"closeTab": "关闭标签页"
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
@@ -367,6 +368,18 @@
|
|||||||
"saveFailed": "保存弹窗编辑器设置失败。"
|
"saveFailed": "保存弹窗编辑器设置失败。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shareEditorTabs": {
|
||||||
|
"title": "编辑器标签页",
|
||||||
|
"enableLabel": "在所有会话间共享编辑器标签页",
|
||||||
|
"description": "如果启用,所有 SSH 会话将共享同一组打开的文件编辑器标签页。如果禁用,每个会话将拥有自己独立的一组标签页。",
|
||||||
|
"saveButton": "保存设置",
|
||||||
|
"success": {
|
||||||
|
"saved": "编辑器标签页共享设置已成功保存。"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"saveFailed": "保存编辑器标签页共享设置失败。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"title": "语言设置",
|
"title": "语言设置",
|
||||||
"selectLabel": "界面语言:",
|
"selectLabel": "界面语言:",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ app.use(i18n); // 使用 i18n
|
|||||||
const settingsStore = useSettingsStore(pinia); // 需要传递 pinia 实例
|
const settingsStore = useSettingsStore(pinia); // 需要传递 pinia 实例
|
||||||
settingsStore.loadInitialSettings().then(() => {
|
settingsStore.loadInitialSettings().then(() => {
|
||||||
app.mount('#app'); // 确保设置加载完成后再挂载
|
app.mount('#app'); // 确保设置加载完成后再挂载
|
||||||
}).catch(error => {
|
}).catch((error: unknown) => { // 为 error 添加 unknown 类型
|
||||||
console.error("Failed to load initial settings before mounting app:", error);
|
console.error("Failed to load initial settings before mounting app:", error);
|
||||||
// 即使加载失败,也尝试挂载应用,可能使用默认设置
|
// 即使加载失败,也尝试挂载应用,可能使用默认设置
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { useSessionStore } from './session.store'; // 导入会话 Store
|
import { useSessionStore } from './session.store'; // 导入会话 Store
|
||||||
import type { EditorFileContent, SaveStatus } from '../types/sftp.types'; // 保持导入 SaveStatus
|
import type { EditorFileContent, SaveStatus } from '../types/sftp.types'; // 保持导入 SaveStatus
|
||||||
|
|
||||||
// --- 新类型定义 ---
|
// --- 类型定义 ---
|
||||||
export interface EditorTab {
|
// 文件信息,用于打开文件操作
|
||||||
|
export interface FileInfo {
|
||||||
|
name: string;
|
||||||
|
fullPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑器标签页状态
|
||||||
|
export interface FileTab {
|
||||||
id: string; // 唯一标识符,例如 `${sessionId}:${filePath}`
|
id: string; // 唯一标识符,例如 `${sessionId}:${filePath}`
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
@@ -20,8 +27,12 @@ export interface EditorTab {
|
|||||||
saveStatus: SaveStatus;
|
saveStatus: SaveStatus;
|
||||||
saveError: string | null;
|
saveError: string | null;
|
||||||
isModified: boolean; // 内容是否已修改
|
isModified: boolean; // 内容是否已修改
|
||||||
|
// 添加 sessionId 以便在共享模式下区分来源 (虽然此 store 主要用于共享模式)
|
||||||
|
// 或者在独立模式下,此 store 可能不被使用或以不同方式使用
|
||||||
|
// sessionId: string; // 暂时不加,因为 session.store 已处理独立模式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 辅助函数:根据文件名获取语言 (保持不变)
|
// 辅助函数:根据文件名获取语言 (保持不变)
|
||||||
const getLanguageFromFilename = (filename: string): string => {
|
const getLanguageFromFilename = (filename: string): string => {
|
||||||
const extension = filename.split('.').pop()?.toLowerCase();
|
const extension = filename.split('.').pop()?.toLowerCase();
|
||||||
@@ -65,7 +76,7 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
// --- 多标签状态 ---
|
// --- 多标签状态 ---
|
||||||
const tabs = ref(new Map<string, EditorTab>()); // 存储所有打开的标签页
|
const tabs = ref(new Map<string, FileTab>()); // 存储所有打开的标签页 (使用 FileTab)
|
||||||
const activeTabId = ref<string | null>(null); // 当前激活的标签页 ID
|
const activeTabId = ref<string | null>(null); // 当前激活的标签页 ID
|
||||||
// const editorVisibleState = ref<'visible' | 'minimized' | 'closed'>('closed'); // 移除,面板可见性由布局控制
|
// const editorVisibleState = ref<'visible' | 'minimized' | 'closed'>('closed'); // 移除,面板可见性由布局控制
|
||||||
const popupTrigger = ref(0); // 新增:用于触发弹窗显示的信号
|
const popupTrigger = ref(0); // 新增:用于触发弹窗显示的信号
|
||||||
@@ -81,7 +92,8 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
get: () => activeTab.value?.content ?? '',
|
get: () => activeTab.value?.content ?? '',
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if (activeTab.value) {
|
if (activeTab.value) {
|
||||||
updateContent(value); // 调用 action 更新内容和修改状态
|
// 调用新的 updateFileContent action,并传递 tabId
|
||||||
|
updateFileContent(activeTab.value.id, value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -100,8 +112,10 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
|
|
||||||
// 打开或切换到文件标签页
|
// 打开或切换到文件标签页
|
||||||
const openFile = async (targetFilePath: string, sessionId: string) => {
|
const openFile = async (targetFilePath: string, sessionId: string) => {
|
||||||
|
// 在共享模式下,我们仍然需要 sessionId 来构建唯一的 tabId
|
||||||
|
// 并与 SFTP 管理器关联
|
||||||
const tabId = `${sessionId}:${targetFilePath}`;
|
const tabId = `${sessionId}:${targetFilePath}`;
|
||||||
console.log(`[文件编辑器 Store] 尝试打开文件: ${targetFilePath} (会话: ${sessionId}, Tab ID: ${tabId})`);
|
console.log(`[文件编辑器 Store - 共享模式] 尝试打开文件: ${targetFilePath} (会话: ${sessionId}, Tab ID: ${tabId})`);
|
||||||
|
|
||||||
// 移除确保编辑器可见的逻辑
|
// 移除确保编辑器可见的逻辑
|
||||||
// if (editorVisibleState.value === 'closed') {
|
// if (editorVisibleState.value === 'closed') {
|
||||||
@@ -118,7 +132,7 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建新标签页
|
// 创建新标签页
|
||||||
const newTab: EditorTab = {
|
const newTab: FileTab = {
|
||||||
id: tabId,
|
id: tabId,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
filePath: targetFilePath,
|
filePath: targetFilePath,
|
||||||
@@ -133,6 +147,7 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
saveStatus: 'idle',
|
saveStatus: 'idle',
|
||||||
saveError: null,
|
saveError: null,
|
||||||
isModified: false,
|
isModified: false,
|
||||||
|
// sessionId: sessionId, // 记录来源会话
|
||||||
};
|
};
|
||||||
tabs.value.set(tabId, newTab);
|
tabs.value.set(tabId, newTab);
|
||||||
setActiveTab(tabId); // 激活新标签页
|
setActiveTab(tabId); // 激活新标签页
|
||||||
@@ -343,20 +358,24 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新当前激活标签页的内容 (由 v-model 调用)
|
// 更新指定标签页的内容 (由 FileEditorContainer 的 v-model 触发)
|
||||||
const updateContent = (newContent: string) => {
|
const updateFileContent = (tabId: string, newContent: string) => {
|
||||||
if (activeTab.value && !activeTab.value.isLoading) {
|
const tab = tabs.value.get(tabId);
|
||||||
activeTab.value.content = newContent;
|
if (tab && !tab.isLoading) {
|
||||||
|
tab.content = newContent;
|
||||||
// 检查是否修改
|
// 检查是否修改
|
||||||
activeTab.value.isModified = activeTab.value.content !== activeTab.value.originalContent;
|
tab.isModified = tab.content !== tab.originalContent;
|
||||||
// 当用户编辑时,重置保存状态
|
// 当用户编辑时,重置保存状态
|
||||||
if (activeTab.value.saveStatus === 'success' || activeTab.value.saveStatus === 'error') {
|
if (tab.saveStatus === 'success' || tab.saveStatus === 'error') {
|
||||||
activeTab.value.saveStatus = 'idle';
|
tab.saveStatus = 'idle';
|
||||||
activeTab.value.saveError = null;
|
tab.saveError = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 移除旧的 updateContent,因为它只更新活动标签页
|
||||||
|
// const updateContent = (newContent: string) => { ... };
|
||||||
|
|
||||||
// 监听会话关闭事件,移除相关标签页
|
// 监听会话关闭事件,移除相关标签页
|
||||||
watch(() => sessionStore.sessions, (newSessions, oldSessions) => {
|
watch(() => sessionStore.sessions, (newSessions, oldSessions) => {
|
||||||
const closedSessionIds = new Set<string>();
|
const closedSessionIds = new Set<string>();
|
||||||
@@ -412,7 +431,7 @@ export const useFileEditorStore = defineStore('fileEditor', () => {
|
|||||||
closeTab,
|
closeTab,
|
||||||
closeAllTabs,
|
closeAllTabs,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
updateContent, // 暴露给 v-model 使用
|
updateFileContent, // 暴露新的更新方法
|
||||||
// setEditorVisibility, // 移除
|
// setEditorVisibility, // 移除
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { ref, computed, shallowRef, type Ref } from 'vue'; // 导入 shallowRef
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useConnectionsStore, type ConnectionInfo } from './connections.store';
|
import { useConnectionsStore, type ConnectionInfo } from './connections.store';
|
||||||
|
// 导入文件编辑器相关的类型
|
||||||
|
import type { FileTab, FileInfo } from './fileEditor.store'; // 导入 FileTab 和 FileInfo
|
||||||
|
|
||||||
// 导入管理器工厂函数 (用于创建实例)
|
// 导入管理器工厂函数 (用于创建实例)
|
||||||
// 导入 WsConnectionStatus 类型
|
// 导入 WsConnectionStatus 类型
|
||||||
@@ -15,6 +17,39 @@ function generateSessionId(): string {
|
|||||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数:根据文件名获取语言 (从 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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- 类型定义 (导出以便其他模块使用) ---
|
// --- 类型定义 (导出以便其他模块使用) ---
|
||||||
export type WsManagerInstance = ReturnType<typeof createWebSocketConnectionManager>;
|
export type WsManagerInstance = ReturnType<typeof createWebSocketConnectionManager>;
|
||||||
export type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
|
export type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
|
||||||
@@ -29,7 +64,10 @@ export interface SessionState {
|
|||||||
sftpManager: SftpManagerInstance;
|
sftpManager: SftpManagerInstance;
|
||||||
terminalManager: SshTerminalInstance;
|
terminalManager: SshTerminalInstance;
|
||||||
statusMonitorManager: StatusMonitorInstance;
|
statusMonitorManager: StatusMonitorInstance;
|
||||||
currentSftpPath: Ref<string>; // SFTP 当前路径 (可能需要保留在此处或移至 SftpManager 内部)
|
currentSftpPath: Ref<string>; // SFTP 当前路径
|
||||||
|
// --- 新增:独立编辑器状态 ---
|
||||||
|
editorTabs: Ref<FileTab[]>; // 编辑器标签页列表
|
||||||
|
activeEditorTabId: Ref<string | null>; // 当前活动的编辑器标签页 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为标签栏定义包含状态的类型
|
// 为标签栏定义包含状态的类型
|
||||||
@@ -129,6 +167,9 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
terminalManager: terminalManager,
|
terminalManager: terminalManager,
|
||||||
statusMonitorManager: statusMonitorManager,
|
statusMonitorManager: statusMonitorManager,
|
||||||
currentSftpPath: currentSftpPath,
|
currentSftpPath: currentSftpPath,
|
||||||
|
// --- 初始化编辑器状态 ---
|
||||||
|
editorTabs: ref([]), // 初始化为空数组
|
||||||
|
activeEditorTabId: ref(null), // 初始化为 null
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. 添加到 Map 并激活 (需要创建 Map 的新实例以触发 shallowRef 更新)
|
// 3. 添加到 Map 并激活 (需要创建 Map 的新实例以触发 shallowRef 更新)
|
||||||
@@ -161,6 +202,88 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定会话中编辑器标签页的内容
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sftpManager = session.sftpManager;
|
||||||
|
console.log(`[SessionStore] 开始保存文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tab.id})`);
|
||||||
|
tab.isSaving = true;
|
||||||
|
tab.saveStatus = 'saving';
|
||||||
|
tab.saveError = null;
|
||||||
|
|
||||||
|
const contentToSave = tab.content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sftpManager.writeFile(tab.filePath, contentToSave);
|
||||||
|
console.log(`[SessionStore] 文件 ${tab.filePath} (会话 ${sessionId}) 保存成功。`);
|
||||||
|
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 的会话标签页
|
* 关闭指定 ID 的会话标签页
|
||||||
*/
|
*/
|
||||||
@@ -181,6 +304,7 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 terminalManager.cleanup()`);
|
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 terminalManager.cleanup()`);
|
||||||
sessionToClose.statusMonitorManager.cleanup();
|
sessionToClose.statusMonitorManager.cleanup();
|
||||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 statusMonitorManager.cleanup()`);
|
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 statusMonitorManager.cleanup()`);
|
||||||
|
// TODO: 清理编辑器相关资源?例如提示保存未保存的文件
|
||||||
|
|
||||||
// 2. 从 Map 中移除会话 (需要创建 Map 的新实例以触发 shallowRef 更新)
|
// 2. 从 Map 中移除会话 (需要创建 Map 的新实例以触发 shallowRef 更新)
|
||||||
const newSessionsMap = new Map(sessions.value);
|
const newSessionsMap = new Map(sessions.value);
|
||||||
@@ -261,6 +385,162 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
activeSessionId.value = null;
|
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 {
|
||||||
|
// 创建新标签页
|
||||||
|
const newTab: FileTab = {
|
||||||
|
id: generateSessionId(), // 复用会话 ID 生成逻辑创建唯一标签页 ID
|
||||||
|
filename: fileInfo.name, // 使用 filename 匹配 FileTab 接口
|
||||||
|
filePath: fileInfo.fullPath, // 使用 filePath 匹配 FileTab 接口
|
||||||
|
// content, originalContent, language, encoding 将在 FileEditorContainer 或 fileEditor.store 中处理
|
||||||
|
content: '', // 初始内容为空
|
||||||
|
originalContent: '', // 初始原始内容为空
|
||||||
|
language: 'plaintext', // 初始语言,稍后会根据文件名更新
|
||||||
|
encoding: 'utf8', // 默认编码
|
||||||
|
isModified: false, // 使用 isModified 匹配 FileTab 接口
|
||||||
|
isLoading: false, // 初始化为 boolean
|
||||||
|
loadingError: null, // 使用 loadingError 匹配 FileTab 接口
|
||||||
|
// --- 编辑器状态相关 ---
|
||||||
|
isSaving: false,
|
||||||
|
saveStatus: 'idle',
|
||||||
|
saveError: null,
|
||||||
|
// --- 关联会话 ID ---
|
||||||
|
sessionId: sessionId, // 记录此标签页属于哪个会话
|
||||||
|
};
|
||||||
|
// 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 tabToLoad = session.editorTabs.value.find(t => t.id === newTab.id);
|
||||||
|
if (!tabToLoad) return; // Tab might have been closed quickly
|
||||||
|
|
||||||
|
tabToLoad.isLoading = true;
|
||||||
|
tabToLoad.loadingError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sftpManager = session.sftpManager; // 获取当前会话的 sftpManager
|
||||||
|
const fileData = await sftpManager.readFile(fileInfo.fullPath);
|
||||||
|
console.log(`[SessionStore ${sessionId}] 文件 ${fileInfo.fullPath} 读取成功。编码: ${fileData.encoding}`);
|
||||||
|
|
||||||
|
let decodedContent = '';
|
||||||
|
let finalEncoding: 'utf8' | 'base64' = 'utf8';
|
||||||
|
|
||||||
|
if (fileData.encoding === 'base64') {
|
||||||
|
finalEncoding = '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(`[SessionStore ${sessionId}] Base64 解码错误 for ${fileInfo.fullPath}:`, decodeError);
|
||||||
|
tabToLoad.loadingError = t('fileManager.errors.fileDecodeError');
|
||||||
|
decodedContent = `// ${tabToLoad.loadingError}\n// Original Base64 content:\n${fileData.content}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalEncoding = 'utf8';
|
||||||
|
decodedContent = fileData.content;
|
||||||
|
if (decodedContent.includes('\uFFFD')) {
|
||||||
|
console.warn(`[SessionStore ${sessionId}] 文件 ${fileInfo.fullPath} 内容可能包含无效字符。`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新标签页状态
|
||||||
|
tabToLoad.content = decodedContent;
|
||||||
|
tabToLoad.originalContent = decodedContent;
|
||||||
|
tabToLoad.encoding = finalEncoding;
|
||||||
|
tabToLoad.language = getLanguageFromFilename(fileInfo.name); // 根据文件名设置语言
|
||||||
|
tabToLoad.isModified = false;
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[SessionStore ${sessionId}] 读取文件 ${fileInfo.fullPath} 失败:`, err);
|
||||||
|
tabToLoad.loadingError = `${t('fileManager.errors.readFileFailed')}: ${err.message || err}`;
|
||||||
|
tabToLoad.content = `// ${tabToLoad.loadingError}`;
|
||||||
|
} finally {
|
||||||
|
tabToLoad.isLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
sessions,
|
sessions,
|
||||||
@@ -276,5 +556,11 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
handleConnectRequest,
|
handleConnectRequest,
|
||||||
handleOpenNewSession,
|
handleOpenNewSession,
|
||||||
cleanupAllSessions,
|
cleanupAllSessions,
|
||||||
|
// --- 新增:导出编辑器相关 Actions ---
|
||||||
|
openFileInSession,
|
||||||
|
closeEditorTabInSession,
|
||||||
|
setActiveEditorTabInSession,
|
||||||
|
updateFileContentInSession, // 导出更新内容 Action
|
||||||
|
saveFileInSession, // 导出保存文件 Action
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ interface SettingsState {
|
|||||||
ipWhitelist: string;
|
ipWhitelist: string;
|
||||||
maxLoginAttempts: string;
|
maxLoginAttempts: string;
|
||||||
loginBanDuration: string;
|
loginBanDuration: string;
|
||||||
showPopupFileEditor: string; // 新增设置项,存储为 'true' 或 'false'
|
showPopupFileEditor: string; // 弹窗编辑器设置
|
||||||
|
shareFileEditorTabs?: string; // 新增:共享编辑器标签页设置 ('true'/'false')
|
||||||
// Add other settings keys here as needed
|
// Add other settings keys here as needed
|
||||||
[key: string]: string; // Allow other string settings
|
[key: string]: string | undefined; // Allow other string settings, make value optional
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
@@ -44,6 +45,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
console.log('[SettingsStore] Setting default for showPopupFileEditor: true');
|
console.log('[SettingsStore] Setting default for showPopupFileEditor: true');
|
||||||
settings.value.showPopupFileEditor = 'true'; // 默认为 true
|
settings.value.showPopupFileEditor = 'true'; // 默认为 true
|
||||||
}
|
}
|
||||||
|
// 共享编辑器标签页设置
|
||||||
|
if (settings.value.shareFileEditorTabs === undefined) {
|
||||||
|
console.log('[SettingsStore] Setting default for shareFileEditorTabs: true');
|
||||||
|
settings.value.shareFileEditorTabs = 'true'; // 默认为 true (共享)
|
||||||
|
}
|
||||||
|
|
||||||
// --- 语言设置 ---
|
// --- 语言设置 ---
|
||||||
// Determine and apply language
|
// Determine and apply language
|
||||||
@@ -131,15 +137,25 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// Getter for the popup editor setting, returning boolean
|
// Getter for the popup editor setting, returning boolean
|
||||||
const showPopupFileEditorBoolean = computed(() => {
|
const showPopupFileEditorBoolean = computed(() => {
|
||||||
// 默认为 true,除非明确设置为 'false'
|
// 默认为 true,除非明确设置为 'false'
|
||||||
return settings.value.showPopupFileEditor !== 'false';
|
// 默认为 true (共享),除非明确设置为 'false'
|
||||||
|
// 默认为 true (共享),除非明确设置为 'false'
|
||||||
|
return settings.value.shareFileEditorTabs !== 'false';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Getter for sharing setting, returning boolean
|
||||||
|
const shareFileEditorTabsBoolean = computed(() => {
|
||||||
|
// 默认为 true (共享),除非明确设置为 'false'
|
||||||
|
return settings.value.shareFileEditorTabs !== 'false';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
language, // Expose language getter
|
language, // Expose language getter
|
||||||
showPopupFileEditorBoolean, // Expose boolean getter for popup editor setting
|
showPopupFileEditorBoolean, // Expose boolean getter for popup editor setting
|
||||||
|
shareFileEditorTabsBoolean: shareFileEditorTabsBoolean, // Expose boolean getter for sharing setting
|
||||||
loadInitialSettings,
|
loadInitialSettings,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
updateMultipleSettings,
|
updateMultipleSettings,
|
||||||
|
|||||||
@@ -55,6 +55,21 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 共享编辑器标签页设置 -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>{{ $t('settings.shareEditorTabs.title') }}</h2>
|
||||||
|
<form @submit.prevent="handleUpdateShareTabsSetting">
|
||||||
|
<div class="form-group form-group-checkbox">
|
||||||
|
<input type="checkbox" id="shareEditorTabs" v-model="shareTabsEnabled">
|
||||||
|
<label for="shareEditorTabs">{{ $t('settings.shareEditorTabs.enableLabel') }}</label>
|
||||||
|
</div>
|
||||||
|
<p class="setting-description">{{ $t('settings.shareEditorTabs.description') }}</p>
|
||||||
|
<button type="submit" :disabled="shareTabsLoading">{{ shareTabsLoading ? $t('common.saving') : $t('settings.shareEditorTabs.saveButton') }}</button>
|
||||||
|
<p v-if="shareTabsMessage" :class="{ 'success-message': shareTabsSuccess, 'error-message': !shareTabsSuccess }">{{ shareTabsMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
@@ -197,6 +212,7 @@ import { ref, onMounted, computed, reactive, watch, toRefs } from 'vue';
|
|||||||
import { useAuthStore } from '../stores/auth.store';
|
import { useAuthStore } from '../stores/auth.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||||
// setLocale is handled by the store now
|
// setLocale is handled by the store now
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
@@ -206,7 +222,8 @@ const settingsStore = useSettingsStore();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// --- Reactive state from store ---
|
// --- Reactive state from store ---
|
||||||
const { settings, isLoading: settingsLoading, error: settingsError, showPopupFileEditorBoolean } = toRefs(settingsStore);
|
// 使用 storeToRefs 获取响应式 getter
|
||||||
|
const { settings, isLoading: settingsLoading, error: settingsError, showPopupFileEditorBoolean, shareFileEditorTabsBoolean } = storeToRefs(settingsStore);
|
||||||
|
|
||||||
// --- Local state for forms ---
|
// --- Local state for forms ---
|
||||||
const ipWhitelistInput = ref('');
|
const ipWhitelistInput = ref('');
|
||||||
@@ -230,6 +247,10 @@ const blacklistSettingsSuccess = ref(false);
|
|||||||
const popupEditorLoading = ref(false);
|
const popupEditorLoading = ref(false);
|
||||||
const popupEditorMessage = ref('');
|
const popupEditorMessage = ref('');
|
||||||
const popupEditorSuccess = ref(false);
|
const popupEditorSuccess = ref(false);
|
||||||
|
const shareTabsEnabled = ref(true); // 本地状态,用于共享标签页 v-model
|
||||||
|
const shareTabsLoading = ref(false);
|
||||||
|
const shareTabsMessage = ref('');
|
||||||
|
const shareTabsSuccess = ref(false);
|
||||||
|
|
||||||
// --- Watcher to sync local form state with store state ---
|
// --- Watcher to sync local form state with store state ---
|
||||||
watch(settings, (newSettings) => {
|
watch(settings, (newSettings) => {
|
||||||
@@ -239,6 +260,8 @@ watch(settings, (newSettings) => {
|
|||||||
blacklistSettingsForm.loginBanDuration = newSettings.loginBanDuration || '300';
|
blacklistSettingsForm.loginBanDuration = newSettings.loginBanDuration || '300';
|
||||||
// 同步弹窗编辑器设置
|
// 同步弹窗编辑器设置
|
||||||
popupEditorEnabled.value = showPopupFileEditorBoolean.value;
|
popupEditorEnabled.value = showPopupFileEditorBoolean.value;
|
||||||
|
// 同步共享标签页设置
|
||||||
|
shareTabsEnabled.value = shareFileEditorTabsBoolean.value;
|
||||||
}, { deep: true, immediate: true }); // immediate: true to run on initial load
|
}, { deep: true, immediate: true }); // immediate: true to run on initial load
|
||||||
|
|
||||||
// --- Popup Editor setting method ---
|
// --- Popup Editor setting method ---
|
||||||
@@ -263,6 +286,27 @@ const handleUpdatePopupEditorSetting = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Share Editor Tabs setting method ---
|
||||||
|
const handleUpdateShareTabsSetting = async () => {
|
||||||
|
shareTabsLoading.value = true;
|
||||||
|
shareTabsMessage.value = '';
|
||||||
|
shareTabsSuccess.value = false;
|
||||||
|
try {
|
||||||
|
const valueToSave = shareTabsEnabled.value ? 'true' : 'false';
|
||||||
|
await settingsStore.updateSetting('shareFileEditorTabs', valueToSave);
|
||||||
|
shareTabsMessage.value = t('settings.shareEditorTabs.success.saved'); // 需要添加翻译
|
||||||
|
shareTabsSuccess.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新共享编辑器标签页设置失败:', error);
|
||||||
|
shareTabsMessage.value = error.message || t('settings.shareEditorTabs.error.saveFailed'); // 需要添加翻译
|
||||||
|
shareTabsSuccess.value = false;
|
||||||
|
// 保存失败时,将本地复选框状态恢复为 Store 中的状态
|
||||||
|
shareTabsEnabled.value = shareFileEditorTabsBoolean.value;
|
||||||
|
} finally {
|
||||||
|
shareTabsLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Passkey state & methods --- (Keep as is)
|
// --- Passkey state & methods --- (Keep as is)
|
||||||
const passkeyName = ref('');
|
const passkeyName = ref('');
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import TerminalTabBar from '../components/TerminalTabBar.vue';
|
|||||||
import CommandInputBar from '../components/CommandInputBar.vue';
|
import CommandInputBar from '../components/CommandInputBar.vue';
|
||||||
import FileEditorContainer from '../components/FileEditorContainer.vue'; // 导入编辑器容器
|
import FileEditorContainer from '../components/FileEditorContainer.vue'; // 导入编辑器容器
|
||||||
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance
|
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance
|
||||||
|
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 Store
|
||||||
|
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入文件编辑器 Store
|
||||||
import type { ConnectionInfo } from '../stores/connections.store';
|
import type { ConnectionInfo } from '../stores/connections.store';
|
||||||
// 导入 splitpanes 组件
|
// 导入 splitpanes 组件
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
import { Splitpanes, Pane } from 'splitpanes';
|
||||||
@@ -20,9 +22,36 @@ import { Splitpanes, Pane } from 'splitpanes';
|
|||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
|
const settingsStore = useSettingsStore(); // 初始化设置 Store
|
||||||
|
const fileEditorStore = useFileEditorStore(); // 初始化文件编辑器 Store (用于共享模式)
|
||||||
|
|
||||||
// --- 从 Store 获取响应式状态和 Getters ---
|
// --- 从 Store 获取响应式状态和 Getters ---
|
||||||
const { sessionTabsWithStatus, activeSessionId, activeSession } = storeToRefs(sessionStore);
|
const { sessionTabsWithStatus, activeSessionId, activeSession } = storeToRefs(sessionStore);
|
||||||
|
const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore); // 获取共享设置
|
||||||
|
const { orderedTabs: globalEditorTabs, activeTabId: globalActiveEditorTabId } = storeToRefs(fileEditorStore); // 获取全局编辑器状态
|
||||||
|
|
||||||
|
// --- 计算属性 (用于动态绑定编辑器 Props) ---
|
||||||
|
// **再次修正:** 确保计算属性在共享模式下严格只依赖全局状态
|
||||||
|
const editorTabs = computed(() => {
|
||||||
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
|
// console.log('[WorkspaceView] Shared Mode: Returning globalEditorTabs');
|
||||||
|
return globalEditorTabs.value; // 共享模式:只依赖全局 store 的 tabs
|
||||||
|
} else {
|
||||||
|
// console.log('[WorkspaceView] Independent Mode: Returning activeSession tabs');
|
||||||
|
return activeSession.value?.editorTabs.value ?? []; // 独立模式:依赖 activeSession
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeEditorTabId = computed(() => {
|
||||||
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
|
// console.log('[WorkspaceView] Shared Mode: Returning globalActiveEditorTabId');
|
||||||
|
return globalActiveEditorTabId.value; // 共享模式:只依赖全局 store 的 activeTabId
|
||||||
|
} else {
|
||||||
|
// console.log('[WorkspaceView] Independent Mode: Returning activeSession activeEditorTabId');
|
||||||
|
return activeSession.value?.activeEditorTabId.value ?? null; // 独立模式:依赖 activeSession
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// --- UI 状态 (保持本地) ---
|
// --- UI 状态 (保持本地) ---
|
||||||
const showAddEditForm = ref(false);
|
const showAddEditForm = ref(false);
|
||||||
@@ -77,6 +106,69 @@ onBeforeUnmount(() => {
|
|||||||
// 可以考虑给用户一个提示
|
// 可以考虑给用户一个提示
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 编辑器操作处理 ---
|
||||||
|
const handleCloseEditorTab = (tabId: string) => {
|
||||||
|
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
||||||
|
console.log(`[WorkspaceView] handleCloseEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
||||||
|
if (isShared) {
|
||||||
|
fileEditorStore.closeTab(tabId);
|
||||||
|
} else {
|
||||||
|
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
||||||
|
if (currentActiveSessionId) {
|
||||||
|
sessionStore.closeEditorTabInSession(currentActiveSessionId, tabId);
|
||||||
|
} else {
|
||||||
|
console.warn('[WorkspaceView] Cannot close editor tab: No active session in independent mode.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivateEditorTab = (tabId: string) => {
|
||||||
|
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
||||||
|
console.log(`[WorkspaceView] handleActivateEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
||||||
|
if (isShared) {
|
||||||
|
fileEditorStore.setActiveTab(tabId);
|
||||||
|
} else {
|
||||||
|
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
||||||
|
if (currentActiveSessionId) {
|
||||||
|
sessionStore.setActiveEditorTabInSession(currentActiveSessionId, tabId);
|
||||||
|
} else {
|
||||||
|
console.warn('[WorkspaceView] Cannot activate editor tab: No active session in independent mode.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理编辑器内容更新事件
|
||||||
|
const handleUpdateEditorContent = (payload: { tabId: string; content: string }) => {
|
||||||
|
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
||||||
|
console.log(`[WorkspaceView] handleUpdateEditorContent for tab ${payload.tabId}, Shared mode: ${isShared}`);
|
||||||
|
if (isShared) {
|
||||||
|
fileEditorStore.updateFileContent(payload.tabId, payload.content);
|
||||||
|
} else {
|
||||||
|
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
||||||
|
if (currentActiveSessionId) {
|
||||||
|
sessionStore.updateFileContentInSession(currentActiveSessionId, payload.tabId, payload.content);
|
||||||
|
} else {
|
||||||
|
console.warn('[WorkspaceView] Cannot update editor content: No active session in independent mode.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理编辑器保存请求事件
|
||||||
|
const handleSaveEditorTab = (tabId: string) => {
|
||||||
|
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
||||||
|
console.log(`[WorkspaceView] handleSaveEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
||||||
|
if (isShared) {
|
||||||
|
fileEditorStore.saveFile(tabId);
|
||||||
|
} else {
|
||||||
|
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
||||||
|
if (currentActiveSessionId) {
|
||||||
|
sessionStore.saveFileInSession(currentActiveSessionId, tabId);
|
||||||
|
} else {
|
||||||
|
console.warn('[WorkspaceView] Cannot save editor tab: No active session in independent mode.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -166,7 +258,15 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<!-- 3. 右侧区域 1 Pane (文件编辑器) -->
|
<!-- 3. 右侧区域 1 Pane (文件编辑器) -->
|
||||||
<pane size="20" min-size="15" class="file-editor-pane"> <!-- 新增编辑器窗格 -->
|
<pane size="20" min-size="15" class="file-editor-pane"> <!-- 新增编辑器窗格 -->
|
||||||
<FileEditorContainer />
|
<FileEditorContainer
|
||||||
|
:tabs="editorTabs"
|
||||||
|
:active-tab-id="activeEditorTabId"
|
||||||
|
:session-id="activeSessionId"
|
||||||
|
@close-tab="handleCloseEditorTab"
|
||||||
|
@activate-tab="handleActivateEditorTab"
|
||||||
|
@update:content="handleUpdateEditorContent"
|
||||||
|
@request-save="handleSaveEditorTab"
|
||||||
|
/>
|
||||||
</pane>
|
</pane>
|
||||||
|
|
||||||
<!-- 4. 右侧区域 2 Pane (状态监视器) -->
|
<!-- 4. 右侧区域 2 Pane (状态监视器) -->
|
||||||
|
|||||||
Reference in New Issue
Block a user