forked from ChaiHao/BPOS-MVP
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user