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
+171
View File
@@ -0,0 +1,171 @@
/**
* 工作台后四要素的共用小件 —— 自 `~/coding/JCAI_PRD/workbench/_components/ModuleWorkspace.tsx`
* 第 21–160 行平移。**DOM 与 `jc-*` 类名原样保留**(README 平移规则 1/2/4)。
*
* 四要素同款三栏布局:左清单 `task-sidebar` / 中文档卡 `execution-panel` / 右会话 `session-chat`。
*/
import type { ReactNode } from 'react';
import { Ico } from '@web-shared/Ico.tsx';
import ScrollArea from '@web-shared/ScrollArea.tsx';
/** 首字头像:名字 hash 取渐变色;外文名取首字母大写 */
const AVA_GRAD: ReadonlyArray<readonly [string, string]> = [
['#2bc7d4', '#0e9fd8'],
['#3370ff', '#245bdb'],
['#7f3bf5', '#5b21cf'],
['#ff8800', '#de7802'],
['#0aa294', '#0a6a64'],
['#f14ba9', '#c92d86'],
];
function avaOf(name: string): { ch: string; grad: readonly [string, string] } {
let h = 0;
for (const c of name) h = (h * 31 + c.charCodeAt(0)) >>> 0;
const first = name[0] ?? '·';
const ch = /[A-Za-z]/.test(first) ? first.toUpperCase() : first;
return { ch, grad: AVA_GRAD[h % AVA_GRAD.length]! };
}
export function Avatar({ name, size = 30 }: { name: string; size?: number }) {
const { ch, grad } = avaOf(name);
return (
<span
className="jc-ava"
style={{
width: size,
height: size,
fontSize: size * 0.42,
background: `linear-gradient(140deg, ${grad[0]}, ${grad[1]})`,
}}
>
{ch}
</span>
);
}
export function AvatarStack({ names, size = 26 }: { names: readonly string[]; size?: number }) {
const show = names.slice(0, 5);
return (
<span className="jc-ava-stack">
{show.map((n, i) => (
<span key={i} className="jc-ava-slot">
<Avatar name={n} size={size} />
</span>
))}
{names.length > show.length && <span className="jc-ava-slot jc-ava-more">+{names.length - show.length}</span>}
</span>
);
}
export type Tone = 'todo' | 'doing' | 'done' | 'warn' | 'brand' | 'ok';
export function Chip({ tone, label, pulse }: { tone: Tone; label: string; pulse?: boolean }) {
return (
<span className={'jc-chip t-' + tone}>
{pulse && <i />}
{label}
</span>
);
}
/** 文档头(渐变图标块 + 标题 + 元信息 + 右上状态章)—— 统一容器,彻底告别贴边 */
export function Hero({
grad,
icon,
kicker,
title,
meta,
chip,
}: {
grad: readonly [string, string];
icon: string;
kicker?: string;
title: string;
meta: ReactNode;
chip?: ReactNode;
}) {
return (
<header className="jc-hero">
<span className="jc-hero-ico" style={{ background: `linear-gradient(140deg, ${grad[0]}, ${grad[1]})` }}>
<Ico name={icon} s={22} />
</span>
<div className="jc-hero-txt">
{kicker && <span className="jc-hero-kicker">{kicker}</span>}
<h2>{title}</h2>
<div className="jc-hero-meta">{meta}</div>
</div>
{chip && <div className="jc-hero-chip">{chip}</div>}
</header>
);
}
export function Section({
icon,
title,
extra,
children,
}: {
icon: string;
title: string;
extra?: ReactNode;
children: ReactNode;
}) {
return (
<section className="jc-sec">
<div className="jc-sec-head">
<Ico name={icon} s={13} />
<span>{title}</span>
{extra && <span className="jc-sec-extra">{extra}</span>}
</div>
{children}
</section>
);
}
/** 侧栏表头 —— 四要素共用 */
export function SidebarHead({ icon, title, sub }: { icon: string; title: string; sub: string }) {
return (
<div className="sidebar-head jc-wl-head">
<span className="jc-wl-ico">
<Ico name={icon} s={16} />
</span>
<div>
<h2>{title}</h2>
<span>{sub}</span>
</div>
</div>
);
}
export function ListEmpty({ icon, children }: { icon: string; children: ReactNode }) {
return (
<div className="jc-list-empty">
<Ico name={icon} s={22} />
<span>{children}</span>
</div>
);
}
/** 中栏文档卡的滚动容器 */
export function DocPanel({ scrollKey, children }: { scrollKey: string; children: ReactNode }) {
return (
<main className="execution-panel jc-panel-card">
<ScrollArea key={scrollKey} className="jc-sa--full" vpClassName="jc-mod-scroll">
{children}
</ScrollArea>
</main>
);
}
/**
* 说明带 —— 原型里用来点明规范口径的那条浅色横条。
* 保留它不是为了好看:这些句子是「为什么这么设计」的现场注解。
*/
export function NoteBand({ children }: { children: ReactNode }) {
return (
<div className="jc-notrig">
<Ico name="info" s={15} />
<span>{children}</span>
</div>
);
}