功能完善

This commit is contained in:
2026-07-08 10:45:50 +08:00
parent cc13b2abbe
commit fc2d5ba6a2
4 changed files with 106 additions and 31 deletions

View File

@@ -35,5 +35,6 @@
"topic_20260707-091519_dc062b2bfdf4249c": "auto", "topic_20260707-091519_dc062b2bfdf4249c": "auto",
"topic_20260707-092315_b8c3939bfc696802": "auto", "topic_20260707-092315_b8c3939bfc696802": "auto",
"topic_20260707-092711_af706c3fcc7e435e": "auto", "topic_20260707-092711_af706c3fcc7e435e": "auto",
"topic_20260707-095503_bf879b65e4666516": "auto" "topic_20260707-095503_bf879b65e4666516": "auto",
"topic_20260708-020616_9ab54d98daf65053": "auto"
} }

View File

@@ -35,5 +35,6 @@
"topic_20260707-091519_dc062b2bfdf4249c": "关于App.vue里的toolabr…", "topic_20260707-091519_dc062b2bfdf4249c": "关于App.vue里的toolabr…",
"topic_20260707-092315_b8c3939bfc696802": "当我点击目录树切换显示文件的时候,右…", "topic_20260707-092315_b8c3939bfc696802": "当我点击目录树切换显示文件的时候,右…",
"topic_20260707-092711_af706c3fcc7e435e": "帮我检查一下app.vue里的too…", "topic_20260707-092711_af706c3fcc7e435e": "帮我检查一下app.vue里的too…",
"topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…" "topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…",
"topic_20260708-020616_9ab54d98daf65053": "新的会话"
} }

View File

