forked from ChaiHao/BPOS-MVP
四个 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
172 lines
4.6 KiB
TypeScript
172 lines
4.6 KiB
TypeScript
/**
|
||
* 工作台后四要素的共用小件 —— 自 `~/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>
|
||
);
|
||
}
|