From f725261370512b4da2228f7499a7f90e25b6c0b8 Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Sun, 19 Jul 2026 23:50:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A2=84=E8=A7=88=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- src/App.vue | 25 ++++++++++ src/components/DirectorySidebar.vue | 5 ++ src/components/MarkdownCodeEditor.vue | 22 +++++++- src/components/MarkdownEditor.vue | 72 +++++++++++++++++++++++---- src/components/SettingsModal.vue | 17 +++++++ 6 files changed, 132 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6ccdc12..ee72727 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This template should help get you started developing with Vue 3 and TypeScript i ## Recommended IDE Setup - [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - - $env:HTTPS_PROXY="http://127.0.0.1:10808" + +hello diff --git a/src/App.vue b/src/App.vue index 5e31ffd..b6934d8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,6 +18,7 @@ type SyncProvider = "none" | "webdav" | "s3"; const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode"; const APP_THEME_KEY = "yurou:app-theme"; const RENDER_SINGLE_LINE_BREAKS_KEY = "yurou:render-single-line-breaks"; +const PARAGRAPH_INDENT_KEY = "yurou:paragraph-indent"; const MARKDOWN_EXTENSIONS = new Set(["md"]); const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif"]); const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "ogv"]); @@ -124,6 +125,21 @@ function saveRenderSingleLineBreaks(value: boolean) { } } +function loadParagraphIndent() { + try { + return localStorage.getItem(PARAGRAPH_INDENT_KEY) === "true"; + } catch { + return false; + } +} + +function saveParagraphIndent(value: boolean) { + try { + localStorage.setItem(PARAGRAPH_INDENT_KEY, String(value)); + } catch { /* noop */ + } +} + interface SyncConfig { provider: SyncProvider; webdav: { @@ -230,6 +246,7 @@ const defaultDocumentMode = ref(loadDefaultDocumentMode()); const documentMode = ref(defaultDocumentMode.value); const appTheme = ref(loadAppTheme()); const renderSingleLineBreaks = ref(loadRenderSingleLineBreaks()); +const paragraphIndent = ref(loadParagraphIndent()); const showMoreMenu = ref(false); const settingsRequest = ref(0); const settingsInitialPage = ref<"about" | "editor" | "sync">("about"); @@ -667,6 +684,11 @@ function updateRenderSingleLineBreaks(value: boolean) { saveRenderSingleLineBreaks(value); } +function updateParagraphIndent(value: boolean) { + paragraphIndent.value = value; + saveParagraphIndent(value); +} + function resetWorkspaceAndEditorState(options: { stopWatcher?: boolean } = {}) { if (options.stopWatcher !== false) { void stopWorkspaceWatcher(); @@ -1269,6 +1291,7 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) { :default-document-mode="defaultDocumentMode" :app-theme="appTheme" :render-single-line-breaks="renderSingleLineBreaks" + :paragraph-indent="paragraphIndent" :recent-workspaces="recentWorkspaces" @open-folder="onOpenFolder" @switch-workspace="(path: string) => switchWorkspace(path)" @@ -1283,6 +1306,7 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) { @update-default-document-mode="updateDefaultDocumentMode" @update-app-theme="updateAppTheme" @update-render-single-line-breaks="updateRenderSingleLineBreaks" + @update-paragraph-indent="updateParagraphIndent" @prepare-workspace-sync="prepareWorkspaceSync" @workspace-synced="handleWorkspaceSynced" /> @@ -1431,6 +1455,7 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) { :document-mode="documentMode" :file-path="currentFilePath" :render-single-line-breaks="renderSingleLineBreaks" + :paragraph-indent="paragraphIndent" placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等" @update:model-value="onUpdate" @save-shortcut="doSave" diff --git a/src/components/DirectorySidebar.vue b/src/components/DirectorySidebar.vue index b404aba..c05d1b1 100644 --- a/src/components/DirectorySidebar.vue +++ b/src/components/DirectorySidebar.vue @@ -37,6 +37,7 @@ const props = withDefaults( defaultDocumentMode?: DocumentMode; appTheme?: AppTheme; renderSingleLineBreaks?: boolean; + paragraphIndent?: boolean; recentWorkspaces?: WorkspaceEntry[]; }>(), { @@ -46,6 +47,7 @@ const props = withDefaults( defaultDocumentMode: "edit", appTheme: "warm", renderSingleLineBreaks: false, + paragraphIndent: false, recentWorkspaces: () => [], }, ); @@ -63,6 +65,7 @@ const emit = defineEmits<{ updateDefaultDocumentMode: [mode: DocumentMode]; updateAppTheme: [theme: AppTheme]; updateRenderSingleLineBreaks: [value: boolean]; + updateParagraphIndent: [value: boolean]; prepareWorkspaceSync: [done: (saved: boolean) => void]; workspaceSynced: []; "search-result-click": [filePath: string, lineNumber: number]; @@ -882,9 +885,11 @@ function setAllFoldersExpanded(expanded: boolean) { :default-document-mode="defaultDocumentMode" :app-theme="appTheme" :render-single-line-breaks="renderSingleLineBreaks" + :paragraph-indent="paragraphIndent" @update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)" @update-app-theme="(theme: AppTheme) => emit('updateAppTheme', theme)" @update-render-single-line-breaks="(value: boolean) => emit('updateRenderSingleLineBreaks', value)" + @update-paragraph-indent="(value: boolean) => emit('updateParagraphIndent', value)" @prepare-workspace-sync="forwardPrepareWorkspaceSync" @workspace-synced="emit('workspaceSynced')" /> diff --git a/src/components/MarkdownCodeEditor.vue b/src/components/MarkdownCodeEditor.vue index b4d189e..cf25c00 100644 --- a/src/components/MarkdownCodeEditor.vue +++ b/src/components/MarkdownCodeEditor.vue @@ -80,6 +80,7 @@ const emit = defineEmits<{ blur: []; escape: []; paste: [payload: PastePayload]; + "backspace-at-start": []; }>(); const hostRef = ref(); @@ -346,6 +347,17 @@ function createEditor() { }, indentWithTab, ...closeBracketsKeymap, + { + key: "Backspace", + run(view) { + const selection = view.state.selection.main; + if (selection.empty && selection.head === 0) { + emit("backspace-at-start"); + return true; + } + return false; + }, + }, ...defaultKeymap, ...searchKeymap, ...historyKeymap, @@ -449,7 +461,15 @@ function scrollToLine(lineNumber: number) { view.focus(); } -defineExpose({ focus, focusAtEnd, replaceRange, scrollToLine }); +function setCursorPosition(pos: number) { + const view = editorView; + if (!view) return; + const safePos = Math.max(0, Math.min(pos, view.state.doc.length)); + view.dispatch({ selection: EditorSelection.cursor(safePos), scrollIntoView: true }); + view.focus(); +} + +defineExpose({ focus, focusAtEnd, replaceRange, scrollToLine, setCursorPosition }); onMounted(createEditor); diff --git a/src/components/MarkdownEditor.vue b/src/components/MarkdownEditor.vue index 4ac91ac..4592c7a 100644 --- a/src/components/MarkdownEditor.vue +++ b/src/components/MarkdownEditor.vue @@ -70,6 +70,7 @@ interface MarkdownCodeEditorApi { focusAtEnd: () => void; replaceRange: (text: string, from?: number, to?: number) => void; scrollToLine: (lineNumber: number) => void; + setCursorPosition: (pos: number) => void; } const props = withDefaults( @@ -80,6 +81,7 @@ const props = withDefaults( previewOnly?: boolean; filePath?: string | null; renderSingleLineBreaks?: boolean; + paragraphIndent?: boolean; }>(), { modelValue: "", @@ -88,6 +90,7 @@ const props = withDefaults( previewOnly: false, filePath: null, renderSingleLineBreaks: false, + paragraphIndent: false, }, ); @@ -271,6 +274,9 @@ const pendingLocalModelUpdate = ref(false); const isEditingEmptyDocument = computed( () => editingBlockId.value === EMPTY_BLOCK_ID, ); +const previewParagraphStyle = computed(() => ({ + "--paragraph-indent": props.paragraphIndent ? "2em" : "0", +})); let modeScrollRestoreFrame: number | null = null; let renderRequestId = 0; const previewImageSelector = ".markdown-preview:not(.markdown-mixed) img"; @@ -591,6 +597,56 @@ function flushPendingChanges(): string | undefined { return finalizeBlockEdit(); } +async function mergeCurrentBlockWithPrevious() { + if (activeDocumentMode.value !== "mixed") return; + const blockId = editingBlockId.value; + if (!blockId) return; + + const blockIndex = markdownBlocks.value.findIndex((b) => b.id === blockId); + if (blockIndex <= 0) return; + + const previousBlock = markdownBlocks.value[blockIndex - 1]; + const currentBlock = markdownBlocks.value[blockIndex]; + + // 将当前编辑中的源码与前一个块的源码合并 + const mergedSource = previousBlock.source + editingSource.value; + + // 构建新文档:用合并后的源码替换两个块的范围 + const nextValue = + props.modelValue.slice(0, previousBlock.startOffset) + + mergedSource + + props.modelValue.slice(currentBlock.endOffset); + + // 记录合并点位置(上一个块的原末尾),用于后续光标定位 + const cursorOffset = previousBlock.source.length; + + // 取消当前编辑状态 + cancelBlockEdit(); + + // 更新文档 + emitModelValue(nextValue); + + // 等待重新解析块结构 + await parseBlocks(nextValue); + + // 找到合并后的新块(startOffset 仍为 previousBlock.startOffset) + const newBlock = markdownBlocks.value.find( + (b) => b.startOffset === previousBlock.startOffset, + ); + if (newBlock) { + // 进入编辑模式,光标定位在合并点 + editingBlockId.value = newBlock.id; + editingSource.value = newBlock.source; + editingBaseValue.value = nextValue; + editingRange.value = { + startOffset: newBlock.startOffset, + endOffset: newBlock.endOffset, + }; + await nextTick(); + blockCodeRef.value?.setCursorPosition(cursorOffset); + } +} + watch(activeDocumentMode, (mode, oldMode) => { const scrollSnapshot = captureScrollSnapshot(); let committedValue: string | undefined; @@ -975,11 +1031,11 @@ defineExpose({
⚠ Markdown 解析错误
-
+
-
+
⚠ Markdown 解析错误
@@ -1030,6 +1086,7 @@ defineExpose({ @escape="cancelBlockEdit" @blur="finalizeBlockEdit" @paste="onBlockPaste" + @backspace-at-start="mergeCurrentBlockWithPrevious" />
p + p, .markdown-mixed @@ -1099,7 +1156,7 @@ defineExpose({ + .markdown-block-paragraph > .markdown-block-preview > p { - margin-top: 1.625em; + margin-top: 0; } .markdown-preview strong { font-weight: 700; color: var(--app-text-strong); } @@ -1224,11 +1281,6 @@ defineExpose({ box-shadow 0.15s ease; } -.markdown-block:not(.markdown-block-editing):hover > .markdown-block-preview > * { - background-color: var(--app-surface-muted); - cursor: text; -} - .markdown-block-editing { display: block; } @@ -1236,7 +1288,7 @@ defineExpose({ .markdown-mixed > .markdown-block-paragraph + .markdown-block-paragraph.markdown-block-editing { - margin-top: 1.625em; + margin-top: 0; } .markdown-block-editing, diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index a4d764f..98f63aa 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -78,6 +78,7 @@ const props = withDefaults( defaultDocumentMode?: DocumentMode; appTheme?: AppTheme; renderSingleLineBreaks?: boolean; + paragraphIndent?: boolean; }>(), { initialPage: "about", @@ -85,6 +86,7 @@ const props = withDefaults( defaultDocumentMode: "edit", appTheme: "warm", renderSingleLineBreaks: false, + paragraphIndent: false, }, ); @@ -93,6 +95,7 @@ const emit = defineEmits<{ updateDefaultDocumentMode: [mode: DocumentMode]; updateAppTheme: [theme: AppTheme]; updateRenderSingleLineBreaks: [value: boolean]; + updateParagraphIndent: [value: boolean]; prepareWorkspaceSync: [done: (saved: boolean) => void]; workspaceSynced: []; }>(); @@ -604,6 +607,20 @@ onBeforeUnmount(() => {
+
+
+

段落首行缩进

+

开启后,预览和混合模式下段落首行会缩进两个字符,更符合中文排版习惯。

+
+ +