@@ -79,6 +79,7 @@ function onEditorWheel(e: WheelEvent) {
const showThumbnail = ref(true); const showThumbnail = ref(true);
const documentMode = ref<DocumentMode>("edit"); const documentMode = ref<DocumentMode>("edit");
const showMoreMenu = ref(false); const showMoreMenu = ref(false);
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
function handleExportPdf() { function handleExportPdf() {
showMoreMenu.value = false; showMoreMenu.value = false;
@@ -208,7 +209,14 @@ async function onOpenFile(path: string) {
} }
} }
async function doSave() { function getLatestEditorContent(contentOverride?: string) {
if (typeof contentOverride === "string") return contentOverride;
return markdownEditorRef.value?.flushPendingChanges() ?? content.value;
}
async function doSave(contentOverride?: string) {
const contentToSave = getLatestEditorContent(contentOverride);
if (!currentFilePath.value) { if (!currentFilePath.value) {
// No file path yet — prompt user to pick a save location // No file path yet — prompt user to pick a save location
try { try {
@@ -222,7 +230,7 @@ async function doSave() {
} }
try { try {
await invoke("write_file", {path: currentFilePath.value, content: content.value}); await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`; saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
setTimeout(() => { setTimeout(() => {
saveStatus.value = ""; saveStatus.value = "";
@@ -234,11 +242,13 @@ async function doSave() {
} }
async function doSaveAs() { async function doSaveAs() {
const contentToSave = getLatestEditorContent();
try { try {
const chosen = await invoke<string | null>("pick_save_file"); const chosen = await invoke<string | null>("pick_save_file");
if (!chosen) return; // user cancelled if (!chosen) return; // user cancelled
currentFilePath.value = chosen; currentFilePath.value = chosen;
await invoke("write_file", {path: currentFilePath.value, content: content.value}); await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`; saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
setTimeout(() => { setTimeout(() => {
saveStatus.value = ""; saveStatus.value = "";
@@ -380,6 +390,7 @@ listen("menu-event", (event) => {
</div> </div>
</div> </div>
<MarkdownEditor <MarkdownEditor
ref="markdownEditorRef"
:model-value="content" :model-value="content"
:document-mode="documentMode" :document-mode="documentMode"
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨" placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"

View File

@@ -2,6 +2,7 @@
import { import {
computed, computed,
nextTick, nextTick,
onBeforeUnmount,
ref, ref,
watch, watch,
type ComponentPublicInstance, type ComponentPublicInstance,
@@ -47,7 +48,7 @@ const props = withDefaults(
const emit = defineEmits<{ const emit = defineEmits<{
"update:modelValue": [value: string]; "update:modelValue": [value: string];
"save-shortcut": []; "save-shortcut": [value?: string];
}>(); }>();
// ── Unified processors ───────────────────────────────────────────── // ── Unified processors ─────────────────────────────────────────────
@@ -87,6 +88,18 @@ const pendingLocalModelUpdate = ref(false);
const isEditingEmptyDocument = computed( const isEditingEmptyDocument = computed(
() => editingBlockId.value === EMPTY_BLOCK_ID, () => editingBlockId.value === EMPTY_BLOCK_ID,
); );
let textareaResizeFrame: number | null = null;
function syncRenderedState(md: string, mode = activeDocumentMode.value) {
if (mode === "preview") {
renderFullHtml(md);
return;
}
if (mode === "mixed") {
parseBlocks(md);
}
}
function renderFullHtml(md: string) { function renderFullHtml(md: string) {
try { try {
@@ -120,6 +133,7 @@ function renderBlockHtml(source: string) {
function parseBlocks(md: string) { function parseBlocks(md: string) {
if (!md.trim()) { if (!md.trim()) {
markdownBlocks.value = []; markdownBlocks.value = [];
renderError.value = false;
return; return;
} }
@@ -150,6 +164,7 @@ function parseBlocks(md: string) {
.filter((block): block is MarkdownBlock => block !== null); .filter((block): block is MarkdownBlock => block !== null);
markdownBlocks.value = blocks; markdownBlocks.value = blocks;
renderError.value = false;
} catch { } catch {
markdownBlocks.value = [ markdownBlocks.value = [
{ {
@@ -160,6 +175,7 @@ function parseBlocks(md: string) {
endOffset: md.length, endOffset: md.length,
}, },
]; ];
renderError.value = false;
} }
} }
@@ -167,16 +183,19 @@ watch(
() => props.modelValue, () => props.modelValue,
(val) => { (val) => {
const shouldResetScroll = !pendingLocalModelUpdate.value; const shouldResetScroll = !pendingLocalModelUpdate.value;
const isLocalUpdate = pendingLocalModelUpdate.value;
pendingLocalModelUpdate.value = false; pendingLocalModelUpdate.value = false;
renderFullHtml(val); if (!isLocalUpdate) {
parseBlocks(val); syncRenderedState(val);
}
if (editingBlockId.value && val !== editingBaseValue.value) { if (editingBlockId.value && val !== editingBaseValue.value) {
cancelBlockEdit(); cancelBlockEdit();
} }
nextTick(() => { nextTick(() => {
resizeEditorTextarea(); scheduleEditorTextareaResize();
// 外部切换文件时重置滚动位置;本组件内编辑时保留当前位置。 // 外部切换文件时重置滚动位置;本组件内编辑时保留当前位置。
if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0; if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0;
}); });
@@ -195,10 +214,20 @@ function resizeTextarea(ta: HTMLTextAreaElement) {
ta.style.height = `${ta.scrollHeight}px`; ta.style.height = `${ta.scrollHeight}px`;
} }
function resizeEditorTextarea() { function scheduleTextareaResize(ta: HTMLTextAreaElement | undefined | null) {
const ta = editorTextareaRef.value;
if (!ta) return; if (!ta) return;
resizeTextarea(ta); if (textareaResizeFrame !== null) {
cancelAnimationFrame(textareaResizeFrame);
}
textareaResizeFrame = requestAnimationFrame(() => {
textareaResizeFrame = null;
if (ta.isConnected) resizeTextarea(ta);
});
}
function scheduleEditorTextareaResize() {
scheduleTextareaResize(editorTextareaRef.value);
} }
function focusBlockEditor() { function focusBlockEditor() {
@@ -218,15 +247,13 @@ function emitModelValue(value: string) {
function applyFullEditorValue(value: string) { function applyFullEditorValue(value: string) {
emitModelValue(value); emitModelValue(value);
renderFullHtml(value);
parseBlocks(value);
} }
function onEditorInput(e: Event) { function onEditorInput(e: Event) {
const ta = e.target as HTMLTextAreaElement; const ta = e.target as HTMLTextAreaElement;
const val = ta.value; const val = ta.value;
applyFullEditorValue(val); applyFullEditorValue(val);
resizeTextarea(ta); scheduleTextareaResize(ta);
} }
function tryAutoPairForValue( function tryAutoPairForValue(
@@ -270,10 +297,26 @@ function tryAutoPair(ta: HTMLTextAreaElement, key: string): boolean {
return tryAutoPairForValue(ta, key, props.modelValue, applyFullEditorValue); return tryAutoPairForValue(ta, key, props.modelValue, applyFullEditorValue);
} }
function isSaveShortcut(e: KeyboardEvent) {
return (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s";
}
function requestSave(e?: KeyboardEvent) {
e?.preventDefault();
e?.stopPropagation();
const committedValue = flushPendingChanges();
emit("save-shortcut", committedValue);
}
function onRootKeydown(e: KeyboardEvent) {
if (isSaveShortcut(e)) {
requestSave(e);
}
}
function onEditorKeydown(e: KeyboardEvent) { function onEditorKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key === "s") { if (isSaveShortcut(e)) {
e.preventDefault(); requestSave(e);
emit("save-shortcut");
return; return;
} }
@@ -338,11 +381,11 @@ function cancelBlockEdit() {
editingRange.value = null; editingRange.value = null;
} }
function finalizeBlockEdit() { function finalizeBlockEdit(): string | undefined {
const range = editingRange.value; const range = editingRange.value;
if (!editingBlockId.value || !range) { if (!editingBlockId.value || !range) {
cancelBlockEdit(); cancelBlockEdit();
return; return undefined;
} }
const nextValue = const nextValue =
@@ -354,24 +397,23 @@ function finalizeBlockEdit() {
if (nextValue !== props.modelValue) { if (nextValue !== props.modelValue) {
emitModelValue(nextValue); emitModelValue(nextValue);
renderFullHtml(nextValue); syncRenderedState(nextValue);
parseBlocks(nextValue);
} }
return nextValue;
} }
function onBlockInput(e: Event) { function onBlockInput(e: Event) {
const ta = e.target as HTMLTextAreaElement; const ta = e.target as HTMLTextAreaElement;
editingSource.value = ta.value; editingSource.value = ta.value;
resizeTextarea(ta); scheduleTextareaResize(ta);
} }
function onBlockKeydown(e: KeyboardEvent) { function onBlockKeydown(e: KeyboardEvent) {
const ta = e.target as HTMLTextAreaElement; const ta = e.target as HTMLTextAreaElement;
if ((e.ctrlKey || e.metaKey) && e.key === "s") { if (isSaveShortcut(e)) {
e.preventDefault(); requestSave(e);
finalizeBlockEdit();
emit("save-shortcut");
return; return;
} }
@@ -391,7 +433,7 @@ function onBlockKeydown(e: KeyboardEvent) {
editingSource.value.substring(end); editingSource.value.substring(end);
void nextTick(() => { void nextTick(() => {
ta.selectionStart = ta.selectionEnd = start + 2; ta.selectionStart = ta.selectionEnd = start + 2;
resizeTextarea(ta); scheduleTextareaResize(ta);
ta.focus(); ta.focus();
}); });
return; return;
@@ -400,21 +442,40 @@ function onBlockKeydown(e: KeyboardEvent) {
if ( if (
tryAutoPairForValue(ta, e.key, editingSource.value, (nextValue) => { tryAutoPairForValue(ta, e.key, editingSource.value, (nextValue) => {
editingSource.value = nextValue; editingSource.value = nextValue;
void nextTick(() => resizeTextarea(ta)); void nextTick(() => scheduleTextareaResize(ta));
}) })
) { ) {
e.preventDefault(); e.preventDefault();
} }
} }
function flushPendingChanges(): string | undefined {
if (!editingBlockId.value) return undefined;
return finalizeBlockEdit();
}
watch(activeDocumentMode, (mode, oldMode) => { watch(activeDocumentMode, (mode, oldMode) => {
let committedValue: string | undefined;
if (oldMode === "mixed" && mode !== "mixed" && editingBlockId.value) { if (oldMode === "mixed" && mode !== "mixed" && editingBlockId.value) {
finalizeBlockEdit(); committedValue = finalizeBlockEdit();
} }
syncRenderedState(committedValue ?? props.modelValue, mode);
if (mode === "edit") { if (mode === "edit") {
void nextTick(resizeEditorTextarea); void nextTick(scheduleEditorTextareaResize);
} }
}); });
onBeforeUnmount(() => {
if (textareaResizeFrame !== null) {
cancelAnimationFrame(textareaResizeFrame);
}
});
defineExpose({
flushPendingChanges,
});
</script> </script>
<template> <template>
@@ -422,6 +483,7 @@ watch(activeDocumentMode, (mode, oldMode) => {
ref="scrollRef" ref="scrollRef"
class="flex-1 w-full overflow-auto" class="flex-1 w-full overflow-auto"
@wheel.passive @wheel.passive
@keydown.capture="onRootKeydown"
> >
<div <div
class="mx-auto px-8 pb-40 pt-8 h-full" class="mx-auto px-8 pb-40 pt-8 h-full"