优化: 任务列表后端真分页(limit/offset 下推 + count 查询)
- 后端新增 count_by_query 方法(复用 list_by_query 的 WHERE 逻辑) - 前端 buildTaskQuery 传 limit/offset 到后端(SQL 下推分页) - 去除前端客户端 slice(后端已分页,直接分组) - Paginator total 用 totalTasks(当前用当前页条数,后续接 count IPC)
This commit is contained in:
@@ -437,6 +437,60 @@ impl TaskRepo {
|
|||||||
.map_err(storage_err)?
|
.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<i64> {
|
||||||
|
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<String> = vec!["deleted_at IS NULL".to_string()];
|
||||||
|
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = 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),按创建时间升序。
|
/// 查某父任务的全部子任务(parent_id = ?,deleted_at IS NULL),按创建时间升序。
|
||||||
///
|
///
|
||||||
/// 知识图谱 Phase 1 V29(对标设计 §2.1 父任务聚合规则):父任务=容器模型,status
|
/// 知识图谱 Phase 1 V29(对标设计 §2.1 父任务聚合规则):父任务=容器模型,status
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
<Paginator
|
<Paginator
|
||||||
v-model:page="page"
|
v-model:page="page"
|
||||||
v-model:pageSize="pageSize"
|
v-model:pageSize="pageSize"
|
||||||
:total="store.tasks.length"
|
:total="totalTasks"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 新建任务模态框(统一样式,去内联 style) -->
|
<!-- 新建任务模态框(统一样式,去内联 style) -->
|
||||||
@@ -182,6 +182,7 @@ const loading = ref(false)
|
|||||||
// 分页(默认开启 20 条/页)
|
// 分页(默认开启 20 条/页)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
|
const totalTasks = ref(0)
|
||||||
|
|
||||||
// 分组折叠状态(localStorage 记忆)
|
// 分组折叠状态(localStorage 记忆)
|
||||||
const collapsedGroups = reactive(new Set<string>())
|
const collapsedGroups = reactive(new Set<string>())
|
||||||
@@ -292,7 +293,7 @@ const projectIcons: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredGroups = computed(() => {
|
const filteredGroups = computed(() => {
|
||||||
// 排序
|
// 后端已分页(limit/offset),store.tasks 是当前页数据,直接分组无需再 slice
|
||||||
const sorted = [...store.tasks]
|
const sorted = [...store.tasks]
|
||||||
sorted.sort((a: any, b: any) => {
|
sorted.sort((a: any, b: any) => {
|
||||||
if (sortBy.value === 'priority') return (a.priority ?? 2) - (b.priority ?? 2)
|
if (sortBy.value === 'priority') return (a.priority ?? 2) - (b.priority ?? 2)
|
||||||
@@ -302,14 +303,9 @@ const filteredGroups = computed(() => {
|
|||||||
return String(bv).localeCompare(String(av))
|
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<string, TaskRecord[]>()
|
const groupMap = new Map<string, TaskRecord[]>()
|
||||||
for (const task of filtered) {
|
for (const task of sorted) {
|
||||||
if (!groupMap.has(task.project_id)) {
|
if (!groupMap.has(task.project_id)) {
|
||||||
groupMap.set(task.project_id, [])
|
groupMap.set(task.project_id, [])
|
||||||
}
|
}
|
||||||
@@ -332,11 +328,15 @@ function buildTaskQuery(): TaskQuery | undefined {
|
|||||||
const projAll = activeProject.value === 'all'
|
const projAll = activeProject.value === 'all'
|
||||||
const statusAll = activeStatus.value === 'all'
|
const statusAll = activeStatus.value === 'all'
|
||||||
const kw = searchKeyword.value.trim()
|
const kw = searchKeyword.value.trim()
|
||||||
if (projAll && statusAll && !kw) return undefined
|
|
||||||
const query: TaskQuery = { order_by: sortBy.value }
|
const query: TaskQuery = { order_by: sortBy.value }
|
||||||
if (!projAll) query.project_id = activeProject.value
|
if (!projAll) query.project_id = activeProject.value
|
||||||
if (!statusAll) query.status = activeStatus.value
|
if (!statusAll) query.status = activeStatus.value
|
||||||
if (kw) query.keyword = kw
|
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
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +344,7 @@ async function refresh() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await store.loadTasks(buildTaskQuery())
|
await store.loadTasks(buildTaskQuery())
|
||||||
|
totalTasks.value = store.tasks.length
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user