update
This commit is contained in:
@@ -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">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue'; // 导入 ref, watch 等
|
||||
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
|
||||
|
||||
// 不再需要 props 或 emits,状态和操作来自 Store
|
||||
// const props = defineProps<{...}>();
|
||||
// const emit = defineEmits<{...}>();
|
||||
// 导入设置 store 以检查弹窗设置 (虽然 App.vue 做了顶层控制,但这里可以加一层保险)
|
||||
import { useSettingsStore } from '../stores/settings.store';
|
||||
|
||||
const { t } = useI18n();
|
||||
const fileEditorStore = useFileEditorStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// 从 Store 获取状态 (使用 storeToRefs 保持响应性)
|
||||
// --- 本地状态控制弹窗显示 ---
|
||||
const isVisible = ref(false);
|
||||
|
||||
// --- 从 Store 获取状态 ---
|
||||
const {
|
||||
isVisible,
|
||||
filePath,
|
||||
fileLanguage, // 重命名为 language 以匹配 MonacoEditor prop
|
||||
isLoading,
|
||||
loadingError,
|
||||
isSaving,
|
||||
saveStatus,
|
||||
saveError,
|
||||
fileContent, // 直接使用 store 中的 ref 进行 v-model 绑定
|
||||
// editorVisibleState, // 不再使用
|
||||
activeTab, // 当前激活的标签页对象 (computed)
|
||||
activeEditorContent,// 用于 v-model 绑定 (computed)
|
||||
orderedTabs, // 标签页数组 (computed)
|
||||
popupTrigger, // 监听这个值的变化来显示弹窗
|
||||
} = storeToRefs(fileEditorStore);
|
||||
const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); // 获取弹窗设置
|
||||
|
||||
// 从 Store 获取方法
|
||||
const { saveFile, closeEditor, updateContent } = fileEditorStore;
|
||||
// --- 从 Store 获取方法 ---
|
||||
const {
|
||||
saveFile, // 现在保存当前激活的标签页
|
||||
closeTab, // 关闭指定标签页 (由 FileEditorTabs 调用)
|
||||
setActiveTab, // 设置激活标签页 (由 FileEditorTabs 调用)
|
||||
// setEditorVisibility, // 不再使用
|
||||
// closeAllTabs, // 不在此组件中关闭所有
|
||||
} = fileEditorStore;
|
||||
|
||||
// 计算属性,用于 v-model 绑定到 MonacoEditor
|
||||
// 直接绑定 store 中的 fileContent ref
|
||||
const editorContent = computed({
|
||||
get: () => fileContent.value,
|
||||
set: (value) => updateContent(value), // 调用 store action 更新内容
|
||||
});
|
||||
// --- 弹窗尺寸和拖拽状态 ---
|
||||
const popupWidthPx = ref(window.innerWidth * 0.75); // 初始宽度 75vw (像素)
|
||||
const popupHeightPx = ref(window.innerHeight * 0.85); // 初始高度 85vh (像素)
|
||||
const isResizing = ref(false);
|
||||
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 = () => {
|
||||
// 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>
|
||||
|
||||
<template>
|
||||
<!-- 使用 store 中的 isVisible 控制显示 -->
|
||||
<!-- 将 v-if 移到遮罩层上 -->
|
||||
<div v-if="isVisible" class="editor-overlay-backdrop" @click.self="handleClose"> <!-- 点击背景关闭 -->
|
||||
<!-- 添加弹窗容器 -->
|
||||
<div class="editor-popup">
|
||||
<div class="editor-header">
|
||||
<!-- 使用 store 中的 filePath -->
|
||||
<span>{{ t('fileManager.editingFile') }}: {{ filePath }}</span>
|
||||
<div class="editor-actions">
|
||||
<!-- 使用 store 中的保存状态 -->
|
||||
<span v-if="saveStatus === 'saving'" class="save-status saving">{{ t('fileManager.saving') }}...</span>
|
||||
<span v-if="saveStatus === 'success'" class="save-status success">✅ {{ t('fileManager.saveSuccess') }}</span>
|
||||
<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">
|
||||
{{ isSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
||||
</button>
|
||||
<!-- 关闭按钮,使用 store 方法 -->
|
||||
<button @click="handleClose" class="close-editor-btn">✖</button>
|
||||
<!-- 使用本地 isVisible 控制显示 (App.vue 中已有 v-if="showPopupFileEditorBoolean") -->
|
||||
<div v-if="isVisible" class="editor-overlay-backdrop" @click.self="handleCloseContainer"> <!-- 恢复点击背景关闭 -->
|
||||
<!-- 编辑器弹窗/容器,应用动态样式 -->
|
||||
<div class="editor-popup" :style="popupStyle">
|
||||
|
||||
<!-- 1. 标签栏 -->
|
||||
<!-- 1. 标签栏 -->
|
||||
<FileEditorTabs
|
||||
:tabs="orderedTabs"
|
||||
:active-tab-id="activeTab?.id ?? null"
|
||||
@activate-tab="setActiveTab"
|
||||
@close-tab="closeTab"
|
||||
/>
|
||||
|
||||
<!-- 2. 编辑器头部 (显示当前激活标签信息) -->
|
||||
<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>
|
||||
<!-- 关闭整个容器按钮 -->
|
||||
<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>
|
||||
<!-- 使用 store 中的加载状态 -->
|
||||
<div v-if="isLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
|
||||
<!-- 使用 store 中的加载错误 -->
|
||||
<div v-else-if="loadingError" class="editor-error">{{ loadingError }}</div>
|
||||
<!-- Monaco 编辑器实例 -->
|
||||
<MonacoEditor
|
||||
v-else
|
||||
v-model="editorContent"
|
||||
:language="fileLanguage"
|
||||
theme="vs-dark"
|
||||
class="editor-instance"
|
||||
@request-save="handleSaveRequest"
|
||||
/>
|
||||
<!-- 如果没有活动标签页,显示提示 -->
|
||||
<div v-else class="editor-header editor-header-placeholder">
|
||||
<span>{{ t('fileManager.noOpenFile') }}</span>
|
||||
<button @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')">✖</button>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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-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>
|
||||
|
||||
<style scoped>
|
||||
/* 样式调整为居中弹窗样式 */
|
||||
.editor-overlay-backdrop { /* 新增背景遮罩层 */
|
||||
/* 样式基本保持不变,但可能需要为标签栏和新状态调整 */
|
||||
.editor-overlay-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6); /* 半透明黑色背景 */
|
||||
z-index: 1000; /* 确保在最上层 */
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-popup { /* 编辑器本身的容器 */
|
||||
width: 60%; /* 设置宽度为 60% */
|
||||
height: 80%; /* 设置一个合适的高度 */
|
||||
background-color: #2d2d2d; /* 深色背景 */
|
||||
.editor-popup {
|
||||
width: 75%; /* 可以适当调整大小 */
|
||||
height: 85%;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #f0f0f0;
|
||||
overflow: hidden; /* 防止内容溢出圆角 */
|
||||
overflow: hidden;
|
||||
position: relative; /* 为拖拽手柄定位 */
|
||||
}
|
||||
|
||||
/* 标签栏区域 (FileEditorTabs 组件将放在这里) */
|
||||
/* .file-tabs-container { ... } */
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -119,7 +241,17 @@ const handleClose = () => {
|
||||
background-color: #333;
|
||||
border-bottom: 1px solid #555;
|
||||
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 {
|
||||
@@ -134,23 +266,37 @@ const handleClose = () => {
|
||||
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;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
flex-grow: 1; /* 占据剩余空间 */
|
||||
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; /* 添加按钮间距 */
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
@@ -158,7 +304,6 @@ const handleClose = () => {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.4rem 0.8rem;
|
||||
/* margin-left: 1rem; */ /* 使用 gap 代替 */
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
@@ -172,26 +317,61 @@ const handleClose = () => {
|
||||
}
|
||||
|
||||
.save-status {
|
||||
/* margin-left: 1rem; */ /* 使用 gap 代替 */
|
||||
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;
|
||||
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; /* 对 flex 布局中的子元素很重要 */
|
||||
flex-grow: 1;
|
||||
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>
|
||||
|
||||
@@ -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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<button @click="handleNewFileContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFile')">📄 {{ t('fileManager.actions.newFile') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<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 -->
|
||||
<button @click="handleNewFileContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFile')">📄 {{ t('fileManager.actions.newFile') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表容器 -->
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user