perf: reduce dodo data loading requests
ci / docker (push) Successful in 6m31s

This commit is contained in:
2026-09-05 19:51:00 +08:00
parent e8c49b045f
commit 2987a787d3
8 changed files with 248 additions and 44 deletions
+27 -12
View File
@@ -6,6 +6,7 @@ import {
Settings, Trash2, X, CalendarRange, Repeat2,
} from 'lucide-vue-next'
import { filterTasks, fromDateTimeLocal, groupTaskTree, renderMarkdown, toDateTimeLocal } from './lib/task-utils'
import { isTaskView } from './lib/mvp-utils'
import { csrfHeader } from './lib/csrf'
import MvpPanel from './MvpPanel.vue'
@@ -41,6 +42,7 @@ const pageSize = 50
const totalTasks = ref(0)
const totalPages = computed(() => Math.max(1, Math.ceil(totalTasks.value / pageSize)))
const expandedFolders = ref(new Set<string>())
const navigationLoaded = ref(false)
const activeName = computed(() => {
if (activeView.value === 'trash') return '回收站'
@@ -66,12 +68,12 @@ const taskTree = computed(() => filteredTaskTree.value)
let searchTimer: number | undefined
watch(query, () => {
if (searchTimer) window.clearTimeout(searchTimer)
if (activeView.value === 'trash') return
if (!isTaskView(activeView.value)) return
page.value = 1
searchTimer = window.setTimeout(() => loadAll(), 250)
})
watch(showCompleted, () => {
if (activeView.value !== 'trash') { page.value = 1; loadAll() }
if (isTaskView(activeView.value)) { page.value = 1; loadAll() }
})
async function api(path: string, options: RequestInit = {}) {
@@ -148,19 +150,30 @@ async function loadTasksPage() {
tasks.value = data.items ?? []
totalTasks.value = data.total ?? tasks.value.length
}
async function loadNavigation(force = false) {
if (!force && navigationLoaded.value) return
const [folderData, listData, tagData] = await Promise.all([
api('/folders'), api('/lists'), api('/tags').catch(() => []),
])
folders.value = folderData; lists.value = listData; tags.value = tagData
navigationLoaded.value = true
if (!lists.value.some((item) => item.id === activeList.value)) {
activeList.value = lists.value.find((item) => item.is_inbox)?.id || lists.value[0]?.id || ''
}
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
}
async function loadAll() {
loading.value = true; error.value = ''
try {
const [folderData, listData, tagData] = await Promise.all([
api('/folders'), api('/lists'), api('/tags').catch(() => []),
])
folders.value = folderData; lists.value = listData; tags.value = tagData
activeList.value ||= lists.value.find((item) => item.is_inbox)?.id || lists.value[0]?.id || ''
if (!navigationLoaded.value) await loadNavigation()
await loadTasksPage()
if (page.value > totalPages.value) { page.value = totalPages.value; await loadTasksPage() }
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
} catch (reason) { fail(reason) } finally { loading.value = false }
}
async function refreshAll() {
navigationLoaded.value = false
await loadAll()
}
async function loadTrashPage() {
const data = await api(`/trash?page=${page.value}&page_size=${pageSize}`)
trash.value = data.items ?? []
@@ -176,6 +189,8 @@ async function switchView(view: View, listId?: string) {
page.value = 1
selectedTask.value = null; mobileSidebar.value = false; mobileDetail.value = false
if (view === 'trash') await loadTrash()
else if (view === 'calendar') return
else if (!isTaskView(view)) tasks.value = []
else await loadAll()
}
async function addTask() {
@@ -238,7 +253,7 @@ async function renameEntity(kind: 'folders' | 'lists', item: FolderItem | TaskLi
}
async function deleteEntity(kind: 'folders' | 'lists', item: FolderItem | TaskList) {
if (!window.confirm(`删除“${item.name}”?`)) return
try { await api(`/${kind}/${item.id}`, { method: 'DELETE' }); await loadAll(); toast('已删除') } catch (reason) { fail(reason) }
try { await api(`/${kind}/${item.id}`, { method: 'DELETE' }); await refreshAll(); toast('已删除') } catch (reason) { fail(reason) }
}
async function createTag() {
const name = window.prompt('标签名称')?.trim(); if (!name) return
@@ -256,12 +271,12 @@ function focusQuick() { nextTick(() => document.querySelector<HTMLInputElement>(
function previousPage() {
if (page.value <= 1 || loading.value) return
page.value -= 1
activeView.value === 'trash' ? loadTrash() : loadAll()
activeView.value === 'trash' ? loadTrash() : isTaskView(activeView.value) ? loadAll() : undefined
}
function nextPage() {
if (page.value >= totalPages.value || loading.value) return
page.value += 1
activeView.value === 'trash' ? loadTrash() : loadAll()
activeView.value === 'trash' ? loadTrash() : isTaskView(activeView.value) ? loadAll() : undefined
}
onMounted(bootstrap)
@@ -308,7 +323,7 @@ onMounted(bootstrap)
<label class="search"><Search/><input v-model="query" placeholder="搜索任务…" aria-label="搜索任务"><kbd> K</kbd></label>
</header>
<template v-if="['calendar','habits','settings'].includes(activeView)">
<MvpPanel :key="activeView" :view="activeView as 'calendar'|'habits'|'settings'" :tasks="tasks" @changed="loadAll" @notice="toast" />
<MvpPanel :key="activeView" :view="activeView as 'calendar'|'habits'|'settings'" @changed="refreshAll" @notice="toast" />
</template>
<template v-else>
<form v-if="activeView!=='trash'" class="quick" @submit.prevent="addTask"><CirclePlus/><input v-model="title" class="quick-input" placeholder="添加任务,按回车保存"><button>添加</button></form>