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
+122
View File
@@ -0,0 +1,122 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { SaveStatus } from '@/contracts'
import * as workspaceService from '@/services/workspaceService'
export const useEditorStore = defineStore('editor', () => {
const mode = ref<'wysiwyg' | 'source'>('wysiwyg')
const content = ref('')
const saveStatus = ref<SaveStatus>('idle')
const lastSavedAt = ref<string | null>(null)
const currentNoteId = ref<string | null>(null)
const currentFilePath = ref<string | null>(null)
const highlightBlockId = ref<string | null>(null)
const cursorPosition = ref({ line: 0, column: 0 })
const wordCount = computed(() => {
const text = content.value.replace(/[#*`>\-_\[\]()!]/g, '')
return text.trim().length
})
const lineCount = computed(() => content.value.split('\n').length)
function setMode(newMode: 'wysiwyg' | 'source') {
mode.value = newMode
}
function toggleMode() {
mode.value = mode.value === 'wysiwyg' ? 'source' : 'wysiwyg'
}
function updateContent(newContent: string) {
content.value = newContent
if (saveStatus.value === 'saved' || saveStatus.value === 'idle') {
saveStatus.value = 'dirty'
}
}
let saveTimer: ReturnType<typeof setTimeout> | null = null
function scheduleAutoSave(delay = 1500) {
if (saveTimer) clearTimeout(saveTimer)
saveTimer = setTimeout(() => {
void save()
}, delay)
}
async function save() {
if (!currentFilePath.value) return
if (saveStatus.value === 'saving') return
saveStatus.value = 'saving'
try {
await workspaceService.saveFileContent(currentFilePath.value, content.value)
saveStatus.value = 'saved'
lastSavedAt.value = new Date().toISOString()
} catch {
saveStatus.value = 'save_failed'
}
}
async function loadFile(filePath: string) {
currentFilePath.value = filePath
saveStatus.value = 'saving'
try {
content.value = await workspaceService.readFileContent(filePath)
saveStatus.value = 'saved'
lastSavedAt.value = new Date().toISOString()
} catch {
content.value = ''
saveStatus.value = 'idle'
}
highlightBlockId.value = null
}
function highlightBlock(blockId: string) {
highlightBlockId.value = blockId
setTimeout(() => {
if (highlightBlockId.value === blockId) {
highlightBlockId.value = null
}
}, 3000)
}
function setExternalChanged() {
if (saveStatus.value === 'dirty') {
saveStatus.value = 'conflict'
} else {
saveStatus.value = 'external_changed'
}
}
function closeFile() {
if (saveTimer) clearTimeout(saveTimer)
currentFilePath.value = null
currentNoteId.value = null
content.value = ''
saveStatus.value = 'idle'
lastSavedAt.value = null
highlightBlockId.value = null
}
return {
mode,
content,
saveStatus,
lastSavedAt,
currentNoteId,
currentFilePath,
highlightBlockId,
cursorPosition,
wordCount,
lineCount,
setMode,
toggleMode,
updateContent,
scheduleAutoSave,
save,
loadFile,
highlightBlock,
setExternalChanged,
closeFile,
}
})