From 3c2fa91fc6d787c3eebe16eace7f6caa0159753c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 1 Jul 2026 00:10:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=90=8E=E7=AB=AF=E7=9C=9F=E5=88=86=E9=A1=B5?= =?UTF-8?q?(limit/offset=20=E4=B8=8B=E6=8E=A8=20+=20count=20=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 count_by_query 方法(复用 list_by_query 的 WHERE 逻辑) - 前端 buildTaskQuery 传 limit/offset 到后端(SQL 下推分页) - 去除前端客户端 slice(后端已分页,直接分组) - Paginator total 用 totalTasks(当前用当前页条数,后续接 count IPC) --- crates/df-storage/src/crud/task_repo.rs | 54 +++++++++++++++++++++++++ src/views/Tasks.vue | 19 ++++----- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/crates/df-storage/src/crud/task_repo.rs b/crates/df-storage/src/crud/task_repo.rs index e7da9b4..0be7cd1 100644 --- a/crates/df-storage/src/crud/task_repo.rs +++ b/crates/df-storage/src/crud/task_repo.rs @@ -437,6 +437,60 @@ impl TaskRepo { .map_err(storage_err)? } + /// 按 TaskQuery 条件计数(不含 limit/offset,用于分页 total)。 + /// + /// 复用 list_by_query 的 WHERE 构造逻辑(仅 WHERE,无 ORDER BY/LIMIT), + /// 返回满足条件的总行数(忽略分页裁剪)。 + pub async fn count_by_query(&self, query: &TaskQuery) -> Result { + let conn = self.conn.clone(); + let project_id = query.project_id.clone(); + let status = query.status.clone(); + let priority = query.priority; + let keyword = query.keyword.clone(); + + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + + let mut where_clauses: Vec = vec!["deleted_at IS NULL".to_string()]; + let mut params_vec: Vec> = Vec::new(); + + if let Some(ref pid) = project_id { + where_clauses.push(format!("project_id = ?{}", params_vec.len() + 1)); + params_vec.push(Box::new(pid.clone())); + } + if let Some(ref s) = status { + where_clauses.push(format!("status = ?{}", params_vec.len() + 1)); + params_vec.push(Box::new(s.clone())); + } + if let Some(p) = priority { + where_clauses.push(format!("priority = ?{}", params_vec.len() + 1)); + params_vec.push(Box::new(p)); + } + if let Some(ref kw) = keyword { + let escaped = kw.replace('%', "\\%").replace('_', "\\_"); + let pat = format!("%{escaped}%"); + let p1 = params_vec.len() + 1; + let p2 = p1 + 1; + where_clauses.push(format!("(title LIKE ?{p1} OR description LIKE ?{p2}) ESCAPE '\\'")); + params_vec.push(Box::new(pat.clone())); + params_vec.push(Box::new(pat)); + } + + let sql = format!( + "SELECT COUNT(*) FROM tasks WHERE {}", + where_clauses.join(" AND ") + ); + let param_refs: Vec<&dyn rusqlite::ToSql> = + params_vec.iter().map(|p| p.as_ref()).collect(); + let count: i64 = guard + .query_row(&sql, param_refs.as_slice(), |row| row.get(0)) + .map_err(storage_err)?; + Ok(count) + }) + .await + .map_err(|e| storage_err(format!("count_by_query join failed: {e}")))? + } + /// 查某父任务的全部子任务(parent_id = ?,deleted_at IS NULL),按创建时间升序。 /// /// 知识图谱 Phase 1 V29(对标设计 §2.1 父任务聚合规则):父任务=容器模型,status diff --git a/src/views/Tasks.vue b/src/views/Tasks.vue index dc68a6f..99c9234 100644 --- a/src/views/Tasks.vue +++ b/src/views/Tasks.vue @@ -115,7 +115,7 @@ @@ -182,6 +182,7 @@ const loading = ref(false) // 分页(默认开启 20 条/页) const page = ref(1) const pageSize = ref(20) +const totalTasks = ref(0) // 分组折叠状态(localStorage 记忆) const collapsedGroups = reactive(new Set()) @@ -292,7 +293,7 @@ const projectIcons: Record = { } const filteredGroups = computed(() => { - // 排序 + // 后端已分页(limit/offset),store.tasks 是当前页数据,直接分组无需再 slice const sorted = [...store.tasks] sorted.sort((a: any, b: any) => { if (sortBy.value === 'priority') return (a.priority ?? 2) - (b.priority ?? 2) @@ -302,14 +303,9 @@ const filteredGroups = computed(() => { return String(bv).localeCompare(String(av)) }) - // 分页切片 - const filtered = pageSize.value > 0 - ? sorted.slice((page.value - 1) * pageSize.value, page.value * pageSize.value) - : sorted - // 按项目分组 const groupMap = new Map() - for (const task of filtered) { + for (const task of sorted) { if (!groupMap.has(task.project_id)) { groupMap.set(task.project_id, []) } @@ -332,11 +328,15 @@ function buildTaskQuery(): TaskQuery | undefined { const projAll = activeProject.value === 'all' const statusAll = activeStatus.value === 'all' const kw = searchKeyword.value.trim() - if (projAll && statusAll && !kw) return undefined const query: TaskQuery = { order_by: sortBy.value } if (!projAll) query.project_id = activeProject.value if (!statusAll) query.status = activeStatus.value if (kw) query.keyword = kw + // 后端真分页:传 limit/offset,配合 count_by_query 获取 total + if (pageSize.value > 0) { + query.limit = pageSize.value + query.offset = (page.value - 1) * pageSize.value + } return query } @@ -344,6 +344,7 @@ async function refresh() { loading.value = true try { await store.loadTasks(buildTaskQuery()) + totalTasks.value = store.tasks.length } finally { loading.value = false }