From bc9629667052aa0f9c3123258fbcc1dbf6e09d1b Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Fri, 3 Jul 2026 00:56:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .reasonix/desktop-topic-title-sources.json | 9 +- .reasonix/desktop-topic-titles.json | 9 +- package.json | 2 + pnpm-lock.yaml | 30 ++ src-tauri/src/lib.rs | 15 +- src-tauri/tauri.conf.json | 4 +- src/App.vue | 161 ++++---- src/components/DirectorySidebar.vue | 459 ++++++++++----------- src/components/MarkdownEditor.vue | 123 +++--- src/components/OutlinePanel.vue | 20 +- src/components/TreeEntry.vue | 93 +++++ src/main.ts | 1 + src/tailwind.css | 63 +++ 13 files changed, 611 insertions(+), 378 deletions(-) create mode 100644 src/components/TreeEntry.vue diff --git a/.reasonix/desktop-topic-title-sources.json b/.reasonix/desktop-topic-title-sources.json index 41f4c3f..222461d 100644 --- a/.reasonix/desktop-topic-title-sources.json +++ b/.reasonix/desktop-topic-title-sources.json @@ -5,5 +5,12 @@ "topic_20260630-161655_edfbcdeb0d6b655b": "auto", "topic_20260701-155310_c69d2b8b32c78f89": "auto", "topic_20260701-161459_aeaacf65cf653c96": "auto", - "topic_20260701-162849_a8bde34cd0ff69c1": "auto" + "topic_20260701-162849_a8bde34cd0ff69c1": "auto", + "topic_20260702-153333_1133e5609dc92d8d": "auto", + "topic_20260702-154849_2270d3ece753aba9": "auto", + "topic_20260702-160011_745ecb81ec319e5e": "auto", + "topic_20260702-161756_b6c7f476849c7ee1": "auto", + "topic_20260702-161832_f8157b81a77b9bfe": "auto", + "topic_20260702-163743_d97b7189a52644d4": "auto", + "topic_20260702-165156_738d9dd96d08e07c": "auto" } \ No newline at end of file diff --git a/.reasonix/desktop-topic-titles.json b/.reasonix/desktop-topic-titles.json index 8a60ef3..663056a 100644 --- a/.reasonix/desktop-topic-titles.json +++ b/.reasonix/desktop-topic-titles.json @@ -5,5 +5,12 @@ "topic_20260630-161655_edfbcdeb0d6b655b": "我现在需要给这个首页做一个三栏布局…", "topic_20260701-155310_c69d2b8b32c78f89": "MarkdownEditor组件应该…", "topic_20260701-161459_aeaacf65cf653c96": "添加一下原生标题栏", - "topic_20260701-162849_a8bde34cd0ff69c1": "帮我完善一个功能,当用户直接打开软件…" + "topic_20260701-162849_a8bde34cd0ff69c1": "帮我完善一个功能,当用户直接打开软件…", + "topic_20260702-153333_1133e5609dc92d8d": "侧边栏需要改动一下,第一个图标我们默…", + "topic_20260702-154849_2270d3ece753aba9": "帮我安装npm install re…", + "topic_20260702-160011_745ecb81ec319e5e": "166-17 @src/compon…", + "topic_20260702-161756_b6c7f476849c7ee1": "打开的应用设置一个最小的高度是600…", + "topic_20260702-161832_f8157b81a77b9bfe": "左侧的工作目录,我希望除了名称之外…", + "topic_20260702-163743_d97b7189a52644d4": "tiptap-editor 里的区域…", + "topic_20260702-165156_738d9dd96d08e07c": "帮我引入@tiptap/markdo…" } \ No newline at end of file diff --git a/package.json b/package.json index c1fac12..79df54d 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-opener": "^2", "@tiptap/extension-placeholder": "^3.27.1", + "@tiptap/markdown": "^3.27.1", "@tiptap/pm": "^3.27.1", "@tiptap/starter-kit": "^3.27.1", "@tiptap/vue-3": "^3.27.1", + "remixicon": "^4.9.1", "vue": "^3.5.13" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e594ffe..c525036 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@tiptap/extension-placeholder': specifier: ^3.27.1 version: 3.27.1(@tiptap/extensions@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)) + '@tiptap/markdown': + specifier: ^3.27.1 + version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1) '@tiptap/pm': specifier: ^3.27.1 version: 3.27.1 @@ -29,6 +32,9 @@ importers: '@tiptap/vue-3': specifier: ^3.27.1 version: 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(vue@3.5.39(typescript@5.6.3)) + remixicon: + specifier: ^4.9.1 + version: 4.9.1 vue: specifier: ^3.5.13 version: 3.5.39(typescript@5.6.3) @@ -710,6 +716,12 @@ packages: '@tiptap/core': 3.27.1 '@tiptap/pm': 3.27.1 + '@tiptap/markdown@3.27.1': + resolution: {integrity: sha512-4m2LZcj/uN0uLlGnXr3i7sfdxbQNNmeMn4wFyFrP2xpshcKPL5WujUAcqT2rgKfaDjKQ8gIK/GpgSQKuRENFwg==} + peerDependencies: + '@tiptap/core': 3.27.1 + '@tiptap/pm': 3.27.1 + '@tiptap/pm@3.27.1': resolution: {integrity: sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw==} @@ -923,6 +935,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@17.0.6: + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} + engines: {node: '>= 20'} + hasBin: true + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -991,6 +1008,9 @@ packages: prosemirror-view@1.41.9: resolution: {integrity: sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A==} + remixicon@4.9.1: + resolution: {integrity: sha512-36gLSoujkabnCFZFDyP17VNh9piuBA/rsXUb4auSJWLGsHVXtmxLj/EM5FjaEAGnk8oIAj1Azob/DZ2N+90lAQ==} + rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1518,6 +1538,12 @@ snapshots: '@tiptap/core': 3.27.1(@tiptap/pm@3.27.1) '@tiptap/pm': 3.27.1 + '@tiptap/markdown@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)': + dependencies: + '@tiptap/core': 3.27.1(@tiptap/pm@3.27.1) + '@tiptap/pm': 3.27.1 + marked: 17.0.6 + '@tiptap/pm@3.27.1': dependencies: prosemirror-changeset: 2.4.1 @@ -1782,6 +1808,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@17.0.6: {} + minimatch@9.0.9: dependencies: brace-expansion: 2.1.1 @@ -1878,6 +1906,8 @@ snapshots: prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 + remixicon@4.9.1: {} + rollup@4.62.2: dependencies: '@types/estree': 1.0.9 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eb0a844..7a2b77d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,10 +22,23 @@ fn pick_folder(app: tauri::AppHandle) -> Option { #[tauri::command] fn read_dir(path: String) -> Result, String> { let entries = fs::read_dir(&path).map_err(|e| format!("无法读取目录: {}", e))?; + let allowed_extensions = ["md", "txt", "png", "jpg", "jpeg", "gif", "svg", "webp", "bmp"]; let mut result = Vec::new(); for entry in entries { let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?; let path = entry.path(); + let is_dir = path.is_dir(); + // Always include directories; filter files by extension + if !is_dir { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + if !allowed_extensions.contains(&ext.as_str()) { + continue; + } + } let name = entry .file_name() .to_string_lossy() @@ -33,7 +46,7 @@ fn read_dir(path: String) -> Result, String> { result.push(DirEntry { name, path: path.to_string_lossy().to_string(), - is_dir: path.is_dir(), + is_dir, }); } // Sort: directories first, then alphabetical diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 112c560..004833c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,8 +13,10 @@ "windows": [ { "title": "yurou", - "width": 800, + "width": 900, "height": 600, + "minWidth": 900, + "minHeight": 600, "decorations": true } ], diff --git a/src/App.vue b/src/App.vue index 0f2c1ab..69d744b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,7 +4,6 @@ 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"; const content = ref(""); const wordCount = ref(0); @@ -14,22 +13,43 @@ const charCount = ref(0); const currentFilePath = ref(null); const saveStatus = ref(""); -// Panel state -const showOutline = ref(true); - // Folder state interface DirEntry { name: string; path: string; is_dir: boolean; + children?: DirEntry[]; } const folderPath = ref(""); const dirEntries = ref([]); -function onUpdate(html: string) { - content.value = html; - // Strip HTML tags for counting - const text = html.replace(/<[^>]*>/g, ""); +function stripMarkdown(md: string): string { + return md + // Remove images ![alt](url) + .replace(/!\[[^\]]*\]\([^)]*\)/g, "") + // Convert links [text](url) to just text + .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") + // Remove heading markers + .replace(/^#{1,6}\s+/gm, "") + // Remove bold/italic markers + .replace(/\*{1,3}|_{1,3}/g, "") + // Remove strikethrough + .replace(/~~/g, "") + // Remove inline code markers + .replace(/`+/g, "") + // Remove blockquote markers + .replace(/^>\s?/gm, "") + // Remove list markers (-, *, +, or 1.) + .replace(/^[\s]*[-*+]\s+/gm, "") + .replace(/^[\s]*\d+\.\s+/gm, "") + // Remove horizontal rules + .replace(/^[-*_]{3,}\s*$/gm, "") + .trim(); +} + +function onUpdate(md: string) { + content.value = md; + const text = stripMarkdown(md); charCount.value = text.length; wordCount.value = text.trim() ? text.trim().split(/\s+/).length : 0; } @@ -58,10 +78,55 @@ async function loadDir(path: string) { } } +async function onOpenFolder() { + try { + const chosen = await invoke("pick_folder"); + if (chosen) { + await loadDir(chosen); + } + } catch (e) { + console.error("Failed to pick folder:", e); + } +} + function getFileName(path: string): string { return path.split(/[/\\]/).pop() || path; } +// ---- file tree helpers ---- +function findEntry(entries: DirEntry[], path: string): DirEntry | null { + for (const entry of entries) { + if (entry.path === path) return entry; + if (entry.children) { + const found = findEntry(entry.children, path); + if (found) return found; + } + } + return null; +} + +async function onToggleFolder(path: string) { + const entry = findEntry(dirEntries.value, path); + if (!entry || entry.children !== undefined) return; + try { + const children = await invoke("read_dir", { path }); + entry.children = children; + } catch (e) { + console.error("Failed to read subdirectory:", e); + } +} + +async function onOpenFile(path: string) { + try { + const fileContent = await invoke("read_file", { path }); + content.value = fileContent; + currentFilePath.value = path; + saveStatus.value = ""; + } catch (e) { + console.error("Failed to open file:", e); + } +} + async function doSave() { if (!currentFilePath.value) { // No file path yet — prompt user to pick a save location @@ -110,10 +175,6 @@ listen("folder-opened", (event) => { loadDir(event.payload); }); -listen("toggle-outline", () => { - showOutline.value = !showOutline.value; -}); - listen("menu-save", () => { doSave(); }); @@ -132,26 +193,23 @@ listen("menu-event", (event) => { - + diff --git a/src/components/DirectorySidebar.vue b/src/components/DirectorySidebar.vue index 872980d..e121c4b 100644 --- a/src/components/DirectorySidebar.vue +++ b/src/components/DirectorySidebar.vue @@ -1,292 +1,283 @@ - -
+ diff --git a/src/components/MarkdownEditor.vue b/src/components/MarkdownEditor.vue index 2a9f636..6056882 100644 --- a/src/components/MarkdownEditor.vue +++ b/src/components/MarkdownEditor.vue @@ -3,6 +3,7 @@ import { watch } from "vue"; import { useEditor, EditorContent } from "@tiptap/vue-3"; import StarterKit from "@tiptap/starter-kit"; import Placeholder from "@tiptap/extension-placeholder"; +import { Markdown } from "@tiptap/markdown"; const props = withDefaults( defineProps<{ @@ -29,6 +30,7 @@ const editor = useEditor({ Placeholder.configure({ placeholder: props.placeholder, }), + Markdown, ], editorProps: { attributes: { @@ -45,7 +47,7 @@ const editor = useEditor({ }, }, onUpdate: ({ editor }) => { - emit("update:modelValue", editor.getHTML()); + emit("update:modelValue", editor.getMarkdown()); }, }); @@ -53,8 +55,8 @@ const editor = useEditor({ watch( () => props.modelValue, (val) => { - if (editor.value && val !== editor.value.getHTML()) { - editor.value.commands.setContent(val, { emitUpdate: false }); + if (editor.value && val !== editor.value.getMarkdown()) { + editor.value.commands.setContent(val, { contentType: "markdown", emitUpdate: false }); } }, ); @@ -62,12 +64,12 @@ watch(