perf: streamline view data loading
This commit is contained in:
+72
-33
@@ -6,7 +6,7 @@ import {
|
||||
Settings, Trash2, X, Repeat2,
|
||||
} from 'lucide-vue-next'
|
||||
import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils'
|
||||
import { formatApiErrorDetail, isTaskView, nextTotalAfterLocalTaskAdd, normalizeRequiredName, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, writeCountdownCache, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils'
|
||||
import { beginLatestRequest, createMutationReconciler, formatApiErrorDetail, isLatestRequest, isTaskView, loadCountdownCache, nextTotalAfterLocalTaskAdd, normalizeRequiredName, readStoredBoolean, readStoredNavigation, runLatestRequest, shouldToggleRowSwipe, startPrimaryWithBackground, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
import { createCompletionPulse } from './lib/completion-motion'
|
||||
import { captureListDragPointer, getAdjacentListMove, hasExceededLongPressMovement, moveListToScope, snapshotListDragPointer, type ListDragPointer } from './lib/list-drag'
|
||||
@@ -460,13 +460,14 @@ async function loadTodayTaskSummary() {
|
||||
todayTaskTotal.value = overdue + open + completed
|
||||
} catch { /* 概览统计失败不阻断今天页 */ }
|
||||
}
|
||||
async function loadOverdueTasks() {
|
||||
async function loadOverdueTasks(request = beginLatestRequest('tasks')) {
|
||||
const params = new URLSearchParams()
|
||||
params.set('due_to', isoAtLocalDayOffset(0))
|
||||
params.set('completed', 'false')
|
||||
overdueTasks.value = await loadTaskPages(`/tasks?${params}`)
|
||||
const loaded = await loadTaskPages(`/tasks?${params}`)
|
||||
if (isLatestRequest('tasks', request)) overdueTasks.value = loaded
|
||||
}
|
||||
async function loadTasksPage() {
|
||||
async function loadTasksPage(request = beginLatestRequest('tasks')) {
|
||||
const params = new URLSearchParams({ page: String(page.value), page_size: String(pageSize) })
|
||||
if (query.value) params.set('q', query.value)
|
||||
else if (activeView.value === 'tasks' && activeList.value) params.set('list_id', activeList.value)
|
||||
@@ -474,6 +475,7 @@ async function loadTasksPage() {
|
||||
if (activeView.value === 'upcoming') { params.set('due_from', isoAtLocalDayOffset(0)); params.set('due_to', isoAtLocalDayOffset(8)) }
|
||||
if (!showCompleted.value && activeView.value !== 'trash') params.set('completed', 'false')
|
||||
const data = await api(`/tasks?${params}`)
|
||||
if (!isLatestRequest('tasks', request)) return
|
||||
tasks.value = data.items ?? []
|
||||
totalTasks.value = data.total ?? tasks.value.length
|
||||
hiddenCompletedTaskCount.value = 0
|
||||
@@ -483,6 +485,7 @@ async function loadTasksPage() {
|
||||
completedParams.set('page_size', '1')
|
||||
completedParams.set('completed', 'true')
|
||||
const completedData = await api(`/tasks?${completedParams}`)
|
||||
if (!isLatestRequest('tasks', request)) return
|
||||
hiddenCompletedTaskCount.value = Number(completedData.total ?? completedData.items?.length ?? 0)
|
||||
}
|
||||
}
|
||||
@@ -499,39 +502,67 @@ async function loadNavigation(force = false) {
|
||||
}
|
||||
async function preloadCountdowns() {
|
||||
try {
|
||||
const active = await api('/countdowns')
|
||||
writeCountdownCache(active ?? [], [])
|
||||
const archived = await api('/countdowns?archived=true')
|
||||
writeCountdownCache(active ?? [], archived ?? [])
|
||||
await loadCountdownCache(async () => {
|
||||
const [active, archived] = await Promise.all([api('/countdowns'), api('/countdowns?archived=true')])
|
||||
return { items: active ?? [], archived: archived ?? [] }
|
||||
})
|
||||
} catch { /* 倒数日页会在打开时重试 */ }
|
||||
}
|
||||
async function loadAll() {
|
||||
loading.value = true; error.value = ''
|
||||
const request = beginLatestRequest('tasks')
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
if (!navigationLoaded.value) await loadNavigation()
|
||||
await loadTasksPage()
|
||||
if (!isLatestRequest('tasks', request)) return
|
||||
if (activeView.value === 'today') {
|
||||
await loadOverdueTasks()
|
||||
void loadTodayTaskSummary()
|
||||
} else overdueTasks.value = []
|
||||
if (page.value > totalPages.value) { page.value = totalPages.value; await loadTasksPage() }
|
||||
} catch (reason) { fail(reason) } finally { loading.value = false }
|
||||
await startPrimaryWithBackground(
|
||||
[() => loadTasksPage(request), () => loadOverdueTasks(request)],
|
||||
loadTodayTaskSummary,
|
||||
)
|
||||
} else {
|
||||
await loadTasksPage(request)
|
||||
if (isLatestRequest('tasks', request)) overdueTasks.value = []
|
||||
}
|
||||
if (isLatestRequest('tasks', request) && page.value > totalPages.value) {
|
||||
page.value = totalPages.value
|
||||
await loadTasksPage(request)
|
||||
}
|
||||
} catch (reason) {
|
||||
if (isLatestRequest('tasks', request)) fail(reason)
|
||||
} finally {
|
||||
if (isLatestRequest('tasks', request)) loading.value = false
|
||||
}
|
||||
}
|
||||
async function refreshAll() {
|
||||
navigationLoaded.value = false
|
||||
await loadAll()
|
||||
}
|
||||
type TrashPage = { items?: Task[]; total?: number }
|
||||
async function loadTrashPage() {
|
||||
const data = await api(`/trash?page=${page.value}&page_size=${pageSize}`)
|
||||
trash.value = data.items ?? []
|
||||
totalTasks.value = data.total ?? trash.value.length
|
||||
return api(`/trash?page=${page.value}&page_size=${pageSize}`) as Promise<TrashPage>
|
||||
}
|
||||
async function loadTrash() {
|
||||
loading.value = true; error.value = ''
|
||||
try { await loadTrashPage() } catch (reason) { fail(reason) } finally { loading.value = false }
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
await runLatestRequest('trash', loadTrashPage, {
|
||||
success: (data) => {
|
||||
trash.value = data.items ?? []
|
||||
totalTasks.value = data.total ?? trash.value.length
|
||||
},
|
||||
error: fail,
|
||||
finally: () => { loading.value = false },
|
||||
})
|
||||
}
|
||||
async function switchView(view: View, listId?: string) {
|
||||
taskMutationNavigation.value += 1
|
||||
activeView.value = view
|
||||
if (view !== 'trash') beginLatestRequest('trash')
|
||||
if (!isTaskView(view)) {
|
||||
beginLatestRequest('tasks')
|
||||
loading.value = false
|
||||
error.value = ''
|
||||
}
|
||||
if (listId) activeList.value = listId
|
||||
writeStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY, view, activeList.value)
|
||||
page.value = 1
|
||||
@@ -547,8 +578,7 @@ async function switchView(view: View, listId?: string) {
|
||||
async function loadTodayView() {
|
||||
await loadAll()
|
||||
}
|
||||
async function patchTask(task: Task, patch: Partial<Task>) {
|
||||
const updated = await api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ ...patch, version: task.version }) })
|
||||
function applyTaskUpdate(task: Task, updated: Task) {
|
||||
const index = tasks.value.findIndex((item) => item.id === task.id)
|
||||
if (index >= 0) tasks.value[index] = { ...tasks.value[index], ...updated }
|
||||
const overdueIndex = overdueTasks.value.findIndex((item) => item.id === task.id)
|
||||
@@ -557,20 +587,29 @@ async function patchTask(task: Task, patch: Partial<Task>) {
|
||||
else overdueTasks.value[overdueIndex] = { ...overdueTasks.value[overdueIndex], ...updated }
|
||||
}
|
||||
if (selectedTask.value?.id === task.id) selectedTask.value = { ...selectedTask.value, ...updated }
|
||||
return updated as Task
|
||||
}
|
||||
async function patchTask(task: Task, patch: Partial<Task>) {
|
||||
const updated = await api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ ...patch, version: task.version }) }) as Task
|
||||
applyTaskUpdate(task, updated)
|
||||
return updated
|
||||
}
|
||||
const taskMutationNavigation = ref(0)
|
||||
const taskMutationReconciler = createMutationReconciler(
|
||||
() => ({ navigation: taskMutationNavigation.value, view: activeView.value, listId: activeList.value, page: page.value, query: query.value, showCompleted: showCompleted.value }),
|
||||
(left, right) => left.navigation === right.navigation && left.view === right.view && left.listId === right.listId && left.page === right.page && left.query === right.query && left.showCompleted === right.showCompleted,
|
||||
loadAll,
|
||||
)
|
||||
async function toggle(task: Task) {
|
||||
const completing = !task.completed
|
||||
try {
|
||||
await patchTask(task, { completed: completing })
|
||||
if (completing) markTaskJustCompleted(task.id)
|
||||
await loadTasksPage()
|
||||
if (activeView.value === 'today') {
|
||||
await loadOverdueTasks()
|
||||
void loadTodayTaskSummary()
|
||||
}
|
||||
toast(task.completed ? '已重新打开' : '完成啦')
|
||||
} catch (reason) { fail(reason) }
|
||||
await taskMutationReconciler.run(
|
||||
() => api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ completed: completing, version: task.version }) }) as Promise<Task>,
|
||||
() => toast(completing ? '完成啦' : '已重新打开'),
|
||||
fail,
|
||||
(updated) => {
|
||||
applyTaskUpdate(task, updated)
|
||||
if (completing) markTaskJustCompleted(task.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
function isInteractiveTarget(target: EventTarget | null) {
|
||||
return target instanceof Element && Boolean(target.closest('button,input,select,textarea,a,label'))
|
||||
|
||||
Reference in New Issue
Block a user