功能完善

This commit is contained in:
2026-07-08 11:08:33 +08:00
parent fc2d5ba6a2
commit 78352dd193
4 changed files with 54 additions and 125 deletions

View File

@@ -4,6 +4,7 @@ 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);
@@ -76,7 +77,7 @@ function onEditorWheel(e: WheelEvent) {
}
// ---- toolbar state ----
const showThumbnail = ref(true);
const showOutline = ref(false);
const documentMode = ref<DocumentMode>("edit");
const showMoreMenu = ref(false);
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
@@ -295,9 +296,7 @@ listen("menu-event", (event) => {
<DirectorySidebar
:folder-path="folderPath"
:entries="dirEntries"
:content="content"
:current-file-path="currentFilePath"
@heading-click="onHeadingClick"
@open-folder="onOpenFolder"
@toggle-folder="onToggleFolder"
@open-file="onOpenFile"
@@ -309,19 +308,14 @@ listen("menu-event", (event) => {
<div
class="flex w-full items-center h-10 px-3 bg-[#faf9f6] border-b border-[#e0dbcf] select-none shrink-0"
>
<!-- Left: Thumbnail toggle -->
<!-- Left: Outline toggle -->
<button
class="toolbar-btn"
:class="{ 'toolbar-btn-active': showThumbnail }"
title="切换缩略图"
@click="showThumbnail = !showThumbnail"
:class="{ 'toolbar-btn-active': showOutline }"
title="切换大纲"
@click="showOutline = !showOutline"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="1" y="1" width="6" height="6" rx="1"/>
<rect x="9" y="1" width="6" height="6" rx="1"/>
<rect x="1" y="9" width="6" height="6" rx="1"/>
<rect x="9" y="9" width="6" height="6" rx="1"/>
</svg>
<i class="ri-menu-line text-base"></i>
</button>
<!-- Center: Filename -->
@@ -389,6 +383,15 @@ listen("menu-event", (event) => {
</div>
</div>
</div>
<div class="flex-1 flex min-h-0 overflow-hidden">
<Transition name="outline-slide">
<OutlinePanel
v-if="showOutline"
:markdown-content="content"
@heading-click="onHeadingClick"
/>
</Transition>
<MarkdownEditor
ref="markdownEditorRef"
:model-value="content"
@@ -396,8 +399,9 @@ listen("menu-event", (event) => {
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"
@update:model-value="onUpdate"
@save-shortcut="doSave"
class="flex-1 w-full"
class="flex-1 w-full min-w-0"
/>
</div>
<!-- Zoom indicator -->
<Transition name="zoom-fade">
@@ -445,6 +449,19 @@ listen("menu-event", (event) => {
opacity: 0;
}
.outline-slide-enter-active,
.outline-slide-leave-active {
transition:
width 0.18s ease,
opacity 0.18s ease;
}
.outline-slide-enter-from,
.outline-slide-leave-to {
width: 0;
opacity: 0;
}
/* ── Toolbar ───────────────────────────────────────────────────── */
.toolbar-btn {

View File

@@ -13,17 +13,14 @@ const props = withDefaults(
defineProps<{
folderPath: string;
entries: DirEntry[];
content?: string;
currentFilePath?: string | null;
}>(),
{
content: "",
currentFilePath: null,
},
);
const emit = defineEmits<{
headingClick: [heading: { text: string; level: number }];
openFolder: [];
toggleFolder: [path: string];
openFile: [path: string];
@@ -44,7 +41,7 @@ const rootEntry = computed<DirEntry | null>(() => {
});
// ---- sidebar view mode ----
type ViewMode = "folder" | "search" | "outline" | "setting";
type ViewMode = "folder" | "search" | "setting";
const activeView = ref<ViewMode>("folder");
const expanded = ref(true);
@@ -57,33 +54,6 @@ function switchView(view: ViewMode) {
}
}
// ---- outline state ----
interface OutlineItem {
text: string;
level: number;
id: string;
}
const headings = computed<OutlineItem[]>(() => {
if (!props.content) return [];
const result: OutlineItem[] = [];
const regex = /^(#{1,3})\s+(.+)$/gm;
let match: RegExpExecArray | null;
while ((match = regex.exec(props.content)) !== null) {
const level = match[1].length;
// Strip any remaining markdown formatting from the heading text
const text = match[2].replace(/\*{1,3}|_{1,3}|`+|~~|\[([^\]]*)\]\([^)]*\)/g, (_, linkText) => linkText || "").trim();
if (text) {
result.push({text, level, id: `heading-${result.length}`});
}
}
return result;
});
function onHeadingClick(item: OutlineItem) {
emit("headingClick", {text: item.text, level: item.level});
}
// ---- settings modal ----
const showSettings = ref(false);
</script>
@@ -112,15 +82,6 @@ const showSettings = ref(false);
>
<i class="ri-search-line text-base"></i>
</button>
<!-- Outline -->
<button
class="p-2 rounded-md transition-colors"
:class="activeView === 'outline' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
title="大纲"
@click="switchView('outline')"
>
<i class="ri-menu-line text-base"></i>
</button>
<!-- ===== Settings ===== -->
<button
class="p-2 rounded-md transition-colors mt-auto"
@@ -183,48 +144,6 @@ const showSettings = ref(false);
</p>
</div>
</div>
<!-- ===== OUTLINE VIEW ===== -->
<div v-if="activeView === 'outline'" class="flex-1 overflow-y-auto px-2 py-2">
<template v-if="!currentFilePath">
<p class="text-xs text-[#b8b3a8] px-2 py-4 text-center leading-relaxed">
暂无打开的文件
</p>
</template>
<template v-else-if="headings.length === 0">
<p class="text-xs text-[#b8b3a8] px-2 py-4 text-center leading-relaxed">
暂无标题<br/>使用 H1-H3 标题<br/>自动生成大纲
</p>
</template>
<template v-else>
<div
v-for="item in headings"
:key="item.id"
:class="[
'px-2 py-1 rounded-md text-sm cursor-pointer hover:bg-[#ede8de] transition-colors truncate',
item.level === 1
? 'text-[#38342e] font-medium ml-0'
: item.level === 2
? 'text-[#8c877d] ml-3'
: 'text-[#b8b3a8] ml-6 text-xs',
]"
:title="item.text"
@click="onHeadingClick(item)"
>
<span
:class="[
'inline-block w-1.5 h-1.5 rounded-full mr-1.5 align-middle',
item.level === 1
? 'bg-[#bf6a3b]'
: item.level === 2
? 'bg-[#7ba587]/60'
: 'bg-gray-500',
]"
/>
{{ item.text }}
</div>
</template>
</div>
</div>
</aside>

View File

@@ -481,12 +481,12 @@ defineExpose({
<template>
<div
ref="scrollRef"
class="flex-1 w-full overflow-auto"
class="flex-1 w-full h-full overflow-auto"
@wheel.passive
@keydown.capture="onRootKeydown"
>
<div
class="mx-auto px-8 pb-40 pt-8 h-full"
class="mx-auto px-8 py-8 h-full"
:style="{
maxWidth: `calc(700px * var(--editor-zoom, 1))`,
fontSize: `calc(1rem * var(--editor-zoom, 1))`,

View File

@@ -2,7 +2,7 @@
import { computed } from "vue";
const props = defineProps<{
htmlContent: string;
markdownContent: string;
}>();
const emit = defineEmits<{
@@ -16,14 +16,18 @@ interface OutlineItem {
}
const headings = computed<OutlineItem[]>(() => {
if (!props.htmlContent) return [];
if (!props.markdownContent) return [];
const result: OutlineItem[] = [];
// Match h1, h2, h3 tags — case-insensitive, capture inner text
const regex = /<h([1-3])[^>]*>(.*?)<\/h\1>/gi;
const regex = /^(#{1,3})\s+(.+)$/gm;
let match: RegExpExecArray | null;
while ((match = regex.exec(props.htmlContent)) !== null) {
const level = parseInt(match[1]);
const text = match[2].replace(/<[^>]*>/g, "").trim();
while ((match = regex.exec(props.markdownContent)) !== null) {
const level = match[1].length;
const text = match[2]
.replace(
/\*{1,3}|_{1,3}|`+|~~|\[([^\]]*)\]\([^)]*\)/g,
(_, linkText) => linkText || "",
)
.trim();
if (text) {
result.push({
text,
@@ -42,7 +46,7 @@ const onHeadingClick = (item: OutlineItem) => {
<template>
<aside
class="flex flex-col border-r border-[#e8e4da] bg-[#f4f1eb]/80 w-48 shrink-0 overflow-hidden"
class="flex flex-col border-r border-[#e8e4da] bg-[#f4f1eb]/80 w-52 shrink-0 overflow-hidden"
>
<!-- Header -->
<div class="flex items-center px-3 py-3 border-b border-[#e8e4da]">
@@ -73,17 +77,6 @@ const onHeadingClick = (item: OutlineItem) => {
:title="item.text"
@click="onHeadingClick(item)"
>
<!-- level indicator dot -->
<span
:class="[
'inline-block w-1.5 h-1.5 rounded-full mr-1.5 align-middle',
item.level === 1
? 'bg-[#bf6a3b]'
: item.level === 2
? 'bg-[#7ba587]/60'
: 'bg-gray-500',
]"
/>
{{ item.text }}
</div>
</template>