This commit is contained in:
@@ -28,6 +28,7 @@ const folders = ref<FolderItem[]>([])
|
||||
const lists = ref<TaskList[]>([])
|
||||
const archivedLists = ref<TaskList[]>([])
|
||||
const tasks = ref<Task[]>([])
|
||||
const overdueTasks = ref<Task[]>([])
|
||||
const trash = ref<Task[]>([])
|
||||
const activeList = ref('')
|
||||
const activeView = ref<View>(defaultView())
|
||||
@@ -227,6 +228,7 @@ const visibleTasks = computed(() => {
|
||||
const selectedTaskSubtasks = computed(() => selectedTask.value?.subtasks ?? [])
|
||||
const filteredTaskTree = computed(() => groupTaskTree(visibleTasks.value))
|
||||
const taskTree = computed(() => filteredTaskTree.value)
|
||||
const overdueTaskTree = computed(() => groupTaskTree(overdueTasks.value))
|
||||
let searchTimer: number | undefined
|
||||
watch(query, () => {
|
||||
if (searchTimer) window.clearTimeout(searchTimer)
|
||||
@@ -321,6 +323,12 @@ function isoAtLocalDayOffset(offset: number) {
|
||||
day.setHours(0, 0, 0, 0)
|
||||
return day.toISOString()
|
||||
}
|
||||
async function loadOverdueTasks() {
|
||||
const params = new URLSearchParams()
|
||||
params.set('due_to', isoAtLocalDayOffset(0))
|
||||
params.set('completed', 'false')
|
||||
overdueTasks.value = await loadTaskPages(`/tasks?${params}`)
|
||||
}
|
||||
async function loadTasksPage() {
|
||||
const params = new URLSearchParams({ page: String(page.value), page_size: String(pageSize) })
|
||||
if (query.value) params.set('q', query.value)
|
||||
@@ -356,6 +364,8 @@ async function loadAll() {
|
||||
try {
|
||||
if (!navigationLoaded.value) await loadNavigation()
|
||||
await loadTasksPage()
|
||||
if (activeView.value === 'today') await loadOverdueTasks()
|
||||
else overdueTasks.value = []
|
||||
if (page.value > totalPages.value) { page.value = totalPages.value; await loadTasksPage() }
|
||||
} catch (reason) { fail(reason) } finally { loading.value = false }
|
||||
}
|
||||
@@ -389,6 +399,11 @@ async function patchTask(task: Task, patch: Partial<Task>) {
|
||||
const updated = await api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ ...patch, version: task.version }) })
|
||||
const index = tasks.value.findIndex((item) => item.id === task.id)
|
||||
if (index >= 0) tasks.value[index] = { ...tasks.value[index], ...updated }
|
||||
const overdueIndex = overdueTasks.value.findIndex((item) => item.id === task.id)
|
||||
if (overdueIndex >= 0) {
|
||||
if (updated.completed) overdueTasks.value.splice(overdueIndex, 1)
|
||||
else overdueTasks.value[overdueIndex] = { ...overdueTasks.value[overdueIndex], ...updated }
|
||||
}
|
||||
if (selectedTask.value?.id === task.id) selectedTask.value = { ...selectedTask.value, ...updated }
|
||||
return updated as Task
|
||||
}
|
||||
@@ -682,6 +697,14 @@ onMounted(bootstrap)
|
||||
<div class="today-stat muted"><strong>{{ totalTasks }}</strong><span>今日任务</span></div>
|
||||
</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">
|
||||
<template v-for="node in overdueTaskTree" :key="`overdue-${node.task.id}`">
|
||||
<article class="task-row overdue-task"><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>{{node.task.title}}</strong><span class="meta"><span><CalendarDays/>{{formatDue(node.task.due_at)}}</span></span></div><span class="overdue-badge">已过期</span></article>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
<h3 class="section-heading"><ListTodo/>任务</h3>
|
||||
</template>
|
||||
<div class="list-toolbar"><label v-if="activeView!=='trash'"><input v-model="showCompleted" type="checkbox"> 显示已完成</label><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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -81,7 +81,7 @@ describe('task and habit row decoration', () => {
|
||||
expect(app).not.toContain('class="sr-only" :aria-label="node.task.completed')
|
||||
expect(app).not.toContain('class="sr-only" :aria-label="subtask.completed')
|
||||
expect(app).toContain('class="task-check"')
|
||||
expect(app.match(/class="task-check"/g)?.length).toBe(2)
|
||||
expect(app.match(/class="task-check"/g)?.length).toBe(3)
|
||||
expect(app).toContain('class="task-check-mark"')
|
||||
expect(css).toContain('.task-check{')
|
||||
expect(css).toContain('.task-check-mark{')
|
||||
@@ -112,6 +112,17 @@ describe('task and habit row decoration', () => {
|
||||
expect(css).toContain('.habit-toolbar{')
|
||||
})
|
||||
|
||||
it('shows unfinished overdue tasks as a separate list inside Today', () => {
|
||||
expect(app).toContain('const overdueTasks = ref<Task[]>([])')
|
||||
expect(app).toContain("params.set('due_to', isoAtLocalDayOffset(0))")
|
||||
expect(app).toContain("params.set('completed', 'false')")
|
||||
expect(app).toContain('class="overdue-section"')
|
||||
expect(app).toContain('已过期')
|
||||
expect(app).toContain('v-for="node in overdueTaskTree"')
|
||||
expect(css).toContain('.overdue-section{')
|
||||
expect(css).toContain('.overdue-heading{')
|
||||
})
|
||||
|
||||
it('keeps Today as a low-noise dashboard with direct empty-state actions', () => {
|
||||
expect(app).toContain('class="today-board"')
|
||||
expect(app).toContain('todayOpenTasks')
|
||||
|
||||
Reference in New Issue
Block a user