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:
1 parent
c4d2bd4636
commit
02d904056c
28 files changed
+2231
-398
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import type { Entry } from './api'
|
||||
import { basename, dirname } from './paths'
|
||||
import { ws } from './workspace'
|
||||
|
||||
export interface TreeNode {
|
||||
name: string
|
||||
path: string
|
||||
dir: boolean
|
||||
children: TreeNode[]
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator('zh-CN', { numeric: true, sensitivity: 'base' })
|
||||
|
||||
function build(entries: Entry[]): TreeNode[] {
|
||||
const root: TreeNode[] = []
|
||||
const dirs = new Map<string, TreeNode>()
|
||||
// 先按路径排序,保证父目录先于子项出现
|
||||
for (const e of [...entries].sort((a, b) => a.path.length - b.path.length)) {
|
||||
const node: TreeNode = { name: basename(e.path), path: e.path, dir: e.dir, children: [] }
|
||||
if (e.dir) dirs.set(e.path, node)
|
||||
;(dirs.get(dirname(e.path))?.children ?? root).push(node)
|
||||
}
|
||||
const sort = (nodes: TreeNode[]) => {
|
||||
nodes.sort((a, b) => (a.dir !== b.dir ? (a.dir ? -1 : 1) : collator.compare(a.name, b.name)))
|
||||
nodes.forEach((n) => sort(n.children))
|
||||
}
|
||||
sort(root)
|
||||
return root
|
||||
}
|
||||
|
||||
export const tree = computed(() => build(ws.entries))
|
||||
|
||||
const EXPANDED_KEY = 'rawmark.expanded'
|
||||
export const expanded = reactive(new Set<string>(JSON.parse(localStorage.getItem(EXPANDED_KEY) ?? '[]') as string[]))
|
||||
watch(expanded, () => localStorage.setItem(EXPANDED_KEY, JSON.stringify([...expanded])))
|
||||
|
||||
// 打开或新建笔记时展开其所在的各级目录
|
||||
watch(
|
||||
() => [ws.active, ws.renaming],
|
||||
(paths) => {
|
||||
for (const p of paths) {
|
||||
for (let d = p ? dirname(p) : ''; d; d = dirname(d)) expanded.add(d)
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in new issue
Block a user