This commit is contained in:
2026-07-03 00:56:13 +08:00
parent 82a64787c3
commit bc96296670
13 changed files with 611 additions and 378 deletions

View File

@@ -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<string | null>(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<DirEntry[]>([]);
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<string | null>("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<DirEntry[]>("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<string>("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<string>("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) => {
</script>
<template>
<div class="min-h-screen bg-gray-950 text-gray-100 flex flex-col">
<div class="h-screen bg-[#faf9f6] text-[#38342e] flex flex-col">
<!-- Three-column body -->
<div class="flex-1 flex overflow-hidden">
<div class="flex-1 flex overflow-hidden bg-[#faf9f6]">
<!-- Left: Directory sidebar -->
<DirectorySidebar
:folder-path="folderPath"
:entries="dirEntries"
:content="content"
:current-file-path="currentFilePath"
@heading-click="onHeadingClick"
@open-folder="onOpenFolder"
@toggle-folder="onToggleFolder"
@open-file="onOpenFile"
/>
<!-- 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">
<main class="flex-1 flex flex-col overflow-y-auto p-3">
<MarkdownEditor
v-model="content"
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"
@@ -161,65 +219,28 @@ listen("menu-event", (event) => {
/>
</main>
</div>
<footer>
<div class="flex items-center gap-4">
<!-- Outline toggle -->
<button
class="flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-md transition-colors"
:class="
showOutline
? 'bg-indigo-600/30 text-indigo-300'
: 'text-gray-400 hover:text-white hover:bg-gray-800'
"
title="切换大纲面板"
@click="showOutline = !showOutline"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-3.5 h-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h7" />
</svg>
大纲
</button>
<span class="w-px h-3 bg-gray-600" />
<footer class="border-t border-[#e8e4da] bg-[#f4f1eb]/60 backdrop-blur-sm">
<div class="flex items-center gap-4 px-4 py-1.5">
<!-- Current file / save status -->
<span
v-if="saveStatus"
class="text-xs"
:class="saveStatus.startsWith('保存失败') ? 'text-red-400' : 'text-green-400'"
:class="saveStatus.startsWith('保存失败') ? 'text-red-500' : 'text-emerald-600'"
>{{ saveStatus }}</span>
<span v-else-if="currentFilePath" class="text-xs text-gray-400">
<span v-else-if="currentFilePath" class="text-xs text-[#8c877d]">
{{ currentFilePath.split(/[/\\]/).pop() }}
</span>
<span v-else class="text-xs text-gray-500">未保存</span>
<span v-else class="text-xs text-[#b8b3a8]">未保存</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>
<span class="text-xs text-[#8c877d]">{{ wordCount.toLocaleString() }} </span>
<span class="w-px h-3 bg-[#d4cfc4]" />
<span class="text-xs text-[#8c877d]">{{ charCount.toLocaleString() }} </span>
</div>
</footer>
</div>
</template>
<style>
/* Outline panel slide transition */
.outline-slide-enter-active,
.outline-slide-leave-active {
transition: width 0.25s ease, opacity 0.2s ease;
overflow: hidden;
}
.outline-slide-enter-from,
.outline-slide-leave-to {
width: 0 !important;
opacity: 0;
}
</style>