From e1137ca861f09b3419a83f955b59633ee64d2933 Mon Sep 17 00:00:00 2001 From: bboysoul Date: Mon, 7 Sep 2026 11:17:13 +0800 Subject: [PATCH] feat: support desktop task swipe toggles --- frontend/src/App.vue | 39 ++++++++++++++++++++++++++++++++++++-- frontend/src/style.test.ts | 7 +++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b53e98f..93476b0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -48,6 +48,7 @@ const totalPages = computed(() => Math.max(1, Math.ceil(totalTasks.value / pageS const expandedFolders = ref(new Set()) const navigationLoaded = ref(false) const taskSwipeStart = ref<{ id: string; x: number; y: number } | null>(null) +const taskPointerStart = ref<{ id: string; x: number; y: number } | null>(null) const taskSwipeOffsets = ref>({}) const taskComposeOpen = ref(false) const composeTitle = ref('') @@ -365,6 +366,40 @@ function startTaskSwipe(task: Task, event: TouchEvent) { taskSwipeOffsets.value[task.id] = 0 } } +function startTaskPointer(task: Task, event: PointerEvent) { + if (activeView.value === 'trash' || loading.value || isInteractiveTarget(event.target)) return + if (event.pointerType === 'touch') return + taskPointerStart.value = { id: task.id, x: event.clientX, y: event.clientY } +} +function moveTaskPointer(task: Task, event: PointerEvent) { + const start = taskPointerStart.value + if (!start || start.id !== task.id) return + const deltaX = event.clientX - start.x + const deltaY = event.clientY - start.y + if (Math.abs(deltaX) > Math.abs(deltaY)) { + if ((!task.completed && deltaX > 0) || (task.completed && deltaX < 0)) { + taskSwipeOffsets.value[task.id] = Math.max(-92, Math.min(deltaX, 92)) + } + } +} +function finishTaskPointer(task: Task, event: PointerEvent) { + const start = taskPointerStart.value + if (!start || start.id !== task.id) return + taskPointerStart.value = null + const deltaX = event.clientX - start.x + const deltaY = event.clientY - start.y + taskSwipeOffsets.value[task.id] = 0 + const expectedDirection = task.completed ? deltaX < 0 : deltaX > 0 + if (expectedDirection && shouldToggleRowSwipe(Math.abs(deltaX), deltaY)) { + suppressTaskClickId = task.id + window.setTimeout(() => { if (suppressTaskClickId === task.id) suppressTaskClickId = '' }, 400) + void toggle(task) + } +} +function cancelTaskPointer(task?: Task) { + taskPointerStart.value = null + if (task) taskSwipeOffsets.value[task.id] = 0 +} function moveTaskSwipe(task: Task, event: TouchEvent) { const start = taskSwipeStart.value const touch = event.touches[0] @@ -538,7 +573,7 @@ onMounted(bootstrap)
{{page}} / {{totalPages}}
{{query?'没有匹配的任务':'这里还很安静'}}{{query?'换个关键词试试':'写下第一件想完成的小事吧'}}
diff --git a/frontend/src/style.test.ts b/frontend/src/style.test.ts index 8072af4..821b879 100644 --- a/frontend/src/style.test.ts +++ b/frontend/src/style.test.ts @@ -13,6 +13,13 @@ describe('mobile navigation styles', () => { }) describe('task and habit row decoration', () => { + it('supports pointer dragging for desktop task and subtask completion', () => { + expect(app.match(/@pointerdown="startTaskPointer/g)?.length).toBe(2) + expect(app.match(/@pointermove="moveTaskPointer/g)?.length).toBe(2) + expect(app.match(/@pointerup="finishTaskPointer/g)?.length).toBe(2) + expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(2) + }) + it('does not draw a left border on task or habit rows', () => { expect(css).toContain('.task-row{border-left:0;') expect(css).toContain('.habit-row{border-left:0;')