update
This commit is contained in:
@@ -5,6 +5,8 @@ import { useAuthStore } from './stores/auth.store';
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
// 导入通知显示组件
|
// 导入通知显示组件
|
||||||
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
||||||
|
// 导入文件编辑器弹窗组件
|
||||||
|
import FileEditorOverlay from './components/FileEditorOverlay.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -39,6 +41,9 @@ const handleLogout = () => {
|
|||||||
<!-- 添加全局通知显示 -->
|
<!-- 添加全局通知显示 -->
|
||||||
<UINotificationDisplay />
|
<UINotificationDisplay />
|
||||||
|
|
||||||
|
<!-- 添加全局文件编辑器弹窗 -->
|
||||||
|
<FileEditorOverlay />
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<!-- 使用 t 函数获取应用名称 -->
|
<!-- 使用 t 函数获取应用名称 -->
|
||||||
<p>© 2025 {{ t('appName') }}</p>
|
<p>© 2025 {{ t('appName') }}</p>
|
||||||
|
|||||||
@@ -1,90 +1,114 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
||||||
import type { SaveStatus } from '../types/sftp.types'; // 导入保存状态类型
|
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入新的 Store
|
||||||
|
|
||||||
const props = defineProps<{
|
// 不再需要 props 或 emits,状态和操作来自 Store
|
||||||
isVisible: boolean; // 控制可见性
|
// const props = defineProps<{...}>();
|
||||||
filePath: string | null; // 当前编辑文件路径
|
// const emit = defineEmits<{...}>();
|
||||||
language: string; // 编辑器语言
|
|
||||||
isLoading: boolean; // 是否正在加载文件内容
|
|
||||||
loadingError: string | null; // 加载错误信息
|
|
||||||
isSaving: boolean; // 是否正在保存
|
|
||||||
saveStatus: SaveStatus; // 保存状态
|
|
||||||
saveError: string | null; // 保存错误信息
|
|
||||||
modelValue: string; // 文件内容 (用于 v-model)
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string): void; // v-model 更新事件
|
|
||||||
(e: 'request-save'): void; // 请求保存事件
|
|
||||||
(e: 'close'): void; // 关闭编辑器事件
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const fileEditorStore = useFileEditorStore();
|
||||||
|
|
||||||
|
// 从 Store 获取状态 (使用 storeToRefs 保持响应性)
|
||||||
|
const {
|
||||||
|
isVisible,
|
||||||
|
filePath,
|
||||||
|
fileLanguage, // 重命名为 language 以匹配 MonacoEditor prop
|
||||||
|
isLoading,
|
||||||
|
loadingError,
|
||||||
|
isSaving,
|
||||||
|
saveStatus,
|
||||||
|
saveError,
|
||||||
|
fileContent, // 直接使用 store 中的 ref 进行 v-model 绑定
|
||||||
|
} = storeToRefs(fileEditorStore);
|
||||||
|
|
||||||
|
// 从 Store 获取方法
|
||||||
|
const { saveFile, closeEditor, updateContent } = fileEditorStore;
|
||||||
|
|
||||||
// 计算属性,用于 v-model 绑定到 MonacoEditor
|
// 计算属性,用于 v-model 绑定到 MonacoEditor
|
||||||
|
// 直接绑定 store 中的 fileContent ref
|
||||||
const editorContent = computed({
|
const editorContent = computed({
|
||||||
get: () => props.modelValue,
|
get: () => fileContent.value,
|
||||||
set: (value) => emit('update:modelValue', value),
|
set: (value) => updateContent(value), // 调用 store action 更新内容
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 保存和关闭操作直接调用 store actions
|
||||||
const handleSaveRequest = () => {
|
const handleSaveRequest = () => {
|
||||||
emit('request-save');
|
saveFile();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
emit('close');
|
closeEditor();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isVisible" class="editor-overlay">
|
<!-- 使用 store 中的 isVisible 控制显示 -->
|
||||||
<div class="editor-header">
|
<!-- 将 v-if 移到遮罩层上 -->
|
||||||
<span>{{ t('fileManager.editingFile') }}: {{ filePath }}</span>
|
<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">
|
<div class="editor-actions">
|
||||||
<!-- 保存状态显示 -->
|
<!-- 使用 store 中的保存状态 -->
|
||||||
<span v-if="saveStatus === 'saving'" class="save-status saving">{{ t('fileManager.saving') }}...</span>
|
<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 === 'success'" class="save-status success">✅ {{ t('fileManager.saveSuccess') }}</span>
|
||||||
<span v-if="saveStatus === 'error'" class="save-status error">❌ {{ t('fileManager.saveError') }}: {{ saveError }}</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">
|
<button @click="handleSaveRequest" :disabled="isSaving || isLoading || !!loadingError" class="save-btn">
|
||||||
{{ isSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
{{ isSaving ? t('fileManager.saving') : t('fileManager.actions.save') }}
|
||||||
</button>
|
</button>
|
||||||
<!-- 关闭按钮 -->
|
<!-- 关闭按钮,使用 store 方法 -->
|
||||||
<button @click="handleClose" class="close-editor-btn">✖</button>
|
<button @click="handleClose" class="close-editor-btn">✖</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 加载状态 -->
|
<!-- 使用 store 中的加载状态 -->
|
||||||
<div v-if="isLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
|
<div v-if="isLoading" class="editor-loading">{{ t('fileManager.loadingFile') }}</div>
|
||||||
<!-- 加载错误 -->
|
<!-- 使用 store 中的加载错误 -->
|
||||||
<div v-else-if="loadingError" class="editor-error">{{ loadingError }}</div>
|
<div v-else-if="loadingError" class="editor-error">{{ loadingError }}</div>
|
||||||
<!-- Monaco 编辑器实例 -->
|
<!-- Monaco 编辑器实例 -->
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
v-else
|
v-else
|
||||||
v-model="editorContent"
|
v-model="editorContent"
|
||||||
:language="language"
|
:language="fileLanguage"
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
class="editor-instance"
|
class="editor-instance"
|
||||||
@request-save="handleSaveRequest"
|
@request-save="handleSaveRequest"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> <!-- 关闭 editor-popup -->
|
||||||
|
</div> <!-- 关闭 editor-overlay-backdrop -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 样式从 FileManager.vue 迁移并保持一致 */
|
/* 样式调整为居中弹窗样式 */
|
||||||
.editor-overlay {
|
.editor-overlay-backdrop { /* 新增背景遮罩层 */
|
||||||
position: absolute; /* 相对于父容器定位 */
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(40, 40, 40, 0.95); /* 深色半透明背景 */
|
background-color: rgba(0, 0, 0, 0.6); /* 半透明黑色背景 */
|
||||||
z-index: 1000; /* 确保在文件列表之上,但在上传弹窗之下 */
|
z-index: 1000; /* 确保在最上层 */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-popup { /* 编辑器本身的容器 */
|
||||||
|
width: 60%; /* 设置宽度为 60% */
|
||||||
|
height: 80%; /* 设置一个合适的高度 */
|
||||||
|
background-color: #2d2d2d; /* 深色背景 */
|
||||||
|
border-radius: 8px;
|
||||||
|
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; /* 防止内容溢出圆角 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick, watchEffect, type
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router'; // 保留用于生成下载 URL (如果下载逻辑移动则可移除)
|
import { useRoute } from 'vue-router'; // 保留用于生成下载 URL (如果下载逻辑移动则可移除)
|
||||||
// 导入 SFTP Actions 工厂函数和所需的类型
|
// 导入 SFTP Actions 工厂函数和所需的类型
|
||||||
import { createSftpActionsManager, type WebSocketDependencies } from '../composables/useSftpActions'; // 恢复 WebSocketDependencies
|
import { createSftpActionsManager, type WebSocketDependencies } from '../composables/useSftpActions';
|
||||||
import { useFileUploader } from '../composables/useFileUploader';
|
import { useFileUploader } from '../composables/useFileUploader';
|
||||||
import { useFileEditor } from '../composables/useFileEditor';
|
// import { useFileEditor } from '../composables/useFileEditor'; // 移除旧的 composable 导入
|
||||||
|
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入新的 Store
|
||||||
// WebSocket composable 不再直接使用
|
// WebSocket composable 不再直接使用
|
||||||
import FileUploadPopup from './FileUploadPopup.vue';
|
import FileUploadPopup from './FileUploadPopup.vue';
|
||||||
import FileEditorOverlay from './FileEditorOverlay.vue';
|
// import FileEditorOverlay from './FileEditorOverlay.vue'; // 不再在此处渲染
|
||||||
// 从类型文件导入所需类型
|
// 从类型文件导入所需类型
|
||||||
import type { FileListItem } from '../types/sftp.types';
|
import type { FileListItem } from '../types/sftp.types';
|
||||||
// 从 websocket 类型文件导入所需类型
|
// 从 websocket 类型文件导入所需类型
|
||||||
@@ -81,25 +82,28 @@ const {
|
|||||||
// useFileUploader 内部创建自己的 ws 连接, 不需要 wsDeps
|
// useFileUploader 内部创建自己的 ws 连接, 不需要 wsDeps
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 实例化新的文件编辑器 Store
|
||||||
|
const fileEditorStore = useFileEditorStore();
|
||||||
|
|
||||||
// 文件编辑器模块 - Needs file operations from sftpManager
|
// 文件编辑器模块 - Needs file operations from sftpManager
|
||||||
const {
|
// const { // 移除旧的 composable 解构
|
||||||
isEditorVisible,
|
// isEditorVisible,
|
||||||
editingFilePath,
|
// editingFilePath,
|
||||||
editingFileLanguage,
|
// editingFileLanguage,
|
||||||
isEditorLoading,
|
// isEditorLoading,
|
||||||
editorError,
|
// editorError,
|
||||||
isSaving,
|
// isSaving,
|
||||||
saveStatus,
|
// saveStatus,
|
||||||
saveError,
|
// saveError,
|
||||||
editingFileContent,
|
// editingFileContent,
|
||||||
openFile,
|
// openFile,
|
||||||
saveFile,
|
// saveFile,
|
||||||
closeEditor,
|
// closeEditor,
|
||||||
// cleanup: cleanupEditor, // 假设 editor 也提供 cleanup
|
// // cleanup: cleanupEditor, // 假设 editor 也提供 cleanup
|
||||||
} = useFileEditor(
|
// } = useFileEditor( // 移除旧的 composable 调用
|
||||||
readFile, // 使用注入的 sftpManager 中的 readFile
|
// readFile, // 使用注入的 sftpManager 中的 readFile
|
||||||
writeFile // Use writeFile from the injected sftpManager
|
// writeFile // Use writeFile from the injected sftpManager
|
||||||
);
|
// );
|
||||||
|
|
||||||
// --- UI 状态 Refs (Remain mostly the same) ---
|
// --- UI 状态 Refs (Remain mostly the same) ---
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||||
@@ -294,7 +298,8 @@ const handleItemClick = (event: MouseEvent, item: FileListItem) => { // item 已
|
|||||||
loadDirectory(newPath); // Use loadDirectory from props
|
loadDirectory(newPath); // Use loadDirectory from props
|
||||||
} else if (item.attrs.isFile) {
|
} else if (item.attrs.isFile) {
|
||||||
const filePath = joinPath(currentPath.value, item.filename); // Use joinPath from props
|
const filePath = joinPath(currentPath.value, item.filename); // Use joinPath from props
|
||||||
openFile(filePath); // Use openFile from useFileEditor
|
// 调用全局 Store 的 openFile,并传入 sessionId
|
||||||
|
fileEditorStore.openFile(filePath, props.sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -793,7 +798,8 @@ const clearError = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 使用 FileEditorOverlay 组件 -->
|
<!-- FileEditorOverlay 不再在此处渲染 -->
|
||||||
|
<!--
|
||||||
<FileEditorOverlay
|
<FileEditorOverlay
|
||||||
:is-visible="isEditorVisible"
|
:is-visible="isEditorVisible"
|
||||||
:file-path="editingFilePath"
|
:file-path="editingFilePath"
|
||||||
@@ -807,6 +813,7 @@ const clearError = () => {
|
|||||||
@request-save="saveFile"
|
@request-save="saveFile"
|
||||||
@close="closeEditor"
|
@close="closeEditor"
|
||||||
/>
|
/>
|
||||||
|
-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { ref, computed, readonly } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useSessionStore } from './session.store'; // 导入会话 Store
|
||||||
|
import type { EditorFileContent, SaveStatus } from '../types/sftp.types';
|
||||||
|
|
||||||
|
// 辅助函数:根据文件名获取语言 (从 useFileEditor.ts 迁移)
|
||||||
|
const getLanguageFromFilename = (filename: string): string => {
|
||||||
|
const extension = filename.split('.').pop()?.toLowerCase();
|
||||||
|
// (保持 switch case 不变)
|
||||||
|
switch (extension) {
|
||||||
|
case 'js': return 'javascript';
|
||||||
|
case 'ts': return 'typescript';
|
||||||
|
case 'json': return 'json';
|
||||||
|
case 'html': return 'html';
|
||||||
|
case 'css': return 'css';
|
||||||
|
case 'scss': return 'scss';
|
||||||
|
case 'less': return 'less';
|
||||||
|
case 'py': return 'python';
|
||||||
|
case 'java': return 'java';
|
||||||
|
case 'c': return 'c';
|
||||||
|
case 'cpp': return 'cpp';
|
||||||
|
case 'cs': return 'csharp';
|
||||||
|
case 'go': return 'go';
|
||||||
|
case 'php': return 'php';
|
||||||
|
case 'rb': return 'ruby';
|
||||||
|
case 'rs': return 'rust';
|
||||||
|
case 'sql': return 'sql';
|
||||||
|
case 'sh': return 'shell';
|
||||||
|
case 'yaml': case 'yml': return 'yaml';
|
||||||
|
case 'md': return 'markdown';
|
||||||
|
case 'xml': return 'xml';
|
||||||
|
case 'ini': return 'ini';
|
||||||
|
case 'bat': return 'bat';
|
||||||
|
case 'dockerfile': return 'dockerfile';
|
||||||
|
default: return 'plaintext';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useFileEditorStore = defineStore('fileEditor', () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
|
// --- 编辑器状态 ---
|
||||||
|
const isVisible = ref(false);
|
||||||
|
const currentSessionId = ref<string | null>(null); // 需要知道文件属于哪个会话
|
||||||
|
const filePath = ref<string | null>(null);
|
||||||
|
const fileContent = ref<string>(''); // 用于 v-model 绑定
|
||||||
|
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(() => ({
|
||||||
|
isVisible: isVisible.value,
|
||||||
|
filePath: filePath.value,
|
||||||
|
language: fileLanguage.value,
|
||||||
|
isLoading: isLoading.value,
|
||||||
|
loadingError: loadingError.value,
|
||||||
|
isSaving: isSaving.value,
|
||||||
|
saveStatus: saveStatus.value,
|
||||||
|
saveError: saveError.value,
|
||||||
|
// modelValue is handled separately via direct ref binding
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- 核心方法 ---
|
||||||
|
|
||||||
|
// 获取当前会话的 SFTP 管理器
|
||||||
|
const getSftpManager = (sessionId: string | null) => {
|
||||||
|
if (!sessionId) return null;
|
||||||
|
const session = sessionStore.sessions.get(sessionId);
|
||||||
|
return session?.sftpManager ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFile = async (targetFilePath: string, sessionId: string) => {
|
||||||
|
console.log(`[文件编辑器 Store] 尝试打开文件: ${targetFilePath} (会话: ${sessionId})`);
|
||||||
|
if (!targetFilePath || !sessionId) return;
|
||||||
|
|
||||||
|
// // 如果已经是同一个文件,则不重新加载(除非需要强制刷新)
|
||||||
|
// if (filePath.value === targetFilePath && isVisible.value) {
|
||||||
|
// console.log(`[文件编辑器 Store] 文件 ${targetFilePath} 已在编辑器中打开。`);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const sftpManager = getSftpManager(sessionId);
|
||||||
|
if (!sftpManager) {
|
||||||
|
console.error(`[文件编辑器 Store] 无法找到会话 ${sessionId} 的 SFTP 管理器。`);
|
||||||
|
// 可以设置一个错误状态或通知用户
|
||||||
|
loadingError.value = t('fileManager.errors.sftpManagerNotFound');
|
||||||
|
isVisible.value = true; // 仍然显示编辑器以展示错误
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible.value = true; // 显示编辑器区域
|
||||||
|
isLoading.value = true; // 显示加载状态
|
||||||
|
loadingError.value = null;
|
||||||
|
saveStatus.value = 'idle'; // 重置保存状态
|
||||||
|
saveError.value = null;
|
||||||
|
filePath.value = targetFilePath;
|
||||||
|
currentSessionId.value = sessionId; // 记录当前会话 ID
|
||||||
|
fileLanguage.value = getLanguageFromFilename(targetFilePath);
|
||||||
|
fileContent.value = ''; // 清空旧内容
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用从 sessionStore 获取的 sftpManager 的 readFile 方法
|
||||||
|
const fileData = await sftpManager.readFile(targetFilePath);
|
||||||
|
console.log(`[文件编辑器 Store] 文件 ${targetFilePath} 读取成功。编码: ${fileData.encoding}`);
|
||||||
|
|
||||||
|
// 处理可能的 Base64 编码
|
||||||
|
if (fileData.encoding === 'base64') {
|
||||||
|
try {
|
||||||
|
fileContent.value = atob(fileData.content); // 解码
|
||||||
|
fileEncoding.value = 'base64'; // 记录原始编码
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.error(`[文件编辑器 Store] Base64 解码错误 for ${targetFilePath}:`, decodeError);
|
||||||
|
loadingError.value = t('fileManager.errors.fileDecodeError');
|
||||||
|
fileContent.value = `// ${t('fileManager.errors.fileDecodeError')}\n${fileData.content}`; // 显示原始 Base64 作为后备
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileContent.value = fileData.content;
|
||||||
|
fileEncoding.value = 'utf8';
|
||||||
|
}
|
||||||
|
isLoading.value = false;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[文件编辑器 Store] 读取文件 ${targetFilePath} 失败:`, err);
|
||||||
|
loadingError.value = `${t('fileManager.errors.readFileFailed')}: ${err.message || err}`;
|
||||||
|
fileContent.value = `// ${loadingError.value}`; // 在编辑器中显示错误
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFile = async () => {
|
||||||
|
if (!filePath.value || !currentSessionId.value || isSaving.value || isLoading.value || loadingError.value) {
|
||||||
|
console.warn('[文件编辑器 Store] 保存条件不满足,无法保存。', {
|
||||||
|
path: filePath.value,
|
||||||
|
sessionId: currentSessionId.value,
|
||||||
|
isSaving: isSaving.value,
|
||||||
|
isLoading: isLoading.value,
|
||||||
|
hasError: !!loadingError.value
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sftpManager = getSftpManager(currentSessionId.value);
|
||||||
|
if (!sftpManager) {
|
||||||
|
console.error(`[文件编辑器 Store] 保存失败:无法找到会话 ${currentSessionId.value} 的 SFTP 管理器。`);
|
||||||
|
saveStatus.value = 'error';
|
||||||
|
saveError.value = t('fileManager.errors.sftpManagerNotFound');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[文件编辑器 Store] 开始保存文件: ${filePath.value} (会话: ${currentSessionId.value})`);
|
||||||
|
isSaving.value = true;
|
||||||
|
saveStatus.value = 'saving';
|
||||||
|
saveError.value = null;
|
||||||
|
|
||||||
|
const contentToSave = fileContent.value; // 获取当前编辑器内容
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用从 sessionStore 获取的 sftpManager 的 writeFile 方法
|
||||||
|
await sftpManager.writeFile(filePath.value, contentToSave);
|
||||||
|
console.log(`[文件编辑器 Store] 文件 ${filePath.value} 保存成功。`);
|
||||||
|
isSaving.value = false;
|
||||||
|
saveStatus.value = 'success';
|
||||||
|
saveError.value = null;
|
||||||
|
|
||||||
|
// 成功提示短暂显示后消失
|
||||||
|
setTimeout(() => {
|
||||||
|
if (saveStatus.value === 'success') {
|
||||||
|
saveStatus.value = 'idle';
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[文件编辑器 Store] 保存文件 ${filePath.value} 失败:`, err);
|
||||||
|
isSaving.value = false;
|
||||||
|
saveStatus.value = 'error';
|
||||||
|
saveError.value = `${t('fileManager.errors.saveFailed')}: ${err.message || err}`;
|
||||||
|
|
||||||
|
// 错误提示显示时间长一些
|
||||||
|
setTimeout(() => {
|
||||||
|
if (saveStatus.value === 'error') {
|
||||||
|
saveStatus.value = 'idle';
|
||||||
|
saveError.value = null;
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditor = () => {
|
||||||
|
console.log('[文件编辑器 Store] 关闭编辑器。');
|
||||||
|
isVisible.value = false;
|
||||||
|
filePath.value = null;
|
||||||
|
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) => {
|
||||||
|
fileContent.value = newContent;
|
||||||
|
// 当用户编辑时,可以重置保存状态(如果需要)
|
||||||
|
if (saveStatus.value === 'success' || saveStatus.value === 'error') {
|
||||||
|
saveStatus.value = 'idle';
|
||||||
|
saveError.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态 (只读的 ref 或计算属性)
|
||||||
|
isVisible: readonly(isVisible),
|
||||||
|
filePath: readonly(filePath),
|
||||||
|
fileLanguage: readonly(fileLanguage),
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
loadingError: readonly(loadingError),
|
||||||
|
isSaving: readonly(isSaving),
|
||||||
|
saveStatus: readonly(saveStatus),
|
||||||
|
saveError: readonly(saveError),
|
||||||
|
editorProps, // 提供一个包含多个只读状态的对象,方便绑定
|
||||||
|
|
||||||
|
// 可写状态 (用于 v-model)
|
||||||
|
fileContent, // 直接暴露 ref 用于 v-model
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
openFile,
|
||||||
|
saveFile,
|
||||||
|
closeEditor,
|
||||||
|
updateContent, // 如果需要从外部更新内容
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user