Files
DevFlow/src/stores/project.ts
T

215 lines
10 KiB
TypeScript

import { reactive, computed } from 'vue'
import { listen } from '@tauri-apps/api/event'
import { createProjectsStore } from './project/projects'
import { createTasksStore } from './project/tasks'
import { createIdeasStore } from './project/ideas'
import { createWorkflowStore } from './project/workflow'
import { state, clearError } from './project/state'
import { taskApi } from '@/api/task'
import type { DfDataChangedPayload, TaskQuery, IdeaQuery } from '@/api/types'
// ── barrel 组合器(零行为变更,纯重构) ──
//
// 原 project.ts 是上帝 store(项目/任务/灵感/工作流多领域 + 越层 invoke 审批 IPC)。
// 已拆为 src/stores/project/{projects,tasks,ideas,workflow}.ts 四子 store,
// 共享 src/stores/project/state.ts 单例 state。
//
// 本文件改为组合器:内部实例化四子 store + re-export 所有成员,
// 使现有 import { useProjectStore } 的 view 零改动。
// 越层 invoke(approve_human_approval / cancel_human_node) 已下沉到 src/api/workflow.ts。
function createStore() {
const projectsStore = createProjectsStore()
const tasksStore = createTasksStore()
const ideasStore = createIdeasStore()
const workflowStore = createWorkflowStore()
// ── AR-11(方案A):数据变更联动刷新 ──
// 后端 AI 工具(create/update/delete/restore/purge/bind_directory)执行成功后 emit
// "df-data-changed" { entity, action },前端按 entity 调对应 load 刷新列表,
// 取代工具执行后需手动刷新。
//
// CR-260615-26(R-1): task 分支不再无参 loadTasks(会把 store.tasks 冲成全量,破坏
// B-260615-29「store.tasks 反映当前筛选」契约)。改为读 _activeTaskProject:
// - undefined: Tasks 视图未挂载/从未设过筛选 → 不触发 task load(避免破坏筛选,
// 让 B-29 watch/handle 主导;此时改动由用户下次进 Tasks 自然刷新)
// - 'all' : 全量拉取
// - 具体 id : 按项目拉取
// 由 Tasks.vue 经 setActiveTaskProject 在 onMounted/watch 更新本变量。
//
// F-260621-02(P1 status 下沉):新增 _activeTaskStatus 追踪当前 status 筛选,数据变更
// 联动刷新时一并下沉到后端 query(保 B-29「store.tasks 反映当前筛选」契约,
// 避免刷新把已选 status 冲掉)。
let _activeTaskProject: string | undefined = undefined
let _activeTaskStatus: string | undefined = undefined
// WK-10:Tasks 视图搜索关键字登记(数据变更联动刷新时一并下沉 keyword,避免冲掉搜索结果)
let _activeTaskKeyword: string | undefined = undefined
function setActiveTaskProject(projectKey: string | undefined) {
_activeTaskProject = projectKey
}
function setActiveTaskStatus(statusKey: string | undefined) {
_activeTaskStatus = statusKey
}
function setActiveTaskKeyword(keyword: string | undefined) {
_activeTaskKeyword = keyword
}
// 构造当前 task 视图筛选对应的 query(供数据变更 listener 复用同一筛选拉取)
function buildActiveTaskQuery(): TaskQuery | undefined {
if (_activeTaskProject === undefined) return undefined // 视图未挂载,不触发
const query: TaskQuery = {}
if (_activeTaskProject !== 'all') query.project_id = _activeTaskProject
if (_activeTaskStatus && _activeTaskStatus !== 'all') query.status = _activeTaskStatus
if (_activeTaskKeyword) query.keyword = _activeTaskKeyword
return query
}
// WK-4:Ideas 视图当前查询登记(数据变更联动刷新按当前筛选重载,不再无参 loadIdeas 全量冲掉筛选)。
// undefined = Ideas 视图未挂载(如 Dashboard)→ 退化为全量刷新,保持面板统计新鲜。
let _activeIdeaQuery: IdeaQuery | undefined = undefined
function setActiveIdeaQuery(q: IdeaQuery | undefined) {
_activeIdeaQuery = q
}
let _dataChangedUnlisten: (() => void) | null = null
async function startDataChangedListener() {
if (_dataChangedUnlisten) return _dataChangedUnlisten
_dataChangedUnlisten = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
const { entity } = event.payload
// 按 entity 调对应 load 刷新(CR-260615-19:action 当前未消费,为未来 delete 本地移除等差异化刷新预留,统一 load 全量拉取)
if (entity === 'project') {
void projectsStore.loadProjects()
} else if (entity === 'task') {
// 无筛选状态(_activeTaskProject undefined)时不触发,保 B-29 筛选契约;
// 有筛选则按当前 activeProject + activeStatus 构造 query 下沉后端拉取
const q = buildActiveTaskQuery()
if (q !== undefined) {
void tasksStore.loadTasks(q)
}
// P1-b A0:统计走独立通道,任务变更(create/update/advance/delete)后刷新
// Dashboard 数字(与 Tasks 视图分页/筛选解耦,不受 q undefined 影响)
void loadStats()
} else if (entity === 'idea') {
// WK-4:按 Ideas 视图当前 query 重载(未挂载时退化为全量刷新 Dashboard 面板)
void ideasStore.loadIdeas(_activeIdeaQuery)
}
})
return _dataChangedUnlisten
}
function stopDataChangedListener() {
if (_dataChangedUnlisten) {
try {
_dataChangedUnlisten()
} catch (e) {
console.error('停止数据变更监听失败:', e)
}
_dataChangedUnlisten = null
}
}
// ── 统计(跨领域,留在组合器) ──
// P1-b A0:activeTasks 改读 state.activeTaskCount(后端 COUNT 独立通道),
// 不再 filter(state.tasks) —— state.tasks 是 Tasks 视图当前筛选+分页数据,
// 直接 filter 会让 Dashboard 数字随 Tasks 翻页/筛选错乱。
const stats = computed(() => ({
ideas: state.ideas.length,
projects: state.projects.length,
activeTasks: state.activeTaskCount,
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({
// 状态用 getter 实时读 state —— 直接 ideas: state.ideas 会快照引用,
// loadIdeas 等重新赋值 state.ideas 时返回对象的 ideas 属性不跟随,刷新后视图为空
get projects() { return state.projects },
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 workflowExecutions() { return state.workflowExecutions },
get liveEvents() { return state.liveEvents },
get loading() { return state.loading },
get error() { return state.error },
get deletedProjects() { return state.deletedProjects },
clearError,
// project actions
loadProjects: projectsStore.loadProjects,
createProject: projectsStore.createProject,
importProject: projectsStore.importProject,
updateProject: projectsStore.updateProject,
deleteProject: projectsStore.deleteProject,
relocateProjectPath: projectsStore.relocateProjectPath,
loadDeletedProjects: projectsStore.loadDeletedProjects,
restoreProject: projectsStore.restoreProject,
purgeProject: projectsStore.purgeProject,
// task actions
loadTasks: tasksStore.loadTasks,
createTask: tasksStore.createTask,
updateTask: tasksStore.updateTask,
deleteTask: tasksStore.deleteTask,
// P1-b A0:统计独立通道(Dashboard 卡片 + ActiveProjectsPanel 各项目任务数)
loadStats,
// idea actions
loadIdeas: ideasStore.loadIdeas,
createIdea: ideasStore.createIdea,
updateIdea: ideasStore.updateIdea,
deleteIdea: ideasStore.deleteIdea,
evaluateIdea: ideasStore.evaluateIdea,
promoteIdea: ideasStore.promoteIdea,
relateIdeas: ideasStore.relateIdeas,
// workflow actions
runWorkflow: workflowStore.runWorkflow,
loadWorkflowExecutions: workflowStore.loadWorkflowExecutions,
startEventListener: workflowStore.startEventListener,
stopEventListener: workflowStore.stopEventListener,
clearLiveEvents: workflowStore.clearLiveEvents,
// B-41: 工作流进度共享推导(按 execId 派生节点状态/进度)
workflowProgress: workflowStore.workflowProgress,
approveHumanApproval: workflowStore.approveHumanApproval,
cancelHumanApproval: workflowStore.cancelHumanApproval,
// AR-11(方案A):数据变更联动刷新监听(后端工具执行后 emit df-data-changed)
startDataChangedListener,
stopDataChangedListener,
// CR-260615-26(R-1): Tasks 视图登记当前 activeProject 筛选,供 listener 按筛选拉取
setActiveTaskProject,
// F-260621-02(P1 status 下沉):Tasks 视图登记当前 status 筛选,供 listener 复用同一筛选拉取
setActiveTaskStatus,
// WK-10:Tasks 视图登记当前搜索关键字,供 listener 复用同一筛选拉取
setActiveTaskKeyword,
// WK-4: Ideas 视图登记当前查询,供 listener 按筛选重载(不冲掉筛选视图)
setActiveIdeaQuery,
pendingApproval: computed(() => state.pendingApproval),
// computed
stats,
})
}
type ProjectStore = ReturnType<typeof createStore>
let _storeInstance: ProjectStore | null = null
/** 项目/任务/灵感/工作流 全局状态(单例,多组件复用同一 reactive 包装) */
export function useProjectStore(): ProjectStore {
if (_storeInstance) return _storeInstance
_storeInstance = createStore()
return _storeInstance
}