864 lines
29 KiB
Vue
864 lines
29 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'; // + nextTick
|
||
import { useI18n } from 'vue-i18n';
|
||
import { storeToRefs } from 'pinia';
|
||
import MonacoEditor from './MonacoEditor.vue';
|
||
import FileEditorTabs from './FileEditorTabs.vue';
|
||
import { useFileEditorStore, type FileTab } from '../stores/fileEditor.store';
|
||
import { useSettingsStore } from '../stores/settings.store';
|
||
import { useSessionStore } from '../stores/session.store';
|
||
|
||
|
||
const { t } = useI18n();
|
||
const fileEditorStore = useFileEditorStore();
|
||
const settingsStore = useSettingsStore();
|
||
const sessionStore = useSessionStore();
|
||
|
||
// --- 本地状态控制弹窗显示 ---
|
||
const isVisible = ref(false);
|
||
|
||
// 接收 isMobile 属性
|
||
const props = defineProps<{
|
||
isMobile?: boolean;
|
||
}>();
|
||
|
||
// --- 从 Store 获取状态 ---
|
||
// 全局 Store (用于共享模式和触发器)
|
||
const {
|
||
popupTrigger,
|
||
popupFileInfo, // 包含 sessionId 和 filePath
|
||
activeTabId: globalActiveTabIdRef, // 获取全局 activeTabId
|
||
// tabs: globalTabsRef, // 不再使用 storeToRefs 获取 tabs
|
||
} = storeToRefs(fileEditorStore);
|
||
|
||
// 设置 Store (用于判断模式)
|
||
const { showPopupFileEditorBoolean, shareFileEditorTabsBoolean } = storeToRefs(settingsStore);
|
||
|
||
// --- 从 Store 获取方法 ---
|
||
// 全局 Store Actions (用于共享模式)
|
||
const {
|
||
saveFile: saveGlobalFile,
|
||
closeTab: closeGlobalTab,
|
||
setActiveTab: setGlobalActiveTab,
|
||
updateFileContent: updateGlobalFileContent,
|
||
// + 添加右键菜单操作 actions
|
||
closeOtherTabs, // 修正:移除 Global 后缀
|
||
closeTabsToTheRight, // 修正:移除 Global 后缀
|
||
closeTabsToTheLeft, // 修正:移除 Global 后缀
|
||
changeEncoding: changeGlobalEncoding, // +++ 全局编码更改 action +++
|
||
} = fileEditorStore;
|
||
|
||
// 会话 Store Actions (用于非共享模式)
|
||
const {
|
||
saveFileInSession,
|
||
closeEditorTabInSession,
|
||
setActiveEditorTabInSession,
|
||
updateFileContentInSession,
|
||
// + 添加右键菜单操作 actions
|
||
closeOtherTabsInSession,
|
||
closeTabsToTheRightInSession,
|
||
closeTabsToTheLeftInSession,
|
||
changeEncodingInSession, // +++ 会话编码更改 action +++
|
||
} = sessionStore;
|
||
|
||
// --- 移除本地文件状态 ---
|
||
// const popupFilePath = ref<string | null>(null);
|
||
// const popupSessionId = ref<string | null>(null);
|
||
// const popupFilename = ref<string>('');
|
||
// const popupContent = ref<string>('');
|
||
// const popupOriginalContent = ref<string>('');
|
||
// const popupLanguage = ref<string>('plaintext');
|
||
// const popupIsLoading = ref<boolean>(false);
|
||
// const popupLoadingError = ref<string | null>(null);
|
||
// const popupIsSaving = ref<boolean>(false);
|
||
// const popupSaveStatus = ref<SaveStatus>('idle');
|
||
// const popupSaveError = ref<string | null>(null);
|
||
// const popupIsModified = computed(() => popupContent.value !== popupOriginalContent.value);
|
||
|
||
// --- 弹窗尺寸和拖拽状态 ---
|
||
const popupWidthPx = ref(window.innerWidth * 0.75); // 初始宽度 75vw (像素)
|
||
const 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; // 最小高度
|
||
const encodingSelectRef = ref<HTMLSelectElement | null>(null); // +++ Ref for the select element +++
|
||
|
||
// --- 计算属性,用于模板绑定 ---
|
||
const popupStyle = computed(() => {
|
||
if (props.isMobile) {
|
||
return {
|
||
width: '100vw',
|
||
height: '100vh',
|
||
maxWidth: '100%',
|
||
maxHeight: '100%',
|
||
borderRadius: '0',
|
||
};
|
||
} else {
|
||
return {
|
||
width: `${popupWidthPx.value}px`,
|
||
height: `${popupHeightPx.value}px`,
|
||
};
|
||
}
|
||
});
|
||
|
||
// --- 动态计算属性 (根据模式选择数据源) ---
|
||
|
||
// +++ Function to calculate and set the select width (copied from FileEditorContainer) +++
|
||
const updateSelectWidth = () => {
|
||
nextTick(() => { // Ensure DOM is updated before measuring
|
||
if (!encodingSelectRef.value) return;
|
||
|
||
const selectElement = encodingSelectRef.value;
|
||
const selectedOption = selectElement.options[selectElement.selectedIndex];
|
||
|
||
if (!selectedOption) return;
|
||
|
||
// Create a temporary span to measure text width
|
||
const tempSpan = document.createElement('span');
|
||
// Copy relevant styles (adjust as needed for accurate measurement)
|
||
const styles = window.getComputedStyle(selectElement);
|
||
tempSpan.style.fontSize = styles.fontSize;
|
||
tempSpan.style.fontFamily = styles.fontFamily;
|
||
tempSpan.style.fontWeight = styles.fontWeight;
|
||
tempSpan.style.letterSpacing = styles.letterSpacing;
|
||
tempSpan.style.paddingLeft = styles.paddingLeft; // Include padding for accuracy
|
||
tempSpan.style.paddingRight = styles.paddingRight;
|
||
tempSpan.style.visibility = 'hidden'; // Make it invisible
|
||
tempSpan.style.position = 'absolute'; // Prevent layout shift
|
||
tempSpan.style.whiteSpace = 'nowrap'; // Prevent wrapping
|
||
tempSpan.style.left = '-9999px'; // Move off-screen
|
||
|
||
tempSpan.textContent = selectedOption.text;
|
||
document.body.appendChild(tempSpan);
|
||
|
||
const textWidth = tempSpan.offsetWidth;
|
||
document.body.removeChild(tempSpan);
|
||
|
||
// Set the select width (add extra space for dropdown arrow, adjust as needed)
|
||
const arrowPadding = 25; // Increased padding for arrow and visual spacing
|
||
selectElement.style.width = `${textWidth + arrowPadding}px`;
|
||
// console.log(`[EditorOverlay] Setting select width for "${selectedOption.text}" to ${textWidth + arrowPadding}px`);
|
||
});
|
||
};
|
||
|
||
// 获取当前弹窗关联的会话 (仅非共享模式需要)
|
||
const currentSession = computed(() => {
|
||
if (shareFileEditorTabsBoolean.value || !popupFileInfo.value?.sessionId) {
|
||
return null;
|
||
}
|
||
return sessionStore.sessions.get(popupFileInfo.value.sessionId) ?? null;
|
||
});
|
||
|
||
// 获取当前模式下的标签页列表
|
||
const orderedTabs = computed(() => {
|
||
// 直接访问 store.tabs
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
return Array.from(fileEditorStore.tabs.values()); // 直接访问 store
|
||
} else {
|
||
// 非共享模式保持不变,因为它依赖 sessionStore
|
||
return currentSession.value?.editorTabs.value ?? [];
|
||
}
|
||
});
|
||
|
||
// 获取当前模式下的活动标签页 ID
|
||
const activeTabId = computed(() => {
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
return globalActiveTabIdRef.value; // 全局 Store
|
||
} else {
|
||
return currentSession.value?.activeEditorTabId.value ?? null; // 会话 Store
|
||
}
|
||
});
|
||
|
||
// 获取当前模式下的活动标签页对象
|
||
const activeTab = computed((): FileTab | null => {
|
||
const currentId = activeTabId.value;
|
||
if (!currentId) return null;
|
||
|
||
// 直接访问 store.tabs
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
return fileEditorStore.tabs.get(currentId) ?? null; // 直接访问 store
|
||
} else {
|
||
// 非共享模式保持不变
|
||
return currentSession.value?.editorTabs.value.find(tab => tab.id === currentId) ?? null;
|
||
}
|
||
});
|
||
|
||
// Monaco 编辑器内容绑定 (根据模式调用不同 action)
|
||
const activeEditorContent = computed({
|
||
get: () => activeTab.value?.content ?? '',
|
||
set: (value) => {
|
||
const currentActiveTab = activeTab.value; // 缓存当前活动标签
|
||
if (!currentActiveTab) return;
|
||
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
updateGlobalFileContent(currentActiveTab.id, value); // 全局 Store
|
||
} else {
|
||
// 非共享模式需要 sessionId
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
updateFileContentInSession(sessionId, currentActiveTab.id, value); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法更新内容:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
},
|
||
});
|
||
|
||
|
||
// --- 从 activeTab 派生的计算属性 (保持不变,因为 activeTab 已动态化) ---
|
||
const currentTabIsLoading = computed(() => activeTab.value?.isLoading ?? false);
|
||
const currentTabLoadingError = computed(() => activeTab.value?.loadingError ?? null);
|
||
const currentTabIsSaving = computed(() => activeTab.value?.isSaving ?? false);
|
||
const currentTabSaveStatus = computed(() => activeTab.value?.saveStatus ?? 'idle');
|
||
const currentTabSaveError = computed(() => activeTab.value?.saveError ?? null);
|
||
const currentTabLanguage = computed(() => activeTab.value?.language ?? 'plaintext');
|
||
const currentTabFilePath = computed(() => activeTab.value?.filePath ?? '');
|
||
const currentTabIsModified = computed(() => activeTab.value?.isModified ?? false);
|
||
// +++ 计算当前选择的编码 (与 Container 逻辑一致) +++
|
||
const currentSelectedEncoding = computed(() => activeTab.value?.selectedEncoding ?? 'utf-8');
|
||
// +++ 计算当前活动标签的会话名称 (与 Container 逻辑一致) +++
|
||
const currentTabSessionName = computed(() => {
|
||
const sessionId = activeTab.value?.sessionId;
|
||
if (!sessionId) return null;
|
||
// sessionStore 已在 setup 中实例化
|
||
return sessionStore.sessions.get(sessionId)?.connectionName ?? null; // 修正:使用 connectionName
|
||
});
|
||
|
||
// --- 事件处理 (根据模式调用不同 action) ---
|
||
|
||
// +++ 编码选项 (copied from FileEditorContainer) +++
|
||
// 注意:这里的 value 需要与 iconv-lite 支持的标签匹配 (后端使用)
|
||
const encodingOptions = ref([
|
||
// Unicode
|
||
{ value: 'utf-8', text: 'UTF-8' },
|
||
{ value: 'utf-16le', text: 'UTF-16 LE' },
|
||
{ value: 'utf-16be', text: 'UTF-16 BE' },
|
||
// Chinese
|
||
{ value: 'gbk', text: 'GBK' },
|
||
{ value: 'gb18030', text: 'GB18030' },
|
||
{ value: 'big5', text: 'Big5 (Traditional Chinese)' },
|
||
// Japanese
|
||
{ value: 'shift_jis', text: 'Shift-JIS' },
|
||
{ value: 'euc-jp', text: 'EUC-JP' },
|
||
// Korean
|
||
{ value: 'euc-kr', text: 'EUC-KR' },
|
||
// Western European
|
||
{ value: 'iso-8859-1', text: 'ISO-8859-1 (Latin-1)' },
|
||
{ value: 'iso-8859-15', text: 'ISO-8859-15 (Latin-9)' },
|
||
{ value: 'cp1252', text: 'Windows-1252' }, // Western European
|
||
// Central European
|
||
{ value: 'iso-8859-2', text: 'ISO-8859-2 (Latin-2)' },
|
||
{ value: 'cp1250', text: 'Windows-1250' }, // Central European
|
||
// Cyrillic
|
||
{ value: 'iso-8859-5', text: 'ISO-8859-5 (Cyrillic)' },
|
||
{ value: 'cp1251', text: 'Windows-1251 (Cyrillic)' },
|
||
{ value: 'koi8-r', text: 'KOI8-R' },
|
||
{ value: 'koi8-u', text: 'KOI8-U' },
|
||
// Greek
|
||
{ value: 'iso-8859-7', text: 'ISO-8859-7 (Greek)' },
|
||
{ value: 'cp1253', text: 'Windows-1253 (Greek)' },
|
||
// Turkish
|
||
{ value: 'iso-8859-9', text: 'ISO-8859-9 (Turkish)' },
|
||
{ value: 'cp1254', text: 'Windows-1254 (Turkish)' },
|
||
// Hebrew
|
||
{ value: 'iso-8859-8', text: 'ISO-8859-8 (Hebrew)' },
|
||
{ value: 'cp1255', text: 'Windows-1255 (Hebrew)' },
|
||
// Arabic
|
||
{ value: 'iso-8859-6', text: 'ISO-8859-6 (Arabic)' },
|
||
{ value: 'cp1256', text: 'Windows-1256 (Arabic)' },
|
||
// Baltic
|
||
{ value: 'iso-8859-4', text: 'ISO-8859-4 (Baltic)' }, // Latin-4
|
||
{ value: 'iso-8859-13', text: 'ISO-8859-13 (Baltic)' }, // Latin-7
|
||
{ value: 'cp1257', text: 'Windows-1257 (Baltic)' },
|
||
// Vietnamese
|
||
{ value: 'cp1258', text: 'Windows-1258 (Vietnamese)' },
|
||
// Thai
|
||
{ value: 'tis-620', text: 'TIS-620 (Thai)' }, // Often cp874
|
||
{ value: 'cp874', text: 'Windows-874 (Thai)' },
|
||
]);
|
||
|
||
// 保存当前激活的标签页
|
||
const handleSaveRequest = () => {
|
||
const currentActiveTab = activeTab.value;
|
||
if (!currentActiveTab) return;
|
||
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
saveGlobalFile(currentActiveTab.id); // 全局 Store
|
||
} else {
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
saveFileInSession(sessionId, currentActiveTab.id); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法保存:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
};
|
||
|
||
// 激活标签页
|
||
const handleActivateTab = (tabId: string) => {
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
setGlobalActiveTab(tabId); // 全局 Store
|
||
} else {
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
setActiveEditorTabInSession(sessionId, tabId); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法激活标签页:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
};
|
||
|
||
// 关闭标签页
|
||
const handleCloseTab = (tabId: string) => {
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
closeGlobalTab(tabId); // 全局 Store
|
||
} else {
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
closeEditorTabInSession(sessionId, tabId); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法关闭标签页:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
};
|
||
|
||
// +++ 处理右键菜单事件 +++
|
||
const handleCloseOtherTabs = (targetTabId: string) => {
|
||
console.log(`[FileEditorOverlay] handleCloseOtherTabs called for target: ${targetTabId}`); // Add log
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
closeOtherTabs(targetTabId); // 修正:调用正确的 action 名称
|
||
} else {
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
closeOtherTabsInSession(sessionId, targetTabId); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法关闭其他标签页:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleCloseRightTabs = (targetTabId: string) => {
|
||
console.log(`[FileEditorOverlay] handleCloseRightTabs called for target: ${targetTabId}`); // Add log
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
closeTabsToTheRight(targetTabId); // 修正:调用正确的 action 名称
|
||
} else {
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
closeTabsToTheRightInSession(sessionId, targetTabId); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法关闭右侧标签页:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleCloseLeftTabs = (targetTabId: string) => {
|
||
console.log(`[FileEditorOverlay] handleCloseLeftTabs called for target: ${targetTabId}`); // Add log
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
closeTabsToTheLeft(targetTabId); // 修正:调用正确的 action 名称
|
||
} else {
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
closeTabsToTheLeftInSession(sessionId, targetTabId); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法关闭左侧标签页:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
};
|
||
|
||
// +++ 处理编码更改事件 +++
|
||
const handleEncodingChange = (event: Event) => {
|
||
const target = event.target as HTMLSelectElement;
|
||
const newEncoding = target.value;
|
||
const currentActiveTab = activeTab.value;
|
||
|
||
if (currentActiveTab && newEncoding && newEncoding !== currentSelectedEncoding.value) {
|
||
console.log(`[EditorOverlay] Encoding changed to ${newEncoding} for tab ${currentActiveTab.id}`);
|
||
if (shareFileEditorTabsBoolean.value) {
|
||
changeGlobalEncoding(currentActiveTab.id, newEncoding); // 全局 Store
|
||
} else {
|
||
const sessionId = popupFileInfo.value?.sessionId;
|
||
if (sessionId) {
|
||
changeEncodingInSession(sessionId, currentActiveTab.id, newEncoding); // 会话 Store
|
||
} else {
|
||
console.error("[FileEditorOverlay] 无法更改编码:非共享模式下缺少 sessionId。");
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// 关闭弹窗 (保持不变)
|
||
const handleCloseContainer = () => {
|
||
// 关闭前不再检查本地修改状态,因为没有本地状态了
|
||
// 如果需要检查 store 中 activeTab 的修改状态,可以在这里添加逻辑
|
||
// if (activeTab.value?.isModified) { ... }
|
||
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 || !popupFileInfo.value) {
|
||
console.log('[FileEditorOverlay] Popup trigger changed, but overlay is disabled or file info is missing.');
|
||
isVisible.value = false;
|
||
return;
|
||
}
|
||
|
||
const { filePath, sessionId } = popupFileInfo.value;
|
||
console.log(`[FileEditorOverlay] Triggered for file: ${filePath} in session: ${sessionId}`);
|
||
|
||
// 仅显示弹窗,激活逻辑由各自 store 的 openFile/openFileInSession 处理
|
||
// 或者由 handleActivateTab 处理用户点击
|
||
isVisible.value = true;
|
||
|
||
// --- 确保激活状态正确 (重要) ---
|
||
// 在非共享模式下,FileManager 调用 openFileInSession 时会设置会话内的 activeTabId。
|
||
// 在共享模式下,FileManager 调用 openFile 时会设置全局的 activeTabId。
|
||
// 这里不再需要强制调用 setActiveTab,因为触发弹窗时,对应的 store 应该已经处理了激活。
|
||
// 如果发现激活不正确,问题可能在 FileManager 或对应的 store action 中。
|
||
|
||
// 检查激活状态 (调试用)
|
||
// nextTick(() => { // 确保在 DOM 更新后检查
|
||
// console.log(`[FileEditorOverlay] Popup shown. Current activeTabId: ${activeTabId.value}, Active Tab Object:`, activeTab.value);
|
||
// });
|
||
|
||
});
|
||
|
||
// +++ 监听 activeTab 的变化,更新 select 宽度 +++
|
||
watch(activeTab, () => {
|
||
updateSelectWidth();
|
||
}, { immediate: true }); // immediate: true ensures it runs on initial load too
|
||
|
||
// +++ Watch for changes in the selected encoding to update width +++
|
||
watch(currentSelectedEncoding, () => {
|
||
updateSelectWidth();
|
||
});
|
||
|
||
|
||
// 组件卸载时清理事件监听器
|
||
onBeforeUnmount(() => {
|
||
stopResize(); // 确保移除监听器
|
||
});
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 使用本地 isVisible 控制显示 (App.vue 中已有 v-if="showPopupFileEditorBoolean") -->
|
||
<div v-if="isVisible" class="editor-overlay-backdrop" @click.self="handleCloseContainer"> <!-- 恢复点击背景关闭 -->
|
||
<!-- 编辑器弹窗/容器,应用动态样式 -->
|
||
<div class="editor-popup" :style="popupStyle">
|
||
|
||
<!-- 标签栏 (使用动态计算属性和事件处理器) -->
|
||
<FileEditorTabs
|
||
:tabs="orderedTabs"
|
||
:active-tab-id="activeTabId"
|
||
@activate-tab="handleActivateTab"
|
||
@close-tab="handleCloseTab"
|
||
@close-other-tabs="handleCloseOtherTabs"
|
||
@close-tabs-to-right="handleCloseRightTabs"
|
||
@close-tabs-to-left="handleCloseLeftTabs"
|
||
/>
|
||
|
||
<!-- 编辑器头部 (使用动态计算属性) -->
|
||
<div v-if="activeTab" class="editor-header" :class="{ 'is-mobile': props.isMobile }">
|
||
<span>
|
||
{{ t('fileManager.editingFile') }}<template v-if="shareFileEditorTabsBoolean && currentTabSessionName">({{ currentTabSessionName }})</template>: {{ currentTabFilePath }}
|
||
<span v-if="currentTabIsModified" class="modified-indicator">*</span>
|
||
</span>
|
||
<div class="editor-actions">
|
||
<!-- +++ 编码选择下拉菜单 +++ -->
|
||
<div class="encoding-select-wrapper" v-if="activeTab && !currentTabIsLoading">
|
||
<select
|
||
ref="encodingSelectRef"
|
||
:value="currentSelectedEncoding"
|
||
@change="handleEncodingChange"
|
||
class="encoding-select"
|
||
:title="t('fileManager.changeEncodingTooltip', '更改文件编码')"
|
||
>
|
||
<option v-for="option in encodingOptions" :key="option.value" :value="option.value">
|
||
{{ option.text }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
<span v-else-if="activeTab" class="encoding-select-placeholder">{{ t('fileManager.loadingEncoding', '加载中...') }}</span>
|
||
++ -->
|
||
|
||
<span v-if="currentTabSaveStatus === 'saving'" class="save-status saving">{{ t('fileManager.saving') }}...</span>
|
||
<span v-if="currentTabSaveStatus === 'success'" class="save-status success">✅ {{ t('fileManager.saveSuccess') }}</span>
|
||
<span v-if="currentTabSaveStatus === 'error'" class="save-status error">❌ {{ t('fileManager.saveError') }}: {{ currentTabSaveError }}</span>
|
||
<button @click="handleSaveRequest" :disabled="currentTabIsSaving || currentTabIsLoading || !!currentTabLoadingError || !activeTab" class="save-btn">
|
||
{{ t('fileManager.actions.save') }}
|
||
</button>
|
||
|
||
<button v-if="!props.isMobile" @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')">✖</button>
|
||
</div>
|
||
<button v-if="props.isMobile" @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')">✖</button>
|
||
</div>
|
||
<!-- 如果没有活动标签页 -->
|
||
<div v-else class="editor-header editor-header-placeholder" :class="{ 'is-mobile': props.isMobile }">
|
||
<span>{{ t('fileManager.noOpenFile') }}</span>
|
||
<button @click="handleCloseContainer" class="close-editor-btn" :title="t('fileManager.actions.closeEditor')">✖</button>
|
||
</div>
|
||
|
||
<!-- 编辑器内容区域 (现在基于 activeTab) -->
|
||
<div class="editor-content-area">
|
||
<div 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 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 {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
z-index: 1000;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.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;
|
||
position: relative; /* 为拖拽手柄定位 */
|
||
}
|
||
|
||
/* 移动设备上布满屏幕 */
|
||
/* @media (max-width: 768px) 的部分样式会通过 .is-mobile 类处理 */
|
||
@media (max-width: 768px) {
|
||
.editor-popup {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
border-radius: 0;
|
||
}
|
||
}
|
||
|
||
/* 适配横向旋转 */
|
||
@media (orientation: landscape) and (max-width: 1024px) {
|
||
.editor-popup {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
border-radius: 0;
|
||
}
|
||
}
|
||
|
||
|
||
.editor-header {
|
||
display: flex;
|
||
padding: 0.5rem 1rem;
|
||
background-color: #333;
|
||
border-bottom: 1px solid #555;
|
||
font-size: 0.9em;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.editor-header.is-mobile {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
position: relative;
|
||
padding: 1.5rem 2.5rem 0.5rem 1rem;
|
||
}
|
||
|
||
.editor-header:not(.is-mobile) {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.editor-header-placeholder {
|
||
color: #888;
|
||
}
|
||
|
||
.editor-header-placeholder.is-mobile {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
position: relative;
|
||
}
|
||
|
||
.editor-header-placeholder:not(.is-mobile) {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modified-indicator {
|
||
color: #ffeb3b;
|
||
margin-left: 4px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.close-editor-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #ccc;
|
||
font-size: 1.2em;
|
||
cursor: pointer;
|
||
padding: 0.2rem 0.5rem;
|
||
}
|
||
.close-editor-btn:hover {
|
||
color: white;
|
||
}
|
||
|
||
|
||
.editor-header:not(.is-mobile) .editor-actions .close-editor-btn {
|
||
position: static;
|
||
}
|
||
|
||
|
||
.editor-header.is-mobile > .close-editor-btn {
|
||
position: absolute;
|
||
top: 1.5rem;
|
||
right: 1rem;
|
||
z-index: 10;
|
||
}
|
||
|
||
|
||
.editor-header-placeholder.is-mobile > .close-editor-btn {
|
||
position: absolute;
|
||
top: 1.5rem;
|
||
right: 1rem;
|
||
z-index: 10;
|
||
}
|
||
|
||
|
||
.editor-header-placeholder:not(.is-mobile) > .close-editor-btn {
|
||
position: static;
|
||
}
|
||
|
||
/* 编辑器内容区域,包含加载、错误、编辑器实例 */
|
||
.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: 0.8rem;
|
||
}
|
||
|
||
.editor-header.is-mobile .editor-actions {
|
||
flex-wrap: wrap;
|
||
max-width: 100%;
|
||
justify-content: flex-start;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.editor-header:not(.is-mobile) .editor-actions {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 拖拽手柄样式 */
|
||
.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;
|
||
}
|
||
*/
|
||
|
||
/* +++ 编码选择器样式 (copied from FileEditorContainer) +++ */
|
||
.encoding-select-wrapper {
|
||
display: inline-block; /* 让 wrapper 包裹内容 */
|
||
vertical-align: middle; /* 垂直居中对齐 */
|
||
}
|
||
|
||
.encoding-select {
|
||
background-color: #444;
|
||
color: #f0f0f0;
|
||
border: 1px solid #666;
|
||
padding: 0.3rem 0.5rem; /* 恢复内边距 */
|
||
border-radius: 3px;
|
||
font-size: 0.85em;
|
||
cursor: pointer;
|
||
outline: none;
|
||
/* width: auto; */ /* JS will control width via style property */
|
||
}
|
||
|
||
.encoding-select:hover {
|
||
background-color: #555;
|
||
}
|
||
|
||
.encoding-select:focus {
|
||
border-color: #888;
|
||
}
|
||
|
||
.encoding-select-placeholder {
|
||
font-size: 0.85em;
|
||
color: #888;
|
||
padding: 0.3rem 0.5rem;
|
||
display: inline-block;
|
||
min-width: 80px; /* 与 select 大致对齐 */
|
||
text-align: center;
|
||
}
|
||
</style>
|