修复: P1-b Dashboard统计源解耦(治翻页/筛选后统计数字错乱 H6/H7)

- 统计走后端 count_tasks 独立通道,不再 filter(state.tasks) 当前页数据

- state 加 activeTaskCount/projectTaskCounts 缓存,loadStats action 并发 count 填充 + df-data-changed task 分支联动刷新

- Dashboard loadAll 删 loadTasks 改 loadStats(不再消费 state.tasks 全量)

- ActiveProjectsPanel getProjectTaskCount 改读 store.projectTaskCounts

- 后端零改(复用已注册 count_tasks IPC + count_by_query)
This commit is contained in:
lxy
2026-08-02 12:29:44 +08:00
parent 9e85cadb3f
commit d1321af880
4 changed files with 45 additions and 4 deletions
@@ -51,8 +51,11 @@ function goDetail(projectId: string) {
router.push(`/projects/${projectId}`) router.push(`/projects/${projectId}`)
} }
// P1-b A0:统计走后端 count_tasks 独立通道,不再 filter(store.tasks)。
// store.tasks 是 Tasks 视图当前筛选+分页数据,filter 会让面板数字随 Tasks 翻页/筛选错乱。
// store.projectTaskCounts[projectId] = 后端该项目 in_progress 任务数(loadStats 填充)。
function getProjectTaskCount(projectId: string): number { function getProjectTaskCount(projectId: string): number {
return store.tasks.filter(t => t.project_id === projectId && t.status === 'in_progress').length return store.projectTaskCounts[projectId] ?? 0
} }
// 阶段 label key:projectStageInfo 返回的 stage 值(coding/testing/release/planning) // 阶段 label key:projectStageInfo 返回的 stage 值(coding/testing/release/planning)
+31 -1
View File
@@ -5,6 +5,7 @@ import { createTasksStore } from './project/tasks'
import { createIdeasStore } from './project/ideas' import { createIdeasStore } from './project/ideas'
import { createWorkflowStore } from './project/workflow' import { createWorkflowStore } from './project/workflow'
import { state, clearError } from './project/state' import { state, clearError } from './project/state'
import { taskApi } from '@/api/task'
import type { DfDataChangedPayload, TaskQuery } from '@/api/types' import type { DfDataChangedPayload, TaskQuery } from '@/api/types'
// ── barrel 组合器(零行为变更,纯重构) ── // ── barrel 组合器(零行为变更,纯重构) ──
@@ -71,6 +72,9 @@ function createStore() {
if (q !== undefined) { if (q !== undefined) {
void tasksStore.loadTasks(q) void tasksStore.loadTasks(q)
} }
// P1-b A0:统计走独立通道,任务变更(create/update/advance/delete)后刷新
// Dashboard 数字(与 Tasks 视图分页/筛选解耦,不受 q undefined 影响)
void loadStats()
} else if (entity === 'idea') { } else if (entity === 'idea') {
void ideasStore.loadIdeas() void ideasStore.loadIdeas()
} }
@@ -90,18 +94,42 @@ function createStore() {
} }
// ── 统计(跨领域,留在组合器) ── // ── 统计(跨领域,留在组合器) ──
// P1-b A0:activeTasks 改读 state.activeTaskCount(后端 COUNT 独立通道),
// 不再 filter(state.tasks) —— state.tasks 是 Tasks 视图当前筛选+分页数据,
// 直接 filter 会让 Dashboard 数字随 Tasks 翻页/筛选错乱。
const stats = computed(() => ({ const stats = computed(() => ({
ideas: state.ideas.length, ideas: state.ideas.length,
projects: state.projects.length, projects: state.projects.length,
activeTasks: state.tasks.filter(t => t.status === 'in_progress').length, activeTasks: state.activeTaskCount,
drafts: state.ideas.filter(i => i.status === 'draft').length, drafts: state.ideas.filter(i => i.status === 'draft').length,
})) }))
// P1-b A0:统计拉取走后端 count_tasks(独立通道),与 tasks 数组解耦。
// activeTaskCount = 全局 in_progress 任务数;projectTaskCounts = 各项目 in_progress 任务数。
// tasks 变更(create/update/delete/advance)后由 df-data-changed listener 调用刷新。
// 注:projectTaskCounts 依赖 state.projects 已加载 —— 调用方需保证先 loadProjects。
// - Dashboard.loadAll:先 Promise.all([loadProjects, loadIdeas]) 完成再 loadStats
// - df-data-changed task 分支:projects 由 onMounted 已加载,直接 loadStats 安全
async function loadStats() {
try {
state.activeTaskCount = await taskApi.count({ status: 'in_progress' } as TaskQuery)
const entries = await Promise.all(state.projects.map(p =>
taskApi.count({ project_id: p.id, status: 'in_progress' } as TaskQuery).then(n => [p.id, n] as const)
))
state.projectTaskCounts = Object.fromEntries(entries)
} catch (e) {
console.error('[loadStats] 统计拉取失败', e)
}
}
return reactive({ return reactive({
// 状态用 getter 实时读 state —— 直接 ideas: state.ideas 会快照引用, // 状态用 getter 实时读 state —— 直接 ideas: state.ideas 会快照引用,
// loadIdeas 等重新赋值 state.ideas 时返回对象的 ideas 属性不跟随,刷新后视图为空 // loadIdeas 等重新赋值 state.ideas 时返回对象的 ideas 属性不跟随,刷新后视图为空
get projects() { return state.projects }, get projects() { return state.projects },
get tasks() { return state.tasks }, get tasks() { return state.tasks },
// P1-b A0:统计缓存读口(供 ActiveProjectsPanel 读 projectTaskCounts)
get activeTaskCount() { return state.activeTaskCount },
get projectTaskCounts() { return state.projectTaskCounts },
get ideas() { return state.ideas }, get ideas() { return state.ideas },
get workflowExecutions() { return state.workflowExecutions }, get workflowExecutions() { return state.workflowExecutions },
get liveEvents() { return state.liveEvents }, get liveEvents() { return state.liveEvents },
@@ -124,6 +152,8 @@ function createStore() {
createTask: tasksStore.createTask, createTask: tasksStore.createTask,
updateTask: tasksStore.updateTask, updateTask: tasksStore.updateTask,
deleteTask: tasksStore.deleteTask, deleteTask: tasksStore.deleteTask,
// P1-b A0:统计独立通道(Dashboard 卡片 + ActiveProjectsPanel 各项目任务数)
loadStats,
// idea actions // idea actions
loadIdeas: ideasStore.loadIdeas, loadIdeas: ideasStore.loadIdeas,
createIdea: ideasStore.createIdea, createIdea: ideasStore.createIdea,
+3
View File
@@ -17,6 +17,9 @@ export const state = reactive({
projects: [] as ProjectRecord[], projects: [] as ProjectRecord[],
deletedProjects: [] as ProjectRecord[], deletedProjects: [] as ProjectRecord[],
tasks: [] as TaskRecord[], tasks: [] as TaskRecord[],
// 统计缓存,与 tasks 数组解耦,供 Dashboard,不被 Tasks 分页/筛选污染(P1-b A0)
activeTaskCount: 0 as number,
projectTaskCounts: {} as Record<string, number>,
ideas: [] as IdeaRecord[], ideas: [] as IdeaRecord[],
workflowExecutions: [] as WorkflowRecord[], workflowExecutions: [] as WorkflowRecord[],
liveEvents: [] as Array<WorkflowEventPayload & { _ts: number }>, liveEvents: [] as Array<WorkflowEventPayload & { _ts: number }>,
+7 -2
View File
@@ -60,14 +60,19 @@ const { t } = useI18n()
const loading = ref(false) const loading = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
// 统一加载入口(projects/tasks/ideas 并行),管理 loading + errorMsg // 统一加载入口(projects/ideas 并行 + 统计独立通道),管理 loading + errorMsg
// refresh 与 onMounted 共用,避免两处 catch 逻辑重复 // refresh 与 onMounted 共用,避免两处 catch 逻辑重复
// P1-b A0:不再调 store.loadTasks() —— Dashboard 模板不消费 state.tasks,
// 统计(activeTaskCount/projectTaskCounts)走 loadStats() 独立通道(后端 count_tasks),
// 不被 Tasks 视图分页/筛选污染。Tasks.vue 自行 loadTasks(onMounted/筛选/变更)。
// loadStats 依赖 state.projects(算 projectTaskCounts),故 loadProjects 完成后再调。
async function loadAll() { async function loadAll() {
if (loading.value) return // 防重复触发 if (loading.value) return // 防重复触发
loading.value = true loading.value = true
errorMsg.value = '' errorMsg.value = ''
try { try {
await Promise.all([store.loadProjects(), store.loadTasks(), store.loadIdeas()]) await Promise.all([store.loadProjects(), store.loadIdeas()])
await store.loadStats()
} catch (e: any) { } catch (e: any) {
console.error('Dashboard load failed:', e) console.error('Dashboard load failed:', e)
errorMsg.value = e?.toString() ?? t('dashboard.err.loadFailed') errorMsg.value = e?.toString() ?? t('dashboard.err.loadFailed')