[verified] harden trash actions and mobile details
This commit is contained in:
+41
-4
@@ -57,6 +57,10 @@ const tasks = ref<Task[]>([])
|
||||
const overdueTasks = ref<Task[]>([])
|
||||
const trash = ref<Task[]>([])
|
||||
const taskListElement = ref<HTMLElement | null>(null)
|
||||
const trashAction = ref<Task | null>(null)
|
||||
const trashMenu = ref<HTMLElement | null>(null)
|
||||
const trashPageTitle = ref<HTMLElement | null>(null)
|
||||
let trashActionTrigger: HTMLElement | null = null
|
||||
const NAVIGATION_STORAGE_KEY = 'dodo.navigation'
|
||||
const restoredNavigation = readStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY)
|
||||
const activeList = ref(restoredNavigation.listId)
|
||||
@@ -1163,12 +1167,45 @@ async function mutateTrashTask(task: Task, mutation: () => Promise<unknown>, suc
|
||||
async function restoreTask(task: Task) {
|
||||
await mutateTrashTask(task, () => api(`/tasks/${task.id}/restore`, { method: 'POST' }), '任务已恢复', true)
|
||||
}
|
||||
function openTrashAction(task: Task, event?: Event) {
|
||||
trashActionTrigger = event?.currentTarget instanceof HTMLElement ? event.currentTarget : null
|
||||
trashAction.value = task
|
||||
nextTick(() => trashMenu.value?.querySelector<HTMLElement>('[role=menuitem]')?.focus())
|
||||
}
|
||||
function focusTrashMenuItem() { trashMenu.value?.querySelector<HTMLElement>('[role=menuitem]')?.focus() }
|
||||
function focusTrashActionTrigger(trigger: HTMLElement | null) {
|
||||
nextTick(() => {
|
||||
if (trigger?.isConnected) trigger.focus()
|
||||
else trashPageTitle.value?.focus()
|
||||
})
|
||||
}
|
||||
function closeTrashAction(restoreFocus = true) {
|
||||
const trigger = trashActionTrigger
|
||||
trashAction.value = null
|
||||
trashActionTrigger = null
|
||||
if (restoreFocus) focusTrashActionTrigger(trigger)
|
||||
}
|
||||
async function requestPurgeTask() {
|
||||
const task = trashAction.value
|
||||
const trigger = trashActionTrigger
|
||||
closeTrashAction(false)
|
||||
if (task) await purgeTask(task)
|
||||
focusTrashActionTrigger(trigger)
|
||||
}
|
||||
async function purgeTask(task: Task) {
|
||||
const childCount = task.subtasks?.length ?? 0
|
||||
const impact = childCount
|
||||
? `此任务及其 ${childCount} 个子任务将被永久删除,不能撤销。`
|
||||
: '此任务将被永久删除,不能撤销。'
|
||||
if (!(await confirmAction(`永久删除“${task.title}”?`, impact, true))) return
|
||||
const entered = await appDialog.value?.show({
|
||||
title: `永久删除“${task.title}”?`,
|
||||
description: impact,
|
||||
label: `输入任务名称“${task.title}”确认`,
|
||||
confirmText: '永久删除',
|
||||
danger: true,
|
||||
validate: (value) => value.trim() === task.title ? null : '任务名称不匹配',
|
||||
})
|
||||
if (typeof entered !== 'string') return
|
||||
await mutateTrashTask(task, () => api(`/trash/${task.id}`, { method: 'DELETE' }), '任务已永久删除', false)
|
||||
}
|
||||
async function addSubtask() {
|
||||
@@ -1737,7 +1774,7 @@ onUnmounted(() => {
|
||||
<button id="today-tasks-heading" class="today-section-toggle today-section-anchor" type="button" :aria-expanded="!todaySectionCollapse.tasks" aria-controls="today-tasks" @click="toggleTodaySection('tasks')"><span class="today-section-title">今天</span><span class="today-section-summary">{{totalTasks}}</span><span class="today-section-chevron" aria-hidden="true">{{ todaySectionCollapse.tasks ? '›' : '⌄' }}</span></button>
|
||||
</template>
|
||||
<section v-if="activeView==='trash'" class="trash-page-context">
|
||||
<div><h1 class="trash-page-title">回收站</h1><p class="trash-page-summary">删除的任务保留在这里,可整组恢复或永久删除。</p></div>
|
||||
<div><h1 ref="trashPageTitle" class="trash-page-title" tabindex="-1">回收站</h1><p class="trash-page-summary">删除的任务保留在这里,可整组恢复或永久删除。</p></div>
|
||||
<span class="trash-page-count">共 {{totalTasks}} 项</span>
|
||||
</section>
|
||||
<div v-if="activeView==='tasks' || activeView==='upcoming'" id="task-list-heading" class="list-section-heading"><span id="task-list-title" class="list-section-title">任务</span><span class="list-section-count">{{ totalTasks }}</span><button v-if="activeView==='tasks' && taskReorderAvailable" class="list-section-action" type="button" :aria-pressed="taskReorderMode" @click="taskReorderMode=!taskReorderMode;cancelTaskReorder()">{{ taskReorderMode ? '完成' : '调整顺序' }}</button></div>
|
||||
@@ -1748,11 +1785,10 @@ onUnmounted(() => {
|
||||
<article v-for="node in group.nodes" :key="node.task.id" :data-task-id="node.task.id" class="task-row task-row--trash">
|
||||
<div class="task-main"><strong :title="node.task.title">{{node.task.title}}</strong><span v-if="node.subtasks.length" class="meta"><span class="meta-item"><ListChecks/>含 {{node.subtasks.length}} 个子任务,整组处理</span></span></div>
|
||||
<span v-if="node.task.due_at" class="task-tail"><TaskDueDisplay :due-at="node.task.due_at" :due-has-time="node.task.due_has_time" :completed="node.task.completed" :now-ms="taskDueNowMs" /></span>
|
||||
<span class="task-actions"><button class="restore" @click.stop="restoreTask(node.task)"><ArchiveRestore/>恢复</button><button class="icon danger ghost trash-more" aria-label="永久删除" :title="`永久删除${node.task.title}`" @click.stop="purgeTask(node.task)"><Ellipsis/></button></span>
|
||||
<span class="task-actions"><button class="restore" @click.stop="restoreTask(node.task)"><ArchiveRestore/>恢复</button><button class="icon danger ghost trash-more" aria-label="打开任务操作" aria-haspopup="menu" :aria-expanded="trashAction?.id===node.task.id" :title="`${node.task.title}操作`" @click.stop="openTrashAction(node.task,$event)"><Ellipsis/></button></span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<p class="trash-safety-note">父任务与子任务始终作为一个整体处理,不支持子任务脱离父任务单独恢复或删除。</p>
|
||||
</section>
|
||||
<section v-else :id="activeView==='today' ? 'today-tasks' : undefined" class="task-list plain-list" :class="{loading}" v-show="activeView!=='today' || !todaySectionCollapse.tasks" :role="activeView==='today' ? 'region' : undefined" :aria-labelledby="activeView==='today' ? 'today-tasks-heading' : (activeView==='tasks' || activeView==='upcoming') ? 'task-list-title' : undefined" ref="taskListElement">
|
||||
<template v-for="node in taskTree" :key="node.task.id">
|
||||
@@ -1837,6 +1873,7 @@ onUnmounted(() => {
|
||||
<Transition name="toast"><div v-if="notice" class="toast" role="status">{{notice}}</div></Transition>
|
||||
<div v-if="error" class="error-toast" role="alert">{{error}}<button @click="error=''"><X/></button></div>
|
||||
<Teleport to="body">
|
||||
<span v-if="trashAction" class="trash-action-mask" @click.self="closeTrashAction()"><span ref="trashMenu" class="trash-action-menu" role="menu" aria-label="回收站任务操作" @keydown.esc.stop="closeTrashAction()" @keydown.tab.prevent="focusTrashMenuItem"><span class="trash-action-title" :title="trashAction.title">{{trashAction.title}}</span><button role="menuitem" class="danger-text" @click="requestPurgeTask"><Trash2/>永久删除</button></span></span>
|
||||
<span v-if="archivedListAction" class="archived-action-mask" @click.self="closeArchivedListAction()"><span ref="archivedMenu" class="archived-row-actions" :style="archivedMenuStyle" role="menu"><button role="menuitem" @click="restoreList(archivedListAction)"><ArchiveRestore/>恢复清单</button><button role="menuitem" class="danger-text" @click="openPurgeList(archivedListAction)"><Trash2/>永久删除清单</button></span></span>
|
||||
</Teleport>
|
||||
<AppSheet :open="Boolean(purgeListTarget)" variant="actions" panel-class="purge-list-dialog" title-id="purge-list-title" description-id="purge-list-description" initial-focus=".secondary" :busy="purgeListSubmitting" @close="closePurgeList">
|
||||
|
||||
Reference in New Issue
Block a user