diff --git a/backend/main.py b/backend/main.py index 231ec9f..170172c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -464,6 +464,8 @@ async def create_task( @app.get("/api/v1/tasks", response_model=TaskPage) async def list_tasks( q: str | None = None, + list_id: UUID | None = None, + completed: bool | None = None, cursor: str | None = None, limit: int = Query(default=50, ge=1, le=100), user: User = Depends(current_user), @@ -472,6 +474,11 @@ async def list_tasks( query = select(Task).where( Task.user_id == user.id, Task.deleted_at.is_(None), Task.parent_id.is_(None) ) + if list_id is not None: + await _owned_list(db, user.id, list_id) + query = query.where(Task.list_id == list_id) + if completed is not None: + query = query.where(Task.completed.is_(completed)) if q: pattern = f"%{q}%" tag_match = exists( diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 85f88a0..efafe79 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -35,7 +35,7 @@ const loading = ref(false) const mobileSidebar = ref(false) const mobileDetail = ref(false) const markdownPreview = ref(false) -const showCompleted = ref(true) +const showCompleted = ref(false) const expandedFolders = ref(new Set()) const activeName = computed(() => { @@ -70,6 +70,9 @@ watch(query, () => { if (activeView.value === 'trash') return searchTimer = window.setTimeout(() => loadAll(), 250) }) +watch(showCompleted, () => { + if (activeView.value !== 'trash') loadAll() +}) async function api(path: string, options: RequestInit = {}) { const response = await fetch('/api/v1' + path, { @@ -125,12 +128,16 @@ async function loadTaskPages(path: string) { async function loadAll() { loading.value = true; error.value = '' try { - const taskPath = query.value ? `/tasks?q=${encodeURIComponent(query.value)}` : '/tasks' - const [folderData, listData, tagData, taskData] = await Promise.all([ - api('/folders'), api('/lists'), api('/tags').catch(() => []), loadTaskPages(taskPath), + const [folderData, listData, tagData] = await Promise.all([ + api('/folders'), api('/lists'), api('/tags').catch(() => []), ]) - folders.value = folderData; lists.value = listData; tags.value = tagData; tasks.value = taskData + 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}` : ''}`) expandedFolders.value = new Set(folders.value.map((folder) => folder.id)) } catch (reason) { fail(reason) } finally { loading.value = false } } @@ -142,6 +149,7 @@ async function switchView(view: View, listId?: string) { if (listId) activeList.value = listId selectedTask.value = null; selectedIds.value = new Set(); mobileSidebar.value = false; mobileDetail.value = false if (view === 'trash') await loadTrash() + else await loadAll() } async function addTask() { if (!title.value.trim() || !activeList.value) return diff --git a/tests/test_mvp_backend.py b/tests/test_mvp_backend.py index 0dc95ce..338d879 100644 --- a/tests/test_mvp_backend.py +++ b/tests/test_mvp_backend.py @@ -107,6 +107,20 @@ def test_attachment_security_ownership_and_size(client, tmp_path, monkeypatch): assert client.delete(f"/api/v1/attachments/{attachment['id']}").status_code == 204 +def test_tasks_can_be_loaded_by_list_and_completion_filter(client): + inbox = boot(client) + other = client.post("/api/v1/lists", json={"name": "其他清单"}).json() + client.post("/api/v1/tasks", json={"title": "收集箱未完成", "list_id": inbox["id"]}) + done = client.post("/api/v1/tasks", json={"title": "收集箱已完成", "list_id": inbox["id"]}).json() + client.patch(f"/api/v1/tasks/{done['id']}", json={"completed": True, "version": done["version"]}) + client.post("/api/v1/tasks", json={"title": "其他清单未完成", "list_id": other["id"]}) + + response = client.get("/api/v1/tasks", params={"list_id": inbox["id"], "completed": False}) + + assert response.status_code == 200 + assert [item["title"] for item in response.json()["items"]] == ["收集箱未完成"] + + def test_ticktick_preview_import_dedupe_and_json_restore(client): boot(client) csv_data = "Title,List Name,Due Date,Status,ID\nImported,Inbox,2026-10-01,0,ext-1\n"