feat: 为弹窗编辑器添加编码选择功能并统一处理逻辑
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { computed, ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'; // + nextTick
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import MonacoEditor from './MonacoEditor.vue';
|
||||
@@ -40,6 +40,7 @@ const {
|
||||
closeOtherTabs, // 修正:移除 Global 后缀
|
||||
closeTabsToTheRight, // 修正:移除 Global 后缀
|
||||
closeTabsToTheLeft, // 修正:移除 Global 后缀
|
||||
changeEncoding: changeGlobalEncoding, // +++ 添加全局编码更改 action +++
|
||||
} = fileEditorStore;
|
||||
|
||||
// 会话 Store Actions (用于非共享模式)
|
||||
@@ -52,6 +53,7 @@ const {
|
||||
closeOtherTabsInSession,
|
||||
closeTabsToTheRightInSession,
|
||||
closeTabsToTheLeftInSession,
|
||||
changeEncodingInSession, // +++ 添加会话编码更改 action +++
|
||||
} = sessionStore;
|
||||
|
||||
// --- 移除本地文件状态 ---
|
||||
@@ -78,6 +80,7 @@ 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(() => ({
|
||||
@@ -87,6 +90,44 @@ const popupStyle = computed(() => ({
|
||||
|
||||
// --- 动态计算属性 (根据模式选择数据源) ---
|
||||
|
||||
// +++ 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) {
|
||||
@@ -160,6 +201,8 @@ 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;
|
||||
@@ -170,6 +213,57 @@ const currentTabSessionName = computed(() => {
|
||||
|
||||
// --- 事件处理 (根据模式调用不同 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;
|
||||
@@ -258,6 +352,27 @@ const handleCloseLeftTabs = (targetTabId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// +++ 新增:处理编码更改事件 +++
|
||||
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 = () => {
|
||||
// 关闭前不再检查本地修改状态,因为没有本地状态了
|
||||
@@ -331,6 +446,16 @@ watch(popupTrigger, () => {
|
||||
|
||||
});
|
||||
|
||||
// +++ 监听 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(() => {
|
||||
@@ -363,6 +488,23 @@ onBeforeUnmount(() => {
|
||||
<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>
|
||||
@@ -504,7 +646,7 @@ onBeforeUnmount(() => {
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 0.8rem; /* 稍微减小间距以容纳下拉菜单 */
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
@@ -582,4 +724,39 @@ onBeforeUnmount(() => {
|
||||
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>
|
||||
|
||||
@@ -679,7 +679,7 @@ export const useSessionStore = defineStore('session', () => {
|
||||
* 在指定会话中更改文件编码并重新解码
|
||||
*/
|
||||
// --- 修改:更改文件编码(通过请求后端重新读取) ---
|
||||
const changeEncodingInSession = async (sessionId: string, tabId: string, newEncoding: string) => {
|
||||
const changeEncodingInSession = (sessionId: string, tabId: string, newEncoding: string) => {
|
||||
const session = sessions.value.get(sessionId);
|
||||
if (!session) {
|
||||
console.warn(`[SessionStore] 尝试更改不存在的会话 ${sessionId} 中标签页 ${tabId} 的编码。`);
|
||||
@@ -691,64 +691,48 @@ export const useSessionStore = defineStore('session', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取默认的 sftpManager 实例
|
||||
const sftpManager = getOrCreateSftpManager(sessionId, 'primary');
|
||||
if (!sftpManager) {
|
||||
console.error(`[SessionStore] 无法获取会话 ${sessionId} 的 primary sftpManager 来更改编码。`);
|
||||
const tab = session.editorTabs.value[tabIndex];
|
||||
|
||||
if (!tab.rawContentBase64) {
|
||||
console.error(`[SessionStore] 无法更改编码:会话 ${sessionId} 标签页 ${tabId} 没有原始文件数据。`);
|
||||
// 更新错误状态
|
||||
const errorTab = { ...session.editorTabs.value[tabIndex], isLoading: false, loadingError: '无法获取 SFTP 实例' };
|
||||
const errorTab = { ...tab, isLoading: false, loadingError: '缺少原始文件数据,无法更改编码' };
|
||||
session.editorTabs.value.splice(tabIndex, 1, errorTab);
|
||||
return;
|
||||
}
|
||||
if (tab.selectedEncoding === newEncoding) {
|
||||
console.log(`[SessionStore] 会话 ${sessionId} 标签页 ${tabId} 编码已经是 ${newEncoding},无需更改。`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = session.editorTabs.value[tabIndex];
|
||||
console.log(`[SessionStore] 请求使用新编码 "${newEncoding}" 重新读取文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tabId})`);
|
||||
|
||||
// 设置加载状态 (使用 splice)
|
||||
const loadingTab = { ...tab, isLoading: true, loadingError: null };
|
||||
session.editorTabs.value.splice(tabIndex, 1, loadingTab);
|
||||
console.log(`[SessionStore] 使用新编码 "${newEncoding}" 在前端重新解码文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tabId})`);
|
||||
|
||||
try {
|
||||
// 向后端发送 readFile 请求,并指定编码
|
||||
const fileData: SftpReadFileSuccessPayload = await sftpManager.readFile(tab.filePath, newEncoding);
|
||||
console.log(`[SessionStore ${sessionId}] 文件 ${tab.filePath} 使用编码 "${newEncoding}" 重新读取成功。后端实际使用编码: ${fileData.encodingUsed}`);
|
||||
// 使用新编码解码存储的原始数据
|
||||
const newContent = decodeRawContent(tab.rawContentBase64, newEncoding);
|
||||
|
||||
// 更新标签页状态 (使用 splice)
|
||||
const currentTabState = session.editorTabs.value.find(t => t.id === tabId); // 获取最新状态
|
||||
if (!currentTabState) return; // 可能在请求期间关闭了
|
||||
|
||||
// --- 修复:使用 decodeRawContent 解码并更新原始数据 ---
|
||||
const newDecodedContent = decodeRawContent(fileData.rawContentBase64, fileData.encodingUsed);
|
||||
// 更新标签页状态 (使用 splice 保证响应性)
|
||||
const updatedTab: FileTab = {
|
||||
...currentTabState,
|
||||
content: newDecodedContent,
|
||||
rawContentBase64: fileData.rawContentBase64, // 更新原始 Base64 数据
|
||||
// originalContent 保持不变
|
||||
selectedEncoding: fileData.encodingUsed, // 使用后端确认的编码
|
||||
isLoading: false,
|
||||
...tab,
|
||||
content: newContent,
|
||||
selectedEncoding: newEncoding, // 更新选择的编码
|
||||
isLoading: false, // 解码完成 (移除加载状态)
|
||||
loadingError: null,
|
||||
// isModified 状态保持不变
|
||||
};
|
||||
const finalTabIndex = session.editorTabs.value.findIndex(t => t.id === tabId); // 重新获取索引
|
||||
if (finalTabIndex !== -1) {
|
||||
session.editorTabs.value.splice(finalTabIndex, 1, updatedTab);
|
||||
} else {
|
||||
console.warn(`[SessionStore ${sessionId}] 尝试更新标签页 ${tabId} 时未找到索引 (changeEncoding)。`);
|
||||
}
|
||||
session.editorTabs.value.splice(tabIndex, 1, updatedTab);
|
||||
console.log(`[SessionStore] 文件 ${tab.filePath} (会话 ${sessionId}) 使用新编码 "${newEncoding}" 解码完成。`);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(`[SessionStore ${sessionId}] 使用编码 "${newEncoding}" 重新读取文件 ${tab.filePath} 失败:`, err);
|
||||
const errorMsg = `${t('fileManager.errors.readFileFailed')} (编码: ${newEncoding}): ${err.message || err}`;
|
||||
} catch (err: any) { // catch 应该在 decodeRawContent 内部处理了,但以防万一
|
||||
console.error(`[SessionStore] 使用编码 "${newEncoding}" 在前端解码文件 ${tab.filePath} (会话 ${sessionId}) 失败:`, err);
|
||||
const errorMsg = `前端解码失败 (编码: ${newEncoding}): ${err.message || err}`;
|
||||
// 更新错误状态 (使用 splice)
|
||||
const errorTabIndex = session.editorTabs.value.findIndex(t => t.id === tabId);
|
||||
if (errorTabIndex !== -1) {
|
||||
const errorTab = {
|
||||
...session.editorTabs.value[errorTabIndex],
|
||||
isLoading: false,
|
||||
loadingError: errorMsg,
|
||||
};
|
||||
session.editorTabs.value.splice(errorTabIndex, 1, errorTab);
|
||||
}
|
||||
const errorTab: FileTab = {
|
||||
...tab,
|
||||
isLoading: false,
|
||||
loadingError: errorMsg,
|
||||
};
|
||||
session.editorTabs.value.splice(tabIndex, 1, errorTab);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user