功能完善
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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": "新的会话"
|
||||
}
|
||||
17
src/App.vue
17
src/App.vue
@@ -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 内容... 支持标题、粗体、斜体、代码块等 ✨"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user