功能完善

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-092315_b8c3939bfc696802": "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-092315_b8c3939bfc696802": "当我点击目录树切换显示文件的时候,右…",
"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 documentMode = ref<DocumentMode>("edit");
const showMoreMenu = ref(false);
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
function handleExportPdf() {
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) {
// No file path yet — prompt user to pick a save location
try {
@@ -222,7 +230,7 @@ async function doSave() {
}
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)}`;
setTimeout(() => {
saveStatus.value = "";
@@ -234,11 +242,13 @@ async function doSave() {
}
async function doSaveAs() {
const contentToSave = getLatestEditorContent();
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});
await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
setTimeout(() => {
saveStatus.value = "";
@@ -380,6 +390,7 @@ listen("menu-event", (event) => {
</div>
</div>
<MarkdownEditor
ref="markdownEditorRef"
:model-value="content"
:document-mode="documentMode"
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"

View File

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