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

+64
View File
@@ -0,0 +1,64 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { api, ApiError } from './api'
const props = defineProps<{ mode: 'setup' | 'login' }>()
const emit = defineEmits<{ done: [] }>()
const code = ref('')
const password = ref('')
const confirm = ref('')
const error = ref('')
const busy = ref(false)
const isSetup = computed(() => props.mode === 'setup')
async function submit() {
error.value = ''
if (isSetup.value && password.value !== confirm.value) {
error.value = '两次输入的密码不一致'
return
}
busy.value = true
try {
if (isSetup.value) await api.setup(code.value, password.value)
else await api.login(password.value)
emit('done')
} catch (err) {
error.value = err instanceof ApiError ? err.message : '网络错误,请检查连接'
} finally {
busy.value = false
}
}
</script>
<template>
<div class="rm-auth">
<form class="rm-auth-card" @submit.prevent="submit">
<h1>RawMark</h1>
<p v-if="isSetup" class="rm-auth-hint">
首次使用,请设置登录密码。启动码位于服务器数据目录的 <code>setup-code.txt</code> 中。
</p>
<label v-if="isSetup">
<span>启动码</span>
<input v-model="code" autocomplete="one-time-code" autocapitalize="characters" placeholder="XXXX-XXXX" required />
</label>
<label>
<span>密码</span>
<input
v-model="password"
type="password"
:autocomplete="isSetup ? 'new-password' : 'current-password'"
:minlength="isSetup ? 8 : undefined"
required
autofocus
/>
</label>
<label v-if="isSetup">
<span>确认密码</span>
<input v-model="confirm" type="password" autocomplete="new-password" required />
</label>
<p v-if="error" class="rm-auth-error">{{ error }}</p>
<button class="rm-btn rm-btn-primary" type="submit" :disabled="busy">{{ isSetup ? '完成设置' : '登录' }}</button>
</form>
</div>
</template>