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 @@
<script setup lang="ts">
import { ref } from 'vue'
import Icon from './Icon.vue'
import { basename } from './paths'
import TrashDialog from './TrashDialog.vue'
import { tree } from './tree'
import TreeNode from './TreeNode.vue'
import { createFolder, createNote, move } from './workspace'
const emit = defineEmits<{ logout: [] }>()
const trashOpen = ref(false)
const rootDrop = ref(false)
function onRootDragOver(e: DragEvent) {
if (!e.dataTransfer?.types.includes('application/x-rawmark-path')) return
e.preventDefault()
rootDrop.value = true
}
function onRootDrop(e: DragEvent) {
rootDrop.value = false
const src = e.dataTransfer?.getData('application/x-rawmark-path')
if (!src || !src.includes('/')) return
e.preventDefault()
void move(src, basename(src))
}
</script>
<template>
<div class="rm-sidebar-inner">
<div class="rm-sidebar-head">
<span class="rm-brand">RawMark</span>
<button class="rm-icon-btn" title="新建笔记" @click="createNote('')"><Icon name="noteAdd" /></button>
<button class="rm-icon-btn" title="新建文件夹" @click="createFolder('')"><Icon name="folderAdd" /></button>
</div>
<div class="rm-tree" :class="{ drop: rootDrop }" @dragover="onRootDragOver" @dragleave="rootDrop = false" @drop="onRootDrop">
<ul class="rm-tree-list">
<TreeNode v-for="node in tree" :key="node.path" :node="node" :depth="0" />
</ul>
<p v-if="!tree.length" class="rm-tree-empty">还没有笔记</p>
</div>
<div class="rm-sidebar-foot">
<button class="rm-foot-btn" @click="trashOpen = true"><Icon name="trash" />回收站</button>
<button class="rm-foot-btn" @click="emit('logout')"><Icon name="logout" />退出</button>
</div>
<TrashDialog v-if="trashOpen" @close="trashOpen = false" />
</div>
</template>