feat(frontend): 搭建桌面端基础界面与 Workspace

- App Shell 壳层:主导航、次导航、Title Bar、状态栏
- Vault 入口页:最近 Vault、打开/创建 Vault、AI Core 状态
- Workspace 与文件树:浏览、打开、创建笔记/文件夹
- Design Token:浅色/深色主题 CSS Variables
- Contracts / Service / Store / Router 基础架构
- 统一 ApiClient 与 SseClient,对接后端 API 契约
This commit is contained in:
2026-08-29 10:36:16 +08:00
parent 694c1b27c1
commit f9efc4f4a2
42 changed files with 8044 additions and 167 deletions
+132
View File
@@ -0,0 +1,132 @@
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 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<{ path: string; name: string }[]>([])
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()
}
async function openVault(path: string) {
isLoading.value = true
try {
const info = await workspaceService.openVault(path)
vaultPath.value = info.path
vaultName.value = info.name
fileTree.value = await workspaceService.getFileTree()
hasVault.value = true
} finally {
isLoading.value = false
}
}
async function createVault(path: string, name: string) {
isLoading.value = true
try {
const info = await workspaceService.createVault(path, name)
vaultPath.value = info.path
vaultName.value = info.name
fileTree.value = await workspaceService.getFileTree()
hasVault.value = true
} finally {
isLoading.value = false
}
}
function addFileToTree(parentPath: string, file: FileNode) {
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)
}
return {
vaultPath,
vaultName,
fileTree,
openFiles,
activeFilePath,
activeFile,
isLoading,
hasVault,
recentVaults,
toggleFolder,
openFile,
closeFile,
setActiveFile,
loadRecentVaults,
openVault,
createVault,
addFileToTree,
removeFromTree,
}
})