Files
JX-BPOS/apps/web/src/shared/desk/module-kit.tsx
T
ChaiHaoandClaude Opus 5 3b9587563a 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
2026-07-27 03:53:36 +08:00

172 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 工作台后四要素的共用小件 —— 自 `~/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>
);
}