From afd49ba00f0f67224deda8ef5c43dfbc8b4e0b1d Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sat, 29 Aug 2026 23:59:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=91=E4=B8=8E=E7=BC=96=E8=BE=91=E5=99=A8=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/workspace/FileTreePanel.vue | 15 ++++++++-- frontend/src/services/workspaceService.ts | 28 +++++++++++++++-- frontend/src/stores/editor.ts | 30 ++++++++++++++----- frontend/src/stores/workspace.ts | 30 +++++++++++++++++++ 4 files changed, 90 insertions(+), 13 deletions(-) diff --git a/frontend/src/features/workspace/FileTreePanel.vue b/frontend/src/features/workspace/FileTreePanel.vue index 1ed6605..b15cdd2 100644 --- a/frontend/src/features/workspace/FileTreePanel.vue +++ b/frontend/src/features/workspace/FileTreePanel.vue @@ -61,8 +61,13 @@ async function renameTarget() { if (!node) return const newName = window.prompt('新名称', node.name)?.trim() if (newName && newName !== node.name) { - await workspaceService.renameFile(node.path, newName) - node.name = newName + const normalizedName = node.type === 'file' && !newName.toLowerCase().endsWith('.md') ? `${newName}.md` : newName + const oldPath = node.path + const separator = oldPath.lastIndexOf('/') + const newPath = `${oldPath.slice(0, separator + 1)}${normalizedName}` + await workspaceService.renameFile(oldPath, normalizedName) + workspaceStore.renamePath(oldPath, newPath, normalizedName) + editorStore.renameFilePath(oldPath, newPath) } closeContextMenu() } @@ -72,8 +77,12 @@ async function deleteTarget() { if (!node) return if (!window.confirm(`确定要删除“${node.name}”吗?`)) return closeContextMenu() await workspaceService.deleteFile(node.path) + const activeWasRemoved = workspaceStore.closePath(node.path) workspaceStore.removeFromTree(node.path) - if (node.type === 'file') workspaceStore.closeFile(node.path) + if (activeWasRemoved) { + if (workspaceStore.activeFilePath) await editorStore.loadFile(workspaceStore.activeFilePath) + else editorStore.closeFile() + } closeContextMenu() } diff --git a/frontend/src/services/workspaceService.ts b/frontend/src/services/workspaceService.ts index bede20c..b734e31 100644 --- a/frontend/src/services/workspaceService.ts +++ b/frontend/src/services/workspaceService.ts @@ -61,6 +61,13 @@ const MOCK_FILE_TREE: FileNode[] = [ { id: 'n-welcome', name: '欢迎使用知笔知己.md', path: '/欢迎使用知笔知己.md', type: 'file' }, ] +const mockFileContents = new Map() + +function rememberContent(path: string, content: string): Promise { + mockFileContents.set(path, content) + return Promise.resolve(content) +} + export function getRecentVaults(): Promise { return Promise.resolve(MOCK_VAULTS) } @@ -79,9 +86,11 @@ export function getFileTree(): Promise { } export function readFileContent(filePath: string): Promise { + const saved = mockFileContents.get(filePath) + if (saved !== undefined) return Promise.resolve(saved) const name = filePath.split('/').pop() || 'Untitled' if (name === '欢迎使用知笔知己.md') { - return Promise.resolve(`# 欢迎使用知笔知己 + return rememberContent(filePath, `# 欢迎使用知笔知己 这是一款本地优先的 AI 笔记软件,支持 Markdown 编辑、智能检索、RAG 问答和 Agent 助手。 @@ -137,7 +146,7 @@ def quick_sort(arr): `) } if (name === '红黑树.md') { - return Promise.resolve(`# 红黑树 + return rememberContent(filePath, `# 红黑树 红黑树(Red-Black Tree)是一种自平衡二叉搜索树,每个节点带有颜色属性(红色或黑色)。 @@ -183,7 +192,7 @@ def quick_sort(arr): - Linux 内核的完全公平调度器 `) } - return Promise.resolve(`# ${name.replace('.md', '')} + return rememberContent(filePath, `# ${name.replace('.md', '')} 这是一篇示例笔记。 @@ -205,12 +214,14 @@ console.log('Hello, Notes Agent!'); export function saveFileContent(filePath: string, content: string): Promise { console.debug(`[workspaceService] Save ${filePath}, ${content.length} chars`) + mockFileContents.set(filePath, content) return Promise.resolve() } export function createFile(folderPath: string, name: string, content = ''): Promise { const path = `${folderPath === '/' ? '' : folderPath}/${name}` const id = `n-${Date.now()}` + mockFileContents.set(path, content) return Promise.resolve({ id, name, path, type: 'file' }) } @@ -221,10 +232,21 @@ export function createFolder(parentPath: string, name: string): Promise { + const separator = oldPath.lastIndexOf('/') + const newPath = `${oldPath.slice(0, separator + 1)}${newName}` + for (const [path, content] of [...mockFileContents]) { + if (path === oldPath || path.startsWith(`${oldPath}/`)) { + mockFileContents.delete(path) + mockFileContents.set(`${newPath}${path.slice(oldPath.length)}`, content) + } + } return Promise.resolve() } export function deleteFile(path: string): Promise { + for (const filePath of [...mockFileContents.keys()]) { + if (filePath === path || filePath.startsWith(`${path}/`)) mockFileContents.delete(filePath) + } return Promise.resolve() } diff --git a/frontend/src/stores/editor.ts b/frontend/src/stores/editor.ts index f8c6b86..199317b 100644 --- a/frontend/src/stores/editor.ts +++ b/frontend/src/stores/editor.ts @@ -30,9 +30,7 @@ export const useEditorStore = defineStore('editor', () => { function updateContent(newContent: string) { content.value = newContent - if (saveStatus.value === 'saved' || saveStatus.value === 'idle') { - saveStatus.value = 'dirty' - } + saveStatus.value = 'dirty' } let saveTimer: ReturnType | null = null @@ -47,24 +45,34 @@ export const useEditorStore = defineStore('editor', () => { async function save() { if (!currentFilePath.value) return if (saveStatus.value === 'saving') return + const targetPath = currentFilePath.value + const snapshot = content.value saveStatus.value = 'saving' try { - await workspaceService.saveFileContent(currentFilePath.value, content.value) - saveStatus.value = 'saved' - lastSavedAt.value = new Date().toISOString() + await workspaceService.saveFileContent(targetPath, snapshot) + if (currentFilePath.value === targetPath) { + saveStatus.value = content.value === snapshot ? 'saved' : 'dirty' + lastSavedAt.value = new Date().toISOString() + } } catch { saveStatus.value = 'save_failed' } } + let loadVersion = 0 + async function loadFile(filePath: string) { + const version = ++loadVersion currentFilePath.value = filePath saveStatus.value = 'saving' try { - content.value = await workspaceService.readFileContent(filePath) + const loadedContent = await workspaceService.readFileContent(filePath) + if (version !== loadVersion || currentFilePath.value !== filePath) return + content.value = loadedContent saveStatus.value = 'saved' lastSavedAt.value = new Date().toISOString() } catch { + if (version !== loadVersion || currentFilePath.value !== filePath) return content.value = '' saveStatus.value = 'idle' } @@ -89,6 +97,7 @@ export const useEditorStore = defineStore('editor', () => { } function closeFile() { + loadVersion++ if (saveTimer) clearTimeout(saveTimer) currentFilePath.value = null currentNoteId.value = null @@ -98,6 +107,12 @@ export const useEditorStore = defineStore('editor', () => { highlightBlockId.value = null } + function renameFilePath(oldPath: string, newPath: string) { + if (currentFilePath.value === oldPath || currentFilePath.value?.startsWith(`${oldPath}/`)) { + currentFilePath.value = `${newPath}${currentFilePath.value.slice(oldPath.length)}` + } + } + return { mode, content, @@ -118,5 +133,6 @@ export const useEditorStore = defineStore('editor', () => { highlightBlock, setExternalChanged, closeFile, + renameFilePath, } }) diff --git a/frontend/src/stores/workspace.ts b/frontend/src/stores/workspace.ts index 50a3812..de6f5d2 100644 --- a/frontend/src/stores/workspace.ts +++ b/frontend/src/stores/workspace.ts @@ -69,6 +69,7 @@ export const useWorkspaceStore = defineStore('workspace', () => { vaultName.value = info.name fileTree.value = await workspaceService.getFileTree() hasVault.value = true + localStorage.setItem('last-vault-path', info.path) } finally { isLoading.value = false } @@ -82,6 +83,7 @@ export const useWorkspaceStore = defineStore('workspace', () => { vaultName.value = info.name fileTree.value = await workspaceService.getFileTree() hasVault.value = true + localStorage.setItem('last-vault-path', info.path) } finally { isLoading.value = false } @@ -113,6 +115,32 @@ export const useWorkspaceStore = defineStore('workspace', () => { remove(fileTree.value) } + function renamePath(oldPath: string, newPath: string, newName: string) { + const node = findNodeByPath(fileTree.value, oldPath) + if (!node) return + const updateNodePath = (current: FileNode) => { + if (current.path === oldPath) current.name = newName + if (current.path === oldPath || current.path.startsWith(`${oldPath}/`)) { + current.path = `${newPath}${current.path.slice(oldPath.length)}` + } + current.children?.forEach(updateNodePath) + } + updateNodePath(node) + openFiles.value = openFiles.value.map((path) => + path === oldPath || path.startsWith(`${oldPath}/`) ? `${newPath}${path.slice(oldPath.length)}` : path + ) + if (activeFilePath.value && (activeFilePath.value === oldPath || activeFilePath.value.startsWith(`${oldPath}/`))) { + activeFilePath.value = `${newPath}${activeFilePath.value.slice(oldPath.length)}` + } + } + + function closePath(path: string) { + const activeWasRemoved = Boolean(activeFilePath.value && (activeFilePath.value === path || activeFilePath.value.startsWith(`${path}/`))) + openFiles.value = openFiles.value.filter((openPath) => openPath !== path && !openPath.startsWith(`${path}/`)) + if (activeWasRemoved) activeFilePath.value = openFiles.value.at(-1) ?? null + return activeWasRemoved + } + return { vaultPath, vaultName, @@ -132,5 +160,7 @@ export const useWorkspaceStore = defineStore('workspace', () => { createVault, addFileToTree, removeFromTree, + renamePath, + closePath, } })