feat: redesign habit detail paper flow
ci / gitleaks (push) Successful in 9s
ci / docker (push) Successful in 3m46s

This commit is contained in:
2026-09-18 21:10:50 +08:00
parent 432a133a42
commit 0baedbc99e
6 changed files with 322 additions and 10 deletions
+19 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { archivePanelFlags, calendarModeLabel, changedHabitFields, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, dayBefore, defaultView, fabBottomReserved, formatArchivedAt, formatAuditAction, formatAuditEntity, formatHabitApiError, formatHabitHistoryNumber, formatLocalShortDateTime, formatUserAgent, habitActionState, habitButtonNotice, habitButtonValue, habitHistoryWindow, habitWeek, invalidateHabitGridCache, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, mergeHabitHistory, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, snapFabPosition, normalizeRequiredName, numericHabitInputValue, performHabitRestore, performTrashMutation, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, validateHabitForm, writeCountdownCache, writeHabitGridCache, writeStoredBoolean, writeStoredNavigation } from './mvp-utils'
import { archivePanelFlags, calendarModeLabel, changedHabitFields, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, dayBefore, defaultView, fabBottomReserved, formatArchivedAt, formatAuditAction, formatAuditEntity, formatHabitApiError, formatHabitDetailDate, formatHabitDetailProgress, formatHabitHistoryNumber, formatHabitRecordMode, formatHabitSchedule, formatLocalShortDateTime, formatUserAgent, habitActionState, habitButtonNotice, habitButtonValue, habitHistoryWindow, habitWeek, invalidateHabitGridCache, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, mergeHabitHistory, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, snapFabPosition, normalizeRequiredName, numericHabitInputValue, performHabitRestore, performTrashMutation, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, validateHabitForm, writeCountdownCache, writeHabitGridCache, writeStoredBoolean, writeStoredNavigation } from './mvp-utils'
describe('MVP view utilities', () => {
it('formats a local date as YYYY-MM-DD', () => {
@@ -90,6 +90,24 @@ describe('MVP view utilities', () => {
expect(formatHabitHistoryNumber(1.25)).toBe('1.25')
})
it('formats paper-flow habit details from real habit fields without invented data', () => {
expect(formatHabitRecordMode({ kind: 'boolean' })).toBe('完成 / 未完成')
expect(formatHabitRecordMode({ kind: 'numeric', target: 100, unit: '个' })).toBe('按数量记录 · 目标 100 个')
expect(formatHabitSchedule({ schedule_type: 'daily' })).toBe('每天')
expect(formatHabitSchedule({ schedule_type: 'weekly', weekdays: [0, 2, 6] })).toBe('每周一、周三、周日')
expect(formatHabitSchedule({ schedule_type: 'monthly', month_days: [1, 15, 31] })).toBe('每月 1、15、31 日')
expect(formatHabitSchedule({ schedule_type: 'interval', interval_days: 3 })).toBe('每 3 天')
expect(formatHabitSchedule({ schedule_type: 'weekly', weekdays: null })).toBe('每周(日期未设置)')
expect(formatHabitDetailDate('2026-08-12')).toBe('2026年8月12日')
expect(formatHabitDetailDate(undefined)).toBe('未提供')
expect(formatHabitDetailProgress({ kind: 'numeric', target: 100, value: 68 })).toEqual({ primary: '68 / 100', note: '今天还差 32', semantic: '68%' })
expect(formatHabitDetailProgress({ kind: 'numeric', target: 3, value: 4 })).toEqual({ primary: '4 / 3', note: '今日目标已达成', semantic: '已达标' })
expect(formatHabitDetailProgress({ kind: 'boolean', value: false })).toEqual({ primary: '未完成', note: '今天尚未完成', semantic: '未完成' })
expect(formatHabitDetailProgress({ kind: 'boolean', value: true })).toEqual({ primary: '已完成', note: '今日目标已达成', semantic: '已完成' })
expect(formatHabitDetailProgress({ kind: 'numeric', target: 100, archived_at: '2026-09-01T00:00:00Z' })).toBeNull()
expect(formatHabitDetailProgress({ kind: 'boolean', archived_at: '2026-09-01T00:00:00Z' })).toBeNull()
})
it('only completes numeric habits when progress reaches the target', () => {
expect(isHabitComplete('numeric', 3, 5)).toBe(false)
expect(isHabitComplete('numeric', 5, 5)).toBe(true)
+57
View File
@@ -107,6 +107,63 @@ export function formatHabitHistoryNumber(value: number) {
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(6)))
}
type HabitDetailFields = {
kind?: string
target?: number
unit?: string
archived_at?: string | null
schedule_type?: 'daily' | 'weekly' | 'monthly' | 'interval'
weekdays?: number[] | null
month_days?: number[] | null
interval_days?: number | null
}
export function formatHabitRecordMode(habit: HabitDetailFields) {
if (habit.kind !== 'numeric') return '完成 / 未完成'
const unit = habit.unit ? ` ${habit.unit}` : ''
return `按数量记录 · 目标 ${formatHabitHistoryNumber(habit.target ?? 1)}${unit}`
}
export function formatHabitSchedule(habit: HabitDetailFields) {
if (habit.schedule_type === 'weekly') {
const weekdays = habit.weekdays ?? []
if (!weekdays.length) return '每周(日期未设置)'
const labels = ['一', '二', '三', '四', '五', '六', '日']
return `${weekdays.map((day) => `${labels[day] ?? day}`).join('、')}`
}
if (habit.schedule_type === 'monthly') {
const monthDays = habit.month_days ?? []
return monthDays.length ? `每月 ${monthDays.join('、')}` : '每月(日期未设置)'
}
if (habit.schedule_type === 'interval') {
return habit.interval_days ? `${habit.interval_days}` : '按间隔(天数未设置)'
}
return '每天'
}
export function formatHabitDetailDate(day?: string) {
if (!day) return '未提供'
const [year, month, date] = day.split('-').map(Number)
if (!year || !month || !date) return day
return `${year}${month}${date}`
}
export function formatHabitDetailProgress(habit: Pick<HabitDetailFields, 'kind' | 'target' | 'unit' | 'archived_at'> & { value?: number | boolean }) {
if (habit.archived_at) return null
const target = habit.target ?? 1
const value = Number(habit.value ?? 0)
if (habit.kind !== 'numeric') {
const complete = Boolean(habit.value)
return { primary: complete ? '已完成' : '未完成', note: complete ? '今日目标已达成' : '今天尚未完成', semantic: complete ? '已完成' : '未完成' }
}
const complete = value >= target
return {
primary: `${formatHabitHistoryNumber(value)} / ${formatHabitHistoryNumber(target)}`,
note: complete ? '今日目标已达成' : `今天还差 ${formatHabitHistoryNumber(Math.max(0, target - value))}${habit.unit ? ` ${habit.unit}` : ''}`,
semantic: complete ? '已达标' : `${Math.round(target > 0 ? value / target * 100 : 0)}%`,
}
}
type HabitSchedule = {
kind?: string
cells?: Array<{ day: string; scheduled?: boolean; paused?: boolean; value?: number | boolean }>