From c0e16f28850e57d5777b124103b3b90a72c50c17 Mon Sep 17 00:00:00 2001 From: thzxx Date: Thu, 4 Jun 2026 15:17:26 +0800 Subject: [PATCH] =?UTF-8?q?v0.3.7:=20=E4=BF=AE=E5=A4=8D=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E7=AB=9E=E4=BA=89=E6=80=81=20+=20=E6=96=B0=E5=A2=9E=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E7=BC=96=E8=BE=91=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug修复: - BUG-10 修复: 保存文件时 watcher restart 触发虚假 change 事件覆盖编辑器内容 - ipc-handlers.ts: restart watcher 在 setSelfWriting(true) 保护内执行 - App.tsx handleReloadModified: 增加 if (tab?.isModified) return 安全守卫 新功能: - 新增源码(Source)视图模式,直接用 textarea 编辑原始 Markdown - ViewMode 扩展为 editor|preview|source - 工具栏增加源码按钮,快捷键 Ctrl+3 - 内容与 WYSIWYG 编辑器共享 tabStore,切换模式不丢内容 验证: TS 0错误, ESLint 0错误(1个预存警告), 90/90测试通过 --- src/main/ipc-handlers.ts | 19 ++---- src/renderer/App.tsx | 12 +++- src/renderer/components/Icons.tsx | 9 +++ .../components/SourceEditor/SourceEditor.tsx | 61 +++++++++++++++++++ src/renderer/components/Toolbar/Toolbar.tsx | 21 +++++-- src/renderer/db/schema.ts | 2 +- src/renderer/hooks/useKeyboard.ts | 1 + src/renderer/styles/global.css | 38 +++++++++++- src/renderer/types/settings.ts | 2 +- 9 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 src/renderer/components/SourceEditor/SourceEditor.tsx diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index 5d1cfa6..a0e3265 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -69,13 +69,11 @@ export function registerIpcHandlers( fileWatcher.stop() fileWatcher.setSelfWriting(true) const result = await saveFileContent(data.filePath, data.content) + // start watcher while selfWriting is still true so any immediate 'change' + // event from the restart is suppressed — prevents overwriting editor content + fileWatcher.start(data.filePath) fileWatcher.setSelfWriting(false) state.activeFilePath = data.filePath - setTimeout(() => { - if (state.activeFilePath === data.filePath && data.filePath) { - fileWatcher.start(data.filePath) - } - }, 300) if (win && !win.isDestroyed()) { win.setTitle(`MarkLite - ${basename(data.filePath)}`) } @@ -88,12 +86,12 @@ export function registerIpcHandlers( if (!saveResult.canceled) { fileWatcher.setSelfWriting(true) const result = await saveFileContent(saveResult.filePath, data.content) - fileWatcher.setSelfWriting(false) if (result.success) { state.activeFilePath = saveResult.filePath fileWatcher.start(saveResult.filePath) win.setTitle(`MarkLite - ${basename(saveResult.filePath)}`) } + fileWatcher.setSelfWriting(false) return result } return { success: false, canceled: true } @@ -116,19 +114,14 @@ export function registerIpcHandlers( filters: [{ name: 'Markdown 文件', extensions: ['md'] }] }) if (!result.canceled) { - fileWatcher.stop() fileWatcher.setSelfWriting(true) const saveResult = await saveFileContent(result.filePath, data.content) - fileWatcher.setSelfWriting(false) if (saveResult.success) { state.activeFilePath = result.filePath - setTimeout(() => { - if (state.activeFilePath === result.filePath) { - fileWatcher.start(result.filePath) - } - }, 300) + fileWatcher.start(result.filePath) win.setTitle(`MarkLite - ${basename(result.filePath)}`) } + fileWatcher.setSelfWriting(false) return saveResult } return { success: false, canceled: true } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 893fee0..bb9ced8 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -16,6 +16,7 @@ import { useConfirm } from './hooks/useConfirm' import { Toolbar } from './components/Toolbar/Toolbar' import { TabBar } from './components/TabBar/TabBar' import { Editor } from './components/Editor/Editor' +import { SourceEditor } from './components/SourceEditor/SourceEditor' import { Preview } from './components/Preview/Preview' import { Sidebar } from './components/Sidebar/Sidebar' import { StatusBar } from './components/StatusBar/StatusBar' @@ -75,10 +76,16 @@ export function App() { const handleReloadModified = useCallback(async () => { if (!externallyModified?.filePath || !window.electronAPI) return + const tab = tabs.find(t => t.filePath === externallyModified.filePath) + // BUG-10: Don't overwrite editor content the user has modified since the last save. + // The watcher restart after save can trigger a false 'change' event, and re-reading + // the file at that point would replace newer editor content with the just-saved content. + if (tab?.isModified) return + if (!tab) return const result = await window.electronAPI.readFile(externallyModified.filePath) if (result.success && result.content !== undefined) { - const tab = tabs.find(t => t.filePath === externallyModified.filePath) - if (tab) { updateTabContent(tab.id, result.content); setModified(tab.id, false) } + updateTabContent(tab.id, result.content) + setModified(tab.id, false) } setExternallyModified(null) }, [externallyModified, tabs, updateTabContent, setModified, setExternallyModified]) @@ -102,6 +109,7 @@ export function App() { {tabs.length > 0 ? (
{viewMode === 'editor' &&
} + {viewMode === 'source' &&
} {viewMode === 'preview' &&
}
) : ( diff --git a/src/renderer/components/Icons.tsx b/src/renderer/components/Icons.tsx index 53e28af..7149de4 100644 --- a/src/renderer/components/Icons.tsx +++ b/src/renderer/components/Icons.tsx @@ -78,6 +78,15 @@ export function PreviewMode({ size = defaultProps.size }: IconProps) { ) } +export function SourceMode({ size = defaultProps.size }: IconProps) { + return ( + + + + + ) +} + export function Moon({ size = defaultProps.size }: IconProps) { return ( diff --git a/src/renderer/components/SourceEditor/SourceEditor.tsx b/src/renderer/components/SourceEditor/SourceEditor.tsx new file mode 100644 index 0000000..80e29e0 --- /dev/null +++ b/src/renderer/components/SourceEditor/SourceEditor.tsx @@ -0,0 +1,61 @@ +import React, { useCallback, useRef } from 'react' +import { useTabStore } from '../../stores/tabStore' + +interface SourceEditorProps { + darkMode: boolean +} + +/** + * 源码编辑模式 — 使用原生 textarea 编辑原始 Markdown 文本。 + * 内容实时同步到 tabStore,与 WYSIWYG 编辑器共享同一数据源。 + */ +export const SourceEditor = React.memo(function SourceEditor({ darkMode }: SourceEditorProps) { + const activeTab = useTabStore(s => s.getActiveTab()) + const activeTabId = useTabStore(s => s.activeTabId) + const updateTabContent = useTabStore(s => s.updateTabContent) + const setModified = useTabStore(s => s.setModified) + const textareaRef = useRef(null) + + const handleChange = useCallback((e: React.ChangeEvent) => { + if (!activeTabId) return + updateTabContent(activeTabId, e.target.value) + setModified(activeTabId, true) + }, [activeTabId, updateTabContent, setModified]) + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + // Tab 插入两个空格而非跳转焦点 + if (e.key === 'Tab') { + e.preventDefault() + const textarea = textareaRef.current + if (!textarea) return + const start = textarea.selectionStart + const end = textarea.selectionEnd + const value = textarea.value + const newValue = value.substring(0, start) + ' ' + value.substring(end) + textarea.value = newValue + textarea.selectionStart = textarea.selectionEnd = start + 2 + if (activeTabId) { + updateTabContent(activeTabId, newValue) + setModified(activeTabId, true) + } + } + }, [activeTabId, updateTabContent, setModified]) + + return ( +
+