/** * 审批要素 —— 自 `~/coding/JCAI_PRD/workbench/_components/ModuleWorkspace.tsx` * 的 `ApprovalsModule` / `ApprovalDetail` 平移;mock 数据换成服务端投影。 * * 「工作台层面的『审批』**专指工作流级的审批点**」:它卡到审批人工位,产出审批单—— * 通过 / 驳回,附依据。**没有预算、没有 Harness、没有任务日志,所以不是任务。** */ import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import type { ApprovalItem, ListApprovalsForMeOutput } from '@contracts'; import { decideApprovalRoute, listApprovalsForMeRoute } from '@contracts'; import { Ico } from '@web-shared/Ico.tsx'; import ScrollArea from '@web-shared/ScrollArea.tsx'; import { call } from '@web-shared/typed-fetch.ts'; import { Chip, DocPanel, Hero, EmptyState, ListEmpty, NoteBand, Section, SidebarHead } from '@web-shared/desk/module-kit.tsx'; const STATE_CHIP = { pending: { tone: 'todo' as const, label: '待审批', pulse: true }, passed: { tone: 'ok' as const, label: '已通过', pulse: false }, rejected: { tone: 'warn' as const, label: '已退回', pulse: false }, }; export function ApprovalDesk() { const queryClient = useQueryClient(); const [selId, setSelId] = useState(undefined); const [filter, setFilter] = useState<'todo' | 'done'>('todo'); const [comment, setComment] = useState(''); const [error, setError] = useState(undefined); const q = useQuery({ queryKey: ['approvals'], queryFn: () => call(listApprovalsForMeRoute), }); const approvals = q.data?.approvals ?? []; const todo = approvals.filter((a) => a.state === 'pending'); const done = approvals.filter((a) => a.state !== 'pending'); const visible = filter === 'todo' ? todo : done; const sel = approvals.find((a) => a.id === selId) ?? visible[0]; const decide = useMutation({ mutationFn: (input: { item: ApprovalItem; verdict: 'pass' | 'reject' }) => call(decideApprovalRoute, { params: { workflowId: input.item.workflowId, nodeId: input.item.nodeId }, body: { verdict: input.verdict, ...(comment.trim() === '' ? {} : { comment: comment.trim() }) }, }), onSuccess: () => { setComment(''); setError(undefined); void queryClient.invalidateQueries({ queryKey: ['approvals'] }); void queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, onError: (e: unknown) => setError((e as { message?: string }).message ?? '裁决失败'), }); return (
{sel === undefined ? (
) : ( {sel.workflowSubject} 工作流 {sel.workflowId.slice(0, 8)} } chip={} />
{sel.basis.map((b) => (
{b.k} {b.v}
))} {sel.basis.length === 0 && 上游尚无输出口载荷}
{sel.rejectRoute}
{sel.state === 'pending' ? (