feat: strengthen backup and mobile workflows
This commit is contained in:
@@ -8,7 +8,7 @@ export type MemoDraft = { id: null; title: string; content: string; version: nul
|
||||
export type Memo = MemoRecord
|
||||
export type MemoEditorValue = MemoRecord | MemoDraft
|
||||
type RequestFn = (path: string, options?: RequestInit) => Promise<unknown>
|
||||
const props = withDefaults(defineProps<{ memo: MemoEditorValue; request: RequestFn; mobile?: boolean; selectionToken?: number }>(), { selectionToken: 0 })
|
||||
const props = withDefaults(defineProps<{ memo: MemoEditorValue; request: RequestFn; mobile?: boolean; selectionToken?: number; confirmAction?: (options: { title: string; description?: string; confirmText?: string; danger?: boolean }) => Promise<boolean> }>(), { selectionToken: 0 })
|
||||
const emit = defineEmits<{ saveStarted: [id: string | null, selectionToken: number]; saveFinished: [selectionToken: number]; lifecycleStarted: [id: string, selectionToken: number]; lifecycleFinished: [selectionToken: number]; saved: [memo: MemoRecord, selectionToken: number]; close: []; deleted: [id: string, selectionToken: number]; restored: [memo: MemoRecord, selectionToken: number]; purged: [id: string, selectionToken: number]; notice: [message: string] }>()
|
||||
const title = ref('')
|
||||
const content = ref('')
|
||||
@@ -18,7 +18,6 @@ let lifecycleGeneration = 0
|
||||
const error = ref('')
|
||||
const conflict = ref(false)
|
||||
const titleInput = ref<HTMLInputElement | null>(null)
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const initial = ref({ title: '', content: '' })
|
||||
const memoPreview = ref(false)
|
||||
const memoBodyEditor = ref<HTMLTextAreaElement | null>(null)
|
||||
@@ -35,8 +34,8 @@ watch(() => props.selectionToken, () => {
|
||||
lifecycleGeneration += 1
|
||||
lifecycleBusy.value = false
|
||||
})
|
||||
function close() {
|
||||
if (dirty.value && !window.confirm('有未保存的更改,确定离开吗?')) return
|
||||
async function close() {
|
||||
if (dirty.value && !(await props.confirmAction?.({ title: '放弃未保存的更改?', description: '关闭后,当前草稿不会保存。', confirmText: '放弃更改', danger: true }))) return
|
||||
emit('close')
|
||||
}
|
||||
function validate() {
|
||||
@@ -91,7 +90,7 @@ async function reload() {
|
||||
async function remove() {
|
||||
const memoId = props.memo.id
|
||||
if (memoId === null || lifecycleBusy.value) return
|
||||
if (!window.confirm(`把“${props.memo.title}”移到回收站?`)) return
|
||||
if (!(await props.confirmAction?.({ title: `把“${props.memo.title}”移到回收站?`, description: '之后可以在回收站恢复。', confirmText: '移到回收站', danger: true }))) return
|
||||
error.value = ''
|
||||
lifecycleBusy.value = true
|
||||
const operationToken = ++lifecycleGeneration
|
||||
@@ -130,7 +129,7 @@ async function restore() {
|
||||
async function purge() {
|
||||
const memoId = props.memo.id
|
||||
if (memoId === null || lifecycleBusy.value) return
|
||||
if (!window.confirm(`永久删除“${props.memo.title}”?此操作无法撤销。`)) return
|
||||
if (!(await props.confirmAction?.({ title: `永久删除“${props.memo.title}”?`, description: '此操作无法撤销。', confirmText: '永久删除', danger: true }))) return
|
||||
error.value = ''
|
||||
lifecycleBusy.value = true
|
||||
const operationToken = ++lifecycleGeneration
|
||||
@@ -170,14 +169,7 @@ function handleMemoBodyShortcut(event: KeyboardEvent) {
|
||||
formatMemoBody(format)
|
||||
}
|
||||
function keydown(event: KeyboardEvent) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') { event.preventDefault(); void save(); return }
|
||||
if (event.key === 'Escape') { event.preventDefault(); close(); return }
|
||||
if (!props.mobile || event.key !== 'Tab' || !root.value) return
|
||||
const controls = [...root.value.querySelectorAll<HTMLElement>('button:not(:disabled),input:not(:disabled),textarea:not(:disabled)')]
|
||||
if (!controls.length) return
|
||||
const first = controls[0], last = controls.at(-1)!
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') { event.preventDefault(); void save() }
|
||||
}
|
||||
function beforeUnload(event: BeforeUnloadEvent) { if (dirty.value) event.preventDefault() }
|
||||
onMounted(() => { window.addEventListener('beforeunload', beforeUnload); nextTick(() => titleInput.value?.focus()) })
|
||||
@@ -186,7 +178,7 @@ defineExpose({ dirty, requestClose: close })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside ref="root" class="memo-editor" role="dialog" :aria-modal="mobile ? 'true' : undefined" aria-labelledby="memo-editor-title" @keydown="keydown">
|
||||
<div class="memo-editor" @keydown="keydown">
|
||||
<header><span id="memo-editor-title">备忘录详情</span><button type="button" aria-label="关闭备忘录" @click="close"><X/></button></header>
|
||||
<div class="memo-editor__fields">
|
||||
<label>标题<input ref="titleInput" v-model="title" maxlength="200" aria-label="备忘录标题" :disabled="Boolean(memo.deleted_at)"></label>
|
||||
@@ -213,5 +205,5 @@ defineExpose({ dirty, requestClose: close })
|
||||
</div>
|
||||
<footer v-if="!memo.deleted_at"><button v-if="memo.id !== null" type="button" class="danger-text" :disabled="saving || lifecycleBusy" @click="remove"><Trash2/>移到回收站</button><button type="button" class="primary-small memo-save" :disabled="saving || lifecycleBusy || !dirty" @click="save">{{ saving ? '正在保存…' : '保存' }}</button></footer>
|
||||
<footer v-else><button type="button" class="secondary" :disabled="lifecycleBusy" @click="restore"><ArchiveRestore/>恢复</button><button type="button" class="danger-button" :disabled="lifecycleBusy" @click="purge"><Trash2/>永久删除</button></footer>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user