更新
This commit is contained in:
203
src/App.vue
203
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,60 +47,99 @@ 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>
|
||||
|
||||
<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" />
|
||||
|
||||
<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 />
|
||||
<DirectorySidebar
|
||||
:folder-path="folderPath"
|
||||
:entries="dirEntries"
|
||||
/>
|
||||
|
||||
<!-- Middle: Outline panel -->
|
||||
<transition name="outline-slide">
|
||||
@@ -97,22 +151,61 @@ function onHeadingClick(heading: { text: string; level: number }) {
|
||||
</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">
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer
|
||||
class="text-center text-xs text-gray-600 py-4 border-t border-gray-800/50"
|
||||
>
|
||||
基于 TipTap · 支持 Markdown 快捷输入
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user