From 34691b6fe897577a2c56150389e6fd0b1cd0c449 Mon Sep 17 00:00:00 2001 From: bboysoul Date: Sun, 20 Sep 2026 23:18:04 +0800 Subject: [PATCH] fix: move task pagination below list --- frontend/e2e/task-pagination-position.spec.ts | 63 +++++++++++++++++++ frontend/src/App.vue | 32 +++++++--- frontend/src/style.css | 2 +- frontend/src/style.test.ts | 5 +- frontend/src/visual-polish.test.ts | 8 +++ 5 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 frontend/e2e/task-pagination-position.spec.ts diff --git a/frontend/e2e/task-pagination-position.spec.ts b/frontend/e2e/task-pagination-position.spec.ts new file mode 100644 index 0000000..795a68b --- /dev/null +++ b/frontend/e2e/task-pagination-position.spec.ts @@ -0,0 +1,63 @@ +import type { APIRequestContext, Page } from '@playwright/test' +import { expect, test } from './fixtures' + +async function csrf(request: APIRequestContext) { + const state = await request.storageState() + return state.cookies.find(cookie => cookie.name === 'dodo_csrf')?.value ?? '' +} + +async function createTask(request: APIRequestContext, baseURL: string, listId: string, title: string) { + const response = await request.post('/api/v1/tasks', { + data: { title, list_id: listId }, + headers: { 'x-csrf-token': await csrf(request), origin: baseURL }, + }) + expect(response.ok(), await response.text()).toBeTruthy() +} + +async function openInbox(page: Page) { + const inbox = page.locator('.sidebar').getByRole('button', { name: '收集箱', exact: true }) + if (await page.evaluate(() => window.innerWidth <= 930)) { + await page.locator('main .topbar > button').first().click() + } + await inbox.click() +} + +test('task pagination stays below the list and returns to the list start after navigation', async ({ page, request, baseURL }) => { + const bootstrapResponse = await request.get('/api/v1/bootstrap') + expect(bootstrapResponse.ok()).toBeTruthy() + const inbox = (await bootstrapResponse.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox) + expect(inbox).toBeTruthy() + + for (let index = 1; index <= 51; index += 1) { + await createTask(request, baseURL!, inbox.id, `分页验收任务 ${String(index).padStart(2, '0')}`) + } + + await page.goto('/') + await openInbox(page) + + const taskList = page.locator('.task-list') + const pager = page.locator('.pager') + await expect(taskList.locator('.task-row')).toHaveCount(50) + await expect(pager).toBeVisible() + await expect(pager).toContainText('1 / 2') + + const firstPageGeometry = await page.evaluate(() => { + const list = document.querySelector('.task-list')!.getBoundingClientRect() + const pagerBox = document.querySelector('.pager')!.getBoundingClientRect() + return { listBottom: list.bottom, pagerTop: pagerBox.top } + }) + expect(firstPageGeometry.pagerTop).toBeGreaterThanOrEqual(firstPageGeometry.listBottom) + + await pager.getByRole('button', { name: '下一页' }).click() + await expect(pager).toContainText('2 / 2') + await expect(taskList.locator('.task-row')).toHaveCount(1) + + const secondPageGeometry = await page.evaluate(() => { + const list = document.querySelector('.task-list')!.getBoundingClientRect() + const pagerBox = document.querySelector('.pager')!.getBoundingClientRect() + return { listTop: list.top, listBottom: list.bottom, pagerTop: pagerBox.top, viewportHeight: innerHeight } + }) + expect(secondPageGeometry.listTop).toBeGreaterThanOrEqual(-1) + expect(secondPageGeometry.listTop).toBeLessThan(secondPageGeometry.viewportHeight / 2) + expect(secondPageGeometry.pagerTop).toBeGreaterThanOrEqual(secondPageGeometry.listBottom) +}) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 30d2e5a..3da0220 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -56,6 +56,7 @@ let purgeListTrigger: HTMLElement | null = null const tasks = ref([]) const overdueTasks = ref([]) const trash = ref([]) +const taskListElement = ref(null) const NAVIGATION_STORAGE_KEY = 'dodo.navigation' const restoredNavigation = readStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY) const activeList = ref(restoredNavigation.listId) @@ -724,7 +725,7 @@ async function loadTrashPage() { async function loadTrash() { loading.value = true error.value = '' - return await runLatestRequest('trash', loadTrashPage, { + const committed = await runLatestRequest('trash', loadTrashPage, { success: (data) => { trash.value = data.items ?? [] totalTasks.value = data.total ?? trash.value.length @@ -732,6 +733,11 @@ async function loadTrash() { error: fail, finally: () => { loading.value = false }, }) + if (committed && page.value > totalPages.value) { + page.value = totalPages.value + return await loadTrash() + } + return committed } async function switchView(view: View, listId?: string) { if (activeView.value === 'memos' && view !== 'memos' && memoPanel.value?.dirty && !(await confirmAction('有未保存的更改', '确定离开当前备忘录吗?'))) return @@ -1535,15 +1541,23 @@ function moveListWithinScope(item: TaskList, direction: 'up' | 'down') { void persistListMove(item, item.folder_id, move.targetId, move.placement) closeSidebarAction() } -function previousPage() { +async function scrollToTaskPageStart() { + await nextTick() + taskListElement.value?.scrollIntoView({ block: 'start' }) +} +async function previousPage() { if (page.value <= 1 || loading.value) return page.value -= 1 - activeView.value === 'trash' ? loadTrash() : isTaskView(activeView.value) ? loadAll() : undefined + if (activeView.value === 'trash') await loadTrash() + else if (isTaskView(activeView.value)) await loadAll() + await scrollToTaskPageStart() } -function nextPage() { +async function nextPage() { if (page.value >= totalPages.value || loading.value) return page.value += 1 - activeView.value === 'trash' ? loadTrash() : isTaskView(activeView.value) ? loadAll() : undefined + if (activeView.value === 'trash') await loadTrash() + else if (isTaskView(activeView.value)) await loadAll() + await scrollToTaskPageStart() } function reconcileDesktopPaneWidths() { @@ -1718,10 +1732,8 @@ onUnmounted(() => {
任务{{ totalTasks }}
-
{{ totalPages > 1 ? `第 ${page} / ${totalPages} 页 · ` : '' }}共 {{totalTasks}} 项
-
第 {{ page }} / {{ totalPages }} 页 · 共 {{ totalTasks }} 项
-
{{page}} / {{totalPages}}
-
+
共 {{totalTasks}} 项
+