diff --git a/README.md b/README.md index e8e9226..7faa376 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ - **快速打开**(`Cmd/Ctrl + P`)—— 模糊匹配 + 最近文件优先 - **符号大纲**(`Cmd/Ctrl + Shift + O`)、**跳转到行**(`Cmd/Ctrl + G`) - **代码片段** —— 自定义前缀,输入后按 `Tab` 展开(`$0` 为光标落点) +- **LSP 语义能力** —— 接入语言服务器,提供精准补全、悬浮文档、跳转定义、查找引用、重命名与实时诊断(需本机安装对应语言服务器,未安装则自动回退,不影响编辑) - **会话恢复** —— 重启自动恢复上次的文件夹与标签页 - **深色模式** —— 跟随系统 / 浅色 / 深色,编辑器主题同步切换 @@ -49,7 +50,8 @@ - **JSON / XML / YAML** —— 可折叠**层级树**,以及卡片 + 连线的**关系图**两种可视化 - **SQL** —— 插件式执行器(内存库 / `.sqlite` 文件 / **MySQL**,可在设置中配置连接、运行时选择数据源),结果渲染为**表格**,失败显示具体错误;执行历史与实时运行一致 - **图表可视化** —— SQL 结果一键切换为图表:**拖拽**字段到「维度 / 指标」即可成图,自动识别数值列,支持聚合(求和/计数/平均/最大/最小)、排序、Top N。基于 ECharts,内置 **27 种**图表:柱状图 · 折线图 · 面积图 · 饼图/环形图 · 玫瑰图 · 散点图 · 涟漪散点图 · 雷达图 · 漏斗图 · 热力图 · 仪表盘 · 桑基图 · 关系图 · 旭日图 · 矩形树图 · 树图 · 箱线图 · K 线图 · 平行坐标 · 主题河流 · 日历热力图 · 极坐标柱状图 · 象形柱图 · 词云 · 水球图 · 中国地图 · 世界地图(配色跟随主题,支持导出 PNG)。配置面板表驱动,组件与数据源解耦,后续 CSV 等本地数据可复用 -- **CSV / TSV** —— 解析为**数据表**(支持引号转义、字段内换行、自动识别分隔符),并可一键切换为上述 27 种图表(与 SQL 共用图表面板) +- **CSV / TSV** —— 解析为**数据表**(支持引号转义、字段内换行、自动识别分隔符、Web Worker 后台解析 + 进度),并可一键切换为上述 27 种图表(与 SQL 共用图表面板) +- **Excel(.xlsx / .xls)** —— 用 SheetJS 解析,**多工作表**切换,同样可切表格 / 27 种图表 / 导出 CSV - **Markdown** —— 实时渲染预览(支持内嵌 HTML,DOMPurify 净化防 XSS) - **GitHub Actions 工作流** —— 自动识别并渲染为 **Jobs 依赖 DAG 图**(触发事件 → 各 Job → Steps) @@ -73,7 +75,7 @@ ## 🧩 支持的语言 -可运行语言均采用**插件化架构**,每种语言独立实现;JSON / XML / YAML / Markdown / CSV / TSV / 纯文本为编辑与可视化类型。 +可运行语言均采用**插件化架构**,每种语言独立实现;JSON / XML / YAML / Markdown / CSV / TSV / Excel / 纯文本为编辑与可视化类型。
@@ -112,12 +114,13 @@ +
-`Python` · `Node.js` · `TypeScript` · `JavaScript` · `Go` · `Rust` · `Java` · `Kotlin` · `Scala` · `Groovy` · `Clojure` · `C` · `C++` · `Objective-C/C++` · `Swift` · `Ruby` · `PHP` · `R` · `Lua` · `Haskell` · `Cangjie` · `Shell` · `AppleScript` · `SQL` · `HTML` · `CSS` · `SVG` · `JSON` · `XML` · `YAML` · `Markdown` · `CSV` · `TSV` · `Text` +`Python` · `Node.js` · `TypeScript` · `JavaScript` · `Go` · `Rust` · `Java` · `Kotlin` · `Scala` · `Groovy` · `Clojure` · `C` · `C++` · `Objective-C/C++` · `Swift` · `Ruby` · `PHP` · `R` · `Lua` · `Haskell` · `Cangjie` · `Shell` · `AppleScript` · `SQL` · `HTML` · `CSS` · `SVG` · `JSON` · `XML` · `YAML` · `Markdown` · `CSV` · `TSV` · `Excel` · `Text`
@@ -151,7 +154,9 @@ pnpm tauri build | 前端 | Vue 3 · TypeScript · Tailwind CSS · CodeMirror 6 · ECharts | | 后端 | Rust · Tauri 2(rusqlite · mysql) | | 存储 | SQLite(执行历史 / AI 对话 / 代码片段 / 应用配置统一入库) | -| 架构 | 插件化语言支持系统 · 插件式数据库执行器 · 可复用图表组件 | +| 架构 | 插件化语言支持系统 · 插件式数据库执行器 · 可复用图表组件 · LSP 桥接 | + +> **LSP 语言服务器**(可选,按需安装):Python `pyright`、TS/JS `typescript-language-server`、Rust `rust-analyzer`、Go `gopls`、C/C++ `clangd`、Lua `lua-language-server`、PHP `intelephense`、Ruby `solargraph`、HTML/CSS/JSON `vscode-langservers-extracted`。未安装的语言会自动跳过 LSP,仅用基础高亮 + AI 预测。 --- diff --git a/package.json b/package.json index e93a1df..75f39a3 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "@codemirror/lang-yaml": "^6.1.3", "@codemirror/language": "^6.11.2", "@codemirror/legacy-modes": "^6.5.1", + "@codemirror/lint": "6.9.6", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.38.1", + "@open-rpc/client-js": "^2.0.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.3.2", "@tauri-apps/plugin-fs": "^2.4.2", @@ -39,6 +41,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "codemirror": "^6.0.2", + "codemirror-languageserver": "^1.22.0", "dompurify": "^3.4.8", "echarts": "^6.1.0", "echarts-liquidfill": "^3.1.0", @@ -47,9 +50,11 @@ "lodash-es": "^4.17.21", "lucide-vue-next": "^0.539.0", "markdown-it": "^14.2.0", + "vscode-languageserver-protocol": "^3.18.0", "vue": "^3.5.13", "vue-codemirror": "^6.1.1", - "vue3-markdown-it": "^1.0.10" + "vue3-markdown-it": "^1.0.10", + "xlsx": "^0.18.5" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", diff --git a/public/icons/xlsx.svg b/public/icons/xlsx.svg new file mode 100644 index 0000000..990753c --- /dev/null +++ b/public/icons/xlsx.svg @@ -0,0 +1,9 @@ + + + + + + + + XLS + diff --git a/src-tauri/src/lsp.rs b/src-tauri/src/lsp.rs new file mode 100644 index 0000000..233c87d --- /dev/null +++ b/src-tauri/src/lsp.rs @@ -0,0 +1,422 @@ +//! LSP 桥接:按语言拉起语言服务器进程,转发 JSON-RPC(Content-Length 帧)。 +//! 后端只做透明转发:前端发送/接收原始 JSON 字符串,握手与协议由前端负责。 + +use serde::Serialize; +use std::collections::HashMap; +use std::io::{BufReader, Read, Write}; +use std::path::PathBuf; +use std::process::{Child, ChildStdin, Command, Stdio}; +use std::sync::Mutex as StdMutex; +use tauri::{AppHandle, Emitter, State}; + +struct Server { + child: Child, + stdin: ChildStdin, +} + +pub struct LspState { + servers: StdMutex>, +} + +impl LspState { + pub fn new() -> Self { + Self { + servers: StdMutex::new(HashMap::new()), + } + } +} + +#[derive(Clone, Serialize)] +struct LspEvent { + language: String, + message: String, +} + +/// 语言 -> (可执行名, 参数)。新增语言在此加一行。 +fn server_cmd(language: &str) -> Option<(&'static str, Vec<&'static str>)> { + match language { + "python3" | "python2" | "python" => Some(("pyright-langserver", vec!["--stdio"])), + "typescript" | "typescript-nodejs" | "typescript-browser" | "javascript-nodejs" + | "javascript-browser" | "javascript-jquery" | "nodejs" => { + Some(("typescript-language-server", vec!["--stdio"])) + } + "rust" => Some(("rust-analyzer", vec![])), + "go" => Some(("gopls", vec![])), + "c" | "cpp" | "objective-c" | "objective-cpp" => Some(("clangd", vec![])), + "lua" => Some(("lua-language-server", vec![])), + "php" => Some(("intelephense", vec!["--stdio"])), + "ruby" => Some(("solargraph", vec!["stdio"])), + "html" => Some(("vscode-html-language-server", vec!["--stdio"])), + "css" => Some(("vscode-css-language-server", vec!["--stdio"])), + "json" => Some(("vscode-json-language-server", vec!["--stdio"])), + _ => None, + } +} + +/// 可在设置中一键安装的语言服务器清单(用于检测与安装) +/// (id, 展示名, 用于检测的可执行名, 安装命令) +fn server_defs() -> Vec<(&'static str, &'static str, &'static str, &'static str)> { + vec![ + ( + "python", + "Python (pyright)", + "pyright-langserver", + "npm i -g pyright", + ), + ( + "typescript", + "TypeScript / JavaScript", + "typescript-language-server", + "npm i -g typescript-language-server typescript", + ), + ( + "rust", + "Rust (rust-analyzer)", + "rust-analyzer", + "rustup component add rust-analyzer", + ), + ( + "go", + "Go (gopls)", + "gopls", + "go install golang.org/x/tools/gopls@latest", + ), + ("clangd", "C / C++ (clangd)", "clangd", "brew install llvm"), + ( + "lua", + "Lua", + "lua-language-server", + "brew install lua-language-server", + ), + ( + "php", + "PHP (intelephense)", + "intelephense", + "npm i -g intelephense", + ), + ( + "ruby", + "Ruby (solargraph)", + "solargraph", + "gem install solargraph", + ), + ( + "web", + "HTML / CSS / JSON", + "vscode-html-language-server", + "npm i -g vscode-langservers-extracted", + ), + ] +} + +#[derive(Serialize)] +pub struct LspServerInfo { + id: String, + label: String, + program: String, + installed: bool, + install: String, +} + +/// 列出可安装的语言服务器及其安装状态 +#[tauri::command] +pub fn lsp_server_list() -> Vec { + server_defs() + .into_iter() + .map(|(id, label, program, install)| LspServerInfo { + id: id.to_string(), + label: label.to_string(), + program: program.to_string(), + installed: find_in_path(program).is_some(), + install: install.to_string(), + }) + .collect() +} + +/// 一键安装某语言服务器:执行其安装命令并实时输出日志(事件 lsp:install / lsp:install-done) +#[tauri::command] +pub fn lsp_install(app: AppHandle, id: String) -> Result<(), String> { + let def = server_defs() + .into_iter() + .find(|(d, ..)| *d == id) + .ok_or_else(|| "未知的语言服务器".to_string())?; + let cmd_str = def.3.to_string(); + + let (shell, flag) = if cfg!(windows) { + ("cmd", "/C") + } else { + ("sh", "-c") + }; + let mut child = Command::new(shell) + .arg(flag) + .arg(&cmd_str) + .env("PATH", augmented_path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("无法执行安装命令: {}", e))?; + + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + let id_done = id.clone(); + let app_done = app.clone(); + + let emit_lines = |app: AppHandle, id: String, reader: Option>| { + if let Some(r) = reader { + std::thread::spawn(move || { + let mut buf = BufReader::new(r); + while let Some(line) = read_raw_line(&mut buf) { + let _ = app.emit("lsp:install", (id.clone(), line)); + } + }); + } + }; + emit_lines( + app.clone(), + id.clone(), + stdout.map(|s| Box::new(s) as Box), + ); + emit_lines( + app.clone(), + id.clone(), + stderr.map(|s| Box::new(s) as Box), + ); + + std::thread::spawn(move || { + let success = child.wait().map(|s| s.success()).unwrap_or(false); + let _ = app_done.emit("lsp:install-done", (id_done, success)); + }); + Ok(()) +} + +/// 读取一行普通文本(以 \n 结尾,用于安装日志) +fn read_raw_line(reader: &mut BufReader) -> Option { + let mut buf = Vec::new(); + let mut byte = [0u8; 1]; + loop { + match reader.read(&mut byte) { + Ok(0) => { + if buf.is_empty() { + return None; + } + return Some(String::from_utf8_lossy(&buf).to_string()); + } + Ok(_) => { + if byte[0] == b'\n' { + return Some(String::from_utf8_lossy(&buf).to_string()); + } + if byte[0] != b'\r' { + buf.push(byte[0]); + } + } + Err(_) => return None, + } + } +} + +/// GUI 应用 PATH 常缺失,补充常见安装目录 +fn extra_bin_dirs() -> Vec { + let mut dirs = vec![ + PathBuf::from("/usr/local/bin"), + PathBuf::from("/opt/homebrew/bin"), + PathBuf::from("/usr/bin"), + ]; + if let Some(home) = dirs_home() { + dirs.push(home.join(".cargo/bin")); + dirs.push(home.join(".local/bin")); + dirs.push(home.join("go/bin")); + dirs.push(home.join(".npm-global/bin")); + } + dirs +} + +fn dirs_home() -> Option { + std::env::var_os("HOME").map(PathBuf::from) +} + +/// 在 PATH 与常见目录中查找可执行文件全路径 +fn find_in_path(prog: &str) -> Option { + let exts: Vec<&str> = if cfg!(windows) { + vec!["", ".cmd", ".exe", ".bat"] + } else { + vec![""] + }; + let mut dirs: Vec = Vec::new(); + if let Some(path) = std::env::var_os("PATH") { + dirs.extend(std::env::split_paths(&path)); + } + dirs.extend(extra_bin_dirs()); + for dir in dirs { + for ext in &exts { + let full = dir.join(format!("{}{}", prog, ext)); + if full.is_file() { + return Some(full); + } + } + } + None +} + +/// 给子进程增广 PATH(语言服务器常依赖 node 等) +fn augmented_path() -> String { + let mut parts: Vec = Vec::new(); + if let Some(path) = std::env::var_os("PATH") { + parts.push(path.to_string_lossy().to_string()); + } + for d in extra_bin_dirs() { + parts.push(d.to_string_lossy().to_string()); + } + parts.join(":") +} + +/// 该语言是否有可用的语言服务器 +#[tauri::command] +pub fn lsp_available(language: String) -> bool { + server_cmd(&language) + .map(|(prog, _)| find_in_path(prog).is_some()) + .unwrap_or(false) +} + +/// 启动语言服务器;已启动则直接返回 true。 +#[tauri::command] +pub fn lsp_start( + app: AppHandle, + state: State<'_, LspState>, + language: String, +) -> Result { + { + let servers = state.servers.lock().map_err(|e| e.to_string())?; + if servers.contains_key(&language) { + return Ok(true); + } + } + let (prog, args) = server_cmd(&language).ok_or_else(|| "该语言暂不支持 LSP".to_string())?; + let exe = find_in_path(prog) + .ok_or_else(|| format!("未找到语言服务器:{}(请先安装并确保在 PATH 中)", prog))?; + + let mut cmd = Command::new(&exe); + cmd.args(&args) + .env("PATH", augmented_path()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = cmd + .spawn() + .map_err(|e| format!("启动 {} 失败: {}", prog, e))?; + let stdin = child.stdin.take().ok_or("无法获取 stdin")?; + let stdout = child.stdout.take().ok_or("无法获取 stdout")?; + let stderr = child.stderr.take(); + + // 读取线程:解析 Content-Length 帧,原样转发 JSON 给前端 + let app_reader = app.clone(); + let lang_reader = language.clone(); + std::thread::spawn(move || { + let mut reader = BufReader::new(stdout); + while let Some(body) = read_message(&mut reader) { + let _ = app_reader.emit( + "lsp:message", + LspEvent { + language: lang_reader.clone(), + message: body, + }, + ); + } + let _ = app_reader.emit("lsp:exit", lang_reader.clone()); + }); + + // 排空 stderr,避免阻塞 + if let Some(mut err) = stderr { + std::thread::spawn(move || { + let mut buf = [0u8; 4096]; + while let Ok(n) = err.read(&mut buf) { + if n == 0 { + break; + } + } + }); + } + + state + .servers + .lock() + .map_err(|e| e.to_string())? + .insert(language, Server { child, stdin }); + Ok(true) +} + +/// 向语言服务器发送一条 JSON-RPC(已是完整 JSON 字符串) +#[tauri::command] +pub fn lsp_send( + state: State<'_, LspState>, + language: String, + message: String, +) -> Result<(), String> { + let mut servers = state.servers.lock().map_err(|e| e.to_string())?; + let server = servers + .get_mut(&language) + .ok_or_else(|| "语言服务器未启动".to_string())?; + let frame = format!("Content-Length: {}\r\n\r\n{}", message.len(), message); + server + .stdin + .write_all(frame.as_bytes()) + .map_err(|e| e.to_string())?; + server.stdin.flush().map_err(|e| e.to_string())?; + Ok(()) +} + +/// 停止语言服务器 +#[tauri::command] +pub fn lsp_stop(state: State<'_, LspState>, language: String) -> Result<(), String> { + if let Some(mut server) = state + .servers + .lock() + .map_err(|e| e.to_string())? + .remove(&language) + { + let _ = server.child.kill(); + } + Ok(()) +} + +/// 读取一条 LSP 消息(Content-Length 帧);EOF 返回 None +fn read_message(reader: &mut BufReader) -> Option { + let mut content_length: usize = 0; + // 逐字节读 header 行直到空行 + loop { + let line = read_line(reader)?; + if line.is_empty() { + break; + } + if let Some(rest) = line.strip_prefix("Content-Length:") { + content_length = rest.trim().parse().unwrap_or(0); + } + } + if content_length == 0 { + return Some(String::new()); + } + let mut body = vec![0u8; content_length]; + reader.read_exact(&mut body).ok()?; + Some(String::from_utf8_lossy(&body).to_string()) +} + +/// 读取一行(以 \r\n 结尾),返回不含结尾的内容;EOF 返回 None +fn read_line(reader: &mut BufReader) -> Option { + let mut buf = Vec::new(); + let mut byte = [0u8; 1]; + loop { + match reader.read(&mut byte) { + Ok(0) => return None, + Ok(_) => { + if byte[0] == b'\n' { + if buf.last() == Some(&b'\r') { + buf.pop(); + } + return Some(String::from_utf8_lossy(&buf).to_string()); + } + buf.push(byte[0]); + } + Err(_) => return None, + } + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3f16a92..e0656b5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -18,6 +18,7 @@ mod filesystem; mod font; mod kv; mod logger; +mod lsp; mod plugin; mod plugins; mod setup; @@ -58,6 +59,9 @@ use crate::filesystem::{ reveal_path, search_in_files, watch_directory, write_file_text, }; use crate::kv::{KvStore, kv_delete, kv_get_all, kv_set}; +use crate::lsp::{ + LspState, lsp_available, lsp_install, lsp_send, lsp_server_list, lsp_start, lsp_stop, +}; use crate::plugin::{get_info, get_supported_languages}; use crate::setup::app::get_app_info; use crate::snippets::{Snippets, delete_snippet, get_snippets, save_snippet}; @@ -97,6 +101,7 @@ fn main() { .manage(Snippets::new().expect("failed to initialize snippets database")) .manage(KvStore::new().expect("failed to initialize kv store database")) .manage(TerminalState::new()) + .manage(LspState::new()) .manage(ExecutionPluginManagerState::new(PluginManager::new())) .manage(EnvironmentManagerState::new(env_manager)) .setup(|app| { @@ -226,7 +231,14 @@ fn main() { terminal_resize, terminal_kill, // SQL 执行 - run_sql + run_sql, + // LSP 桥接 + lsp_available, + lsp_start, + lsp_send, + lsp_stop, + lsp_server_list, + lsp_install ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/plugins/manager.rs b/src-tauri/src/plugins/manager.rs index f55f759..53b9f55 100644 --- a/src-tauri/src/plugins/manager.rs +++ b/src-tauri/src/plugins/manager.rs @@ -38,6 +38,7 @@ use crate::plugins::tsv::TsvPlugin; use crate::plugins::typescript::TypeScriptPlugin; use crate::plugins::typescript_browser::TypeScriptBrowserPlugin; use crate::plugins::typescript_nodejs::TypeScriptNodeJsPlugin; +use crate::plugins::xlsx::XlsxPlugin; use crate::plugins::xml::XmlPlugin; use crate::plugins::yaml::YamlPlugin; use std::collections::HashMap; @@ -80,6 +81,7 @@ impl PluginManager { ("text".to_string(), Box::new(TextPlugin)), ("csv".to_string(), Box::new(CsvPlugin)), ("tsv".to_string(), Box::new(TsvPlugin)), + ("xlsx".to_string(), Box::new(XlsxPlugin)), ("sql".to_string(), Box::new(SqlPlugin)), ("php".to_string(), Box::new(PHPPlugin)), ("r".to_string(), Box::new(RPlugin)), diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index 931c24d..8cdbca7 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -438,6 +438,7 @@ pub mod tsv; pub mod typescript; pub mod typescript_browser; pub mod typescript_nodejs; +pub mod xlsx; pub mod xml; pub mod yaml; diff --git a/src-tauri/src/plugins/xlsx.rs b/src-tauri/src/plugins/xlsx.rs new file mode 100644 index 0000000..1209f0c --- /dev/null +++ b/src-tauri/src/plugins/xlsx.rs @@ -0,0 +1,56 @@ +use super::{LanguagePlugin, PluginConfig}; +use std::vec; + +pub struct XlsxPlugin; + +impl LanguagePlugin for XlsxPlugin { + fn get_order(&self) -> i32 { + 42 + } + + fn get_language_name(&self) -> &'static str { + "Excel" + } + + fn get_language_key(&self) -> &'static str { + "xlsx" + } + + fn get_file_extension(&self) -> String { + self.get_config() + .map(|config| config.extension.clone()) + .unwrap_or_else(|| "xlsx".to_string()) + } + + fn get_version_args(&self) -> Vec<&'static str> { + vec!["--"] + } + + fn get_path_command(&self) -> String { + "--".to_string() + } + + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: String::from("xlsx"), + before_compile: None, + // 多扩展名:xlsx / xls + extension: String::from("xlsx,xls"), + execute_home: None, + // Excel 为二进制,运行时输出文件路径,由前端用 SheetJS 读取解析 + run_command: Some(String::from("echo $filename")), + after_compile: None, + template: None, + timeout: Some(30), + console_type: Some(String::from("xlsx")), + icon_path: None, + } + } + + fn get_default_command(&self) -> String { + self.get_config() + .and_then(|config| config.run_command.clone()) + .unwrap_or_else(|| "echo".to_string()) + } +} diff --git a/src/App.vue b/src/App.vue index c2ef41e..589c822 100644 --- a/src/App.vue +++ b/src/App.vue @@ -91,6 +91,7 @@ +
@@ -105,7 +106,7 @@
- + + + +
@@ -217,6 +226,7 @@ +
@@ -231,7 +241,7 @@
- + { const showViewer = ref(false) const viewerFile = ref<{ path: string, lineCount: number, sizeBytes: number } | null>(null) +// 二进制数据文件(Excel 等):切到对应语言并在控制台用数据视图展示,不进代码编辑器 +const openDataFile = (filePath: string, lang: string) => { + addRecentFile(filePath) + applyLanguage(lang) + currentFilePath.value = filePath + output.value = filePath + showConsole.value = true + consoleType.value = getCurrentConsoleType() +} + // 按文件大小决定:可编辑打开 / 只读查看 const smartOpen = async (filePath: string) => { try { + // Excel 等二进制数据文件:直接路由到数据视图,不当文本打开 + if (/\.(xlsx|xls)$/i.test(filePath)) { + openDataFile(filePath, 'xlsx') + return + } + const meta = await invoke<{ size_bytes: number, line_count: number, is_text: boolean }>('get_text_file_meta', {path: filePath}) if (!meta.is_text) { toast.error('不是文本文件,无法打开') diff --git a/src/components/AiSql.vue b/src/components/AiSql.vue new file mode 100644 index 0000000..00b7b6d --- /dev/null +++ b/src/components/AiSql.vue @@ -0,0 +1,152 @@ +