This commit is contained in:
+11
-1
@@ -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 {
|
||||
|
||||
@@ -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"')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('就是今天')
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user