Files
NotesAgentic/frontend/src/stores/workspace.ts
T

206 lines
6.6 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { FileNode } from '@/contracts'
import * as workspaceService from '@/services/workspaceService'
export const useWorkspaceStore = defineStore('workspace', () => {
const vaultPath = ref('')
const vaultId = ref('')
const vaultName = ref('')
const fileTree = ref<FileNode[]>([])
const openFiles = ref<string[]>([])
const activeFilePath = ref<string | null>(null)
const isLoading = ref(false)
const hasVault = ref(false)
const recentVaults = ref<workspaceService.VaultInfo[]>([])
const treeRefreshError = ref<string | null>(null)
let refreshSequence = 0
const activeFile = computed(() => {
if (!activeFilePath.value) return null
return findNodeByPath(fileTree.value, activeFilePath.value)
})
function findNodeByPath(nodes: FileNode[], path: string): FileNode | null {
for (const node of nodes) {
if (node.path === path) return node
if (node.children) {
const found = findNodeByPath(node.children, path)
if (found) return found
}
}
return null
}
function toggleFolder(path: string) {
const node = findNodeByPath(fileTree.value, path)
if (node && node.type === 'folder') {
node.is_open = !node.is_open
}
}
function openFile(path: string) {
if (!openFiles.value.includes(path)) {
openFiles.value.push(path)
}
activeFilePath.value = path
}
function closeFile(path: string) {
const idx = openFiles.value.indexOf(path)
if (idx > -1) {
openFiles.value.splice(idx, 1)
if (activeFilePath.value === path) {
activeFilePath.value = openFiles.value[Math.min(idx, openFiles.value.length - 1)] || null
}
}
}
function setActiveFile(path: string | null) {
activeFilePath.value = path
}
async function loadRecentVaults() {
recentVaults.value = await workspaceService.getRecentVaults()
}
/** 重新拉取文件树。插件命令返回 refresh:workspace 时需要。 */
async function refreshFileTree() {
if (!hasVault.value) return
const sequence = ++refreshSequence
const path = vaultPath.value
try {
const fresh = await workspaceService.refreshTree()
if (sequence !== refreshSequence || path !== vaultPath.value || !hasVault.value) return
const open = new Map<string, boolean>()
const collect = (nodes: FileNode[]) => nodes.forEach(node => { if (node.type === 'folder') open.set(node.path, !!node.is_open); if (node.children) collect(node.children) })
collect(fileTree.value)
const restore = (nodes: FileNode[]) => nodes.forEach(node => { if (node.type === 'folder') node.is_open = open.get(node.path) ?? false; if (node.children) restore(node.children) })
restore(fresh)
// Avoid redrawing an unchanged tree on every background check.
if (JSON.stringify(fresh) !== JSON.stringify(fileTree.value)) fileTree.value = fresh
treeRefreshError.value = null
} catch (error) {
if (sequence === refreshSequence) treeRefreshError.value = error instanceof Error ? error.message : '文件树刷新失败'
throw error
}
}
async function openVault(path: string) {
refreshSequence++
isLoading.value = true
try {
const info = await workspaceService.openVault(path)
const tree = await workspaceService.getFileTree()
vaultPath.value = info.path
vaultId.value = info.vault_id
vaultName.value = info.name
fileTree.value = tree
openFiles.value = []
activeFilePath.value = null
hasVault.value = true
localStorage.setItem('last-vault-path', info.path)
} finally {
isLoading.value = false
}
}
async function createVault(path: string, name: string) {
refreshSequence++
isLoading.value = true
try {
const info = await workspaceService.createVault(path, name)
const tree = await workspaceService.getFileTree()
vaultPath.value = info.path
vaultId.value = info.vault_id
vaultName.value = info.name
fileTree.value = tree
openFiles.value = []
activeFilePath.value = null
hasVault.value = true
localStorage.setItem('last-vault-path', info.path)
} finally {
isLoading.value = false
}
}
function addFileToTree(parentPath: string, file: FileNode) {
if (parentPath === '/' || parentPath === '') {
fileTree.value.push(file)
return
}
const parent = findNodeByPath(fileTree.value, parentPath)
if (parent?.children) {
parent.children.push(file)
parent.is_open = true
}
}
function removeFromTree(path: string) {
function remove(nodes: FileNode[]): boolean {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].path === path) {
nodes.splice(i, 1)
return true
}
if (nodes[i].children && remove(nodes[i].children!)) return true
}
return false
}
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,
vaultId,
vaultName,
fileTree,
openFiles,
activeFilePath,
activeFile,
isLoading,
hasVault,
recentVaults,
treeRefreshError,
toggleFolder,
openFile,
closeFile,
setActiveFile,
loadRecentVaults,
refreshFileTree,
openVault,
createVault,
addFileToTree,
removeFromTree,
renamePath,
closePath,
}
})