feat(web): add Vue workspace with file tree, tabs, autosave and live sync

- Login and first-run setup screens
- File tree with inline create/rename, context menu, drag-and-drop
  move and trash with undo; all operations update the UI first and roll
  back on failure
- Tabs keep a CodeMirror state each; edits autosave after 500 ms with
  If-Match revisions, unsent edits persist in IndexedDB, and conflicts
  or remote deletions surface as banners with explicit choices
- WebSocket events sync external edits, moves and deletions into open
  tabs; reconnects trigger a full resync
- Pasted or dropped images show instantly and upload to note.assets/
- Responsive layout with a swipe-in sidebar drawer on phones
- Switch to TypeScript 6: vue-tsc does not support TypeScript 7 yet
This commit is contained in:
eezd committed 2026-10-16 18:37:14 +08:00
1 parent c4d2bd4636
commit 02d904056c
28 files changed
+2231 -398

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
// 订阅服务端的仓库变更事件;断线后指数退避重连,重连成功时触发全量同步
export interface VaultEvent {
type: 'tree' | 'changed' | 'deleted' | 'moved'
path?: string
from?: string
rev?: string
}
export function connectEvents(handlers: { onEvents: (evs: VaultEvent[]) => void; onResync: () => void }): () => void {
let ws: WebSocket | undefined
let delay = 1000
let timer: ReturnType<typeof setTimeout> | undefined
let stopped = false
let connectedBefore = false
const connect = () => {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
ws = new WebSocket(`${proto}//${location.host}/api/v1/events`)
ws.onopen = () => {
delay = 1000
// 首次连接前的状态由初始化加载,之后每次重连都可能错过事件
if (connectedBefore) handlers.onResync()
connectedBefore = true
}
ws.onmessage = (e) => handlers.onEvents(JSON.parse(e.data))
ws.onclose = () => {
if (stopped) return
timer = setTimeout(connect, delay)
delay = Math.min(delay * 2, 30000)
}
}
connect()
// 页面从后台恢复时立即重连,不等退避计时
const onVisible = () => {
if (document.visibilityState === 'visible' && ws?.readyState === WebSocket.CLOSED) {
clearTimeout(timer)
connect()
}
}
document.addEventListener('visibilitychange', onVisible)
return () => {
stopped = true
clearTimeout(timer)
document.removeEventListener('visibilitychange', onVisible)
ws?.close()
}
}