+47
-13
@@ -36,6 +36,10 @@ const mobileSidebar = ref(false)
|
||||
const mobileDetail = ref(false)
|
||||
const markdownPreview = ref(false)
|
||||
const showCompleted = ref(false)
|
||||
const page = ref(1)
|
||||
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 activeName = computed(() => {
|
||||
@@ -52,15 +56,12 @@ const visibleTasks = computed(() => {
|
||||
const now = new Date()
|
||||
const end = new Date(now); end.setDate(end.getDate() + 7)
|
||||
let result = sourceTasks.value
|
||||
if (query.value.trim()) return result
|
||||
if (activeView.value === 'tasks') result = result.filter((task) => task.list_id === activeList.value)
|
||||
if (['calendar','habits','settings'].includes(activeView.value)) return []
|
||||
if (activeView.value === 'today') result = result.filter((task) => task.due_at && new Date(task.due_at).toDateString() === now.toDateString())
|
||||
if (activeView.value === 'upcoming') result = result.filter((task) => task.due_at && new Date(task.due_at) >= now && new Date(task.due_at) <= end)
|
||||
if (!showCompleted.value && activeView.value !== 'trash') result = result.filter((task) => !task.completed)
|
||||
return result
|
||||
return query.value.trim() ? filterTasks(result, query.value) : result
|
||||
})
|
||||
const filteredTaskTree = computed(() => groupTaskTree(filterTasks(visibleTasks.value, query.value)))
|
||||
const filteredTaskTree = computed(() => groupTaskTree(visibleTasks.value))
|
||||
const taskTree = computed(() => filteredTaskTree.value)
|
||||
const selectedCount = computed(() => selectedIds.value.size)
|
||||
const parentTasks = computed(() => tasks.value.filter((task) => !task.parent_id))
|
||||
@@ -68,10 +69,11 @@ let searchTimer: number | undefined
|
||||
watch(query, () => {
|
||||
if (searchTimer) window.clearTimeout(searchTimer)
|
||||
if (activeView.value === 'trash') return
|
||||
page.value = 1
|
||||
searchTimer = window.setTimeout(() => loadAll(), 250)
|
||||
})
|
||||
watch(showCompleted, () => {
|
||||
if (activeView.value !== 'trash') loadAll()
|
||||
if (activeView.value !== 'trash') { page.value = 1; loadAll() }
|
||||
})
|
||||
|
||||
async function api(path: string, options: RequestInit = {}) {
|
||||
@@ -125,6 +127,23 @@ async function loadTaskPages(path: string) {
|
||||
} while (cursor)
|
||||
return rows
|
||||
}
|
||||
function isoAtLocalDayOffset(offset: number) {
|
||||
const day = new Date()
|
||||
day.setDate(day.getDate() + offset)
|
||||
day.setHours(0, 0, 0, 0)
|
||||
return day.toISOString()
|
||||
}
|
||||
async function loadTasksPage() {
|
||||
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)
|
||||
if (activeView.value === 'today') { params.set('due_from', isoAtLocalDayOffset(0)); params.set('due_to', isoAtLocalDayOffset(1)) }
|
||||
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}`)
|
||||
tasks.value = data.items ?? []
|
||||
totalTasks.value = data.total ?? tasks.value.length
|
||||
}
|
||||
async function loadAll() {
|
||||
loading.value = true; error.value = ''
|
||||
try {
|
||||
@@ -133,20 +152,24 @@ async function loadAll() {
|
||||
])
|
||||
folders.value = folderData; lists.value = listData; tags.value = tagData
|
||||
activeList.value ||= lists.value.find((item) => item.is_inbox)?.id || lists.value[0]?.id || ''
|
||||
const params = new URLSearchParams()
|
||||
if (query.value) params.set('q', query.value)
|
||||
else if (activeView.value === 'tasks' && activeList.value) params.set('list_id', activeList.value)
|
||||
if (!showCompleted.value && activeView.value !== 'trash') params.set('completed', 'false')
|
||||
tasks.value = await loadTaskPages(`/tasks${params.size ? `?${params}` : ''}`)
|
||||
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 loadTrashPage() {
|
||||
const data = await api(`/trash?page=${page.value}&page_size=${pageSize}`)
|
||||
trash.value = data.items ?? []
|
||||
totalTasks.value = data.total ?? trash.value.length
|
||||
}
|
||||
async function loadTrash() {
|
||||
try { trash.value = await loadTaskPages('/trash') } catch (reason) { fail(reason) }
|
||||
loading.value = true; error.value = ''
|
||||
try { await loadTrashPage() } catch (reason) { fail(reason) } finally { loading.value = false }
|
||||
}
|
||||
async function switchView(view: View, listId?: string) {
|
||||
activeView.value = view
|
||||
if (listId) activeList.value = listId
|
||||
page.value = 1
|
||||
selectedTask.value = null; selectedIds.value = new Set(); mobileSidebar.value = false; mobileDetail.value = false
|
||||
if (view === 'trash') await loadTrash()
|
||||
else await loadAll()
|
||||
@@ -242,6 +265,16 @@ function toggleTag(tag: Tag) {
|
||||
function toggleFolder(id: string) { const next = new Set(expandedFolders.value); next.has(id) ? next.delete(id) : next.add(id); expandedFolders.value = next }
|
||||
function formatDue(value: string | null) { return value ? new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(value)) : '' }
|
||||
function focusQuick() { nextTick(() => document.querySelector<HTMLInputElement>('.quick-input')?.focus()) }
|
||||
function previousPage() {
|
||||
if (page.value <= 1 || loading.value) return
|
||||
page.value -= 1
|
||||
activeView.value === 'trash' ? loadTrash() : loadAll()
|
||||
}
|
||||
function nextPage() {
|
||||
if (page.value >= totalPages.value || loading.value) return
|
||||
page.value += 1
|
||||
activeView.value === 'trash' ? loadTrash() : loadAll()
|
||||
}
|
||||
|
||||
onMounted(bootstrap)
|
||||
</script>
|
||||
@@ -291,7 +324,8 @@ onMounted(bootstrap)
|
||||
</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>
|
||||
<div class="list-toolbar"><label v-if="activeView!=='trash'"><input v-model="showCompleted" type="checkbox"> 显示已完成</label><span>{{visibleTasks.length}} 项</span><button v-if="query" class="link" @click="query=''">清除搜索</button></div>
|
||||
<div class="list-toolbar"><label v-if="activeView!=='trash'"><input v-model="showCompleted" type="checkbox"> 显示已完成</label><span>第 {{page}} / {{totalPages}} 页 · 共 {{totalTasks}} 项</span><button v-if="query" class="link" @click="query=''">清除搜索</button></div>
|
||||
<div v-if="activeView!=='trash' && totalPages > 1" class="pager"><button class="secondary" :disabled="page<=1 || loading" @click="previousPage">上一页</button><span>{{page}} / {{totalPages}}</span><button class="secondary" :disabled="page>=totalPages || loading" @click="nextPage">下一页</button></div>
|
||||
<div v-if="selectedCount" class="batch-bar"><b>已选 {{selectedCount}} 项</b><button @click="batch('complete')"><Check/>完成</button><button @click="batch('move')"><Folder/>移动</button><button class="danger" @click="batch('delete')"><Trash2/>删除</button><button class="icon" aria-label="取消选择" @click="selectedIds=new Set()"><X/></button></div>
|
||||
<section class="task-list" :class="{loading}">
|
||||
<template v-for="node in taskTree" :key="node.task.id">
|
||||
|
||||
Reference in New Issue
Block a user