From 0fb98e894e5b197f345cc6b251ed532b8d86834b Mon Sep 17 00:00:00 2001 From: bboysoul Date: Sun, 20 Sep 2026 06:41:57 +0800 Subject: [PATCH] fix: restore numeric habit progress bars --- frontend/e2e/visual-polish.spec.ts | 27 ++++++++++++++++++++++----- frontend/src/MvpPanel.vue | 8 ++++++++ frontend/src/style.css | 13 ++++++++++--- frontend/src/style.test.ts | 7 +++++-- frontend/src/visual-polish.test.ts | 4 ++-- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/frontend/e2e/visual-polish.spec.ts b/frontend/e2e/visual-polish.spec.ts index 06efacd..d896297 100644 --- a/frontend/e2e/visual-polish.spec.ts +++ b/frontend/e2e/visual-polish.spec.ts @@ -77,6 +77,7 @@ test('boolean habits complete in place while paused and unscheduled rows explain if ((page.viewportSize()?.width ?? 0) <= 930) await bottomTab(page, '习惯').click() else await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click() const booleanRow = page.locator('.habit-row').filter({ hasText: booleanName }) + await expect(booleanRow.getByRole('progressbar')).toHaveCount(0) const booleanCheck = booleanRow.getByRole('button', { name: `完成${booleanName}一次` }) const checkGeometry = await booleanCheck.evaluate((element) => { const control = element.getBoundingClientRect() @@ -153,16 +154,17 @@ test('approved polish keeps dense rows, title-only memos, and unique detail titl const transparent = 'rgba(0, 0, 0, 0)' await expect(taskRow).toHaveCSS('background-color', transparent) await taskRow.hover() - await expect(taskRow).toHaveCSS('background-color', transparent) + await expect(taskRow).toHaveCSS('background-color', testInfo.project.name === 'desktop-1440' ? 'rgb(255, 248, 238)' : transparent) await taskRow.locator('.task-main').click() await expect(taskRow).toHaveClass(/selected/) - await expect(taskRow).toHaveCSS('background-color', transparent) + await expect(taskRow).toHaveCSS('background-color', 'rgb(255, 244, 229)') const taskDetail = page.getByRole('dialog', { name: '任务详情' }) await expect(taskDetail.getByRole('button', { name: '保存更改' })).toBeEnabled() - await page.keyboard.press('Escape') + await taskDetail.getByRole('button', { name: '关闭详情' }).click() await expect(taskDetail).toBeHidden() - await bottomTab(page, '习惯').click() + if ((page.viewportSize()?.width ?? 0) <= 930) await bottomTab(page, '习惯').click() + else await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click() const mobileHabitGeometry = await page.evaluate(() => { const heading = document.querySelector('.habit-section-heading')!.getBoundingClientRect() const list = document.querySelector('.habit-list')!.getBoundingClientRect() @@ -178,14 +180,29 @@ test('approved polish keeps dense rows, title-only memos, and unique detail titl await page.getByRole('button', { name: '添加习惯', exact: true }).click() const habitRow = page.locator('.habit-row').filter({ hasText: habitName }) await expect(habitRow).toHaveCount(1) + await expect(habitRow.getByRole('progressbar', { name: `${habitName}进度:0 / 8` })).toHaveAttribute('max', '8') + await expect(habitRow.getByRole('progressbar', { name: `${habitName}进度:0 / 8` })).toHaveAttribute('value', '0') + await expect(habitRow.locator('.habit-row-meta')).toHaveText('0 / 8') await expectHeightInRange(habitRow, 57, 59) await expectNotClipped(habitRow) - await habitRow.getByRole('button', { name: `查看习惯详情:${habitName}` }).click() + await habitRow.getByRole('button', { name: `完成${habitName}一次` }).click() + await expect(habitRow.getByRole('progressbar', { name: `${habitName}进度:1 / 8` })).toHaveAttribute('value', '1') + await expect(habitRow.locator('.habit-row-meta')).toHaveText('1 / 8') + if ((page.viewportSize()?.width ?? 0) <= 930) await bottomTab(page, '今天').click() + else await page.locator('.sidebar').getByRole('button', { name: '今天', exact: true }).click() + const todayHabitRow = page.locator('.today-habit-row').filter({ hasText: habitName }) + await expect(todayHabitRow.getByRole('progressbar', { name: `${habitName}进度:1 / 8` })).toHaveAttribute('max', '8') + await expect(todayHabitRow.getByRole('progressbar', { name: `${habitName}进度:1 / 8` })).toHaveAttribute('value', '1') + await expect(todayHabitRow.locator('.habit-row-meta')).toHaveText('1 / 8') + await expectHeightInRange(todayHabitRow, 57, 59) + await expectNotClipped(todayHabitRow) + await todayHabitRow.getByRole('button', { name: `查看习惯详情:${habitName}` }).click() const habitDetail = page.getByRole('dialog', { name: habitName }) await expect(habitDetail.getByRole('heading', { name: habitName, exact: true })).toHaveCount(1) await expect(habitDetail).not.toContainText('习惯详情') await habitDetail.getByRole('button', { name: '关闭习惯详情' }).click() await page.setViewportSize({ width: 1440, height: 900 }) + await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click() const desktopHabitGeometry = await page.evaluate(() => { const view = document.querySelector('main.list-main > .mvp-view')!.getBoundingClientRect() const heading = document.querySelector('.habit-section-heading')!.getBoundingClientRect() diff --git a/frontend/src/MvpPanel.vue b/frontend/src/MvpPanel.vue index 34e38b3..9ad6bfb 100644 --- a/frontend/src/MvpPanel.vue +++ b/frontend/src/MvpPanel.vue @@ -253,6 +253,12 @@ function habitProgressText(h: Habit) { if (h.kind !== 'numeric') return '' return `${Number(logFor(h, todayKey.value)?.value ?? 0)} / ${h.target ?? 1}` } +function habitProgressMax(h: Habit) { + return Math.max(Number(h.target ?? 1), 1) +} +function habitProgressValue(h: Habit) { + return Math.min(Math.max(Number(logFor(h, todayKey.value)?.value ?? 0), 0), habitProgressMax(h)) +} function habitRowStatus(h: Habit) { return habitAction(h).reason || habitProgressText(h) || (isDone(h, todayKey.value) ? '已完成' : '未完成') } @@ -813,6 +819,7 @@ onBeforeUnmount(() => {
{{ h.name }}
+ {{ habitRowStatus(h) }}
{{ !showCompleted && todayHabits.length ? '已完成的习惯已隐藏。' : '今天没有安排习惯,轻松一下吧。' }}
@@ -846,6 +853,7 @@ onBeforeUnmount(() => {
{{ h.name }}
+ {{ habitRowStatus(h) }}
{{ !showCompleted && habits.length ? '已完成的习惯已隐藏。' : '还没有习惯,从一件容易坚持的小事开始。' }}
diff --git a/frontend/src/style.css b/frontend/src/style.css index dfff9dc..cd28ec3 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -101,10 +101,17 @@ input,select,textarea{background:var(--surface-raised);border-color:var(--border .plain-list>.task-row:not(.task-row--trash).selected::before,.plain-list>.habit-row.selected::before{background:var(--accent)} .plain-list .habit-check{margin-left:0} .task-row{grid-template-columns:44px minmax(0,1fr) auto} -.habit-row{grid-template-columns:44px minmax(0,1fr) auto} +.habit-row{grid-template-columns:44px minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr) 4px;row-gap:5px} .task-row,.habit-row{display:grid} -.task-row:has(>.drag-handle),.habit-row:has(>.drag-handle){grid-template-columns:44px 44px minmax(0,1fr) auto} +.task-row:has(>.drag-handle){grid-template-columns:44px 44px minmax(0,1fr) auto} +.habit-row:has(>.drag-handle){grid-template-columns:44px 44px minmax(0,1fr) auto} .task-main,.habit-main{min-width:0;padding:0} +.habit-row>.habit-check,.habit-row>.habit-drag-handle,.habit-row>.habit-row-meta{grid-row:1/3} +.habit-row>.habit-main{grid-row:1;align-self:end} +.habit-row>.habit-progress-bar{grid-column:2;grid-row:2;align-self:start;width:100%;height:4px} +.habit-row:has(>.habit-drag-handle)>.habit-progress-bar{grid-column:3} +.habit-row:not(:has(>.habit-progress-bar))>.habit-main{grid-row:1/3;align-self:center} +.habit-row>.habit-row-meta{grid-column:-2;align-self:center} .task-main strong,.habit-name{display:block;min-width:0;font-size:15px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .task-tail,.habit-row-meta{min-width:0;max-width:132px;padding-left:12px;font-size:12px;font-weight:400;white-space:nowrap;text-align:right;overflow:hidden;text-overflow:ellipsis} .plain-list .task-due--timed .task-due__text{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}.plain-list .task-due--timed .task-due__separator{display:inline}.plain-list>.task-row:has(>.task-tail .task-due--timed){height:58px;min-height:58px;max-height:58px} @@ -148,7 +155,7 @@ main.today-main .topbar{margin-bottom:12px} .today-environment__item small{margin-top:2px;font-size:10.5px} .today-main :is(.task-row,.habit-row){height:58px;min-height:58px;max-height:58px;padding:0;background:transparent;border:0;border-bottom:1px solid #e8e0d5;border-radius:0;box-shadow:none} .today-main .task-row{gap:0}.today-main .task-main{padding:0}.today-main .task-main strong,.today-main .habit-name{font-size:15px;font-weight:400} -.today-main .task-tail{padding-left:12px;font-size:12px}.today-main .habit-row{gap:0}.today-main .habit-main{display:flex;align-items:center;min-width:0;padding:0} +.today-main .task-tail{padding-left:12px;font-size:12px}.today-main .habit-row{gap:0;row-gap:5px}.today-main .habit-main{min-width:0;padding:0} .shell.today-active .unified-fab{box-shadow:0 8px 18px rgba(174,65,29,.22)}.shell.today-active .unified-fab>svg{width:30px;height:30px;stroke-width:1.5} @media(max-width:930px){main.today-main{padding-left:max(44px,calc((100% - 630px)/2));padding-right:max(44px,calc((100% - 630px)/2))}} @media(max-width:720.98px){main.today-main{padding-left:29px!important;padding-right:29px!important;padding-bottom:calc(78px + var(--safe-area-bottom))}.today-environment{grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:21px auto;align-items:stretch;column-gap:0;row-gap:7px;padding-bottom:10px}.today-environment__calendar{grid-column:1/-1;grid-row:1;height:21px!important;padding:0;justify-content:space-between;align-items:start;border-left:0}.today-environment__weather,.today-environment__gold{grid-row:2;height:clamp(38px,calc(25vw - 55.75px),42px);padding-top:7px;border-top:1px solid #e8e0d5}.today-environment__weather{grid-column:1;padding-left:0;padding-right:10px;border-left:0}.today-environment__gold{grid-column:2;padding-left:12px}.today-environment__weather strong,.today-environment__gold strong,.today-environment__gold-primary{line-height:16px}.today-environment__item small{line-height:13px}.today-heading{margin-top:16px}.today-page-title{font-size:24px}.today-heading .today-remaining{margin-top:4px}.today-main .task-row,.today-main .habit-row{height:58px;min-height:58px;max-height:58px}.today-context .completed-filter-pill{width:87px;height:44px;min-height:44px}.today-context .completed-filter-pill__track{width:30px;height:18px}.today-context .completed-filter-pill__thumb{width:14px;height:14px}.today-context .completed-filter-pill[aria-checked="true"] .completed-filter-pill__thumb{transform:translateX(12px)}} diff --git a/frontend/src/style.test.ts b/frontend/src/style.test.ts index 5cedf67..1cdb2ed 100644 --- a/frontend/src/style.test.ts +++ b/frontend/src/style.test.ts @@ -921,8 +921,11 @@ describe('task and habit row decoration', () => { expect(mvpPanel).not.toMatch(/today-habit-list[\s\S]*?habit-drag-handle[\s\S]*?/) }) - it('uses the same visible text status tail in Today and full habits', () => { - expect(mvpPanel).not.toContain(' { + expect(mvpPanel.match(/\n\s* { expect(css).toContain('.plain-list{display:grid;gap:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible}') expect(css).toContain('.plain-list-row,.plain-list>.task-row,.plain-list>.habit-row{height:58px;min-height:58px;max-height:58px;background:transparent;border:0;border-bottom:1px solid #e8e0d5;border-radius:0;box-shadow:none}') expect(css).toContain('.task-row{grid-template-columns:44px minmax(0,1fr) auto}') - expect(css).toContain('.habit-row{grid-template-columns:44px minmax(0,1fr) auto}') + expect(css).toContain('.habit-row{grid-template-columns:44px minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr) 4px;row-gap:5px}') expect(css).toContain('.task-main strong,.habit-name{display:block;min-width:0;font-size:15px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}') expect(css).toContain('.task-tail,.habit-row-meta{min-width:0;max-width:132px;padding-left:12px;font-size:12px;font-weight:400;white-space:nowrap;text-align:right;overflow:hidden;text-overflow:ellipsis}') expect(css).toContain('.task-check{width:44px;') @@ -148,7 +148,7 @@ describe('approved five-detail polish', () => { const fullRows = habits.slice(habits.indexOf('class="habit-list plain-list">'), habits.indexOf('class="habit-archive-section"')) for (const rows of [todayRows, fullRows]) { expect(rows).not.toContain('habit-week') - expect(rows).not.toContain('habit-progress-bar') + expect(rows).toContain('