diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3b44f2e..e3e62cf 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -6,7 +6,7 @@ import { Settings, Trash2, X, Repeat2, } from 'lucide-vue-next' import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils' -import { beginLatestRequest, createMutationReconciler, formatApiErrorDetail, isLatestRequest, isTaskView, loadCountdownCache, nextTotalAfterLocalTaskAdd, normalizeRequiredName, readStoredBoolean, readStoredNavigation, runLatestRequest, shouldToggleRowSwipe, startPrimaryWithBackground, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils' +import { beginLatestRequest, createMutationReconciler, formatApiErrorDetail, isLatestRequest, isTaskView, loadCountdownCache, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, normalizeRequiredName, performTrashMutation, readStoredBoolean, readStoredNavigation, runLatestRequest, shouldToggleRowSwipe, startPrimaryWithBackground, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils' import { csrfHeader } from './lib/csrf' import { createCompletionPulse } from './lib/completion-motion' import { captureListDragPointer, getAdjacentListMove, hasExceededLongPressMovement, moveListToScope, snapshotListDragPointer, type ListDragPointer } from './lib/list-drag' @@ -549,7 +549,7 @@ async function loadTrashPage() { async function loadTrash() { loading.value = true error.value = '' - await runLatestRequest('trash', loadTrashPage, { + return await runLatestRequest('trash', loadTrashPage, { success: (data) => { trash.value = data.items ?? [] totalTasks.value = data.total ?? trash.value.length @@ -808,12 +808,29 @@ async function removeTask(task: Task) { toast('已移到回收站') } catch (reason) { fail(reason) } } +async function mutateTrashTask(task: Task, mutation: () => Promise, successMessage: string) { + const result = await performTrashMutation( + mutation, + () => { + trash.value = trash.value.filter((item) => item.id !== task.id) + totalTasks.value = nextTotalAfterLocalTaskRemoval(totalTasks.value) + if (page.value > totalPages.value) page.value = totalPages.value + }, + loadTrash, + ) + if (!result.mutated) { + fail(result.error) + return + } + if (!result.refreshed) toast(`${successMessage},但列表刷新失败,请重试刷新`) + else toast(successMessage) +} async function restoreTask(task: Task) { - try { await api(`/tasks/${task.id}/restore`, { method: 'POST' }); trash.value = trash.value.filter((item) => item.id !== task.id); toast('任务已恢复') } catch (reason) { fail(reason) } + await mutateTrashTask(task, () => api(`/tasks/${task.id}/restore`, { method: 'POST' }), '任务已恢复') } async function purgeTask(task: Task) { if (!window.confirm(`永久删除“${task.title}”?这个操作不能撤销。`)) return - try { await api(`/trash/${task.id}`, { method: 'DELETE' }); trash.value = trash.value.filter((item) => item.id !== task.id); toast('已永久删除') } catch (reason) { fail(reason) } + await mutateTrashTask(task, () => api(`/trash/${task.id}`, { method: 'DELETE' }), '任务已永久删除') } async function addSubtask() { if (!selectedTask.value) return @@ -1203,8 +1220,8 @@ onUnmounted(() => {

已过期 {{overdueTaskTree.length}}

@@ -1217,13 +1234,10 @@ onUnmounted(() => {
-
{{node.task.title}}{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}
- {{['','低','中','高'][node.task.priority]}} - - - +
{{node.task.title}}{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}{{['','低','中','高'][node.task.priority]}}
+
-
{{subtask.title}}
+
{{subtask.title}}
{{ query ? '没有匹配的任务' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静' }}{{query?'换个关键词试试':hiddenCompletedTaskCount > 0 ? '开启“显示已完成”即可查看。':'写下第一件想完成的小事吧'}}
diff --git a/frontend/src/components/TaskDueDisplay.test.ts b/frontend/src/components/TaskDueDisplay.test.ts index 6b585ae..8411ef4 100644 --- a/frontend/src/components/TaskDueDisplay.test.ts +++ b/frontend/src/components/TaskDueDisplay.test.ts @@ -34,7 +34,7 @@ describe('TaskDueDisplay', () => { expect(time.textContent).toContain('今天') expect(time.getAttribute('title')).toBe(time.getAttribute('aria-label')) expect(time.getAttribute('title')).toContain('2026年9月10日') - expect(root.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true') + expect(root.querySelector('svg')).toBeNull() }) it('distinguishes incomplete overdue and completed neutral states', async () => { diff --git a/frontend/src/components/TaskDueDisplay.vue b/frontend/src/components/TaskDueDisplay.vue index 7655939..fc06063 100644 --- a/frontend/src/components/TaskDueDisplay.vue +++ b/frontend/src/components/TaskDueDisplay.vue @@ -1,6 +1,5 @@