From 5d0daa366901e23dede522f03bfc22fa4b2ddc75 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 16:39:51 +0800 Subject: [PATCH 01/26] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E8=AF=AD=E8=A8=80=E5=90=8E=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=A2=AB=E6=B8=85=E7=A9=BA=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useLanguageManager.ts | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/composables/useLanguageManager.ts b/src/composables/useLanguageManager.ts index 67c2b23..0806f0c 100644 --- a/src/composables/useLanguageManager.ts +++ b/src/composables/useLanguageManager.ts @@ -13,6 +13,9 @@ export function useLanguageManager( const supportedLanguages = ref([]) const globalConfig = ref(null as any) + // 🔥 按语言缓存编辑器代码,切换语言时保留各语言已编写的代码 + const codeCache = ref>({}) + // 🔥 添加加载状态 const isLoadingEnvInfo = ref(false) @@ -157,10 +160,24 @@ export function useLanguageManager( } const handleLanguageChange = async (newLanguage: string) => { + if (newLanguage === currentLanguage.value) { + return + } + + // 保存当前语言的代码,避免切换语言后已编写的代码丢失 + if (currentLanguage.value) { + codeCache.value[currentLanguage.value] = code.value + } + currentLanguage.value = newLanguage - // 更新代码模板 - code.value = filterPluginTemplate(newLanguage) + // 优先恢复该语言之前编写的代码,没有则使用代码模板 + if (Object.prototype.hasOwnProperty.call(codeCache.value, newLanguage)) { + code.value = codeCache.value[newLanguage] + } + else { + code.value = filterPluginTemplate(newLanguage) + } // 清空输出 clearOutput() @@ -185,7 +202,14 @@ export function useLanguageManager( if (!currentStillAvailable && supportedLanguages.value.length > 0) { currentLanguage.value = supportedLanguages.value[0].value - code.value = filterPluginTemplate(currentLanguage.value) + // 恢复该语言已缓存的代码,没有则使用代码模板 + if (Object.prototype.hasOwnProperty.call(codeCache.value, currentLanguage.value)) { + code.value = codeCache.value[currentLanguage.value] + } + else { + code.value = filterPluginTemplate(currentLanguage.value) + codeCache.value[currentLanguage.value] = code.value + } console.log('当前语言已禁用,切换到:', currentLanguage.value) } @@ -205,6 +229,7 @@ export function useLanguageManager( const template = filterPluginTemplate(currentLanguage.value) console.log('使用的模板:', template) code.value = template + codeCache.value[currentLanguage.value] = template refreshEnvInfo() } From d23db9ba39c608f5a395e317867b50abbf3f0325 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 16:54:45 +0800 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E4=B8=8E=E6=8E=A7=E5=88=B6=E5=8F=B0=E7=9A=84?= =?UTF-8?q?=E5=B7=A6=E5=8F=B3=E3=80=81=E4=B8=8A=E4=B8=8B=E5=8F=8A=E4=BB=85?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/config.rs | 8 ++ src/App.vue | 151 ++++++++++++++++++------- src/components/AppHeader.vue | 28 ++++- src/components/ResizablePanels.vue | 171 ++++++++++++++++------------- src/composables/useEditorConfig.ts | 2 + src/types/app.ts | 6 + 6 files changed, 251 insertions(+), 115 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 8ce5336..a2666fa 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -20,6 +20,8 @@ pub struct EditorConfig { pub show_line_numbers: Option, // 是否显示行号 pub show_function_help: Option, // 是否显示函数帮助 pub space_dot_omission: Option, // 是否显示空格省略 + pub layout: Option, // 编辑器/控制台布局: horizontal | vertical | editor + pub last_direction: Option, // 仅编辑器模式下控制台弹出方向: horizontal | vertical } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -65,6 +67,8 @@ impl Default for AppConfig { show_line_numbers: Some(true), show_function_help: Some(false), space_dot_omission: Some(false), + layout: Some("horizontal".to_string()), + last_direction: Some("horizontal".to_string()), }), environment_mirror: Some(EnvironmentMirrorConfig { enabled: Some(false), @@ -131,6 +135,8 @@ impl ConfigManager { show_line_numbers: Some(true), show_function_help: Some(false), space_dot_omission: Some(false), + layout: Some("horizontal".to_string()), + last_direction: Some("horizontal".to_string()), }); println!("读取配置 -> 添加默认 editor 配置"); } @@ -250,6 +256,8 @@ impl ConfigManager { show_line_numbers: Some(true), show_function_help: Some(false), space_dot_omission: Some(false), + layout: Some("horizontal".to_string()), + last_direction: Some("horizontal".to_string()), }), environment_mirror: Some(EnvironmentMirrorConfig { enabled: Some(false), diff --git a/src/App.vue b/src/App.vue index 4499914..35a14fc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,57 +4,87 @@ :env-installed="envInfo.installed" :supported-languages="supportedLanguages" :current-language="currentLanguage" - @run-code="() => runCode(currentLanguage, envInfo.installed, envInfo.language)" + :current-layout="layoutMode" + @run-code="handleRunCode" @stop-code="() => stopCode(currentLanguage)" @clear-output="clearOutput" @language-change="handleLanguageChange" + @layout-change="handleLayoutChange" @show-settings="showSettings = true" @load-example="loadExample">
- - - - - +
+
+ +
+ @@ -75,7 +105,9 @@ diff --git a/src/composables/useEditorConfig.ts b/src/composables/useEditorConfig.ts index bd40dbf..95e9bdb 100644 --- a/src/composables/useEditorConfig.ts +++ b/src/composables/useEditorConfig.ts @@ -158,6 +158,8 @@ export function useEditorConfig(emit?: any) theme: 'githubLight', font_size: 14, font_family: 'Roboto', + layout: 'horizontal', + last_direction: 'horizontal', } } diff --git a/src/types/app.ts b/src/types/app.ts index 8621d5a..9b5aa00 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -55,8 +55,14 @@ export interface EditorConfig show_line_numbers?: boolean show_function_help?: boolean space_dot_omission?: boolean + layout?: LayoutMode + last_direction?: SplitDirection } +export type SplitDirection = 'horizontal' | 'vertical' + +export type LayoutMode = SplitDirection | 'editor' + export interface EnvironmentVersion { version: string From f38dc2b2c32d7df38a05f2f7870e7e3b5cfb91f0 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 16:57:30 +0800 Subject: [PATCH 03/26] =?UTF-8?q?feat:=20=E5=B8=83=E5=B1=80=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E6=B7=BB=E5=8A=A0=E6=89=8B=E5=9E=8B=E5=85=89=E6=A0=87?= =?UTF-8?q?=E5=B9=B6=E5=B0=86=E6=B8=85=E7=A9=BA=E6=8C=89=E9=92=AE=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=E8=BE=93=E5=87=BA=E5=B7=A5=E5=85=B7=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 7 ++++--- src/components/AppHeader.vue | 17 ++--------------- src/components/ConsoleOutput.vue | 14 +++++++++++++- src/components/WebOutput.vue | 14 +++++++++++++- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/App.vue b/src/App.vue index 35a14fc..7d91b44 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,7 +7,6 @@ :current-layout="layoutMode" @run-code="handleRunCode" @stop-code="() => stopCode(currentLanguage)" - @clear-output="clearOutput" @language-change="handleLanguageChange" @layout-change="handleLayoutChange" @show-settings="showSettings = true" @@ -53,7 +52,8 @@ :output="output" :is-running="isRunning" :is-success="isSuccess" - :execution-time="lastExecutionTime"> + :execution-time="lastExecutionTime" + @clear="clearOutput"> @@ -61,7 +61,8 @@ class="flex-1" :web-content="output" :is-running="isRunning" - :execution-time="lastExecutionTime"> + :execution-time="lastExecutionTime" + @clear="clearOutput"> diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 180901d..47826b7 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -35,7 +35,7 @@
- - -
diff --git a/src/composables/useFileManager.ts b/src/composables/useFileManager.ts index a5ebb85..2993d28 100644 --- a/src/composables/useFileManager.ts +++ b/src/composables/useFileManager.ts @@ -1,26 +1,31 @@ -import {computed, ref, type Ref} from 'vue' +import {computed, type Ref} from 'vue' import {open as openFileDialog, save as saveFileDialog} from '@tauri-apps/plugin-dialog' import {readTextFile, writeTextFile} from '@tauri-apps/plugin-fs' +interface FileManagerOptions +{ + // 编辑器内容(双向绑定的 ref) + code: Ref + toast: any + // 另存为时的建议文件名(通常根据当前语言扩展名生成) + getDefaultFileName: () => string + // 当前关联的本地文件路径(由上层注入,便于与多标签共享) + currentFilePath: Ref + // 最近一次保存/打开时的内容,用于判断是否有未保存改动(由上层注入) + savedContent: Ref + // 选定文件、即将载入前回调(如新建标签页) + onBeforeLoad?: () => void + // 打开文件后回调(内容已写入编辑器),用于按扩展名切换语言等 + onOpened?: (filePath: string, content: string) => void +} + /** - * 编辑器本地文件管理:打开、保存、另存为 - * - * @param code 编辑器内容(双向绑定的 ref) - * @param toast 提示 - * @param getDefaultFileName 另存为时的建议文件名(通常根据当前语言扩展名生成) - * @param onOpened 打开文件后回调(内容已写入编辑器),用于按扩展名切换语言等 + * 编辑器本地文件管理:打开、保存、另存为。 + * 文件状态 ref 由上层注入,以便与多标签工作区共享。 */ -export function useFileManager( - code: Ref, - toast: any, - getDefaultFileName: () => string, - onOpened?: (filePath: string, content: string) => void -) +export function useFileManager(options: FileManagerOptions) { - // 当前关联的本地文件路径(未保存到磁盘时为 null) - const currentFilePath = ref(null) - // 最近一次保存/打开时的内容,用于判断是否有未保存改动 - const savedContent = ref(null) + const {code, toast, getDefaultFileName, currentFilePath, savedContent, onBeforeLoad, onOpened} = options const currentFileName = computed(() => { if (!currentFilePath.value) { @@ -39,6 +44,8 @@ export function useFileManager( return } + onBeforeLoad?.() + const content = await readTextFile(selected) code.value = content currentFilePath.value = selected @@ -93,7 +100,6 @@ export function useFileManager( } return { - currentFilePath, currentFileName, isDirty, openFile, diff --git a/src/composables/useWorkspace.ts b/src/composables/useWorkspace.ts new file mode 100644 index 0000000..3ba1caa --- /dev/null +++ b/src/composables/useWorkspace.ts @@ -0,0 +1,155 @@ +import {computed, ref, watch, type Ref} from 'vue' + +export interface WorkspaceTab +{ + id: string + language: string + code: string + filePath: string | null + savedContent: string | null +} + +interface WorkspaceDeps +{ + code: Ref // live 编辑器内容 + currentLanguage: Ref // live 语言 + applyLanguage: (lang: string) => void // 仅切语言,不动内容 + currentFilePath: Ref + savedContent: Ref + restoreFile: (filePath: string | null, savedContent: string | null) => void +} + +let seq = 0 +const genId = () => `tab-${Date.now()}-${seq++}` + +/** + * 多标签工作区:每个 tab 保存 {语言, 内容, 文件路径, 已保存内容} 快照。 + * live 编辑状态(code/currentLanguage/文件状态)实时同步进当前 tab; + * 切换 tab 时把目标 tab 的快照还原到 live。单 tab 时行为与原先一致。 + */ +export function useWorkspace(deps: WorkspaceDeps) +{ + const {code, currentLanguage, applyLanguage, currentFilePath, savedContent, restoreFile} = deps + + const tabs = ref([]) + const activeTabId = ref('') + // 还原 tab 期间抑制同步 watch,避免把中间状态写错 tab + let isRestoring = false + + const activeTab = computed(() => tabs.value.find(t => t.id === activeTabId.value) || null) + + const captureToActive = () => { + const t = tabs.value.find(t => t.id === activeTabId.value) + if (!t) { + return + } + t.code = code.value + t.language = currentLanguage.value + t.filePath = currentFilePath.value + t.savedContent = savedContent.value + } + + // 实时把编辑状态同步进当前 tab,使标签栏文件名/脏标记保持最新 + watch([code, currentLanguage, currentFilePath, savedContent], () => { + if (isRestoring) { + return + } + captureToActive() + }) + + const loadTab = (t: WorkspaceTab) => { + isRestoring = true + code.value = t.code + applyLanguage(t.language) + restoreFile(t.filePath, t.savedContent) + isRestoring = false + } + + // 用当前实时状态初始化第一个 tab + const initFirstTab = () => { + const t: WorkspaceTab = { + id: genId(), + language: currentLanguage.value, + code: code.value, + filePath: currentFilePath.value, + savedContent: savedContent.value + } + tabs.value = [t] + activeTabId.value = t.id + } + + const switchTab = (id: string) => { + if (id === activeTabId.value) { + return + } + captureToActive() + const t = tabs.value.find(t => t.id === id) + if (!t) { + return + } + activeTabId.value = id + loadTab(t) + } + + const newTab = (init: { language: string, code?: string, filePath?: string | null, savedContent?: string | null }) => { + captureToActive() + const t: WorkspaceTab = { + id: genId(), + language: init.language, + code: init.code ?? '', + filePath: init.filePath ?? null, + savedContent: init.savedContent ?? null + } + tabs.value.push(t) + activeTabId.value = t.id + loadTab(t) + return t + } + + const closeTab = (id: string, fallback: { language: string }) => { + const idx = tabs.value.findIndex(t => t.id === id) + if (idx === -1) { + return + } + const wasActive = id === activeTabId.value + tabs.value.splice(idx, 1) + + // 至少保留一个 tab + if (tabs.value.length === 0) { + const t: WorkspaceTab = { + id: genId(), + language: fallback.language, + code: '', + filePath: null, + savedContent: null + } + tabs.value = [t] + activeTabId.value = t.id + loadTab(t) + return + } + + if (wasActive) { + const next = tabs.value[Math.min(idx, tabs.value.length - 1)] + activeTabId.value = next.id + loadTab(next) + } + } + + // 当前 tab 是否为可复用的空白草稿(未关联文件且内容为空) + const isActiveReusableScratch = () => { + const t = activeTab.value + return !!t && t.filePath === null && (t.code ?? '') === '' + } + + return { + tabs, + activeTabId, + activeTab, + initFirstTab, + switchTab, + newTab, + closeTab, + isActiveReusableScratch + } +} From ff77be3601f8b0c19e945451e5e718b504c9a7f1 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 18:00:26 +0800 Subject: [PATCH 15/26] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=B7=A6?= =?UTF-8?q?=E4=BE=A7=E6=96=87=E4=BB=B6=E6=A0=91=E4=BE=A7=E6=A0=8F=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=89=93=E5=BC=80=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/filesystem.rs | 46 ++++++++++++++++++ src-tauri/src/main.rs | 6 ++- src/App.vue | 44 ++++++++++++++++- src/components/AppHeader.vue | 12 ++++- src/components/FileTreeNode.vue | 65 +++++++++++++++++++++++++ src/components/Sidebar.vue | 80 +++++++++++++++++++++++++++++++ src/composables/useFileManager.ts | 28 +++++++---- 7 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 src-tauri/src/filesystem.rs create mode 100644 src/components/FileTreeNode.vue create mode 100644 src/components/Sidebar.vue diff --git a/src-tauri/src/filesystem.rs b/src-tauri/src/filesystem.rs new file mode 100644 index 0000000..9407612 --- /dev/null +++ b/src-tauri/src/filesystem.rs @@ -0,0 +1,46 @@ +use serde::Serialize; +use std::fs; +use std::path::Path; + +#[derive(Serialize)] +pub struct FileNode { + name: String, + path: String, + is_dir: bool, +} + +/// 读取目录的直接子项(单层,懒加载用)。目录在前,按名称排序。 +#[tauri::command] +pub fn read_directory_tree(path: String) -> Result, String> { + let dir = Path::new(&path); + if !dir.is_dir() { + return Err(format!("不是有效目录: {}", path)); + } + + let entries = fs::read_dir(dir).map_err(|e| format!("读取目录失败: {}", e))?; + let mut nodes: Vec = Vec::new(); + + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + // 忽略 macOS 元数据文件 + if name == ".DS_Store" { + continue; + } + let p = entry.path(); + let is_dir = p.is_dir(); + nodes.push(FileNode { + name, + path: p.to_string_lossy().to_string(), + is_dir, + }); + } + + // 目录优先,其次按名称(忽略大小写)排序 + nodes.sort_by(|a, b| match (a.is_dir, b.is_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), + }); + + Ok(nodes) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e57ce79..dbb1b9d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,6 +11,7 @@ mod env_manager; mod env_providers; mod example; mod execution; +mod filesystem; mod font; mod logger; mod plugin; @@ -37,6 +38,7 @@ use crate::execution::{ ExecutionHistory, PluginManagerState as ExecutionPluginManagerState, clear_execution_history, execute_code, get_execution_history, is_execution_running, stop_execution, }; +use crate::filesystem::read_directory_tree; use crate::plugin::{get_info, get_supported_languages}; use crate::setup::app::get_app_info; use crate::utils::logger::{ @@ -148,7 +150,9 @@ fn main() { check_for_updates, start_update, load_example, - open_font_picker + open_font_picker, + // 文件系统相关命令 + read_directory_tree ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.vue b/src/App.vue index 8860534..378347e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,6 +5,8 @@ :supported-languages="supportedLanguages" :current-language="currentLanguage" :current-layout="layoutMode" + :sidebar-visible="sidebarVisible" + @toggle-sidebar="toggleSidebar" @run-code="handleRunCode" @stop-code="() => stopCode(currentLanguage)" @language-change="onLanguageChange" @@ -15,7 +17,15 @@ @load-example="loadExample"> -
+
+ + + +