perf: preload and cache countdowns
ci / docker (push) Successful in 3m49s

This commit is contained in:
2026-09-07 17:50:26 +08:00
parent 635930db50
commit a02b6f660c
6 changed files with 64 additions and 5 deletions
+11 -1
View File
@@ -6,7 +6,7 @@ import {
Settings, Trash2, X, Repeat2,
} from 'lucide-vue-next'
import { filterTasks, fromDateTimeLocal, groupTaskTree, renderMarkdown, toDateTimeLocal } from './lib/task-utils'
import { defaultView, isTaskView, nextTotalAfterLocalTaskAdd, quickTaskFields, shouldToggleRowSwipe } from './lib/mvp-utils'
import { defaultView, isTaskView, nextTotalAfterLocalTaskAdd, quickTaskFields, shouldToggleRowSwipe, writeCountdownCache } from './lib/mvp-utils'
import { csrfHeader } from './lib/csrf'
import MvpPanel from './MvpPanel.vue'
import CountdownPanel from './CountdownPanel.vue'
@@ -216,6 +216,7 @@ async function bootstrap() {
activeList.value = data.inbox_id || lists.value.find((item: TaskList) => item.is_inbox)?.id || lists.value[0]?.id || ''
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
await loadArchivedLists()
void preloadCountdowns()
await loadAll()
}
} catch {
@@ -237,6 +238,7 @@ async function submitAuth() {
navigationLoaded.value = true
activeList.value = data.inbox_id || lists.value.find((item: TaskList) => item.is_inbox)?.id || lists.value[0]?.id || ''
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
void preloadCountdowns()
await loadAll()
} catch (reason) { fail(reason) }
}
@@ -279,6 +281,14 @@ async function loadNavigation(force = false) {
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
await loadArchivedLists()
}
async function preloadCountdowns() {
try {
const active = await api('/countdowns')
writeCountdownCache(active ?? [], [])
const archived = await api('/countdowns?archived=true')
writeCountdownCache(active ?? [], archived ?? [])
} catch { /* 倒数日页会在打开时重试 */ }
}
async function loadAll() {
loading.value = true; error.value = ''
try {
+8
View File
@@ -30,6 +30,14 @@ describe('countdown modal accessibility', () => {
expect(source).toContain(':disabled="form.ignore_year"')
})
it('shows cached countdowns immediately and refreshes active items first', () => {
expect(source).toContain('readCountdownCache<Countdown>()')
expect(source).toContain("const active = await request('/countdowns')")
expect(source).toContain("const archivedItems = await request('/countdowns?archived=true')")
expect(source).toContain('writeCountdownCache(items.value, archived.value)')
expect(source).not.toContain("Promise.all([request('/countdowns'),request('/countdowns?archived=true')])")
})
it('keeps the empty state directly actionable', () => {
expect(source).toContain('添加第一个重要日子')
expect(source).toContain('@click="openFromEmpty"')
+20 -2
View File
@@ -2,7 +2,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { Archive, ArchiveRestore, CalendarHeart, ChevronDown, Pencil, Pin, Trash2, X } from 'lucide-vue-next'
import { csrfHeader } from './lib/csrf'
import { calendarModeLabel, countdownDayText, countdownKindLabel, dateKey, formatApiErrorDetail } from './lib/mvp-utils'
import { calendarModeLabel, countdownDayText, countdownKindLabel, dateKey, formatApiErrorDetail, readCountdownCache, writeCountdownCache } from './lib/mvp-utils'
type Countdown = {
id: string; title: string; event_date: string; display_date: string; kind: 'countdown'|'anniversary'|'birthday'
@@ -99,7 +99,25 @@ async function request(path:string, options:RequestInit={}) {
return response.status === 204 ? null : response.json()
}
async function safe(work:()=>Promise<void>) { busy.value=true; error.value=''; try { await work() } catch(reason) { error.value=reason instanceof Error ? reason.message : '请求失败' } finally { busy.value=false } }
async function load() { await safe(async()=>{ const [a,b]=await Promise.all([request('/countdowns'),request('/countdowns?archived=true')]); items.value=a; archived.value=b }) }
async function load() {
const cached = readCountdownCache<Countdown>()
if (cached) { items.value=cached.items; archived.value=cached.archived }
if (!cached) busy.value=true
error.value=''
try {
const active = await request('/countdowns') as Countdown[]
items.value=active
writeCountdownCache(items.value, archived.value)
busy.value=false
const archivedItems = await request('/countdowns?archived=true') as Countdown[]
archived.value=archivedItems
writeCountdownCache(items.value, archived.value)
} catch(reason) {
error.value=reason instanceof Error ? reason.message : '请求失败'
} finally {
busy.value=false
}
}
function edit(item:Countdown) {
detailItem.value=null
editingId.value=item.id
+8 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { calendarModeLabel, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, habitButtonValue, habitWeek, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, readHabitGridCache, shouldToggleRowSwipe, writeHabitGridCache } from './mvp-utils'
import { calendarModeLabel, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, habitButtonValue, habitWeek, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, shouldToggleRowSwipe, writeCountdownCache, writeHabitGridCache } from './mvp-utils'
describe('MVP view utilities', () => {
it('formats a local date as YYYY-MM-DD', () => {
@@ -77,6 +77,13 @@ describe('MVP view utilities', () => {
expect(readHabitGridCache('2026-09-14')).toBeNull()
})
it('reuses countdown data while the page refreshes in the background', () => {
const active = [{ id: 'countdown-1', title: '旅行' }]
const archived = [{ id: 'countdown-2', title: '旧日期' }]
writeCountdownCache(active, archived)
expect(readCountdownCache()).toEqual({ items: active, archived })
})
it('renders countdown labels from date-only day values', () => {
expect(countdownDayText(8)).toBe('还有 8 天')
expect(countdownDayText(0)).toBe('就是今天')
+9
View File
@@ -115,6 +115,7 @@ export function isFabDrag(deltaX: number, deltaY: number, threshold = 8) {
}
let habitGridCache: { week: string; habits: unknown[] } | null = null
let countdownCache: { items: unknown[]; archived: unknown[] } | null = null
export function readHabitGridCache<T>(week: string): T[] | null {
return habitGridCache?.week === week ? habitGridCache.habits as T[] : null
@@ -124,6 +125,14 @@ export function writeHabitGridCache<T>(week: string, habits: T[]) {
habitGridCache = { week, habits }
}
export function readCountdownCache<T>() {
return countdownCache as { items: T[]; archived: T[] } | null
}
export function writeCountdownCache<T>(items: T[], archived: T[]) {
countdownCache = { items, archived }
}
export function formatApiErrorDetail(detail: unknown): string {
if (typeof detail === 'string') return detail
if (Array.isArray(detail)) {
+8 -1
View File
@@ -156,6 +156,13 @@ describe('mobile touch targets', () => {
})
describe('unified floating add interaction', () => {
it('preloads countdowns after authentication', () => {
expect(app).toContain('void preloadCountdowns()')
expect(app).toContain("const active = await api('/countdowns')")
expect(app).toContain("const archived = await api('/countdowns?archived=true')")
expect(app).toContain('writeCountdownCache(active ?? [], archived ?? [])')
})
it('reuses one draggable FAB component for task, habit, and countdown views', () => {
expect(app).toContain("import FloatingAddButton from './components/FloatingAddButton.vue'")
expect(app).toContain('<FloatingAddButton')
@@ -254,7 +261,7 @@ describe('sidebar layout', () => {
})
it('loads archived lists during bootstrap so archived rows are visible after refresh', () => {
expect(app).toContain('expandedFolders.value = new Set(folders.value.map((folder) => folder.id))\n await loadArchivedLists()\n await loadAll()')
expect(app).toContain('expandedFolders.value = new Set(folders.value.map((folder) => folder.id))\n await loadArchivedLists()\n void preloadCountdowns()\n await loadAll()')
})
it('styles archived rows through the new list-row-main structure', () => {