feat: collapse Today sections
This commit is contained in:
+32
-12
@@ -22,6 +22,7 @@ import CompletedFilterPill from './components/CompletedFilterPill.vue'
|
||||
import CalendarPicker from './components/CalendarPicker.vue'
|
||||
import TaskDueDisplay from './components/TaskDueDisplay.vue'
|
||||
import { useTaskDueClock } from './lib/task-due-clock'
|
||||
import { readTodaySectionCollapse, writeTodaySectionCollapse, type TodaySectionCollapse } from './lib/today-section-collapse'
|
||||
|
||||
type FolderItem = { id: string; name: string }
|
||||
type TaskList = { id: string; folder_id: string | null; name: string; is_inbox: boolean }
|
||||
@@ -97,6 +98,8 @@ const markdownPreview = ref(false)
|
||||
const taskNoteEditor = ref<HTMLTextAreaElement | null>(null)
|
||||
const SHOW_COMPLETED_STORAGE_KEY = 'dodo.show-completed'
|
||||
const showCompleted = ref(readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true))
|
||||
const TODAY_SECTION_COLLAPSE_KEY = 'dodo.today-section-collapse.v1'
|
||||
const todaySectionCollapse = ref<TodaySectionCollapse>(readTodaySectionCollapse(window.localStorage, TODAY_SECTION_COLLAPSE_KEY))
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const totalTasks = ref(0)
|
||||
@@ -334,8 +337,23 @@ const todayHabitProgress = computed(() => `${todayHabitCompleted.value} / ${toda
|
||||
const progressWidth = (completed: number, total: number) => `${total > 0 ? Math.min(100, Math.round(completed / total * 100)) : 0}%`
|
||||
const todayTaskProgressPercent = computed(() => progressWidth(todayTaskCompleted.value, todayTaskTotal.value))
|
||||
const todayHabitProgressPercent = computed(() => progressWidth(todayHabitCompleted.value, todayHabitTotal.value))
|
||||
function scrollTodaySection(id: 'today-tasks' | 'today-habits') {
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
function persistTodaySectionCollapse() {
|
||||
writeTodaySectionCollapse(window.localStorage, TODAY_SECTION_COLLAPSE_KEY, todaySectionCollapse.value)
|
||||
}
|
||||
function toggleTodaySection(section: keyof TodaySectionCollapse) {
|
||||
todaySectionCollapse.value[section] = !todaySectionCollapse.value[section]
|
||||
persistTodaySectionCollapse()
|
||||
}
|
||||
async function navigateTodaySection(section: 'tasks' | 'habits') {
|
||||
if (todaySectionCollapse.value[section]) {
|
||||
todaySectionCollapse.value[section] = false
|
||||
persistTodaySectionCollapse()
|
||||
}
|
||||
await nextTick()
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||
const heading = document.getElementById(`today-${section}-heading`)
|
||||
heading?.focus({ preventScroll: true })
|
||||
document.getElementById(`today-${section}`)?.scrollIntoView({ behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'start' })
|
||||
}
|
||||
function updateTodayHabitSummary(value: { total: number; completed: number }) {
|
||||
todayHabitTotal.value = value.total
|
||||
@@ -1438,29 +1456,29 @@ onUnmounted(() => {
|
||||
<CountdownPanel ref="countdownComposer" v-else-if="activeView==='countdowns'" @notice="toast" />
|
||||
<template v-else>
|
||||
<section v-if="activeView==='today'" class="today-board" aria-label="今日进度">
|
||||
<button class="today-track today-task-track" type="button" aria-controls="today-tasks" @click="scrollTodaySection('today-tasks')">
|
||||
<button class="today-track today-task-track" type="button" aria-controls="today-tasks" @click="navigateTodaySection('tasks')">
|
||||
<span class="today-track-head"><strong>任务 {{ todayTaskCompleted }} / {{ todayTaskTotal }}</strong></span>
|
||||
<span class="today-track-rail" role="progressbar" aria-label="今日任务进度" aria-valuemin="0" :aria-valuemax="todayTaskTotal" :aria-valuenow="todayTaskCompleted"><i class="today-track-fill" :style="{ width: todayTaskProgressPercent }" /></span>
|
||||
</button>
|
||||
<button class="today-track today-habit-track" type="button" aria-controls="today-habits" @click="scrollTodaySection('today-habits')">
|
||||
<button class="today-track today-habit-track" type="button" aria-controls="today-habits" @click="navigateTodaySection('habits')">
|
||||
<span class="today-track-head"><strong>习惯 {{ todayHabitProgress }}</strong></span>
|
||||
<span class="today-track-rail" role="progressbar" aria-label="今日习惯进度" aria-valuemin="0" :aria-valuemax="todayHabitTotal" :aria-valuenow="todayHabitCompleted"><i class="today-track-fill" :style="{ width: todayHabitProgressPercent }" /></span>
|
||||
</button>
|
||||
</section>
|
||||
<template v-if="activeView==='today'">
|
||||
<section v-if="overdueTaskTree.length" class="overdue-section">
|
||||
<h3 class="section-heading overdue-heading"><CalendarDays/>已过期 <span>{{overdueTaskTree.length}}</span></h3>
|
||||
<div class="task-list overdue-list">
|
||||
<section v-if="overdueTaskTree.length" class="overdue-section today-collapsible-section">
|
||||
<button id="today-overdue-heading" class="today-section-toggle" type="button" :aria-expanded="!todaySectionCollapse.overdue" aria-controls="today-overdue" @click="toggleTodaySection('overdue')"><span class="today-section-title"><CalendarDays/>已过期</span><span class="today-section-summary">{{overdueTaskTree.length}} 项</span><ChevronRight v-if="todaySectionCollapse.overdue" aria-hidden="true"/><ChevronDown v-else aria-hidden="true"/></button>
|
||||
<div v-show="!todaySectionCollapse.overdue" id="today-overdue" class="task-list overdue-list" role="region" aria-labelledby="today-overdue-heading">
|
||||
<template v-for="node in overdueTaskTree" :key="`overdue-${node.task.id}`">
|
||||
<article :data-task-id="node.task.id" class="task-row overdue-task" :class="{'completion-exiting':completionExitingTaskIds.has(node.task.id)}"><button class="task-check" :aria-label="`完成${node.task.title}`" :aria-pressed="false" @click.stop="toggle(node.task)"><span class="task-check-mark" :class="`p${node.task.priority}`"></span></button><div class="task-main" role="button" tabindex="0" @click="selectTask(node.task)" @keydown.enter="selectTask(node.task)"><strong :title="node.task.title">{{node.task.title}}</strong><span v-if="node.subtasks.length" class="meta"><span class="meta-item"><ListChecks/>{{node.subtasks.filter(t=>t.completed).length}}/{{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></article>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
<h3 id="today-tasks" class="section-heading today-section-anchor"><ListTodo/>任务</h3>
|
||||
<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"><ListTodo/>任务</span><span class="today-section-summary">{{totalTasks}} 项</span><ChevronRight v-if="todaySectionCollapse.tasks" aria-hidden="true"/><ChevronDown v-else aria-hidden="true"/></button>
|
||||
</template>
|
||||
<div v-if="activeView!=='today'" class="list-toolbar"><span v-if="activeView==='trash' || totalPages > 1 || totalTasks > 0">{{ totalPages > 1 ? `第 ${page} / ${totalPages} 页 · ` : '' }}共 {{totalTasks}} 项</span><button v-if="query" class="link" @click="query=''">清除搜索</button></div>
|
||||
<div v-if="activeView!=='trash' && totalPages > 1" class="pager"><button class="secondary" :disabled="page<=1 || loading" @click="previousPage">上一页</button><span>{{page}} / {{totalPages}}</span><button class="secondary" :disabled="page>=totalPages || loading" @click="nextPage">下一页</button></div>
|
||||
<section class="task-list" :class="{loading}">
|
||||
<section :id="activeView==='today' ? 'today-tasks' : undefined" class="task-list" :class="{loading}" v-show="activeView!=='today' || !todaySectionCollapse.tasks" :role="activeView==='today' ? 'region' : undefined" :aria-labelledby="activeView==='today' ? 'today-tasks-heading' : undefined">
|
||||
<template v-for="node in taskTree" :key="node.task.id">
|
||||
<article :data-task-id="node.task.id" class="task-row swipeable" :class="{done:node.task.completed,'just-completed': justCompletedTaskIds.has(node.task.id),'completion-exiting':completionExitingTaskIds.has(node.task.id),selected:selectedTask?.id===node.task.id,ready:Math.abs(taskSwipeOffsets[node.task.id] ?? 0) >= 64,reordering:taskReorder?.id===node.task.id,'reorder-target':taskReorderTarget===node.task.id}" :style="{ '--swipe-x': `${taskSwipeOffsets[node.task.id] ?? 0}px`, '--reorder-y': `${taskReorder?.id === node.task.id ? taskReorder.offsetY : 0}px` }" @pointerdown="startTaskPointer(node.task, $event)" @pointermove="moveTaskPointer(node.task, $event)" @pointerup="finishTaskPointer(node.task, $event)" @pointercancel="cancelTaskPointer(node.task)" @touchstart.passive="startTaskSwipe(node.task, $event)" @touchmove.passive="moveTaskSwipe(node.task, $event)" @touchend="finishTaskSwipe(node.task, $event)" @touchcancel="cancelTaskSwipe(node.task)">
|
||||
<button v-if="activeView!=='trash'" class="drag-handle task-drag-handle" :disabled="Boolean(query) || totalPages > 1" aria-label="上下拖动任务排序" title="上下拖动排序" @pointerdown.stop="startTaskReorder(node.task, $event)" @pointermove.stop="moveTaskReorder(node.task, $event)" @pointerup.stop="finishTaskReorder(node.task, $event)" @pointercancel.stop="cancelTaskReorder"><GripVertical/></button>
|
||||
@@ -1472,9 +1490,11 @@ onUnmounted(() => {
|
||||
<div v-if="activeView==='today' && !query && hiddenCompletedTaskCount > 0 && !visibleTasks.length && !loading" class="today-filtered-empty-note"><span>已隐藏已完成任务</span><button type="button" class="link" @click="showCompleted=true">显示</button></div>
|
||||
<div v-else-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{ query ? '没有匹配的任务' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静' }}</b><span>{{query?'换个关键词试试':hiddenCompletedTaskCount > 0 ? '开启“显示已完成”即可查看。':'写下第一件想完成的小事吧'}}</span><button v-if="activeView==='today' && !query && hiddenCompletedTaskCount === 0" class="soft-button empty-action" @click="openTaskCompose"><Plus/>添加今天任务</button></div>
|
||||
</section>
|
||||
<div v-if="activeView==='today'" id="today-habits" class="today-section-anchor">
|
||||
<h3 class="section-heading"><Repeat2/>习惯</h3>
|
||||
<MvpPanel ref="habitComposer" view="today-habits" :show-completed="showCompleted" @changed="refreshAll" @notice="toast" @summary="updateTodayHabitSummary" />
|
||||
<div v-if="activeView==='today'" class="today-habits-section today-section-anchor">
|
||||
<button id="today-habits-heading" class="today-section-toggle" type="button" :aria-expanded="!todaySectionCollapse.habits" aria-controls="today-habits" @click="toggleTodaySection('habits')"><span class="today-section-title"><Repeat2/>习惯</span><span class="today-section-summary">{{todayHabitCompleted}} / {{todayHabitTotal}}</span><ChevronRight v-if="todaySectionCollapse.habits" aria-hidden="true"/><ChevronDown v-else aria-hidden="true"/></button>
|
||||
<div v-show="!todaySectionCollapse.habits" id="today-habits" role="region" aria-labelledby="today-habits-heading">
|
||||
<MvpPanel ref="habitComposer" view="today-habits" :show-completed="showCompleted" @changed="refreshAll" @notice="toast" @summary="updateTodayHabitSummary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user