M6(4/n): 工作台五要素齐活 —— 审批 / 消息 / 纪要 / 汇报

四个 desk-* feature 自 workbench 原型 ModuleWorkspace.tsx 平移(jc-* 类名原样),
共用件落 shared/desk/module-kit;mock 数据换成服务端投影:
  · desk-approval:审批点卡到审批人工位;依据快照、驳回走向(声明式)、驳回必附意见
  · desk-message:通知 + 跨任务征答**就地作答**;卡面表单由 card.ts schema 驱动,
    与后端回传校验同 schema;跨人卡不占任务清单,不开放任务对话流
  · desk-minute:纪要维护 + 决议登记起单;正文可续、决议只增
  · desk-report:四周期草案 + 度量八指标 + 确认即提交
左轨角标常驻(待批 / 未读),责任面要一眼看见什么在等我。

修一个真 bug:process.start 从不校验流程包的 callInterface,
先插工作流再由 task.start 撞活动契约 —— 失败就留下一条永远推不动的在途单。
改为**先校验后建单**(CallInterfaceViolationError),一条工作流都不留;
路由回 400 并带上缺哪个字段。已清掉这次触发出的两条孤儿单。

判据 ⑩ 纪要一环跑通:决议 → 登记触发 → 工作流 → A1 落到 BD 冯兵,
登记人即来源已记在单上;同一决议重复登记被 412 挡下。

gates: tsc / eslint / depcruise(316 模块)/ api-report / 380 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 03:53:36 +08:00
co-authored by Claude Opus 5
parent a60acb2e02
commit 3b9587563a
25 changed files with 1680 additions and 49 deletions
+26 -33
View File
@@ -9,8 +9,12 @@
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type { Session, TaskCard, TaskDetail } from '@contracts';
import { listMessagesRoute, listTasksRoute, replyWorkOrderRoute, sayRoute, sessionRoute, taskDetailRoute } from '@contracts';
import { listApprovalsForMeRoute, listMessagesRoute, listTasksRoute, replyWorkOrderRoute, sayRoute, sessionRoute, taskDetailRoute } from '@contracts';
import { TaskChat, TaskList, ViewRegion } from '@features/desk-task';
import { ApprovalDesk } from '@features/desk-approval';
import { MessageDesk } from '@features/desk-message';
import { MinuteDesk } from '@features/desk-minute';
import { ReportDesk } from '@features/desk-report';
import { Ico } from '@web-shared/Ico.tsx';
import { call } from '@web-shared/typed-fetch.ts';
import { useChatWidth } from '@web-shared/useChatWidth.ts';
@@ -43,12 +47,14 @@ export function AppShell() {
queryFn: () => call<{ tasks: TaskCard[] }>(listTasksRoute),
});
// 角标常驻:责任面要一眼看见「什么在等我」,不能等切过去才知道
const messages = useQuery({
queryKey: queryKeys.messages,
queryFn: () => call<{ messages: Array<{ id: string; title: string; body: string; at: string; read: boolean }> }>(
listMessagesRoute,
),
enabled: module === 'messages',
queryFn: () => call<{ messages: Array<{ id: string; read: boolean }> }>(listMessagesRoute),
});
const approvals = useQuery({
queryKey: ['approvals'],
queryFn: () => call<{ approvals: Array<{ state: string }> }>(listApprovalsForMeRoute),
});
const list = tasks.data?.tasks ?? [];
@@ -89,7 +95,14 @@ export function AppShell() {
</div>
<span className="jc-rail-brand">JCAI</span>
{NAV_MODULES.map((m) => {
const badge = m.key === 'tasks' ? active.length : undefined;
const badge =
m.key === 'tasks'
? active.length
: m.key === 'approvals'
? (approvals.data?.approvals ?? []).filter((a) => a.state === 'pending').length
: m.key === 'messages'
? (messages.data?.messages ?? []).filter((x) => !x.read).length
: undefined;
return (
<button
key={m.key}
@@ -161,34 +174,14 @@ export function AppShell() {
/>
)}
</div>
) : module === 'approvals' ? (
<ApprovalDesk />
) : module === 'messages' ? (
<MessageDesk />
) : module === 'minutes' ? (
<MinuteDesk />
) : (
<main className="execution-panel jc-panel-card">
<header className="jc-vr-head">
<div className="jc-vr-title">
<h1>{NAV_MODULES.find((m) => m.key === module)?.label}</h1>
<span className="jc-vr-sub">工作台五要素 —— 同一份过程事实的五种投影</span>
</div>
</header>
{module === 'messages' ? (
<ul className="jc-msg-list">
{(messages.data?.messages ?? []).map((m) => (
<li key={m.id} className={m.read ? 'read' : ''}>
<b>{m.title}</b>
<p>{m.body}</p>
<time className="jc-mono">{m.at.slice(0, 19).replace('T', ' ')}</time>
</li>
))}
{(messages.data?.messages ?? []).length === 0 && (
<li className="jc-list-empty">暂无消息</li>
)}
</ul>
) : (
<div className="jc-list-empty">
<Ico name="bar-chart" s={18} />
该要素随 M6 后续接入
</div>
)}
</main>
<ReportDesk />
)}
</div>
</div>