fix(frontend): 修复文件树与编辑器状态一致性
This commit is contained in:
@@ -61,8 +61,13 @@ async function renameTarget() {
|
|||||||
if (!node) return
|
if (!node) return
|
||||||
const newName = window.prompt('新名称', node.name)?.trim()
|
const newName = window.prompt('新名称', node.name)?.trim()
|
||||||
if (newName && newName !== node.name) {
|
if (newName && newName !== node.name) {
|
||||||
await workspaceService.renameFile(node.path, newName)
|
const normalizedName = node.type === 'file' && !newName.toLowerCase().endsWith('.md') ? `${newName}.md` : newName
|
||||||
node.name = 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()
|
closeContextMenu()
|
||||||
}
|
}
|
||||||
@@ -72,8 +77,12 @@ async function deleteTarget() {
|
|||||||
if (!node) return
|
if (!node) return
|
||||||
if (!window.confirm(`确定要删除“${node.name}”吗?`)) return closeContextMenu()
|
if (!window.confirm(`确定要删除“${node.name}”吗?`)) return closeContextMenu()
|
||||||
await workspaceService.deleteFile(node.path)
|
await workspaceService.deleteFile(node.path)
|
||||||
|
const activeWasRemoved = workspaceStore.closePath(node.path)
|
||||||
workspaceStore.removeFromTree(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()
|
closeContextMenu()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ const MOCK_FILE_TREE: FileNode[] = [
|
|||||||
{ id: 'n-welcome', name: '欢迎使用知笔知己.md', path: '/欢迎使用知笔知己.md', type: 'file' },
|
{ id: 'n-welcome', name: '欢迎使用知笔知己.md', path: '/欢迎使用知笔知己.md', type: 'file' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const mockFileContents = new Map<string, string>()
|
||||||
|
|
||||||
|
function rememberContent(path: string, content: string): Promise<string> {
|
||||||
|
mockFileContents.set(path, content)
|
||||||
|
return Promise.resolve(content)
|
||||||
|
}
|
||||||
|
|
||||||
export function getRecentVaults(): Promise<VaultInfo[]> {
|
export function getRecentVaults(): Promise<VaultInfo[]> {
|
||||||
return Promise.resolve(MOCK_VAULTS)
|
return Promise.resolve(MOCK_VAULTS)
|
||||||
}
|
}
|
||||||
@@ -79,9 +86,11 @@ export function getFileTree(): Promise<FileNode[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function readFileContent(filePath: string): Promise<string> {
|
export function readFileContent(filePath: string): Promise<string> {
|
||||||
|
const saved = mockFileContents.get(filePath)
|
||||||
|
if (saved !== undefined) return Promise.resolve(saved)
|
||||||
const name = filePath.split('/').pop() || 'Untitled'
|
const name = filePath.split('/').pop() || 'Untitled'
|
||||||
if (name === '欢迎使用知笔知己.md') {
|
if (name === '欢迎使用知笔知己.md') {
|
||||||
return Promise.resolve(`# 欢迎使用知笔知己
|
return rememberContent(filePath, `# 欢迎使用知笔知己
|
||||||
|
|
||||||
这是一款本地优先的 AI 笔记软件,支持 Markdown 编辑、智能检索、RAG 问答和 Agent 助手。
|
这是一款本地优先的 AI 笔记软件,支持 Markdown 编辑、智能检索、RAG 问答和 Agent 助手。
|
||||||
|
|
||||||
@@ -137,7 +146,7 @@ def quick_sort(arr):
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
if (name === '红黑树.md') {
|
if (name === '红黑树.md') {
|
||||||
return Promise.resolve(`# 红黑树
|
return rememberContent(filePath, `# 红黑树
|
||||||
|
|
||||||
红黑树(Red-Black Tree)是一种自平衡二叉搜索树,每个节点带有颜色属性(红色或黑色)。
|
红黑树(Red-Black Tree)是一种自平衡二叉搜索树,每个节点带有颜色属性(红色或黑色)。
|
||||||
|
|
||||||
@@ -183,7 +192,7 @@ def quick_sort(arr):
|
|||||||
- Linux 内核的完全公平调度器
|
- 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<void> {
|
export function saveFileContent(filePath: string, content: string): Promise<void> {
|
||||||
console.debug(`[workspaceService] Save ${filePath}, ${content.length} chars`)
|
console.debug(`[workspaceService] Save ${filePath}, ${content.length} chars`)
|
||||||
|
mockFileContents.set(filePath, content)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFile(folderPath: string, name: string, content = ''): Promise<FileNode> {
|
export function createFile(folderPath: string, name: string, content = ''): Promise<FileNode> {
|
||||||
const path = `${folderPath === '/' ? '' : folderPath}/${name}`
|
const path = `${folderPath === '/' ? '' : folderPath}/${name}`
|
||||||
const id = `n-${Date.now()}`
|
const id = `n-${Date.now()}`
|
||||||
|
mockFileContents.set(path, content)
|
||||||
return Promise.resolve({ id, name, path, type: 'file' })
|
return Promise.resolve({ id, name, path, type: 'file' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +232,21 @@ export function createFolder(parentPath: string, name: string): Promise<FileNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renameFile(oldPath: string, newName: string): Promise<void> {
|
export function renameFile(oldPath: string, newName: string): Promise<void> {
|
||||||
|
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()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteFile(path: string): Promise<void> {
|
export function deleteFile(path: string): Promise<void> {
|
||||||
|
for (const filePath of [...mockFileContents.keys()]) {
|
||||||
|
if (filePath === path || filePath.startsWith(`${path}/`)) mockFileContents.delete(filePath)
|
||||||
|
}
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
function updateContent(newContent: string) {
|
function updateContent(newContent: string) {
|
||||||
content.value = newContent
|
content.value = newContent
|
||||||
if (saveStatus.value === 'saved' || saveStatus.value === 'idle') {
|
saveStatus.value = 'dirty'
|
||||||
saveStatus.value = 'dirty'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -47,24 +45,34 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
async function save() {
|
async function save() {
|
||||||
if (!currentFilePath.value) return
|
if (!currentFilePath.value) return
|
||||||
if (saveStatus.value === 'saving') return
|
if (saveStatus.value === 'saving') return
|
||||||
|
const targetPath = currentFilePath.value
|
||||||
|
const snapshot = content.value
|
||||||
saveStatus.value = 'saving'
|
saveStatus.value = 'saving'
|
||||||
try {
|
try {
|
||||||
await workspaceService.saveFileContent(currentFilePath.value, content.value)
|
await workspaceService.saveFileContent(targetPath, snapshot)
|
||||||
saveStatus.value = 'saved'
|
if (currentFilePath.value === targetPath) {
|
||||||
lastSavedAt.value = new Date().toISOString()
|
saveStatus.value = content.value === snapshot ? 'saved' : 'dirty'
|
||||||
|
lastSavedAt.value = new Date().toISOString()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
saveStatus.value = 'save_failed'
|
saveStatus.value = 'save_failed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let loadVersion = 0
|
||||||
|
|
||||||
async function loadFile(filePath: string) {
|
async function loadFile(filePath: string) {
|
||||||
|
const version = ++loadVersion
|
||||||
currentFilePath.value = filePath
|
currentFilePath.value = filePath
|
||||||
saveStatus.value = 'saving'
|
saveStatus.value = 'saving'
|
||||||
try {
|
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'
|
saveStatus.value = 'saved'
|
||||||
lastSavedAt.value = new Date().toISOString()
|
lastSavedAt.value = new Date().toISOString()
|
||||||
} catch {
|
} catch {
|
||||||
|
if (version !== loadVersion || currentFilePath.value !== filePath) return
|
||||||
content.value = ''
|
content.value = ''
|
||||||
saveStatus.value = 'idle'
|
saveStatus.value = 'idle'
|
||||||
}
|
}
|
||||||
@@ -89,6 +97,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeFile() {
|
function closeFile() {
|
||||||
|
loadVersion++
|
||||||
if (saveTimer) clearTimeout(saveTimer)
|
if (saveTimer) clearTimeout(saveTimer)
|
||||||
currentFilePath.value = null
|
currentFilePath.value = null
|
||||||
currentNoteId.value = null
|
currentNoteId.value = null
|
||||||
@@ -98,6 +107,12 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
highlightBlockId.value = null
|
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 {
|
return {
|
||||||
mode,
|
mode,
|
||||||
content,
|
content,
|
||||||
@@ -118,5 +133,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
highlightBlock,
|
highlightBlock,
|
||||||
setExternalChanged,
|
setExternalChanged,
|
||||||
closeFile,
|
closeFile,
|
||||||
|
renameFilePath,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
vaultName.value = info.name
|
vaultName.value = info.name
|
||||||
fileTree.value = await workspaceService.getFileTree()
|
fileTree.value = await workspaceService.getFileTree()
|
||||||
hasVault.value = true
|
hasVault.value = true
|
||||||
|
localStorage.setItem('last-vault-path', info.path)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
vaultName.value = info.name
|
vaultName.value = info.name
|
||||||
fileTree.value = await workspaceService.getFileTree()
|
fileTree.value = await workspaceService.getFileTree()
|
||||||
hasVault.value = true
|
hasVault.value = true
|
||||||
|
localStorage.setItem('last-vault-path', info.path)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -113,6 +115,32 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
remove(fileTree.value)
|
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 {
|
return {
|
||||||
vaultPath,
|
vaultPath,
|
||||||
vaultName,
|
vaultName,
|
||||||
@@ -132,5 +160,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
createVault,
|
createVault,
|
||||||
addFileToTree,
|
addFileToTree,
|
||||||
removeFromTree,
|
removeFromTree,
|
||||||
|
renamePath,
|
||||||
|
closePath,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user