更新
This commit is contained in:
@@ -2,5 +2,8 @@
|
||||
"topic_20260630-144952_fd0584fe8f266d6a": "auto",
|
||||
"topic_20260630-151112_1109fa65a65cc25f": "auto",
|
||||
"topic_20260630-160436_83c4100e5ffeaf1a": "auto",
|
||||
"topic_20260630-161655_edfbcdeb0d6b655b": "auto"
|
||||
"topic_20260630-161655_edfbcdeb0d6b655b": "auto",
|
||||
"topic_20260701-155310_c69d2b8b32c78f89": "auto",
|
||||
"topic_20260701-161459_aeaacf65cf653c96": "auto",
|
||||
"topic_20260701-162849_a8bde34cd0ff69c1": "auto"
|
||||
}
|
||||
@@ -2,5 +2,8 @@
|
||||
"topic_20260630-144952_fd0584fe8f266d6a": "PS D:\\Code\\yurou\u003e …",
|
||||
"topic_20260630-151112_1109fa65a65cc25f": "这个项目cargo安装的很慢,帮我配…",
|
||||
"topic_20260630-160436_83c4100e5ffeaf1a": "我要用这个项目做一个markdown…",
|
||||
"topic_20260630-161655_edfbcdeb0d6b655b": "我现在需要给这个首页做一个三栏布局…"
|
||||
"topic_20260630-161655_edfbcdeb0d6b655b": "我现在需要给这个首页做一个三栏布局…",
|
||||
"topic_20260701-155310_c69d2b8b32c78f89": "MarkdownEditor组件应该…",
|
||||
"topic_20260701-161459_aeaacf65cf653c96": "添加一下原生标题栏",
|
||||
"topic_20260701-162849_a8bde34cd0ff69c1": "帮我完善一个功能,当用户直接打开软件…"
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tiptap/extension-placeholder": "^3.27.1",
|
||||
"@tiptap/pm": "^3.27.1",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2
|
||||
version: 2.11.1
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ^2.7.1
|
||||
version: 2.7.1
|
||||
'@tauri-apps/plugin-opener':
|
||||
specifier: ^2
|
||||
version: 2.5.4
|
||||
@@ -563,6 +566,9 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.7.1':
|
||||
resolution: {integrity: sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==}
|
||||
|
||||
'@tauri-apps/plugin-opener@2.5.4':
|
||||
resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==}
|
||||
|
||||
@@ -1388,6 +1394,10 @@ snapshots:
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.11.4
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.11.4
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.7.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
'@tauri-apps/plugin-opener@2.5.4':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.11.1
|
||||
|
||||
144
src-tauri/Cargo.lock
generated
144
src-tauri/Cargo.lock
generated
@@ -2180,6 +2180,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -2696,6 +2697,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
@@ -3378,6 +3403,48 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"log",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.4"
|
||||
@@ -4419,6 +4486,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -4452,13 +4528,30 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
@@ -4489,6 +4582,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -4501,6 +4600,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -4513,12 +4618,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -4531,6 +4648,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -4543,6 +4666,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -4555,6 +4684,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -4567,6 +4702,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
@@ -4709,6 +4850,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"time",
|
||||
]
|
||||
|
||||
@@ -19,6 +19,7 @@ tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,64 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
use std::fs;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
use tauri::Emitter;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct DirEntry {
|
||||
name: String,
|
||||
path: String,
|
||||
is_dir: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn pick_folder(app: tauri::AppHandle) -> Option<String> {
|
||||
app.dialog()
|
||||
.file()
|
||||
.blocking_pick_folder()
|
||||
.map(|p| p.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn read_dir(path: String) -> Result<Vec<DirEntry>, String> {
|
||||
let entries = fs::read_dir(&path).map_err(|e| format!("无法读取目录: {}", e))?;
|
||||
let mut result = Vec::new();
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?;
|
||||
let path = entry.path();
|
||||
let name = entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
result.push(DirEntry {
|
||||
name,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
is_dir: path.is_dir(),
|
||||
});
|
||||
}
|
||||
// Sort: directories first, then alphabetical
|
||||
result.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then_with(|| a.name.cmp(&b.name)));
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn read_file(path: String) -> Result<String, String> {
|
||||
fs::read_to_string(&path).map_err(|e| format!("无法读取文件: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn write_file(path: String, content: String) -> Result<(), String> {
|
||||
fs::write(&path, &content).map_err(|e| format!("无法保存文件: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn pick_save_file(app: tauri::AppHandle) -> Option<String> {
|
||||
app.dialog()
|
||||
.file()
|
||||
.blocking_save_file()
|
||||
.map(|p| p.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
@@ -8,7 +68,139 @@ fn greet(name: &str) -> String {
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.invoke_handler(tauri::generate_handler![greet, pick_folder, read_dir, read_file, write_file, pick_save_file])
|
||||
.setup(|app| {
|
||||
// ---- File menu ----
|
||||
let new = MenuItemBuilder::with_id("new", "新建")
|
||||
.accelerator("CmdOrCtrl+N")
|
||||
.build(app)?;
|
||||
let open = MenuItemBuilder::with_id("open", "打开文件夹...")
|
||||
.accelerator("CmdOrCtrl+O")
|
||||
.build(app)?;
|
||||
let save = MenuItemBuilder::with_id("save", "保存")
|
||||
.accelerator("CmdOrCtrl+S")
|
||||
.build(app)?;
|
||||
let save_as = MenuItemBuilder::with_id("save_as", "另存为...")
|
||||
.accelerator("CmdOrCtrl+Shift+S")
|
||||
.build(app)?;
|
||||
let quit = MenuItemBuilder::with_id("quit", "退出")
|
||||
.accelerator("CmdOrCtrl+Q")
|
||||
.build(app)?;
|
||||
|
||||
let file_menu = SubmenuBuilder::new(app, "文件")
|
||||
.item(&new)
|
||||
.item(&open)
|
||||
.item(&save)
|
||||
.item(&save_as)
|
||||
.separator()
|
||||
.item(&quit)
|
||||
.build()?;
|
||||
|
||||
// ---- Edit menu ----
|
||||
let undo = MenuItemBuilder::with_id("undo", "撤销")
|
||||
.accelerator("CmdOrCtrl+Z")
|
||||
.build(app)?;
|
||||
let redo = MenuItemBuilder::with_id("redo", "重做")
|
||||
.accelerator("CmdOrCtrl+Shift+Z")
|
||||
.build(app)?;
|
||||
let cut = MenuItemBuilder::with_id("cut", "剪切")
|
||||
.accelerator("CmdOrCtrl+X")
|
||||
.build(app)?;
|
||||
let copy = MenuItemBuilder::with_id("copy", "复制")
|
||||
.accelerator("CmdOrCtrl+C")
|
||||
.build(app)?;
|
||||
let paste = MenuItemBuilder::with_id("paste", "粘贴")
|
||||
.accelerator("CmdOrCtrl+V")
|
||||
.build(app)?;
|
||||
let select_all = MenuItemBuilder::with_id("select_all", "全选")
|
||||
.accelerator("CmdOrCtrl+A")
|
||||
.build(app)?;
|
||||
|
||||
let edit_menu = SubmenuBuilder::new(app, "编辑")
|
||||
.item(&undo)
|
||||
.item(&redo)
|
||||
.separator()
|
||||
.item(&cut)
|
||||
.item(©)
|
||||
.item(&paste)
|
||||
.separator()
|
||||
.item(&select_all)
|
||||
.build()?;
|
||||
|
||||
// ---- View menu ----
|
||||
let toggle_outline = MenuItemBuilder::with_id("toggle_outline", "切换大纲")
|
||||
.accelerator("CmdOrCtrl+B")
|
||||
.build(app)?;
|
||||
|
||||
let view_menu = SubmenuBuilder::new(app, "视图")
|
||||
.item(&toggle_outline)
|
||||
.build()?;
|
||||
|
||||
// ---- Help menu ----
|
||||
let about = MenuItemBuilder::with_id("about", "关于")
|
||||
.build(app)?;
|
||||
|
||||
let help_menu = SubmenuBuilder::new(app, "帮助")
|
||||
.item(&about)
|
||||
.build()?;
|
||||
|
||||
// ---- Build the menu bar ----
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&file_menu)
|
||||
.item(&edit_menu)
|
||||
.item(&view_menu)
|
||||
.item(&help_menu)
|
||||
.build()?;
|
||||
|
||||
app.set_menu(menu)?;
|
||||
|
||||
// ---- Handle menu events ----
|
||||
let app_handle = app.handle().clone();
|
||||
app.on_menu_event(move |_app_handle, event| {
|
||||
let id = event.id().0.as_str();
|
||||
match id {
|
||||
"quit" => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
"open" => {
|
||||
let handle = app_handle.clone();
|
||||
// Spawn because dialog is blocking
|
||||
std::thread::spawn(move || {
|
||||
if let Some(folder) = handle
|
||||
.dialog()
|
||||
.file()
|
||||
.blocking_pick_folder()
|
||||
{
|
||||
let path = folder.to_string();
|
||||
let _ = handle.emit("folder-opened", path);
|
||||
}
|
||||
});
|
||||
}
|
||||
"save" => {
|
||||
let _ = app_handle.emit("menu-save", ());
|
||||
}
|
||||
"save_as" => {
|
||||
let _ = app_handle.emit("menu-save-as", ());
|
||||
}
|
||||
"new" => {
|
||||
let _ = app_handle.emit("menu-new", ());
|
||||
}
|
||||
"toggle_outline" => {
|
||||
let _ = app_handle.emit("toggle-outline", ());
|
||||
}
|
||||
"about" => {
|
||||
let _ = app_handle.emit("show-about", ());
|
||||
}
|
||||
_ => {
|
||||
// Forward other menu events to frontend
|
||||
let _ = app_handle.emit("menu-event", id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
{
|
||||
"title": "yurou",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
"height": 600,
|
||||
"decorations": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
183
src/App.vue
183
src/App.vue
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
||||
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
||||
import OutlinePanel from "./components/OutlinePanel.vue";
|
||||
@@ -8,9 +10,22 @@ const content = ref("");
|
||||
const wordCount = ref(0);
|
||||
const charCount = ref(0);
|
||||
|
||||
// File state
|
||||
const currentFilePath = ref<string | null>(null);
|
||||
const saveStatus = ref("");
|
||||
|
||||
// Panel state
|
||||
const showOutline = ref(true);
|
||||
|
||||
// Folder state
|
||||
interface DirEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
is_dir: boolean;
|
||||
}
|
||||
const folderPath = ref("");
|
||||
const dirEntries = ref<DirEntry[]>([]);
|
||||
|
||||
function onUpdate(html: string) {
|
||||
content.value = html;
|
||||
// Strip HTML tags for counting
|
||||
@@ -32,23 +47,121 @@ function onHeadingClick(heading: { text: string; level: number }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDir(path: string) {
|
||||
folderPath.value = path;
|
||||
try {
|
||||
dirEntries.value = await invoke<DirEntry[]>("read_dir", { path });
|
||||
} catch (e) {
|
||||
console.error("Failed to read directory:", e);
|
||||
dirEntries.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function getFileName(path: string): string {
|
||||
return path.split(/[/\\]/).pop() || path;
|
||||
}
|
||||
|
||||
async function doSave() {
|
||||
if (!currentFilePath.value) {
|
||||
// No file path yet — prompt user to pick a save location
|
||||
try {
|
||||
const chosen = await invoke<string | null>("pick_save_file");
|
||||
if (!chosen) return; // user cancelled
|
||||
currentFilePath.value = chosen;
|
||||
} catch (e) {
|
||||
console.error("Save dialog failed:", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke("write_file", { path: currentFilePath.value, content: content.value });
|
||||
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
|
||||
setTimeout(() => { saveStatus.value = ""; }, 3000);
|
||||
} catch (e) {
|
||||
console.error("Save failed:", e);
|
||||
saveStatus.value = `保存失败: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function doSaveAs() {
|
||||
try {
|
||||
const chosen = await invoke<string | null>("pick_save_file");
|
||||
if (!chosen) return; // user cancelled
|
||||
currentFilePath.value = chosen;
|
||||
await invoke("write_file", { path: currentFilePath.value, content: content.value });
|
||||
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
|
||||
setTimeout(() => { saveStatus.value = ""; }, 3000);
|
||||
} catch (e) {
|
||||
console.error("Save failed:", e);
|
||||
saveStatus.value = `保存失败: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
function doNew() {
|
||||
content.value = "";
|
||||
currentFilePath.value = null;
|
||||
saveStatus.value = "";
|
||||
}
|
||||
|
||||
// Listen for menu events from Rust
|
||||
listen<string>("folder-opened", (event) => {
|
||||
loadDir(event.payload);
|
||||
});
|
||||
|
||||
listen("toggle-outline", () => {
|
||||
showOutline.value = !showOutline.value;
|
||||
});
|
||||
|
||||
listen("menu-save", () => {
|
||||
doSave();
|
||||
});
|
||||
|
||||
listen("menu-save-as", () => {
|
||||
doSaveAs();
|
||||
});
|
||||
|
||||
listen("menu-new", () => {
|
||||
doNew();
|
||||
});
|
||||
|
||||
listen("menu-event", (event) => {
|
||||
console.log("Menu event:", event.payload);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-950 text-gray-100 flex flex-col">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="flex items-center justify-between px-6 py-3 border-b border-gray-800 bg-gray-900/70 backdrop-blur-md sticky top-0 z-10"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center text-sm font-bold"
|
||||
>
|
||||
M
|
||||
</div>
|
||||
<h1 class="text-lg font-semibold tracking-tight">Markdown Editor</h1>
|
||||
</div>
|
||||
<!-- Three-column body -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left: Directory sidebar -->
|
||||
<DirectorySidebar
|
||||
:folder-path="folderPath"
|
||||
:entries="dirEntries"
|
||||
/>
|
||||
|
||||
<!-- Middle: Outline panel -->
|
||||
<transition name="outline-slide">
|
||||
<OutlinePanel
|
||||
v-if="showOutline"
|
||||
:html-content="content"
|
||||
@heading-click="onHeadingClick"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
<!-- Right: Editor area -->
|
||||
<main class="flex-1 flex flex-col overflow-y-auto p-2">
|
||||
<MarkdownEditor
|
||||
v-model="content"
|
||||
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"
|
||||
@update:model-value="onUpdate"
|
||||
@save-shortcut="doSave"
|
||||
class="flex-1 w-full"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Outline toggle -->
|
||||
<button
|
||||
@@ -76,43 +189,23 @@ function onHeadingClick(heading: { text: string; level: number }) {
|
||||
|
||||
<span class="w-px h-3 bg-gray-600" />
|
||||
|
||||
<!-- Current file / save status -->
|
||||
<span
|
||||
v-if="saveStatus"
|
||||
class="text-xs"
|
||||
:class="saveStatus.startsWith('保存失败') ? 'text-red-400' : 'text-green-400'"
|
||||
>{{ saveStatus }}</span>
|
||||
<span v-else-if="currentFilePath" class="text-xs text-gray-400">
|
||||
{{ currentFilePath.split(/[/\\]/).pop() }}
|
||||
</span>
|
||||
<span v-else class="text-xs text-gray-500">未保存</span>
|
||||
|
||||
<span class="flex-1" />
|
||||
|
||||
<span class="text-xs text-gray-400">{{ wordCount.toLocaleString() }} 词</span>
|
||||
<span class="w-px h-3 bg-gray-600" />
|
||||
<span class="text-xs text-gray-400">{{ charCount.toLocaleString() }} 字</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Three-column body -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left: Directory sidebar -->
|
||||
<DirectorySidebar />
|
||||
|
||||
<!-- Middle: Outline panel -->
|
||||
<transition name="outline-slide">
|
||||
<OutlinePanel
|
||||
v-if="showOutline"
|
||||
:html-content="content"
|
||||
@heading-click="onHeadingClick"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
<!-- Right: Editor area -->
|
||||
<main class="flex-1 flex justify-center overflow-y-auto px-4 py-8">
|
||||
<div class="w-full max-w-4xl">
|
||||
<MarkdownEditor
|
||||
v-model="content"
|
||||
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"
|
||||
@update:model-value="onUpdate"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer
|
||||
class="text-center text-xs text-gray-600 py-4 border-t border-gray-800/50"
|
||||
>
|
||||
基于 TipTap · 支持 Markdown 快捷输入
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,64 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
interface DirEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
is_dir: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
folderPath: string;
|
||||
entries: DirEntry[];
|
||||
}>();
|
||||
|
||||
const collapsed = ref(false);
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
collapsed.value = !collapsed.value;
|
||||
};
|
||||
|
||||
// ---- file tree state ----
|
||||
// ---- file tree state (for opened folder) ----
|
||||
interface FileItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "file" | "folder";
|
||||
path: string;
|
||||
children?: FileItem[];
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
const files = ref<FileItem[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "文档",
|
||||
type: "folder",
|
||||
expanded: true,
|
||||
children: [
|
||||
{ id: "2", name: "README.md", type: "file" },
|
||||
{ id: "3", name: "笔记.md", type: "file" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "工作",
|
||||
type: "folder",
|
||||
expanded: false,
|
||||
children: [{ id: "5", name: "项目计划.md", type: "file" }],
|
||||
},
|
||||
]);
|
||||
const expandedFolders = ref<Set<string>>(new Set());
|
||||
|
||||
const toggleFolder = (item: FileItem) => {
|
||||
if (item.type === "folder") {
|
||||
item.expanded = !item.expanded;
|
||||
if (expandedFolders.value.has(item.path)) {
|
||||
expandedFolders.value.delete(item.path);
|
||||
} else {
|
||||
expandedFolders.value.add(item.path);
|
||||
}
|
||||
// Trigger reactivity
|
||||
expandedFolders.value = new Set(expandedFolders.value);
|
||||
}
|
||||
};
|
||||
|
||||
const newFile = () => {
|
||||
const name = prompt("文件名称(含扩展名):");
|
||||
if (name) {
|
||||
files.value.push({ id: crypto.randomUUID(), name, type: "file" });
|
||||
// files are managed externally
|
||||
}
|
||||
};
|
||||
|
||||
const newFolder = () => {
|
||||
const name = prompt("文件夹名称:");
|
||||
if (name) {
|
||||
files.value.push({
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
type: "folder",
|
||||
expanded: true,
|
||||
children: [],
|
||||
});
|
||||
// folders are managed externally
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,7 +88,7 @@ const removeTag = (tag: string) => {
|
||||
:class="collapsed ? 'justify-center' : ''"
|
||||
>
|
||||
<span v-if="!collapsed" class="text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
目录
|
||||
语柔
|
||||
</span>
|
||||
<button
|
||||
class="p-1 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
|
||||
@@ -154,20 +148,34 @@ const removeTag = (tag: string) => {
|
||||
<div v-if="!collapsed" class="flex flex-col flex-1 overflow-hidden">
|
||||
<!-- File tree -->
|
||||
<div class="flex-1 overflow-y-auto px-2 py-2 space-y-0.5">
|
||||
<!-- Folder path indicator -->
|
||||
<div
|
||||
v-for="item in files"
|
||||
:key="item.id"
|
||||
v-if="folderPath"
|
||||
class="px-2 py-1 text-xs text-gray-500 truncate mb-2 border-b border-gray-800/50 pb-2"
|
||||
:title="folderPath"
|
||||
>
|
||||
📁 {{ folderPath.split(/[/\\]/).pop() || folderPath }}
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-if="!folderPath"
|
||||
class="px-2 py-4 text-xs text-gray-500 text-center"
|
||||
>
|
||||
点击 文件 → 打开文件夹 或按 Ctrl+O
|
||||
</div>
|
||||
|
||||
<template v-for="entry in entries" :key="entry.path">
|
||||
<!-- Folder -->
|
||||
<div
|
||||
v-if="item.type === 'folder'"
|
||||
v-if="entry.is_dir"
|
||||
class="flex items-center gap-1 px-2 py-1 rounded-md text-sm text-gray-300 hover:bg-gray-800 cursor-pointer select-none"
|
||||
@click="toggleFolder(item)"
|
||||
@click="toggleFolder({ id: entry.path, name: entry.name, type: 'folder', path: entry.path })"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-3.5 h-3.5 text-gray-500 transition-transform"
|
||||
:class="item.expanded ? 'rotate-90' : ''"
|
||||
:class="expandedFolders.has(entry.path) ? 'rotate-90' : ''"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -185,17 +193,12 @@ const removeTag = (tag: string) => {
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ item.name }}</span>
|
||||
<span class="truncate">{{ entry.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Folder children -->
|
||||
<!-- File -->
|
||||
<div
|
||||
v-if="item.type === 'folder' && item.expanded"
|
||||
class="ml-4 space-y-0.5"
|
||||
>
|
||||
<div
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
v-else
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-sm text-gray-400 hover:bg-gray-800 hover:text-gray-200 cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
@@ -208,28 +211,9 @@ const removeTag = (tag: string) => {
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ child.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top-level file -->
|
||||
<div
|
||||
v-else-if="item.type === 'file'"
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-sm text-gray-400 hover:bg-gray-800 hover:text-gray-200 cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-3.5 h-3.5 text-indigo-400/60"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ item.name }}</span>
|
||||
</div>
|
||||
<span class="truncate">{{ entry.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
|
||||
@@ -17,6 +17,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
"save-shortcut": [];
|
||||
}>();
|
||||
|
||||
const editor = useEditor({
|
||||
@@ -34,6 +35,14 @@ const editor = useEditor({
|
||||
class:
|
||||
"min-h-[320px] px-6 py-4 outline-none focus:outline-none",
|
||||
},
|
||||
handleKeyDown: (_view, event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
emit("save-shortcut");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
emit("update:modelValue", editor.getHTML());
|
||||
@@ -53,7 +62,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col rounded-xl border border-gray-700 bg-gray-900 shadow-2xl overflow-hidden"
|
||||
class="flex flex-col flex-1 w-full border border-gray-700 bg-gray-900 shadow-2xl overflow-hidden"
|
||||
>
|
||||
<!-- Toolbar -->
|
||||
<div
|
||||
@@ -205,7 +214,7 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- Editor content -->
|
||||
<EditorContent :editor="editor" class="tiptap-editor" />
|
||||
<EditorContent :editor="editor" class="tiptap-editor flex-1 flex flex-col" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -214,6 +223,7 @@ watch(
|
||||
.tiptap-editor .ProseMirror {
|
||||
color: #f3f4f6;
|
||||
min-height: 320px;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user