From 3a23c6cd44f503d52b28d2693a9360749fda50d1 Mon Sep 17 00:00:00 2001 From: bboysoul Date: Sun, 20 Sep 2026 10:52:39 +0800 Subject: [PATCH] fix: align desktop habit detail with task pane --- frontend/e2e/habit-detail-paper-flow.spec.ts | 32 +++++++++++++++++--- frontend/src/App.vue | 14 ++++++--- frontend/src/MvpPanel.vue | 14 +++++---- frontend/src/components/AppSheet.vue | 8 ++++- frontend/src/style.css | 1 + frontend/src/style.test.ts | 19 ++++++++++-- 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/frontend/e2e/habit-detail-paper-flow.spec.ts b/frontend/e2e/habit-detail-paper-flow.spec.ts index 846931e..298b66e 100644 --- a/frontend/e2e/habit-detail-paper-flow.spec.ts +++ b/frontend/e2e/habit-detail-paper-flow.spec.ts @@ -61,9 +61,24 @@ test('habit detail paper flow is responsive, ordered, scrollable, and preserves await openHabits(page) await expectNoHorizontalOverflow(page) + const shell = page.locator('.shell') + const main = page.locator('main') + const mainBeforeDetail = await main.boundingBox() + const numericOpener = page.locator('.habit-row').filter({ hasText: numericName }).getByRole('button', { name: `查看习惯详情:${numericName}` }) await numericOpener.click() let dialog = page.getByRole('dialog', { name: numericName }) + await expect(shell).toHaveClass(/detail-open/) + if ((page.viewportSize()?.width ?? 0) > 930) { + const [mainAfterDetail, detailBox] = await Promise.all([main.boundingBox(), dialog.boundingBox()]) + expect(mainBeforeDetail).not.toBeNull() + expect(mainAfterDetail).not.toBeNull() + expect(detailBox).not.toBeNull() + expect(mainBeforeDetail!.width - mainAfterDetail!.width).toBeCloseTo(350, 0) + expect(detailBox!.width).toBeCloseTo(350, 0) + expect(detailBox!.x).toBeCloseTo(mainAfterDetail!.x + mainAfterDetail!.width, 0) + expect(detailBox!.height).toBeCloseTo(page.viewportSize()!.height, 0) + } await expect(dialog.locator('.habit-detail-hero')).toContainText('68 / 100') await expect(dialog.locator('.habit-detail-progress-row')).toContainText('68%') await expect(dialog.locator('.habit-detail-archive-note')).toHaveCount(0) @@ -135,8 +150,10 @@ test('habit detail paper flow is responsive, ordered, scrollable, and preserves const restoreGate = new Promise(resolve => { releaseRestore = resolve }) await page.route(`**/api/v1/habits/${archived.id}/restore`, async route => { await restoreGate; await route.continue() }) await dialog.getByRole('button', { name: '恢复习惯' }).click() - const mask = page.locator('.app-sheet-mask').filter({ has: dialog }) - await expect(mask).toHaveAttribute('aria-busy', 'true') + const overlay = (page.viewportSize()?.width ?? 0) <= 930 + ? page.locator('.app-sheet-mask').filter({ has: dialog }) + : dialog + await expect(overlay).toHaveAttribute('aria-busy', 'true') const closeButton = dialog.getByRole('button', { name: '关闭习惯详情' }) const deleteButton = dialog.getByRole('button', { name: '永久删除' }) const restoreButton = dialog.getByRole('button', { name: '恢复习惯' }) @@ -145,11 +162,18 @@ test('habit detail paper flow is responsive, ordered, scrollable, and preserves await expect(restoreButton).toBeDisabled() await closeButton.click({ force: true }) await expect(dialog).toBeVisible() - await mask.click({ position: { x: 2, y: 2 }, force: true }) + if ((page.viewportSize()?.width ?? 0) <= 930) await overlay.click({ position: { x: 2, y: 2 }, force: true }) await expect(dialog).toBeVisible() await page.keyboard.press('Escape') await expect(dialog).toBeVisible() + + // Desktop same-view navigation must force-close a busy detail even though user dismissal stays locked. + if ((page.viewportSize()?.width ?? 0) > 930) { + await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click() + await expect(dialog).toBeHidden() + await expect(shell).not.toHaveClass(/detail-open/) + } releaseRestore() await expect(dialog).toBeHidden() - await expect(archiveToggle).toBeFocused() + await expect(page.locator('.habit-row').filter({ hasText: archivedName })).toBeVisible() }) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2be075c..ac8895d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -168,6 +168,7 @@ const countdownComposer = ref | null>(null) const memoPanel = ref | null>(null) const memoTrash = ref(false) const memoDetailOpen = ref(false) +const habitDetailOpen = ref(false) const compactLayout = ref(window.innerWidth <= 930) const memoShellState = computed(() => deriveMemoShellState({ view: activeView.value, detailOpen: memoDetailOpen.value, compact: compactLayout.value, trash: memoTrash.value })) const memoBackgroundInert = computed(() => memoShellState.value.backgroundInert) @@ -696,7 +697,7 @@ async function switchView(view: View, listId?: string) { if (listId) activeList.value = listId writeStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY, view, activeList.value) page.value = 1 - selectedTask.value = null; taskSelectionGeneration.value += 1; mobileSidebar.value = false; mobileDetail.value = false; taskComposeGeneration.value += 1; taskComposeOpen.value = false; sidebarCreateOpen.value = false; sidebarAction.value = null + habitComposer.value?.closeHabitDetail(true); selectedTask.value = null; habitDetailOpen.value = false; taskSelectionGeneration.value += 1; mobileSidebar.value = false; mobileDetail.value = false; taskComposeGeneration.value += 1; taskComposeOpen.value = false; sidebarCreateOpen.value = false; sidebarAction.value = null if (view !== 'memos') memoDetailOpen.value = false if (view === 'trash') await loadTrash() else if (view === 'today') await loadTodayView() @@ -1159,6 +1160,10 @@ function closeTaskDetail() { mobileDetail.value = false selectedTask.value = null } +function handleHabitDetail(open: boolean) { + if (open) closeTaskDetail() + habitDetailOpen.value = open +} function addSelectedDueTime() { selectedDueHasTime.value = true nextTick(() => { @@ -1172,6 +1177,7 @@ function clearSelectedDueDate() { selectedTaskRepeat.value = 'none' } function selectTask(task: Task) { + habitComposer.value?.closeHabitDetail(true) taskSelectionGeneration.value += 1 selectedTask.value = { ...task, subtasks: task.subtasks ? [...task.subtasks] : [] } const due = parseTaskDueDraft(task.due_at, task.due_has_time) @@ -1521,7 +1527,7 @@ onUnmounted(() => {

登录后继续你的清单

-
+
- +