/** * feature `gov-task` —— 任务管理:在途监控、控制事件(暂停/叫停/终止)、恢复、改派、任务日志查看 * * 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移: * DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。 * 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。 */ import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { CONTROL_SEMANTIC_LABEL, TASK_RUNTIME_STATE_LABEL, TERMINAL_STATE_LABEL, WORKFLOW_STATE_LABEL, controlTaskRoute, reassignTaskRoute, resumeTaskRoute, } from '@contracts'; import type { ControlSemantic } from '@contracts'; import { Ico } from '@web-shared/Ico.tsx'; import { call } from '@web-shared/typed-fetch.ts'; import { Ava, Card, Chip, Clear, Entry, Facts, Kpi, NList, NRow, Note, PageHead, QueryGate, Sec, St } from '@web-shared/gov/primitives.tsx'; import type { ActSpec } from '@web-shared/gov/primitives.tsx'; import { APR_ST, PARA, RUNTIME_ST, TERMINAL_ST, WF_ST } from '@web-shared/gov/tone.ts'; import type { Nav } from '@web-shared/gov/gov-route.ts'; import { TriggerOpsTab } from '@features/gov-trigger'; import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx'; import { useGovApprovals, useGovStaff, useGovTaskLog, useGovTasks, useGovWorkflow, } from '@web-shared/gov/gov-api.ts'; export type HomeProps = { tab: string; nav: Nav }; export function TasksHome({ tab, nav }: HomeProps) { // 起单开关 = 触发器的**处置面**:今天开不开是当班主管的事,规则怎么写在设计中心改 if (tab === 'trg') return ; return ; } function RuntimeHome({ tab, nav }: HomeProps) { const q = useGovTasks(); const approvals = useGovApprovals(); const tasks = q.data?.tasks ?? []; const workflows = q.data?.workflows ?? []; /** 工作流不设负责人:要看「这条流现在落在谁身上」,读它当前节点的任务负责人 */ const holderOf = (workflowId: string) => { const live = tasks.find((t) => t.workflowId === workflowId && t.terminal === undefined); if (live) return { name: live.ownerName, label: '当前任务负责人' }; const stuck = tasks.find((t) => t.workflowId === workflowId && t.terminal !== undefined && t.terminal !== 'released'); return stuck ? { name: stuck.ownerName, label: '最后经手 · 待处置' } : undefined; }; if (tab === 'board') { const lanes = [ { k: 'running', tone: 'brand' }, { k: 'waiting', tone: 'warn' }, { k: 'abnormal', tone: 'danger' }, { k: 'completed', tone: 'ok' }, ] as const; return (
{lanes.map((ln) => { const items = workflows.filter((w) => w.state === ln.k); return (
{WORKFLOW_STATE_LABEL[ln.k]} {items.length}
{items.map((w) => { const mine = tasks.filter((t) => t.workflowId === w.id); const done = mine.filter((t) => t.terminal === 'released').length; const total = Math.max(mine.length, 1); const holder = holderOf(w.id); const cur = mine.find((t) => t.terminal === undefined)?.title ?? '—'; return ( ); })} {items.length === 0 &&
—
}
); })}
); } if (tab === 'apr') { const rows = approvals.data?.approvals ?? []; return ( <> 审批点是流程层的通用人裁节点,与活动、网关、触发器、流程包并列为编排层五要素——它是 系统内置要素,声明即用、零开发、不进活动库。它卡到审批人工位产出审批单:通过 / 驳回,附依据;没有预算、没有 Harness、没有任务日志,所以不是任务。 {rows.map((a) => ( {APR_ST[a.state].label} } onClick={() => nav.push({ k: 'wf', id: a.workflowId })} /> ))} {rows.length === 0 && 暂无审批单——流上还没有走到审批点。} ); } if (tab === 'list') { return ( <> 任务 = 工作流上的节点,一个活动 = 一个任务;它派进员工工作台的任务清单,负责人由节点声明的岗位解析而来。 {tasks.map((t) => { const st = t.terminal !== undefined ? TERMINAL_ST[t.terminal] : RUNTIME_ST[t.runtime]; const label = t.terminal !== undefined ? TERMINAL_STATE_LABEL[t.terminal] : TASK_RUNTIME_STATE_LABEL[t.runtime]; return ( {t.usedSteps}/{t.capSteps} ), }, { k: '起始', v: {t.startedAt.slice(5, 16).replace('T', ' ')} }, ]} right={ {label} } onClick={() => nav.push({ k: 'task', id: t.id })} /> ); })} {tasks.length === 0 && 暂无任务——起单后这里会出现。} ); } const abnormal = workflows.filter((w) => w.state === 'abnormal'); const stuck = tasks.filter((t) => t.terminal === 'blocked' || t.terminal === 'exhausted' || t.terminal === 'stalled'); return (
w.state === 'running' || w.state === 'waiting').length} sub="运行中 + 等待中" /> t.terminal === 'released').length} sub="唯一成功终态" />
{abnormal.map((w) => ( nav.push({ k: 'wf', id: w.id })} /> ))} {stuck.map((t) => ( nav.push({ k: 'task', id: t.id })} /> ))} {abnormal.length + stuck.length === 0 && 无待处置——在途实例全部正常推进。}
); } /* ---------------- ② 员工管理 ---------------- */ /** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */ function useGovAction(mutate: (input: TInput) => Promise, keys: readonly string[]) { const queryClient = useQueryClient(); return useMutation({ mutationFn: mutate, onSuccess: () => { for (const key of keys) void queryClient.invalidateQueries({ queryKey: ['gov', key] }); }, }); } /* ---------------- 工作流 ---------------- */ export function WfPage({ id, nav }: { id: string; nav: Nav }) { const workflowId = id.includes(':') ? id.split(':')[0]! : id; const q = useGovWorkflow(workflowId); if (q.data === undefined) return 载入中…; const { workflow, pkg, instances, tasks } = q.data; const st = WF_ST[workflow.state]; return ( <> {WORKFLOW_STATE_LABEL[workflow.state]} {workflow.packageId} v{workflow.packageVersion} 来源 · {workflow.source.kind} · {workflow.source.ref} } desc="工作流状态由它上面的节点(任务与审批单)聚合推导,工作流层不另设状态机;重开产生新的一条工作流,旧的已终、不再入算。" /> {tasks.map((t) => ( nav.push({ k: 'task', id: t.id })} /> ))} {tasks.length === 0 && 暂无任务。}
{instances.map((i) => (
{pkg.nodes.find((n) => n.id === i.nodeId)?.label ?? i.nodeId} {i.instance > 1 ? ` #${i.instance}` : ''} {i.terminal !== undefined ? TERMINAL_STATE_LABEL[i.terminal] : i.running ? '运行中' : i.waiting ? '等待中' : i.activated ? '已激活' : '未激活'}
))}
); } /* ---------------- 任务:在途监控 · 控制 · 改派 ---------------- */ export function TaskPage({ id, nav }: { id: string; nav: Nav }) { const q = useGovTasks(); const staff = useGovStaff(); const task = (q.data?.tasks ?? []).find((t) => t.id === id); const [assignee, setAssignee] = useState(''); const control = useGovAction( (semantic: ControlSemantic) => call(controlTaskRoute, { params: { taskId: id }, body: { semantic, reason: '治理台干预' } }), ['tasks', 'workflow', 'tasklog'], ); const resume = useGovAction(() => call(resumeTaskRoute, { params: { taskId: id } }), ['tasks', 'workflow', 'tasklog']); const reassign = useGovAction( (to: string) => call(reassignTaskRoute, { params: { taskId: id }, body: { to } }), ['tasks', 'staff'], ); if (task === undefined) return 任务不存在或已不在近期窗口内。; const st = task.terminal !== undefined ? TERMINAL_ST[task.terminal] : RUNTIME_ST[task.runtime]; const label = task.terminal !== undefined ? TERMINAL_STATE_LABEL[task.terminal] : TASK_RUNTIME_STATE_LABEL[task.runtime]; const live = task.terminal === undefined; const suspended = task.runtime === 'suspended'; /** 控制三语义:暂停可恢复、叫停归负责人、终止级联作废 */ const controlAct = (semantic: ControlSemantic, danger: boolean): ActSpec => ({ label: CONTROL_SEMANTIC_LABEL[semantic], ico: semantic === 'terminate' ? 'power' : semantic === 'halt' ? 'hand' : 'pause', danger, disabled: !live, ask: { title: `控制事件 · ${CONTROL_SEMANTIC_LABEL[semantic]}:${task.title}`, note: 控制事件走统一事件信封(四类事件之控制事件),先持久后投递。, impact: { who: semantic === 'terminate' ? '在途任务终止、待答卡与征答消息作废、审批点撤卡(级联)' : semantic === 'halt' ? `负责人 ${task.ownerName} —— 挂起并记最重档纠正信号` : '任务挂起,监听与预算冻结', undo: semantic === 'terminate' ? '不可恢复' : '可恢复', when: '确认后即时', }, okLabel: `确认${CONTROL_SEMANTIC_LABEL[semantic]}`, onOk: () => control.mutate(semantic), }, }); return ( <> {label} {PARA[task.paradigm].label} {task.ownerName} } desc={PARA[task.paradigm].full + ' —— 治理不改定义:控制只作用于这一次运行。'} acts={[ controlAct('pause', false), controlAct('halt', true), controlAct('terminate', true), { label: '恢复', ico: 'play', primary: true, disabled: !suspended, ask: { title: `恢复任务:${task.title}`, note: 恢复 = 从挂起点继续;预算与监听解冻,任务记忆只追加、不回滚。, impact: { who: `负责人 ${task.ownerName}`, undo: '可再次控制', when: '确认后即时' }, okLabel: '确认恢复', onOk: () => resume.mutate(undefined), }, }, ]} /> {`${task.usedSteps}/${task.capSteps} 步`}}> 改派 = 负责人变更;改派留痕并通知双方,任务记忆与草稿层不动。
新负责人
{(staff.data?.staff ?? []) .filter((s) => s.state === 'active' && s.name !== task.ownerName) .map((s) => ( ))}
nav.push({ k: 'tasklog', id: task.id })} /> ); } export function TaskLogPage({ id }: { id: string }) { const q = useGovTaskLog(id); const lines = q.data?.log ?? []; return ( <>
{lines.map((l) => (
{l.title} {l.at.slice(0, 19).replace('T', ' ')} {l.sideLabel}
))} {lines.length === 0 && 暂无留痕。}
); } /* ---------------- 员工 / 岗位 ---------------- */