diff --git a/frontend/src/components/common/CommandPalette.vue b/frontend/src/components/common/CommandPalette.vue index b501f1a..a661469 100644 --- a/frontend/src/components/common/CommandPalette.vue +++ b/frontend/src/components/common/CommandPalette.vue @@ -53,8 +53,8 @@ async function createNote() { const name = rawName.endsWith('.md') ? rawName : `${rawName}.md` const file = await workspaceService.createFile('/', name, `# ${rawName}\n\n`) workspaceStore.addFileToTree('/', file) - workspaceStore.openFile(file.path) await editorStore.loadFile(file.path) + workspaceStore.openFile(file.path) await router.push('/workspace') } diff --git a/frontend/src/features/chat/ChatView.vue b/frontend/src/features/chat/ChatView.vue index 2846e60..b269d5f 100644 --- a/frontend/src/features/chat/ChatView.vue +++ b/frontend/src/features/chat/ChatView.vue @@ -41,8 +41,8 @@ watch(() => chatStore.selectedProviderId, async (providerId) => { function send() { void chatStore.sendMessage(chatStore.inputText) } async function openCitation(citation: Citation) { - workspaceStore.openFile(citation.file_path) await editorStore.loadFile(citation.file_path) + workspaceStore.openFile(citation.file_path) editorStore.highlightBlock(citation.block_id) await router.push('/workspace') } diff --git a/frontend/src/features/search/SearchView.vue b/frontend/src/features/search/SearchView.vue index 88e28f6..d078c0e 100644 --- a/frontend/src/features/search/SearchView.vue +++ b/frontend/src/features/search/SearchView.vue @@ -23,8 +23,8 @@ function submitSearch() { } async function openResult(result: SearchResult) { - workspaceStore.openFile(result.file_path) await editorStore.loadFile(result.file_path) + workspaceStore.openFile(result.file_path) editorStore.highlightBlock(result.block_id) await router.push('/workspace') } diff --git a/frontend/src/features/workspace/FileTreePanel.vue b/frontend/src/features/workspace/FileTreePanel.vue index b15cdd2..d59804b 100644 --- a/frontend/src/features/workspace/FileTreePanel.vue +++ b/frontend/src/features/workspace/FileTreePanel.vue @@ -29,8 +29,8 @@ async function createItem() { const name = rawName.endsWith('.md') ? rawName : `${rawName}.md` const file = await workspaceService.createFile(parentPath.value, name, `# ${rawName}\n\n`) workspaceStore.addFileToTree(parentPath.value, file) - workspaceStore.openFile(file.path) await editorStore.loadFile(file.path) + workspaceStore.openFile(file.path) await router.push('/workspace') } else { const folder = await workspaceService.createFolder(parentPath.value, rawName) @@ -42,8 +42,8 @@ async function createItem() { async function openNode(node: FileNode) { if (node.type === 'folder') return workspaceStore.toggleFolder(node.path) - workspaceStore.openFile(node.path) await editorStore.loadFile(node.path) + workspaceStore.openFile(node.path) await router.push('/workspace') } @@ -80,8 +80,8 @@ async function deleteTarget() { const activeWasRemoved = workspaceStore.closePath(node.path) workspaceStore.removeFromTree(node.path) if (activeWasRemoved) { + editorStore.closeFile() if (workspaceStore.activeFilePath) await editorStore.loadFile(workspaceStore.activeFilePath) - else editorStore.closeFile() } closeContextMenu() } diff --git a/frontend/src/features/workspace/WorkspaceView.vue b/frontend/src/features/workspace/WorkspaceView.vue index 2fc276a..b702bd0 100644 --- a/frontend/src/features/workspace/WorkspaceView.vue +++ b/frontend/src/features/workspace/WorkspaceView.vue @@ -13,8 +13,9 @@ onMounted(() => { // Already loaded } if (!workspaceStore.activeFilePath && workspaceStore.fileTree.length === 0) { - workspaceStore.openFile('/欢迎使用知笔知己.md') - editorStore.loadFile('/欢迎使用知笔知己.md') + void editorStore.loadFile('/欢迎使用知笔知己.md').then(() => { + workspaceStore.openFile('/欢迎使用知笔知己.md') + }) } }) diff --git a/frontend/src/stores/editor.ts b/frontend/src/stores/editor.ts index 199317b..e5f67f3 100644 --- a/frontend/src/stores/editor.ts +++ b/frontend/src/stores/editor.ts @@ -34,34 +34,54 @@ export const useEditorStore = defineStore('editor', () => { } let saveTimer: ReturnType | null = null + let pendingSave: Promise | null = null function scheduleAutoSave(delay = 1500) { if (saveTimer) clearTimeout(saveTimer) saveTimer = setTimeout(() => { + saveTimer = null void save() }, delay) } async function save() { if (!currentFilePath.value) return - if (saveStatus.value === 'saving') return + if (pendingSave) return pendingSave const targetPath = currentFilePath.value const snapshot = content.value saveStatus.value = 'saving' - try { - await workspaceService.saveFileContent(targetPath, snapshot) - if (currentFilePath.value === targetPath) { - saveStatus.value = content.value === snapshot ? 'saved' : 'dirty' - lastSavedAt.value = new Date().toISOString() + pendingSave = (async () => { + try { + await workspaceService.saveFileContent(targetPath, snapshot) + if (currentFilePath.value === targetPath) { + saveStatus.value = content.value === snapshot ? 'saved' : 'dirty' + lastSavedAt.value = new Date().toISOString() + } + } catch { + if (currentFilePath.value === targetPath) saveStatus.value = 'save_failed' + } finally { + pendingSave = null } - } catch { - saveStatus.value = 'save_failed' - } + })() + return pendingSave } let loadVersion = 0 async function loadFile(filePath: string) { + if (currentFilePath.value === filePath) return + if (saveTimer) { + clearTimeout(saveTimer) + saveTimer = null + } + if (saveStatus.value === 'conflict') { + throw new Error('当前文件存在编辑冲突,请处理后再切换文件。') + } + if (pendingSave) await pendingSave + if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') await save() + if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') { + throw new Error('当前文件保存失败,已阻止切换以避免内容丢失。') + } const version = ++loadVersion currentFilePath.value = filePath saveStatus.value = 'saving' diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts index 8fcf86f..5b591f6 100644 --- a/frontend/src/stores/theme.ts +++ b/frontend/src/stores/theme.ts @@ -14,6 +14,7 @@ export const useThemeStore = defineStore('theme', () => { const fontEditorSize = ref(15) const fontEditorFamily = ref('system-ui') const lineHeight = ref(1.7) + let appearanceHydrated = false const currentTheme = computed(() => themes.value.find((t) => t.theme_id === currentThemeId.value) || themes.value[0] @@ -47,6 +48,8 @@ export const useThemeStore = defineStore('theme', () => { } catch { localStorage.removeItem('editor-appearance') } } const saved = localStorage.getItem('theme') + appearanceHydrated = true + persistAppearance() if (saved && themes.value.find((t) => t.theme_id === saved)) { applyTheme(saved) return @@ -72,17 +75,17 @@ export const useThemeStore = defineStore('theme', () => { watch(fontEditorSize, (v) => { document.documentElement.style.setProperty('--font-editor-size', `${v}px`) - persistAppearance() + if (appearanceHydrated) persistAppearance() }, { immediate: true }) watch(lineHeight, (v) => { document.documentElement.style.setProperty('--font-editor-line-height', String(v)) - persistAppearance() + if (appearanceHydrated) persistAppearance() }, { immediate: true }) watch(fontEditorFamily, (v) => { document.documentElement.style.setProperty('--font-editor-sans', v) - persistAppearance() + if (appearanceHydrated) persistAppearance() }, { immediate: true }) return {