update
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import { RouterLink, RouterView } from 'vue-router';
|
import { RouterLink, RouterView } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAuthStore } from './stores/auth.store';
|
import { useAuthStore } from './stores/auth.store';
|
||||||
|
import { useSettingsStore } from './stores/settings.store'; // 导入设置 Store
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
// 导入通知显示组件
|
// 导入通知显示组件
|
||||||
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
||||||
@@ -10,7 +11,9 @@ import FileEditorOverlay from './components/FileEditorOverlay.vue';
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const settingsStore = useSettingsStore(); // 实例化设置 Store
|
||||||
const { isAuthenticated } = storeToRefs(authStore); // 获取登录状态
|
const { isAuthenticated } = storeToRefs(authStore); // 获取登录状态
|
||||||
|
const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); // 获取弹窗编辑器设置
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
@@ -41,8 +44,8 @@ const handleLogout = () => {
|
|||||||
<!-- 添加全局通知显示 -->
|
<!-- 添加全局通知显示 -->
|
||||||
<UINotificationDisplay />
|
<UINotificationDisplay />
|
||||||
|
|
||||||
<!-- 添加全局文件编辑器弹窗 -->
|
<!-- 根据设置条件渲染全局文件编辑器弹窗 -->
|
||||||
<FileEditorOverlay />
|
<FileEditorOverlay v-if="showPopupFileEditorBoolean" />
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<!-- 使用 t 函数获取应用名称 -->
|
<!-- 使用 t 函数获取应用名称 -->
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
||||||
|
import FileEditorTabs from './FileEditorTabs.vue'; // 导入标签栏组件
|
||||||
|
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入新的 Store
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const fileEditorStore = useFileEditorStore();
|
||||||
|
|
||||||
|
// 从 Store 获取新的多标签状态和方法
|
||||||
|
const {
|
||||||
|
// editorVisibleState, // 可见性由父组件 (WorkspaceView) 控制 Pane 大小决定,不再需要内部状态
|
||||||
|
activeTab, // 当前激活的标签页对象 (computed)
|
||||||
|
activeEditorContent,// 用于 v-model 绑定 (computed)
|
||||||
|
orderedTabs, // 标签页数组 (computed)
|
||||||
|
} = storeToRefs(fileEditorStore);
|
||||||
|
|
||||||
|
// 从 Store 获取方法
|
||||||
|
const {
|
||||||
|
saveFile, // 现在保存当前激活的标签页
|
||||||
|
closeTab, // 关闭指定标签页 (由 FileEditorTabs 调用)
|
||||||
|
setActiveTab, // 设置激活标签页 (由 FileEditorTabs 调用)
|
||||||
|
// setEditorVisibility, // 不再由此组件控制
|
||||||
|
// closeAllTabs, // 关闭所有标签页 (如果需要,可以添加按钮触发)
|
||||||
|
} = fileEditorStore;
|
||||||
|
|
||||||
|
// --- 计算属性,用于模板绑定 ---
|
||||||
|
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); // 用于显示修改状态
|
||||||
|
|
||||||
|
// --- 事件处理 ---
|
||||||
|
const handleSaveRequest = () => {
|
||||||
|
// saveFile() 默认保存当前激活的标签页
|
||||||
|
if (activeTab.value) { // 确保有活动标签才保存
|
||||||
|
saveFile();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注意:关闭/最小化按钮现在应该在 WorkspaceView 控制 Pane,而不是这里
|
||||||
|
// const handleCloseContainer = () => { ... };
|
||||||
|
// const handleMinimizeContainer = () => { ... };
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 这个容器不再控制自己的显示/隐藏,由 WorkspaceView 的 Pane 控制 -->
|
||||||
|
<div class="file-editor-container">
|
||||||
|
|
||||||
|
<!-- 1. 标签栏 -->
|
||||||
|
<FileEditorTabs
|
||||||
|
:tabs="orderedTabs"
|
||||||
|
:active-tab-id="activeTab?.id ?? null"
|
||||||
|
@activate-tab="setActiveTab"
|
||||||
|
@close-tab="closeTab"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
|
||||||
|
<!-- 移除关闭/最小化按钮,这些由 WorkspaceView 控制 -->
|
||||||
|
<div v-if="activeTab" class="editor-header">
|
||||||
|
<span>
|
||||||
|
{{ t('fileManager.editingFile') }}: {{ currentTabFilePath }}
|
||||||
|
<span v-if="currentTabIsModified" class="modified-indicator">*</span>
|
||||||
|
</span>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<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 === 'error'" class="save-status error">❌ {{ t('fileManager.saveError') }}: {{ currentTabSaveError }}</span>
|
||||||
|
<button @click="handleSaveRequest" :disabled="currentTabIsSaving || currentTabIsLoading || !!currentTabLoadingError || !activeTab" class="save-btn">
|
||||||
|
{{ currentTabIsSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
||||||
|
</button>
|
||||||
|
<!-- 关闭/最小化按钮已移除 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 如果没有活动标签页,显示简化头部 -->
|
||||||
|
<div v-else class="editor-header editor-header-placeholder">
|
||||||
|
<span>{{ t('fileManager.noOpenFile') }}</span>
|
||||||
|
<!-- 动作区域留空或只显示通用按钮 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. 编辑器内容区域 -->
|
||||||
|
<div class="editor-content-area">
|
||||||
|
<div v-if="currentTabIsLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
|
||||||
|
<div v-else-if="currentTabLoadingError" class="editor-error">{{ currentTabLoadingError }}</div>
|
||||||
|
<MonacoEditor
|
||||||
|
v-else-if="activeTab"
|
||||||
|
:key="activeTab.id"
|
||||||
|
v-model="activeEditorContent"
|
||||||
|
:language="currentTabLanguage"
|
||||||
|
theme="vs-dark"
|
||||||
|
class="editor-instance"
|
||||||
|
@request-save="handleSaveRequest"
|
||||||
|
/>
|
||||||
|
<div v-else class="editor-placeholder">{{ t('fileManager.selectFileToEdit') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 样式与 FileEditorOverlay 类似,但移除 backdrop 和 popup 结构 */
|
||||||
|
.file-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%; /* 填充父级 Pane */
|
||||||
|
background-color: #2d2d2d; /* 编辑器背景 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #f0f0f0;
|
||||||
|
overflow: hidden; /* 重要:防止内容溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签栏区域 */
|
||||||
|
/* FileEditorTabs 组件自带样式 */
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: #333;
|
||||||
|
border-bottom: 1px solid #555;
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.editor-header-placeholder {
|
||||||
|
justify-content: flex-start; /* 左对齐提示文本 */
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modified-indicator {
|
||||||
|
color: #ffeb3b;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑器内容区域 */
|
||||||
|
.editor-content-area {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-loading, .editor-error, .editor-placeholder {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1em;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.editor-error { color: #ff8a8a; }
|
||||||
|
.editor-placeholder { color: #666; }
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.save-btn:disabled { background-color: #aaa; cursor: not-allowed; }
|
||||||
|
.save-btn:hover:not(:disabled) { background-color: #45a049; }
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.save-status.saving { color: #888; }
|
||||||
|
.save-status.success { color: #4CAF50; background-color: #e8f5e9; }
|
||||||
|
.save-status.error { color: #f44336; background-color: #ffebee; }
|
||||||
|
|
||||||
|
.editor-instance {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,116 +1,238 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue'; // 导入 ref, watch 等
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
||||||
|
import FileEditorTabs from './FileEditorTabs.vue'; // 导入标签栏组件
|
||||||
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入新的 Store
|
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入新的 Store
|
||||||
|
// 导入设置 store 以检查弹窗设置 (虽然 App.vue 做了顶层控制,但这里可以加一层保险)
|
||||||
// 不再需要 props 或 emits,状态和操作来自 Store
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
// const props = defineProps<{...}>();
|
|
||||||
// const emit = defineEmits<{...}>();
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const fileEditorStore = useFileEditorStore();
|
const fileEditorStore = useFileEditorStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
// 从 Store 获取状态 (使用 storeToRefs 保持响应性)
|
// --- 本地状态控制弹窗显示 ---
|
||||||
|
const isVisible = ref(false);
|
||||||
|
|
||||||
|
// --- 从 Store 获取状态 ---
|
||||||
const {
|
const {
|
||||||
isVisible,
|
// editorVisibleState, // 不再使用
|
||||||
filePath,
|
activeTab, // 当前激活的标签页对象 (computed)
|
||||||
fileLanguage, // 重命名为 language 以匹配 MonacoEditor prop
|
activeEditorContent,// 用于 v-model 绑定 (computed)
|
||||||
isLoading,
|
orderedTabs, // 标签页数组 (computed)
|
||||||
loadingError,
|
popupTrigger, // 监听这个值的变化来显示弹窗
|
||||||
isSaving,
|
|
||||||
saveStatus,
|
|
||||||
saveError,
|
|
||||||
fileContent, // 直接使用 store 中的 ref 进行 v-model 绑定
|
|
||||||
} = storeToRefs(fileEditorStore);
|
} = storeToRefs(fileEditorStore);
|
||||||
|
const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); // 获取弹窗设置
|
||||||
|
|
||||||
// 从 Store 获取方法
|
// --- 从 Store 获取方法 ---
|
||||||
const { saveFile, closeEditor, updateContent } = fileEditorStore;
|
const {
|
||||||
|
saveFile, // 现在保存当前激活的标签页
|
||||||
|
closeTab, // 关闭指定标签页 (由 FileEditorTabs 调用)
|
||||||
|
setActiveTab, // 设置激活标签页 (由 FileEditorTabs 调用)
|
||||||
|
// setEditorVisibility, // 不再使用
|
||||||
|
// closeAllTabs, // 不在此组件中关闭所有
|
||||||
|
} = fileEditorStore;
|
||||||
|
|
||||||
// 计算属性,用于 v-model 绑定到 MonacoEditor
|
// --- 弹窗尺寸和拖拽状态 ---
|
||||||
// 直接绑定 store 中的 fileContent ref
|
const popupWidthPx = ref(window.innerWidth * 0.75); // 初始宽度 75vw (像素)
|
||||||
const editorContent = computed({
|
const popupHeightPx = ref(window.innerHeight * 0.85); // 初始高度 85vh (像素)
|
||||||
get: () => fileContent.value,
|
const isResizing = ref(false);
|
||||||
set: (value) => updateContent(value), // 调用 store action 更新内容
|
const startX = ref(0);
|
||||||
});
|
const startY = ref(0);
|
||||||
|
const startWidthPx = ref(0);
|
||||||
|
const startHeightPx = ref(0);
|
||||||
|
const minWidth = 400; // 最小宽度
|
||||||
|
const minHeight = 300; // 最小高度
|
||||||
|
|
||||||
// 保存和关闭操作直接调用 store actions
|
// --- 计算属性,用于模板绑定 ---
|
||||||
|
const popupStyle = computed(() => ({
|
||||||
|
width: `${popupWidthPx.value}px`,
|
||||||
|
height: `${popupHeightPx.value}px`,
|
||||||
|
}));
|
||||||
|
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); // 用于显示修改状态
|
||||||
|
|
||||||
|
// --- 事件处理 ---
|
||||||
const handleSaveRequest = () => {
|
const handleSaveRequest = () => {
|
||||||
|
// saveFile() 默认保存当前激活的标签页
|
||||||
saveFile();
|
saveFile();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
// 关闭弹窗
|
||||||
closeEditor();
|
const handleCloseContainer = () => {
|
||||||
|
isVisible.value = false; // 只隐藏弹窗
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 最小化编辑器容器 (如果需要实现)
|
||||||
|
// const handleMinimizeContainer = () => {
|
||||||
|
// setEditorVisibility('minimized');
|
||||||
|
// };
|
||||||
|
|
||||||
|
// --- 拖拽调整大小逻辑 ---
|
||||||
|
const startResize = (event: MouseEvent) => {
|
||||||
|
isResizing.value = true;
|
||||||
|
startX.value = event.clientX;
|
||||||
|
startY.value = event.clientY;
|
||||||
|
startWidthPx.value = popupWidthPx.value;
|
||||||
|
startHeightPx.value = popupHeightPx.value;
|
||||||
|
document.addEventListener('mousemove', handleResize);
|
||||||
|
document.addEventListener('mouseup', stopResize);
|
||||||
|
document.body.style.cursor = 'nwse-resize'; // 设置拖拽光标
|
||||||
|
document.body.style.userSelect = 'none'; // 禁止拖拽时选中文本
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = (event: MouseEvent) => {
|
||||||
|
if (!isResizing.value) return;
|
||||||
|
const diffX = event.clientX - startX.value;
|
||||||
|
const diffY = event.clientY - startY.value;
|
||||||
|
popupWidthPx.value = Math.max(minWidth, startWidthPx.value + diffX);
|
||||||
|
popupHeightPx.value = Math.max(minHeight, startHeightPx.value + diffY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
if (isResizing.value) {
|
||||||
|
isResizing.value = false;
|
||||||
|
document.removeEventListener('mousemove', handleResize);
|
||||||
|
document.removeEventListener('mouseup', stopResize);
|
||||||
|
document.body.style.cursor = ''; // 恢复默认光标
|
||||||
|
document.body.style.userSelect = ''; // 恢复文本选择
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 popupTrigger 的变化来显示弹窗 (如果设置允许)
|
||||||
|
watch(popupTrigger, () => {
|
||||||
|
if (showPopupFileEditorBoolean.value) {
|
||||||
|
console.log('[FileEditorOverlay] Popup trigger changed, showing overlay.');
|
||||||
|
isVisible.value = true;
|
||||||
|
} else {
|
||||||
|
console.log('[FileEditorOverlay] Popup trigger changed, but overlay is disabled in settings.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 组件卸载时清理事件监听器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopResize(); // 确保移除监听器
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 使用 store 中的 isVisible 控制显示 -->
|
<!-- 使用本地 isVisible 控制显示 (App.vue 中已有 v-if="showPopupFileEditorBoolean") -->
|
||||||
<!-- 将 v-if 移到遮罩层上 -->
|
<div v-if="isVisible" class="editor-overlay-backdrop" @click.self="handleCloseContainer"> <!-- 恢复点击背景关闭 -->
|
||||||
<div v-if="isVisible" class="editor-overlay-backdrop" @click.self="handleClose"> <!-- 点击背景关闭 -->
|
<!-- 编辑器弹窗/容器,应用动态样式 -->
|
||||||
<!-- 添加弹窗容器 -->
|
<div class="editor-popup" :style="popupStyle">
|
||||||
<div class="editor-popup">
|
|
||||||
<div class="editor-header">
|
<!-- 1. 标签栏 -->
|
||||||
<!-- 使用 store 中的 filePath -->
|
<!-- 1. 标签栏 -->
|
||||||
<span>{{ t('fileManager.editingFile') }}: {{ filePath }}</span>
|
<FileEditorTabs
|
||||||
<div class="editor-actions">
|
:tabs="orderedTabs"
|
||||||
<!-- 使用 store 中的保存状态 -->
|
:active-tab-id="activeTab?.id ?? null"
|
||||||
<span v-if="saveStatus === 'saving'" class="save-status saving">{{ t('fileManager.saving') }}...</span>
|
@activate-tab="setActiveTab"
|
||||||
<span v-if="saveStatus === 'success'" class="save-status success">✅ {{ t('fileManager.saveSuccess') }}</span>
|
@close-tab="closeTab"
|
||||||
<span v-if="saveStatus === 'error'" class="save-status error">❌ {{ t('fileManager.saveError') }}: {{ saveError }}</span>
|
/>
|
||||||
<!-- 保存按钮,使用 store 状态和方法 -->
|
|
||||||
<button @click="handleSaveRequest" :disabled="isSaving || isLoading || !!loadingError" class="save-btn">
|
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
|
||||||
{{ isSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
<div v-if="activeTab" class="editor-header">
|
||||||
</button>
|
<!-- 显示当前激活标签的文件路径和修改状态 -->
|
||||||
<!-- 关闭按钮,使用 store 方法 -->
|
<span>
|
||||||
<button @click="handleClose" class="close-editor-btn">✖</button>
|
{{ t('fileManager.editingFile') }}: {{ currentTabFilePath }}
|
||||||
|
<span v-if="currentTabIsModified" class="modified-indicator">*</span>
|
||||||
|
</span>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<!-- 显示当前激活标签的保存状态 -->
|
||||||
|
<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 === 'error'" class="save-status error">❌ {{ t('fileManager.saveError') }}: {{ currentTabSaveError }}</span>
|
||||||
|
<!-- 保存按钮,状态基于当前激活标签 -->
|
||||||
|
<button @click="handleSaveRequest" :disabled="currentTabIsSaving || currentTabIsLoading || !!currentTabLoadingError || !activeTab" class="save-btn">
|
||||||
|
{{ currentTabIsSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
||||||
|
</button>
|
||||||
|
<!-- 关闭整个容器按钮 -->
|
||||||
|
<button @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')">✖</button>
|
||||||
|
<!-- 可以添加最小化按钮 -->
|
||||||
|
<!-- <button @click="handleMinimizeContainer" class="minimize-editor-btn" title="Minimize">_</button> -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- 如果没有活动标签页,显示提示 -->
|
||||||
<!-- 使用 store 中的加载状态 -->
|
<div v-else class="editor-header editor-header-placeholder">
|
||||||
<div v-if="isLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
|
<span>{{ t('fileManager.noOpenFile') }}</span>
|
||||||
<!-- 使用 store 中的加载错误 -->
|
<button @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')">✖</button>
|
||||||
<div v-else-if="loadingError" class="editor-error">{{ loadingError }}</div>
|
</div>
|
||||||
<!-- Monaco 编辑器实例 -->
|
|
||||||
<MonacoEditor
|
<!-- 3. 编辑器内容区域 -->
|
||||||
v-else
|
<div class="editor-content-area">
|
||||||
v-model="editorContent"
|
<!-- 显示当前激活标签的加载状态 -->
|
||||||
:language="fileLanguage"
|
<div v-if="currentTabIsLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
|
||||||
theme="vs-dark"
|
<!-- 显示当前激活标签的加载错误 -->
|
||||||
class="editor-instance"
|
<div v-else-if="currentTabLoadingError" class="editor-error">{{ currentTabLoadingError }}</div>
|
||||||
@request-save="handleSaveRequest"
|
<!-- Monaco 编辑器实例 (仅当有活动标签且未加载/错误时显示) -->
|
||||||
/>
|
<MonacoEditor
|
||||||
|
v-else-if="activeTab"
|
||||||
|
:key="activeTab.id"
|
||||||
|
v-model="activeEditorContent"
|
||||||
|
:language="currentTabLanguage"
|
||||||
|
theme="vs-dark"
|
||||||
|
class="editor-instance"
|
||||||
|
@request-save="handleSaveRequest"
|
||||||
|
/>
|
||||||
|
<!-- 如果容器可见但没有活动标签页 -->
|
||||||
|
<div v-else class="editor-placeholder">{{ t('fileManager.selectFileToEdit') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加拖拽手柄 -->
|
||||||
|
<div class="resize-handle" @mousedown.prevent="startResize"></div>
|
||||||
|
|
||||||
</div> <!-- 关闭 editor-popup -->
|
</div> <!-- 关闭 editor-popup -->
|
||||||
</div> <!-- 关闭 editor-overlay-backdrop -->
|
</div> <!-- 关闭 editor-overlay-backdrop -->
|
||||||
|
|
||||||
|
<!-- 可以添加一个最小化状态的显示 -->
|
||||||
|
<!--
|
||||||
|
<div v-if="editorVisibleState === 'minimized'" class="editor-minimized-bar" @click="setEditorVisibility('visible')">
|
||||||
|
<span>File Editor</span>
|
||||||
|
<button @click.stop="handleCloseContainer">✖</button>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 样式调整为居中弹窗样式 */
|
/* 样式基本保持不变,但可能需要为标签栏和新状态调整 */
|
||||||
.editor-overlay-backdrop { /* 新增背景遮罩层 */
|
.editor-overlay-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.6); /* 半透明黑色背景 */
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
z-index: 1000; /* 确保在最上层 */
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-popup { /* 编辑器本身的容器 */
|
.editor-popup {
|
||||||
width: 60%; /* 设置宽度为 60% */
|
width: 75%; /* 可以适当调整大小 */
|
||||||
height: 80%; /* 设置一个合适的高度 */
|
height: 85%;
|
||||||
background-color: #2d2d2d; /* 深色背景 */
|
background-color: #2d2d2d;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: #f0f0f0;
|
color: #f0f0f0;
|
||||||
overflow: hidden; /* 防止内容溢出圆角 */
|
overflow: hidden;
|
||||||
|
position: relative; /* 为拖拽手柄定位 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标签栏区域 (FileEditorTabs 组件将放在这里) */
|
||||||
|
/* .file-tabs-container { ... } */
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -119,7 +241,17 @@ const handleClose = () => {
|
|||||||
background-color: #333;
|
background-color: #333;
|
||||||
border-bottom: 1px solid #555;
|
border-bottom: 1px solid #555;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
flex-shrink: 0; /* 防止头部被压缩 */
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.editor-header-placeholder {
|
||||||
|
justify-content: space-between; /* 保持关闭按钮在右侧 */
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modified-indicator {
|
||||||
|
color: #ffeb3b; /* 黄色星号表示修改 */
|
||||||
|
margin-left: 4px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-editor-btn {
|
.close-editor-btn {
|
||||||
@@ -134,23 +266,37 @@ const handleClose = () => {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-loading, .editor-error {
|
/* 编辑器内容区域,包含加载、错误、编辑器实例 */
|
||||||
|
.editor-content-area {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex; /* 使内部元素能填充 */
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden; /* 内部编辑器滚动 */
|
||||||
|
position: relative; /* 用于占位符定位 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-loading, .editor-error, .editor-placeholder {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
flex-grow: 1; /* 占据剩余空间 */
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
color: #888;
|
||||||
}
|
}
|
||||||
.editor-error {
|
.editor-error {
|
||||||
color: #ff8a8a;
|
color: #ff8a8a;
|
||||||
}
|
}
|
||||||
|
.editor-placeholder {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.editor-actions {
|
.editor-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem; /* 添加按钮间距 */
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn {
|
.save-btn {
|
||||||
@@ -158,7 +304,6 @@ const handleClose = () => {
|
|||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
/* margin-left: 1rem; */ /* 使用 gap 代替 */
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
@@ -172,26 +317,61 @@ const handleClose = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.save-status {
|
.save-status {
|
||||||
/* margin-left: 1rem; */ /* 使用 gap 代替 */
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
white-space: nowrap; /* 防止状态文本换行 */
|
white-space: nowrap;
|
||||||
}
|
|
||||||
.save-status.saving {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
.save-status.success {
|
|
||||||
color: #4CAF50;
|
|
||||||
background-color: #e8f5e9;
|
|
||||||
}
|
|
||||||
.save-status.error {
|
|
||||||
color: #f44336;
|
|
||||||
background-color: #ffebee;
|
|
||||||
}
|
}
|
||||||
|
.save-status.saving { color: #888; }
|
||||||
|
.save-status.success { color: #4CAF50; background-color: #e8f5e9; }
|
||||||
|
.save-status.error { color: #f44336; background-color: #ffebee; }
|
||||||
|
|
||||||
.editor-instance {
|
.editor-instance {
|
||||||
flex-grow: 1; /* 让编辑器占据剩余空间 */
|
flex-grow: 1;
|
||||||
min-height: 0; /* 对 flex 布局中的子元素很重要 */
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 拖拽手柄样式 */
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2); /* 半透明手柄 */
|
||||||
|
border-top: 1px solid #555;
|
||||||
|
border-left: 1px solid #555;
|
||||||
|
cursor: nwse-resize; /* 斜向拖拽光标 */
|
||||||
|
z-index: 1001; /* 确保在内容之上 */
|
||||||
|
}
|
||||||
|
.resize-handle:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 最小化状态样式 (可选) */
|
||||||
|
/*
|
||||||
|
.editor-minimized-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #333;
|
||||||
|
color: #f0f0f0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.editor-minimized-bar button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
*/
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import type { EditorTab } from '../stores/fileEditor.store'; // 导入 EditorTab 类型
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
tabs: {
|
||||||
|
type: Array as PropType<EditorTab[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
activeTabId: {
|
||||||
|
type: String as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'activate-tab', tabId: string): void;
|
||||||
|
(e: 'close-tab', tabId: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const handleActivate = (tabId: string) => {
|
||||||
|
emit('activate-tab', tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event: MouseEvent, tabId: string) => {
|
||||||
|
event.stopPropagation(); // 防止触发 activateTab
|
||||||
|
emit('close-tab', tabId);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="file-editor-tabs">
|
||||||
|
<div
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: tab.id === activeTabId }"
|
||||||
|
@click="handleActivate(tab.id)"
|
||||||
|
:title="tab.filePath"
|
||||||
|
>
|
||||||
|
<span class="tab-filename">{{ tab.filename }}</span>
|
||||||
|
<span v-if="tab.isModified" class="modified-indicator">*</span>
|
||||||
|
<button
|
||||||
|
class="close-tab-btn"
|
||||||
|
@click.stop="handleClose($event, tab.id)"
|
||||||
|
:title="t('fileManager.actions.closeTab')"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="tabs.length === 0" class="no-tabs-placeholder">
|
||||||
|
<!-- 可以留空或添加提示 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-editor-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap; /* 防止标签换行 */
|
||||||
|
overflow-x: auto; /* 水平滚动 */
|
||||||
|
background-color: #252526; /* VSCode 风格的标签背景 */
|
||||||
|
border-bottom: 1px solid #3f3f46; /* 分隔线 */
|
||||||
|
flex-shrink: 0; /* 防止标签栏被压缩 */
|
||||||
|
scrollbar-width: thin; /* Firefox */
|
||||||
|
scrollbar-color: #555 #252526; /* Firefox */
|
||||||
|
}
|
||||||
|
/* Webkit 滚动条样式 */
|
||||||
|
.file-editor-tabs::-webkit-scrollbar {
|
||||||
|
height: 4px; /* 滚动条高度 */
|
||||||
|
}
|
||||||
|
.file-editor-tabs::-webkit-scrollbar-track {
|
||||||
|
background: #252526;
|
||||||
|
}
|
||||||
|
.file-editor-tabs::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #555;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.file-editor-tabs::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px 6px 12px; /* 调整内边距 */
|
||||||
|
cursor: pointer;
|
||||||
|
border-right: 1px solid #3f3f46; /* 标签分隔线 */
|
||||||
|
color: #cccccc; /* 未激活标签颜色 */
|
||||||
|
background-color: #2d2d2d; /* 未激活标签背景 */
|
||||||
|
white-space: nowrap; /* 防止文件名换行 */
|
||||||
|
font-size: 0.85em;
|
||||||
|
position: relative; /* 用于关闭按钮定位 */
|
||||||
|
transition: background-color 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
background-color: #3e3e42; /* 悬停背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background-color: #1e1e1e; /* 激活标签背景 (编辑器背景色) */
|
||||||
|
color: #ffffff; /* 激活标签文字颜色 */
|
||||||
|
border-bottom: 1px solid #1e1e1e; /* 覆盖下边框,使其看起来连接内容 */
|
||||||
|
margin-bottom: -1px; /* 轻微上移以覆盖边框 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-filename {
|
||||||
|
margin-right: 4px;
|
||||||
|
max-width: 150px; /* 限制文件名最大宽度 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modified-indicator {
|
||||||
|
color: #cccccc; /* 修改指示器颜色 */
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 4px; /* 与关闭按钮保持距离 */
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.tab-item.active .modified-indicator {
|
||||||
|
color: #ffffff; /* 激活标签的修改指示器颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.close-tab-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 1.1em; /* 调整大小 */
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0.6; /* 默认稍透明 */
|
||||||
|
transition: opacity 0.1s ease-in-out, background-color 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover .close-tab-btn,
|
||||||
|
.tab-item.active .close-tab-btn {
|
||||||
|
opacity: 1; /* 悬停或激活时完全显示 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-tab-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15); /* 悬停背景 */
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tabs-placeholder {
|
||||||
|
flex-grow: 1; /* 占据剩余空间 */
|
||||||
|
/* 可以添加样式 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -692,11 +692,11 @@ const cancelPathEdit = () => {
|
|||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
||||||
<button @click="triggerFileUpload" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.uploadFile')">📤 {{ t('fileManager.actions.upload') }}</button>
|
<button @click="triggerFileUpload" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.uploadFile')">📤 {{ t('fileManager.actions.upload') }}</button>
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
||||||
<button @click="handleNewFolderContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFolder')">➕ {{ t('fileManager.actions.newFolder') }}</button>
|
<button @click="handleNewFolderContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFolder')">➕ {{ t('fileManager.actions.newFolder') }}</button>
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
||||||
<button @click="handleNewFileContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFile')">📄 {{ t('fileManager.actions.newFile') }}</button>
|
<button @click="handleNewFileContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFile')">📄 {{ t('fileManager.actions.newFile') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件列表容器 -->
|
<!-- 文件列表容器 -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -249,7 +249,9 @@
|
|||||||
"saveError": "Save error",
|
"saveError": "Save error",
|
||||||
"editPathTooltip": "Click path to edit",
|
"editPathTooltip": "Click path to edit",
|
||||||
"noActiveSession": "No active session",
|
"noActiveSession": "No active session",
|
||||||
"loadDirectoryFailed": "Failed to load directory"
|
"loadDirectoryFailed": "Failed to load directory",
|
||||||
|
"noOpenFile": "No file open",
|
||||||
|
"selectFileToEdit": "Select a file from the file manager to start editing."
|
||||||
},
|
},
|
||||||
"statusMonitor": {
|
"statusMonitor": {
|
||||||
"noActiveSession": "No active session"
|
"noActiveSession": "No active session"
|
||||||
@@ -351,6 +353,17 @@
|
|||||||
"saveFailed": "Failed to save IP whitelist."
|
"saveFailed": "Failed to save IP whitelist."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"popupEditor": {
|
||||||
|
"title": "Popup File Editor",
|
||||||
|
"enableLabel": "Show popup editor when opening files",
|
||||||
|
"saveButton": "Save Setting",
|
||||||
|
"success": {
|
||||||
|
"saved": "Popup editor setting saved successfully."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"saveFailed": "Failed to save popup editor setting."
|
||||||
|
}
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"title": "Language Settings",
|
"title": "Language Settings",
|
||||||
"selectLabel": "Interface Language:",
|
"selectLabel": "Interface Language:",
|
||||||
@@ -516,5 +529,8 @@
|
|||||||
"untagged": "Untagged",
|
"untagged": "Untagged",
|
||||||
"searchPlaceholder": "Search name or host...",
|
"searchPlaceholder": "Search name or host...",
|
||||||
"noResults": "No connections found matching \"{searchTerm}\"."
|
"noResults": "No connections found matching \"{searchTerm}\"."
|
||||||
}
|
},
|
||||||
|
"commandInputBar": {
|
||||||
|
"placeholder": "Enter command and press Enter to send..."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,9 @@
|
|||||||
"saveSuccess": "保存成功",
|
"saveSuccess": "保存成功",
|
||||||
"saveError": "保存出错",
|
"saveError": "保存出错",
|
||||||
"editPathTooltip": "点击路径进行编辑",
|
"editPathTooltip": "点击路径进行编辑",
|
||||||
"noActiveSession": "无活动会话"
|
"noActiveSession": "无活动会话",
|
||||||
|
"noOpenFile": "未打开文件",
|
||||||
|
"selectFileToEdit": "请从文件管理器中选择文件以开始编辑。"
|
||||||
},
|
},
|
||||||
"statusMonitor": {
|
"statusMonitor": {
|
||||||
"noActiveSession": "无活动会话"
|
"noActiveSession": "无活动会话"
|
||||||
@@ -354,6 +356,17 @@
|
|||||||
"saveFailed": "保存 IP 白名单失败。"
|
"saveFailed": "保存 IP 白名单失败。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"popupEditor": {
|
||||||
|
"title": "弹窗文件编辑器",
|
||||||
|
"enableLabel": "打开文件时显示弹窗编辑器",
|
||||||
|
"saveButton": "保存设置",
|
||||||
|
"success": {
|
||||||
|
"saved": "弹窗编辑器设置已成功保存。"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"saveFailed": "保存弹窗编辑器设置失败。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"title": "语言设置",
|
"title": "语言设置",
|
||||||
"selectLabel": "界面语言:",
|
"selectLabel": "界面语言:",
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import { ref, computed, readonly } from 'vue';
|
import { ref, computed, readonly, watch } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
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';
|
import type { EditorFileContent, SaveStatus } from '../types/sftp.types'; // 保持导入 SaveStatus
|
||||||
|
|
||||||
// 辅助函数:根据文件名获取语言 (从 useFileEditor.ts 迁移)
|
// --- 新类型定义 ---
|
||||||
|
export interface EditorTab {
|
||||||
|
id: string; // 唯一标识符,例如 `${sessionId}:${filePath}`
|
||||||
|
sessionId: string;
|
||||||
|
filePath: string;
|
||||||
|
filename: string; // 文件名,用于标签显示
|
||||||
|
content: string; // 当前编辑器内容
|
||||||
|
originalContent: string; // 加载或上次保存时的内容
|
||||||
|
language: string;
|
||||||
|
encoding: 'utf8' | 'base64'; // 原始编码
|
||||||
|
isLoading: boolean;
|
||||||
|
loadingError: string | null;
|
||||||
|
isSaving: boolean;
|
||||||
|
saveStatus: SaveStatus;
|
||||||
|
saveError: string | null;
|
||||||
|
isModified: boolean; // 内容是否已修改
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:根据文件名获取语言 (保持不变)
|
||||||
const getLanguageFromFilename = (filename: string): string => {
|
const getLanguageFromFilename = (filename: string): string => {
|
||||||
const extension = filename.split('.').pop()?.toLowerCase();
|
const extension = filename.split('.').pop()?.toLowerCase();
|
||||||
// (保持 switch case 不变)
|
|
||||||
switch (extension) {
|
switch (extension) {
|
||||||
case 'js': return 'javascript';
|
case 'js': return 'javascript';
|
||||||
case 'ts': return 'typescript';
|
case 'ts': return 'typescript';
|
||||||
@@ -37,221 +54,365 @@ const getLanguageFromFilename = (filename: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 辅助函数:从路径获取文件名
|
||||||
|
const getFilenameFromPath = (filePath: string): string => {
|
||||||
|
return filePath.split('/').pop() || filePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useFileEditorStore = defineStore('fileEditor', () => {
|
export const useFileEditorStore = defineStore('fileEditor', () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
// --- 编辑器状态 ---
|
// --- 多标签状态 ---
|
||||||
const isVisible = ref(false);
|
const tabs = ref(new Map<string, EditorTab>()); // 存储所有打开的标签页
|
||||||
const currentSessionId = ref<string | null>(null); // 需要知道文件属于哪个会话
|
const activeTabId = ref<string | null>(null); // 当前激活的标签页 ID
|
||||||
const filePath = ref<string | null>(null);
|
// const editorVisibleState = ref<'visible' | 'minimized' | 'closed'>('closed'); // 移除,面板可见性由布局控制
|
||||||
const fileContent = ref<string>(''); // 用于 v-model 绑定
|
const popupTrigger = ref(0); // 新增:用于触发弹窗显示的信号
|
||||||
const fileLanguage = ref<string>('plaintext');
|
|
||||||
const fileEncoding = ref<'utf8' | 'base64'>('utf8'); // 文件内容的原始编码
|
|
||||||
const isLoading = ref<boolean>(false);
|
|
||||||
const loadingError = ref<string | null>(null);
|
|
||||||
const isSaving = ref<boolean>(false);
|
|
||||||
const saveStatus = ref<SaveStatus>('idle');
|
|
||||||
const saveError = ref<string | null>(null);
|
|
||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
const editorProps = computed(() => ({
|
const orderedTabs = computed(() => Array.from(tabs.value.values())); // 获取标签页数组,用于渲染
|
||||||
isVisible: isVisible.value,
|
const activeTab = computed(() => {
|
||||||
filePath: filePath.value,
|
if (!activeTabId.value) return null;
|
||||||
language: fileLanguage.value,
|
return tabs.value.get(activeTabId.value) || null;
|
||||||
isLoading: isLoading.value,
|
});
|
||||||
loadingError: loadingError.value,
|
// 提供给 MonacoEditor 的内容绑定
|
||||||
isSaving: isSaving.value,
|
const activeEditorContent = computed({
|
||||||
saveStatus: saveStatus.value,
|
get: () => activeTab.value?.content ?? '',
|
||||||
saveError: saveError.value,
|
set: (value) => {
|
||||||
// modelValue is handled separately via direct ref binding
|
if (activeTab.value) {
|
||||||
}));
|
updateContent(value); // 调用 action 更新内容和修改状态
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// --- 核心方法 ---
|
// --- 核心方法 ---
|
||||||
|
|
||||||
// 获取当前会话的 SFTP 管理器
|
// 获取指定会话的 SFTP 管理器 (保持不变)
|
||||||
const getSftpManager = (sessionId: string | null) => {
|
const getSftpManager = (sessionId: string | null) => {
|
||||||
if (!sessionId) return null;
|
if (!sessionId) return null;
|
||||||
const session = sessionStore.sessions.get(sessionId);
|
const session = sessionStore.sessions.get(sessionId);
|
||||||
return session?.sftpManager ?? null;
|
return session?.sftpManager ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFile = async (targetFilePath: string, sessionId: string) => {
|
// 移除 setEditorVisibility 方法
|
||||||
console.log(`[文件编辑器 Store] 尝试打开文件: ${targetFilePath} (会话: ${sessionId})`);
|
// const setEditorVisibility = ...
|
||||||
if (!targetFilePath || !sessionId) return;
|
|
||||||
|
|
||||||
// // 如果已经是同一个文件,则不重新加载(除非需要强制刷新)
|
// 打开或切换到文件标签页
|
||||||
// if (filePath.value === targetFilePath && isVisible.value) {
|
const openFile = async (targetFilePath: string, sessionId: string) => {
|
||||||
// console.log(`[文件编辑器 Store] 文件 ${targetFilePath} 已在编辑器中打开。`);
|
const tabId = `${sessionId}:${targetFilePath}`;
|
||||||
// return;
|
console.log(`[文件编辑器 Store] 尝试打开文件: ${targetFilePath} (会话: ${sessionId}, Tab ID: ${tabId})`);
|
||||||
|
|
||||||
|
// 移除确保编辑器可见的逻辑
|
||||||
|
// if (editorVisibleState.value === 'closed') {
|
||||||
|
// setEditorVisibility('visible');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const sftpManager = getSftpManager(sessionId);
|
// 如果标签页已存在,则激活它
|
||||||
if (!sftpManager) {
|
if (tabs.value.has(tabId)) {
|
||||||
console.error(`[文件编辑器 Store] 无法找到会话 ${sessionId} 的 SFTP 管理器。`);
|
console.log(`[文件编辑器 Store] 标签页 ${tabId} 已存在,激活它。`);
|
||||||
// 可以设置一个错误状态或通知用户
|
setActiveTab(tabId);
|
||||||
loadingError.value = t('fileManager.errors.sftpManagerNotFound');
|
// 触发弹窗 (如果设置允许)
|
||||||
isVisible.value = true; // 仍然显示编辑器以展示错误
|
popupTrigger.value++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isVisible.value = true; // 显示编辑器区域
|
// 创建新标签页
|
||||||
isLoading.value = true; // 显示加载状态
|
const newTab: EditorTab = {
|
||||||
loadingError.value = null;
|
id: tabId,
|
||||||
saveStatus.value = 'idle'; // 重置保存状态
|
sessionId: sessionId,
|
||||||
saveError.value = null;
|
filePath: targetFilePath,
|
||||||
filePath.value = targetFilePath;
|
filename: getFilenameFromPath(targetFilePath),
|
||||||
currentSessionId.value = sessionId; // 记录当前会话 ID
|
content: '', // 初始为空
|
||||||
fileLanguage.value = getLanguageFromFilename(targetFilePath);
|
originalContent: '', // 初始为空
|
||||||
fileContent.value = ''; // 清空旧内容
|
language: getLanguageFromFilename(targetFilePath),
|
||||||
|
encoding: 'utf8', // 默认为 utf8
|
||||||
|
isLoading: true, // 开始加载
|
||||||
|
loadingError: null,
|
||||||
|
isSaving: false,
|
||||||
|
saveStatus: 'idle',
|
||||||
|
saveError: null,
|
||||||
|
isModified: false,
|
||||||
|
};
|
||||||
|
tabs.value.set(tabId, newTab);
|
||||||
|
setActiveTab(tabId); // 激活新标签页
|
||||||
|
// 触发弹窗 (如果设置允许)
|
||||||
|
popupTrigger.value++;
|
||||||
|
|
||||||
|
// 获取 SFTP 管理器
|
||||||
|
const sftpManager = getSftpManager(sessionId);
|
||||||
|
if (!sftpManager) {
|
||||||
|
console.error(`[文件编辑器 Store] 无法找到会话 ${sessionId} 的 SFTP 管理器。`);
|
||||||
|
const tabToUpdate = tabs.value.get(tabId);
|
||||||
|
if (tabToUpdate) {
|
||||||
|
tabToUpdate.isLoading = false;
|
||||||
|
tabToUpdate.loadingError = t('fileManager.errors.sftpManagerNotFound');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
try {
|
try {
|
||||||
// 使用从 sessionStore 获取的 sftpManager 的 readFile 方法
|
|
||||||
const fileData = await sftpManager.readFile(targetFilePath);
|
const fileData = await sftpManager.readFile(targetFilePath);
|
||||||
console.log(`[文件编辑器 Store] 文件 ${targetFilePath} 读取成功。编码: ${fileData.encoding}`);
|
console.log(`[文件编辑器 Store] 文件 ${targetFilePath} 读取成功。编码: ${fileData.encoding}`);
|
||||||
|
|
||||||
// 处理可能的 Base64 编码
|
let decodedContent = '';
|
||||||
|
let finalEncoding: 'utf8' | 'base64' = 'utf8';
|
||||||
|
|
||||||
if (fileData.encoding === 'base64') {
|
if (fileData.encoding === 'base64') {
|
||||||
|
finalEncoding = 'base64';
|
||||||
try {
|
try {
|
||||||
// 1. Decode Base64 to raw bytes string
|
|
||||||
const binaryString = atob(fileData.content);
|
const binaryString = atob(fileData.content);
|
||||||
// 2. Convert binary string to Uint8Array
|
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
}
|
}
|
||||||
// 3. Decode bytes as UTF-8
|
|
||||||
const decoder = new TextDecoder('utf-8'); // 显式使用 UTF-8
|
const decoder = new TextDecoder('utf-8'); // 显式使用 UTF-8
|
||||||
fileContent.value = decoder.decode(bytes);
|
decodedContent = decoder.decode(bytes);
|
||||||
fileEncoding.value = 'base64'; // 记录原始编码是 Base64
|
|
||||||
console.log(`[文件编辑器 Store] Base64 文件 ${targetFilePath} 已解码为 UTF-8。`);
|
console.log(`[文件编辑器 Store] Base64 文件 ${targetFilePath} 已解码为 UTF-8。`);
|
||||||
} catch (decodeError) {
|
} catch (decodeError) {
|
||||||
console.error(`[文件编辑器 Store] Base64 或 UTF-8 解码错误 for ${targetFilePath}:`, decodeError);
|
console.error(`[文件编辑器 Store] Base64 或 UTF-8 解码错误 for ${targetFilePath}:`, decodeError);
|
||||||
loadingError.value = t('fileManager.errors.fileDecodeError');
|
const errorMsg = t('fileManager.errors.fileDecodeError');
|
||||||
// Fallback: Show raw base64 content if decoding fails
|
decodedContent = `// ${errorMsg}\n// Original Base64 content:\n${fileData.content}`;
|
||||||
fileContent.value = `// ${t('fileManager.errors.fileDecodeError')}\n// Original Base64 content:\n${fileData.content}`;
|
// 更新标签页状态以反映错误
|
||||||
|
const tabToUpdate = tabs.value.get(tabId);
|
||||||
|
if (tabToUpdate) {
|
||||||
|
tabToUpdate.loadingError = errorMsg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 假设非 Base64 内容是 UTF-8 字符串
|
finalEncoding = 'utf8';
|
||||||
fileContent.value = fileData.content;
|
decodedContent = fileData.content;
|
||||||
// 在这个 else 分支中,编码不是 base64,我们假定它是 utf8
|
console.log(`[文件编辑器 Store] 文件 ${targetFilePath} 已按 ${finalEncoding} 处理。`);
|
||||||
fileEncoding.value = 'utf8';
|
if (decodedContent.includes('\uFFFD')) {
|
||||||
console.log(`[文件编辑器 Store] 文件 ${targetFilePath} 已按 ${fileEncoding.value} 处理。`);
|
|
||||||
// 添加检查:如果内容看起来像乱码,可以加日志
|
|
||||||
if (fileContent.value.includes('\uFFFD')) { // '\uFFFD' () 是无效序列的替换字符
|
|
||||||
console.warn(`[文件编辑器 Store] 文件 ${targetFilePath} 内容可能包含无效字符,原始编码可能不是 UTF-8。`);
|
console.warn(`[文件编辑器 Store] 文件 ${targetFilePath} 内容可能包含无效字符,原始编码可能不是 UTF-8。`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isLoading.value = false;
|
|
||||||
|
// 更新标签页状态
|
||||||
|
const tabToUpdate = tabs.value.get(tabId);
|
||||||
|
if (tabToUpdate) {
|
||||||
|
tabToUpdate.content = decodedContent;
|
||||||
|
tabToUpdate.originalContent = decodedContent; // 设置初始内容
|
||||||
|
tabToUpdate.encoding = finalEncoding;
|
||||||
|
tabToUpdate.isLoading = false;
|
||||||
|
tabToUpdate.isModified = false; // 初始未修改
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`[文件编辑器 Store] 读取文件 ${targetFilePath} 失败:`, err);
|
console.error(`[文件编辑器 Store] 读取文件 ${targetFilePath} 失败:`, err);
|
||||||
loadingError.value = `${t('fileManager.errors.readFileFailed')}: ${err.message || err}`;
|
const errorMsg = `${t('fileManager.errors.readFileFailed')}: ${err.message || err}`;
|
||||||
fileContent.value = `// ${loadingError.value}`; // 在编辑器中显示错误
|
const tabToUpdate = tabs.value.get(tabId);
|
||||||
isLoading.value = false;
|
if (tabToUpdate) {
|
||||||
|
tabToUpdate.isLoading = false;
|
||||||
|
tabToUpdate.loadingError = errorMsg;
|
||||||
|
tabToUpdate.content = `// ${errorMsg}`; // 在编辑器中显示错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveFile = async () => {
|
// 保存指定(或当前激活)标签页的文件
|
||||||
if (!filePath.value || !currentSessionId.value || isSaving.value || isLoading.value || loadingError.value) {
|
const saveFile = async (tabIdToSave?: string) => {
|
||||||
console.warn('[文件编辑器 Store] 保存条件不满足,无法保存。', {
|
const targetTabId = tabIdToSave ?? activeTabId.value;
|
||||||
path: filePath.value,
|
if (!targetTabId) {
|
||||||
sessionId: currentSessionId.value,
|
console.warn('[文件编辑器 Store] 保存失败:没有活动的标签页。');
|
||||||
isSaving: isSaving.value,
|
|
||||||
isLoading: isLoading.value,
|
|
||||||
hasError: !!loadingError.value
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sftpManager = getSftpManager(currentSessionId.value);
|
const tab = tabs.value.get(targetTabId);
|
||||||
if (!sftpManager) {
|
if (!tab) {
|
||||||
console.error(`[文件编辑器 Store] 保存失败:无法找到会话 ${currentSessionId.value} 的 SFTP 管理器。`);
|
console.warn(`[文件编辑器 Store] 保存失败:找不到标签页 ${targetTabId}。`);
|
||||||
saveStatus.value = 'error';
|
|
||||||
saveError.value = t('fileManager.errors.sftpManagerNotFound');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[文件编辑器 Store] 开始保存文件: ${filePath.value} (会话: ${currentSessionId.value})`);
|
if (tab.isSaving || tab.isLoading || tab.loadingError) {
|
||||||
isSaving.value = true;
|
console.warn(`[文件编辑器 Store] 保存条件不满足 for ${tab.filePath},无法保存。`, { tab });
|
||||||
saveStatus.value = 'saving';
|
return;
|
||||||
saveError.value = null;
|
}
|
||||||
|
|
||||||
const contentToSave = fileContent.value; // 获取当前编辑器内容
|
// 检查会话是否存在且连接
|
||||||
|
const session = sessionStore.sessions.get(tab.sessionId);
|
||||||
|
if (!session || !session.wsManager.isConnected.value || !session.wsManager.isSftpReady.value) {
|
||||||
|
console.error(`[文件编辑器 Store] 保存失败:会话 ${tab.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(`[文件编辑器 Store] 开始保存文件: ${tab.filePath} (Tab ID: ${tab.id})`);
|
||||||
|
tab.isSaving = true;
|
||||||
|
tab.saveStatus = 'saving';
|
||||||
|
tab.saveError = null;
|
||||||
|
|
||||||
|
const contentToSave = tab.content;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用从 sessionStore 获取的 sftpManager 的 writeFile 方法
|
await sftpManager.writeFile(tab.filePath, contentToSave);
|
||||||
await sftpManager.writeFile(filePath.value, contentToSave);
|
console.log(`[文件编辑器 Store] 文件 ${tab.filePath} 保存成功。`);
|
||||||
console.log(`[文件编辑器 Store] 文件 ${filePath.value} 保存成功。`);
|
tab.isSaving = false;
|
||||||
isSaving.value = false;
|
tab.saveStatus = 'success';
|
||||||
saveStatus.value = 'success';
|
tab.saveError = null;
|
||||||
saveError.value = null;
|
tab.originalContent = contentToSave; // 更新原始内容
|
||||||
|
tab.isModified = false; // 重置修改状态
|
||||||
|
|
||||||
// 成功提示短暂显示后消失
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (saveStatus.value === 'success') {
|
if (tab.saveStatus === 'success') {
|
||||||
saveStatus.value = 'idle';
|
tab.saveStatus = 'idle';
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`[文件编辑器 Store] 保存文件 ${filePath.value} 失败:`, err);
|
console.error(`[文件编辑器 Store] 保存文件 ${tab.filePath} 失败:`, err);
|
||||||
isSaving.value = false;
|
tab.isSaving = false;
|
||||||
saveStatus.value = 'error';
|
tab.saveStatus = 'error';
|
||||||
saveError.value = `${t('fileManager.errors.saveFailed')}: ${err.message || err}`;
|
tab.saveError = `${t('fileManager.errors.saveFailed')}: ${err.message || err}`;
|
||||||
|
|
||||||
// 错误提示显示时间长一些
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (saveStatus.value === 'error') {
|
if (tab.saveStatus === 'error') {
|
||||||
saveStatus.value = 'idle';
|
tab.saveStatus = 'idle';
|
||||||
saveError.value = null;
|
tab.saveError = null;
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeEditor = () => {
|
// 关闭指定标签页
|
||||||
console.log('[文件编辑器 Store] 关闭编辑器。');
|
const closeTab = (tabId: string) => {
|
||||||
isVisible.value = false;
|
const tabToClose = tabs.value.get(tabId);
|
||||||
filePath.value = null;
|
if (!tabToClose) return;
|
||||||
currentSessionId.value = null;
|
|
||||||
fileContent.value = '';
|
|
||||||
loadingError.value = null;
|
|
||||||
isLoading.value = false;
|
|
||||||
saveStatus.value = 'idle';
|
|
||||||
saveError.value = null;
|
|
||||||
isSaving.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提供一个方法来更新内容,主要用于 v-model
|
// 简单处理:如果修改过,提醒用户(实际应用可能需要更复杂的确认对话框)
|
||||||
const updateContent = (newContent: string) => {
|
if (tabToClose.isModified) {
|
||||||
fileContent.value = newContent;
|
// 这里可以集成 UI 通知库来提示
|
||||||
// 当用户编辑时,可以重置保存状态(如果需要)
|
console.warn(`[文件编辑器 Store] 标签页 ${tabId} (${tabToClose.filename}) 已修改但未保存。正在关闭...`);
|
||||||
if (saveStatus.value === 'success' || saveStatus.value === 'error') {
|
// alert(`文件 ${tabToClose.filename} 已修改但未保存。确定要关闭吗?`); // 简单的 alert 示例
|
||||||
saveStatus.value = 'idle';
|
// if (!confirm(`文件 ${tabToClose.filename} 已修改但未保存。确定要关闭吗?`)) {
|
||||||
saveError.value = null;
|
// return; // 用户取消关闭
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[文件编辑器 Store] 关闭标签页: ${tabId}`);
|
||||||
|
tabs.value.delete(tabId);
|
||||||
|
|
||||||
|
// 如果关闭的是当前激活的标签页,则切换到另一个标签页
|
||||||
|
if (activeTabId.value === tabId) {
|
||||||
|
const remainingTabs = Array.from(tabs.value.keys());
|
||||||
|
if (remainingTabs.length > 0) {
|
||||||
|
// 简单切换到最后一个标签页
|
||||||
|
setActiveTab(remainingTabs[remainingTabs.length - 1]);
|
||||||
|
} else {
|
||||||
|
activeTabId.value = null; // 没有标签页了
|
||||||
|
// setEditorVisibility('closed'); // 移除:容器可见性由外部控制
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果关闭的不是活动标签页,或者活动标签页已成功切换,检查是否需要关闭容器
|
||||||
|
else if (tabs.value.size === 0) {
|
||||||
|
// setEditorVisibility('closed'); // 移除:容器可见性由外部控制
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭所有标签页
|
||||||
|
const closeAllTabs = () => {
|
||||||
|
// 简单处理:直接关闭所有,不检查修改状态(实际应用需要确认)
|
||||||
|
console.log('[文件编辑器 Store] 关闭所有标签页...');
|
||||||
|
tabs.value.clear();
|
||||||
|
activeTabId.value = null;
|
||||||
|
// setEditorVisibility('closed'); // 移除:容器可见性由外部控制
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置当前激活的标签页
|
||||||
|
const setActiveTab = (tabId: string) => {
|
||||||
|
if (tabs.value.has(tabId)) {
|
||||||
|
activeTabId.value = tabId;
|
||||||
|
console.log(`[文件编辑器 Store] 激活标签页: ${tabId}`);
|
||||||
|
// 移除:切换标签不应改变容器可见性状态
|
||||||
|
// if (editorVisibleState.value === 'closed' || editorVisibleState.value === 'minimized') {
|
||||||
|
// setEditorVisibility('visible');
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
console.warn(`[文件编辑器 Store] 尝试激活不存在的标签页: ${tabId}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
// 更新当前激活标签页的内容 (由 v-model 调用)
|
||||||
// 状态 (只读的 ref 或计算属性)
|
const updateContent = (newContent: string) => {
|
||||||
isVisible: readonly(isVisible),
|
if (activeTab.value && !activeTab.value.isLoading) {
|
||||||
filePath: readonly(filePath),
|
activeTab.value.content = newContent;
|
||||||
fileLanguage: readonly(fileLanguage),
|
// 检查是否修改
|
||||||
isLoading: readonly(isLoading),
|
activeTab.value.isModified = activeTab.value.content !== activeTab.value.originalContent;
|
||||||
loadingError: readonly(loadingError),
|
// 当用户编辑时,重置保存状态
|
||||||
isSaving: readonly(isSaving),
|
if (activeTab.value.saveStatus === 'success' || activeTab.value.saveStatus === 'error') {
|
||||||
saveStatus: readonly(saveStatus),
|
activeTab.value.saveStatus = 'idle';
|
||||||
saveError: readonly(saveError),
|
activeTab.value.saveError = null;
|
||||||
editorProps, // 提供一个包含多个只读状态的对象,方便绑定
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 可写状态 (用于 v-model)
|
// 监听会话关闭事件,移除相关标签页
|
||||||
fileContent, // 直接暴露 ref 用于 v-model
|
watch(() => sessionStore.sessions, (newSessions, oldSessions) => {
|
||||||
|
const closedSessionIds = new Set<string>();
|
||||||
|
oldSessions.forEach((_, sessionId) => {
|
||||||
|
if (!newSessions.has(sessionId)) {
|
||||||
|
closedSessionIds.add(sessionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (closedSessionIds.size > 0) {
|
||||||
|
console.log('[文件编辑器 Store] 检测到会话关闭:', Array.from(closedSessionIds));
|
||||||
|
const tabsToRemove = Array.from(tabs.value.values()).filter(tab => closedSessionIds.has(tab.sessionId));
|
||||||
|
tabsToRemove.forEach(tab => {
|
||||||
|
console.log(`[文件编辑器 Store] 移除与已关闭会话 ${tab.sessionId} 相关的标签页: ${tab.id}`);
|
||||||
|
// 这里不调用 closeTab 以避免潜在的修改提示,直接移除
|
||||||
|
tabs.value.delete(tab.id);
|
||||||
|
// 如果移除的是活动标签页,需要重新设置活动标签页
|
||||||
|
if (activeTabId.value === tab.id) {
|
||||||
|
const remainingTabs = Array.from(tabs.value.keys());
|
||||||
|
if (remainingTabs.length > 0) {
|
||||||
|
activeTabId.value = remainingTabs[remainingTabs.length - 1];
|
||||||
|
} else {
|
||||||
|
activeTabId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 如果移除后没有标签页了
|
||||||
|
if (tabs.value.size === 0) {
|
||||||
|
// setEditorVisibility('closed'); // 移除:容器可见性由外部控制
|
||||||
|
} else if (!activeTabId.value && tabs.value.size > 0) {
|
||||||
|
// 如果活动标签页被移除且没有自动设置新的,手动设置一个
|
||||||
|
activeTabId.value = Array.from(tabs.value.keys())[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { deep: false }); // 只监听 Map 本身的增删
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
tabs: readonly(tabs), // 只读 Map
|
||||||
|
activeTabId: readonly(activeTabId),
|
||||||
|
// editorVisibleState: readonly(editorVisibleState), // 移除
|
||||||
|
popupTrigger: readonly(popupTrigger), // 暴露触发器 (只读)
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
orderedTabs,
|
||||||
|
activeTab, // 只读的当前激活标签页对象
|
||||||
|
activeEditorContent, // 用于 v-model 绑定到 MonacoEditor
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
openFile,
|
openFile,
|
||||||
saveFile,
|
saveFile,
|
||||||
closeEditor,
|
closeTab,
|
||||||
updateContent, // 如果需要从外部更新内容
|
closeAllTabs,
|
||||||
|
setActiveTab,
|
||||||
|
updateContent, // 暴露给 v-model 使用
|
||||||
|
// setEditorVisibility, // 移除
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface SettingsState {
|
|||||||
ipWhitelist: string;
|
ipWhitelist: string;
|
||||||
maxLoginAttempts: string;
|
maxLoginAttempts: string;
|
||||||
loginBanDuration: string;
|
loginBanDuration: string;
|
||||||
|
showPopupFileEditor: 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; // Allow other string settings
|
||||||
}
|
}
|
||||||
@@ -37,6 +38,14 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
settings.value = response.data; // Store all fetched settings
|
settings.value = response.data; // Store all fetched settings
|
||||||
console.log('[SettingsStore] Fetched settings:', JSON.stringify(settings.value)); // 添加日志
|
console.log('[SettingsStore] Fetched settings:', JSON.stringify(settings.value)); // 添加日志
|
||||||
|
|
||||||
|
// --- 设置默认值 (如果后端未返回) ---
|
||||||
|
// 弹窗编辑器设置
|
||||||
|
if (settings.value.showPopupFileEditor === undefined) {
|
||||||
|
console.log('[SettingsStore] Setting default for showPopupFileEditor: true');
|
||||||
|
settings.value.showPopupFileEditor = 'true'; // 默认为 true
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 语言设置 ---
|
||||||
// Determine and apply language
|
// Determine and apply language
|
||||||
const langFromSettings = settings.value.language;
|
const langFromSettings = settings.value.language;
|
||||||
if (langFromSettings === 'en' || langFromSettings === 'zh') {
|
if (langFromSettings === 'en' || langFromSettings === 'zh') {
|
||||||
@@ -116,14 +125,21 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
|
|
||||||
|
|
||||||
// --- Getters ---
|
// --- Getters ---
|
||||||
// Example getter (can add more as needed)
|
// --- Getters ---
|
||||||
const language = computed(() => settings.value.language || defaultLng);
|
const language = computed(() => settings.value.language || defaultLng);
|
||||||
|
|
||||||
|
// Getter for the popup editor setting, returning boolean
|
||||||
|
const showPopupFileEditorBoolean = computed(() => {
|
||||||
|
// 默认为 true,除非明确设置为 'false'
|
||||||
|
return settings.value.showPopupFileEditor !== 'false';
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
language, // Expose getter
|
language, // Expose language getter
|
||||||
|
showPopupFileEditorBoolean, // Expose boolean getter for popup editor setting
|
||||||
loadInitialSettings,
|
loadInitialSettings,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
updateMultipleSettings,
|
updateMultipleSettings,
|
||||||
|
|||||||
@@ -42,6 +42,19 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 弹窗编辑器设置 -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>{{ $t('settings.popupEditor.title') }}</h2>
|
||||||
|
<form @submit.prevent="handleUpdatePopupEditorSetting">
|
||||||
|
<div class="form-group form-group-checkbox">
|
||||||
|
<input type="checkbox" id="showPopupEditor" v-model="popupEditorEnabled">
|
||||||
|
<label for="showPopupEditor">{{ $t('settings.popupEditor.enableLabel') }}</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="popupEditorLoading">{{ popupEditorLoading ? $t('common.saving') : $t('settings.popupEditor.saveButton') }}</button>
|
||||||
|
<p v-if="popupEditorMessage" :class="{ 'success-message': popupEditorSuccess, 'error-message': !popupEditorSuccess }">{{ popupEditorMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
@@ -193,7 +206,7 @@ const settingsStore = useSettingsStore();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// --- Reactive state from store ---
|
// --- Reactive state from store ---
|
||||||
const { settings, isLoading: settingsLoading, error: settingsError } = toRefs(settingsStore);
|
const { settings, isLoading: settingsLoading, error: settingsError, showPopupFileEditorBoolean } = toRefs(settingsStore);
|
||||||
|
|
||||||
// --- Local state for forms ---
|
// --- Local state for forms ---
|
||||||
const ipWhitelistInput = ref('');
|
const ipWhitelistInput = ref('');
|
||||||
@@ -202,6 +215,7 @@ const blacklistSettingsForm = reactive({ // Renamed to avoid conflict with store
|
|||||||
maxLoginAttempts: '5',
|
maxLoginAttempts: '5',
|
||||||
loginBanDuration: '300',
|
loginBanDuration: '300',
|
||||||
});
|
});
|
||||||
|
const popupEditorEnabled = ref(true); // 本地状态,用于 v-model
|
||||||
|
|
||||||
// --- Local UI feedback state ---
|
// --- Local UI feedback state ---
|
||||||
const ipWhitelistLoading = ref(false);
|
const ipWhitelistLoading = ref(false);
|
||||||
@@ -213,6 +227,9 @@ const languageSuccess = ref(false);
|
|||||||
const blacklistSettingsLoading = ref(false);
|
const blacklistSettingsLoading = ref(false);
|
||||||
const blacklistSettingsMessage = ref('');
|
const blacklistSettingsMessage = ref('');
|
||||||
const blacklistSettingsSuccess = ref(false);
|
const blacklistSettingsSuccess = ref(false);
|
||||||
|
const popupEditorLoading = ref(false);
|
||||||
|
const popupEditorMessage = ref('');
|
||||||
|
const popupEditorSuccess = 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) => {
|
||||||
@@ -220,8 +237,33 @@ watch(settings, (newSettings) => {
|
|||||||
selectedLanguage.value = newSettings.language || 'en';
|
selectedLanguage.value = newSettings.language || 'en';
|
||||||
blacklistSettingsForm.maxLoginAttempts = newSettings.maxLoginAttempts || '5';
|
blacklistSettingsForm.maxLoginAttempts = newSettings.maxLoginAttempts || '5';
|
||||||
blacklistSettingsForm.loginBanDuration = newSettings.loginBanDuration || '300';
|
blacklistSettingsForm.loginBanDuration = newSettings.loginBanDuration || '300';
|
||||||
|
// 同步弹窗编辑器设置
|
||||||
|
popupEditorEnabled.value = showPopupFileEditorBoolean.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 ---
|
||||||
|
const handleUpdatePopupEditorSetting = async () => {
|
||||||
|
popupEditorLoading.value = true;
|
||||||
|
popupEditorMessage.value = '';
|
||||||
|
popupEditorSuccess.value = false;
|
||||||
|
try {
|
||||||
|
// 将布尔值转换为字符串 'true' 或 'false' 来存储
|
||||||
|
const valueToSave = popupEditorEnabled.value ? 'true' : 'false';
|
||||||
|
await settingsStore.updateSetting('showPopupFileEditor', valueToSave);
|
||||||
|
popupEditorMessage.value = t('settings.popupEditor.success.saved'); // 需要添加翻译
|
||||||
|
popupEditorSuccess.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新弹窗编辑器设置失败:', error);
|
||||||
|
popupEditorMessage.value = error.message || t('settings.popupEditor.error.saveFailed'); // 需要添加翻译
|
||||||
|
popupEditorSuccess.value = false;
|
||||||
|
// 保存失败时,将本地复选框状态恢复为 Store 中的状态
|
||||||
|
popupEditorEnabled.value = showPopupFileEditorBoolean.value;
|
||||||
|
} finally {
|
||||||
|
popupEditorLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Passkey state & methods --- (Keep as is)
|
// --- Passkey state & methods --- (Keep as is)
|
||||||
const passkeyName = ref('');
|
const passkeyName = ref('');
|
||||||
const passkeyMessage = ref<string | null>(null);
|
const passkeyMessage = ref<string | null>(null);
|
||||||
@@ -565,6 +607,23 @@ img {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 复选框组样式 */
|
||||||
|
.form-group-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-checkbox input[type="checkbox"] {
|
||||||
|
width: auto; /* 不要占满宽度 */
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-checkbox label {
|
||||||
|
display: inline-block; /* 让标签和复选框在同一行 */
|
||||||
|
margin-bottom: 0; /* 移除默认的块级标签下边距 */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.blacklist-table th {
|
.blacklist-table th {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import StatusMonitorComponent from '../components/StatusMonitor.vue';
|
|||||||
import WorkspaceConnectionListComponent from '../components/WorkspaceConnectionList.vue';
|
import WorkspaceConnectionListComponent from '../components/WorkspaceConnectionList.vue';
|
||||||
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
|
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
|
||||||
import TerminalTabBar from '../components/TerminalTabBar.vue';
|
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 { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance
|
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance
|
||||||
import type { ConnectionInfo } from '../stores/connections.store';
|
import type { ConnectionInfo } from '../stores/connections.store';
|
||||||
// 导入 splitpanes 组件
|
// 导入 splitpanes 组件
|
||||||
@@ -88,11 +89,11 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="main-content-area">
|
<div class="main-content-area">
|
||||||
<!-- 最外层:左右分割 (连接列表 | 中间区域 + 右侧区域) -->
|
<!-- 最外层:左右分割 (连接列表 | 中间区域 | 编辑器 | 状态监视器) -->
|
||||||
<splitpanes class="default-theme" :horizontal="false" style="height: 100%">
|
<splitpanes class="default-theme" :horizontal="false" style="height: 100%">
|
||||||
|
|
||||||
<!-- 左侧边栏 Pane -->
|
<!-- 1. 左侧边栏 Pane (连接列表) -->
|
||||||
<pane size="20" min-size="15" class="sidebar-pane">
|
<pane size="15" min-size="10" class="sidebar-pane"> <!-- 调整大小 -->
|
||||||
<WorkspaceConnectionListComponent
|
<WorkspaceConnectionListComponent
|
||||||
@connect-request="(id) => { console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`); sessionStore.handleConnectRequest(id); }"
|
@connect-request="(id) => { console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`); sessionStore.handleConnectRequest(id); }"
|
||||||
@open-new-session="(id) => { console.log(`[WorkspaceView] Received 'open-new-session' event for ID: ${id}`); sessionStore.handleOpenNewSession(id); }"
|
@open-new-session="(id) => { console.log(`[WorkspaceView] Received 'open-new-session' event for ID: ${id}`); sessionStore.handleOpenNewSession(id); }"
|
||||||
@@ -101,22 +102,18 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</pane>
|
</pane>
|
||||||
|
|
||||||
<!-- 中间区域 Pane (包含终端、命令栏、文件管理器) -->
|
<!-- 2. 中间区域 Pane (终端/命令栏/文件管理器) -->
|
||||||
<pane size="65" min-size="30" class="middle-pane">
|
<pane size="50" min-size="30" class="middle-pane"> <!-- 调整大小 -->
|
||||||
<!-- 上下分割 (终端 | 命令栏 | 文件管理器) - 禁用双击分割线行为 -->
|
<!-- 上下分割 (终端 | 命令栏 | 文件管理器) -->
|
||||||
<splitpanes :horizontal="true" style="height: 100%" :dbl-click-splitter="false">
|
<splitpanes :horizontal="true" style="height: 100%" :dbl-click-splitter="false">
|
||||||
<!-- 上方 Pane (终端) -->
|
<!-- 上方 Pane (终端) -->
|
||||||
<pane size="59" min-size="20" class="terminal-pane"> <!-- 调整 size -->
|
<pane size="55" min-size="20" class="terminal-pane"> <!-- 调整大小 -->
|
||||||
<!-- 会话终端区域: 只渲染活动会话的终端 -->
|
|
||||||
<div
|
<div
|
||||||
v-for="tabInfo in sessionTabsWithStatus"
|
v-for="tabInfo in sessionTabsWithStatus"
|
||||||
:key="tabInfo.sessionId"
|
:key="tabInfo.sessionId"
|
||||||
v-show="tabInfo.sessionId === activeSessionId"
|
v-show="tabInfo.sessionId === activeSessionId"
|
||||||
class="terminal-session-wrapper"
|
class="terminal-session-wrapper"
|
||||||
>
|
>
|
||||||
<!-- 移除 v-if,依赖外层 v-show 控制显隐 -->
|
|
||||||
<!-- :key 绑定到 tabInfo.sessionId 保证每个会话对应唯一组件实例 -->
|
|
||||||
<!-- :is-active 动态绑定 -->
|
|
||||||
<TerminalComponent
|
<TerminalComponent
|
||||||
:key="tabInfo.sessionId"
|
:key="tabInfo.sessionId"
|
||||||
:session-id="tabInfo.sessionId"
|
:session-id="tabInfo.sessionId"
|
||||||
@@ -126,24 +123,22 @@ onBeforeUnmount(() => {
|
|||||||
@resize="(dims) => { console.log(`[工作区视图 ${tabInfo.sessionId}] 收到 resize 事件:`, dims); sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalResize(dims); }"
|
@resize="(dims) => { console.log(`[工作区视图 ${tabInfo.sessionId}] 收到 resize 事件:`, dims); sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalResize(dims); }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 终端占位符 -->
|
|
||||||
<div v-if="!activeSessionId" class="terminal-placeholder">
|
<div v-if="!activeSessionId" class="terminal-placeholder">
|
||||||
<h2>{{ t('workspace.selectConnectionPrompt') }}</h2>
|
<h2>{{ t('workspace.selectConnectionPrompt') }}</h2>
|
||||||
<p>{{ t('workspace.selectConnectionHint') }}</p>
|
<p>{{ t('workspace.selectConnectionHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</pane> <!-- End Terminal Pane -->
|
</pane> <!-- End Terminal Pane -->
|
||||||
|
|
||||||
<!-- 中间 Pane (命令栏) - 略微增加 min-size 和初始 size -->
|
<!-- 中间 Pane (命令栏) -->
|
||||||
<pane size="6" min-size="6" class="command-bar-pane">
|
<pane size="5" min-size="5" class="command-bar-pane">
|
||||||
<CommandInputBar
|
<CommandInputBar
|
||||||
v-if="activeSessionId"
|
v-if="activeSessionId"
|
||||||
@send-command="handleSendCommand"
|
@send-command="handleSendCommand"
|
||||||
/>
|
/>
|
||||||
</pane> <!-- End Command Bar Pane -->
|
</pane> <!-- End Command Bar Pane -->
|
||||||
|
|
||||||
<!-- 下方 Pane (文件管理器) - 恢复原始 size -->
|
<!-- 下方 Pane (文件管理器) -->
|
||||||
<pane size="35" min-size="15" class="file-manager-pane">
|
<pane size="40" min-size="15" class="file-manager-pane"> <!-- 调整大小 -->
|
||||||
<!-- 为每个会话渲染文件管理器实例,用 v-show 控制 -->
|
|
||||||
<div
|
<div
|
||||||
v-for="tabInfo in sessionTabsWithStatus"
|
v-for="tabInfo in sessionTabsWithStatus"
|
||||||
:key="tabInfo.sessionId + '-fm-wrapper'"
|
:key="tabInfo.sessionId + '-fm-wrapper'"
|
||||||
@@ -164,15 +159,18 @@ onBeforeUnmount(() => {
|
|||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 文件管理器占位符 -->
|
|
||||||
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('fileManager.noActiveSession') }}</div>
|
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('fileManager.noActiveSession') }}</div>
|
||||||
</pane>
|
</pane> <!-- End File Manager Pane -->
|
||||||
</splitpanes> <!-- End Terminal/FM Splitpanes -->
|
</splitpanes> <!-- End Middle Area Splitpanes -->
|
||||||
</pane> <!-- End Middle Pane -->
|
</pane> <!-- End Middle Pane -->
|
||||||
|
|
||||||
<!-- 右侧边栏 Pane (状态监视器) - 添加 status-monitor-pane 类 -->
|
<!-- 3. 右侧区域 1 Pane (文件编辑器) -->
|
||||||
<pane size="15" min-size="10" class="sidebar-pane status-monitor-pane">
|
<pane size="20" min-size="15" class="file-editor-pane"> <!-- 新增编辑器窗格 -->
|
||||||
<!-- 为每个会话渲染状态监视器实例,用 v-show 控制 -->
|
<FileEditorContainer />
|
||||||
|
</pane>
|
||||||
|
|
||||||
|
<!-- 4. 右侧区域 2 Pane (状态监视器) -->
|
||||||
|
<pane size="15" min-size="10" class="sidebar-pane status-monitor-pane"> <!-- 调整大小 -->
|
||||||
<div
|
<div
|
||||||
v-for="tabInfo in sessionTabsWithStatus"
|
v-for="tabInfo in sessionTabsWithStatus"
|
||||||
:key="tabInfo.sessionId + '-sm-wrapper'"
|
:key="tabInfo.sessionId + '-sm-wrapper'"
|
||||||
@@ -187,7 +185,6 @@ onBeforeUnmount(() => {
|
|||||||
:status-error="sessionStore.sessions.get(tabInfo.sessionId)!.statusMonitorManager.statusError.value"
|
:status-error="sessionStore.sessions.get(tabInfo.sessionId)!.statusMonitorManager.statusError.value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 状态监视器占位符 -->
|
|
||||||
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('statusMonitor.noActiveSession') }}</div>
|
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('statusMonitor.noActiveSession') }}</div>
|
||||||
</pane>
|
</pane>
|
||||||
|
|
||||||
@@ -226,8 +223,10 @@ onBeforeUnmount(() => {
|
|||||||
.sidebar-pane, /* 用于左右侧边栏 */
|
.sidebar-pane, /* 用于左右侧边栏 */
|
||||||
.middle-pane, /* 中间包含终端、命令栏、文件管理器的 Pane */
|
.middle-pane, /* 中间包含终端、命令栏、文件管理器的 Pane */
|
||||||
.terminal-pane,
|
.terminal-pane,
|
||||||
.command-bar-pane, /* 命令栏 Pane */
|
.command-bar-pane,
|
||||||
.file-manager-pane {
|
.file-editor-pane, /* 新增编辑器窗格样式 */
|
||||||
|
.file-manager-pane,
|
||||||
|
.status-monitor-pane { /* 添加状态监视器样式 */
|
||||||
display: flex; /* 确保 Pane 内容可以正确布局 */
|
display: flex; /* 确保 Pane 内容可以正确布局 */
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden; /* Pane 内部内容溢出时隐藏 */
|
overflow: hidden; /* Pane 内部内容溢出时隐藏 */
|
||||||
@@ -255,9 +254,19 @@ onBeforeUnmount(() => {
|
|||||||
background-color: #1e1e1e; /* 终端背景 */
|
background-color: #1e1e1e; /* 终端背景 */
|
||||||
position: relative; /* 保持相对定位用于占位符 */
|
position: relative; /* 保持相对定位用于占位符 */
|
||||||
}
|
}
|
||||||
|
.file-editor-pane {
|
||||||
|
background-color: #2d2d2d; /* 与编辑器容器背景一致 */
|
||||||
|
}
|
||||||
.file-manager-pane {
|
.file-manager-pane {
|
||||||
/* 分隔线由 splitpanes 提供 */
|
/* 分隔线由 splitpanes 提供 */
|
||||||
|
background-color: #ffffff; /* 文件管理器使用浅色背景 */
|
||||||
}
|
}
|
||||||
|
.status-monitor-pane {
|
||||||
|
/* 状态监视器样式 */
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 终端会话包装器 */
|
/* 终端会话包装器 */
|
||||||
.terminal-session-wrapper {
|
.terminal-session-wrapper {
|
||||||
@@ -283,23 +292,6 @@ onBeforeUnmount(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新增:状态监视器 Pane 样式,使其内容居中 */
|
|
||||||
.status-monitor-pane {
|
|
||||||
/* 尝试使用 flex 居中,如果 StatusMonitorComponent 本身是块级元素 */
|
|
||||||
/* display: flex; */
|
|
||||||
/* justify-content: center; */
|
|
||||||
/* align-items: center; */
|
|
||||||
|
|
||||||
/* 或者如果内容主要是文本,可以尝试 text-align */
|
|
||||||
text-align: center; /* 尝试文本居中 */
|
|
||||||
padding: 1rem; /* 添加一些内边距 */
|
|
||||||
}
|
|
||||||
.status-monitor-pane > .status-monitor-wrapper {
|
|
||||||
/* 如果需要包装器也居中(如果它不是 flex: 1 的话) */
|
|
||||||
/* margin: auto; */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* 终端占位符 */
|
/* 终端占位符 */
|
||||||
.terminal-placeholder {
|
.terminal-placeholder {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user