Files
JX-BPOS/apps/web/src/shared/gov/flow/FlowFigure.tsx
T
ChaiHaoandClaude Fable 5 7f0d25c337 三面拆分⑭:纯编排第三轮 —— 活动卸岗位、卡点废除全真边、边号 E1–En 图上可选
活动契约不带负责人岗位(用户定):
- schema:活动节点删 owner(审批点的 approver 是「卡到人的工位」的功能语义,保留)
- 运行时:活动任务默认**全员轮值**(在岗候选并集、最闲者先、staffId 稳定排序,
  owner-resolved 事件岗位记「(全员轮值)」);运营分派机制落地后让位 —— 债已记
- closure:活动点不核岗位,missing-owner-position 只出自审批点
- 员工面链条:活动点报**实际承接人**(实例→任务→人),没起的如实报「轮值待派」;
  NodeInstance 公开 taskId/approvalId(店面读回,与端口型合流)
- 画布/导出依赖文件/属性卡全线摘岗位;属性卡活动点明说「分派归运营」

卡点画法废除,顺序流一律真边(用户定):
- 种子图重画:四条回跳/直达(E4 直接定价、E6 选型后定价、E10 不通过、E12 有异议)
  全部画成折线真边,车道分层零交叉(顶 y30 / y190 / y230 / 缝隙对开 / 底 y380);
  卡点节点删净,图=恰 10 语义节点 + 14 真边一一对应(测试守着)
- graph-to-pkg 卡点认亲机制删除;pkg-to-graph 直接按 from→to 认亲
- 元素盘删卡点件;图例、属性卡卡点分支、kindLabel/kindTone 随删

边号 E1、E2…(用户定):
- 语义边 id 即边号:种子 E1–E14;合并器铸号(旧边保号、新边取下一空号,回归测试守着)
- 画布线标带号(`E3 直接寻源`,无注记的边也标 `E1`);拣选条以边号立牌,
  **条件不再上按钮** —— 条件/映射只住属性面
- 边在图上可选中:FlowDiagram 连线加宽透明命中垫(边可点时注解层提到形状层上),
  点线即切属性卡到该边(实测 E4 命中)

测试:closure 28、advance-order 6、graph-to-pkg 5(新增铸号例)、seed 8、
端到端八例真库全绿(全员轮值下照跑);五闸退出码验过。
库:purge 后重种,唯一 unified-quote@v1 生效(双面判据齐)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-28 19:02:07 +08:00

147 lines
5.1 KiB
TypeScript

/**
* 编排画布 —— diagram-js 画布件的宿主(编码规范 §6.1「PRD 画布件 1:1 平移」)。
*
* `FlowFigureBlock` 自原型 `HostExample` 平移:区块小标题右侧「编辑」芯片进手绘编辑器
* (FlowEditor,画布内工具栏含 取消/保存绘图);展示态 hero/cap 框 + FlowDiagram
* (单击节点下钻/选中、双击活动就地改文字);图例贴在流程图正下方。
* 必须包在 `.process-desk > .process-doc` 纸面容器内(flow-canvas.css 规则 scoped 到它)。
*
* `FlowFigure` 保留为只读便捷壳(任务页的流程图用它)。
*/
import { useState } from 'react';
import type { ProcessPackage } from '@contracts';
import { Ico } from '@ui';
import { FlowDiagram } from './FlowDiagram.tsx';
import { FlowEditor } from './FlowEditor.tsx';
import type { FlowEdge, FlowGraph } from './flow-layout.ts';
import type { SpPosition } from './support.tsx';
import { packageToGraph } from './pkg-to-graph.ts';
import type { InstanceMark } from './pkg-to-graph.ts';
import './flow-canvas.css';
export function FlowFigureBlock({
title,
oneLiner,
graph,
drilledIds,
positions,
canEdit,
onNodeClick,
onEdgeClick,
onEditNodeText,
onSaveFlowGraph,
}: {
title: string;
oneLiner?: string | undefined;
graph: FlowGraph | null;
/** 已建立下钻内容的节点 id(删除这些节点时编辑器会二次确认) */
drilledIds: string[];
positions?: SpPosition[] | undefined;
canEdit: boolean;
onNodeClick: (nodeId: string) => void;
/** 点击连线选中边(属性卡随之切到该边);不传则边不可点 */
onEdgeClick?: ((edge: FlowEdge) => void) | undefined;
/** 双击流程图活动就地改文字(标题 / 主办) */
onEditNodeText?: ((nodeId: string, patch: { label: string; owner?: string }) => Promise<void>) | undefined;
/** 手工绘图:整图保存(idMap = 本次编辑会话 原id→现id,宿主用它重键下钻内容) */
onSaveFlowGraph?: ((graph: FlowGraph, idMap: Record<string, string>) => Promise<void>) | undefined;
}) {
const [editing, setEditing] = useState(false);
return (
<>
<div className="view-lbl">
流程图 · 顺序
{canEdit && onSaveFlowGraph !== undefined && !editing && (
<button
type="button"
className="dc2-addbtn"
title="手工绘图(珊格正交:活动点/审批点/起止,拖拽连线);画布内工具栏有 取消/保存绘图"
onClick={() => setEditing(true)}
>
<Ico name="pen-line" s={11} /> 编辑
</button>
)}
</div>
{editing && onSaveFlowGraph !== undefined ? (
<div className="hero">
<div className="cap">
<span>FIG — {title}流程 · 手工绘图</span>
<span>珊格 10px · 直线与直角折线</span>
</div>
<FlowEditor
initial={graph}
drilledIds={drilledIds}
positions={positions}
onSave={async (next, idMap) => {
await onSaveFlowGraph(next, idMap);
setEditing(false);
}}
onCancel={() => setEditing(false)}
/>
</div>
) : (
<div className="hero">
<div className="cap">
<span>FIG — {title}流程</span>
<span>{oneLiner ?? '活动 · 网关 · 顺序流'}</span>
</div>
<div className="fig-body">
<FlowDiagram
graph={graph}
variant="business"
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onEditNode={canEdit ? onEditNodeText : undefined}
ownerOptions={positions}
ariaLabel={`${title}业务流程图`}
/>
</div>
</div>
)}
{/* 图例贴在流程图正下方,图标与绘图工具(元素盘)同款 SVG */}
<div className="legend">
<span>
<b>图例</b>
</span>
<span><i className="lg-i fe-pi-start" aria-hidden /> 起始</span>
<span><i className="lg-i fe-pi-end" aria-hidden /> 结束</span>
{/* 节点只分活动点/审批点 —— 标准/案例是实现方式,不进编排面的词汇表 */}
<span><i className="lg-i fe-pi-sop" aria-hidden /> 活动点</span>
<span><i className="lg-i fe-pi-approval" aria-hidden /> 审批点</span>
<span><i className="lg-i lg-flow" aria-hidden /> 顺序流</span>
<span className="sep">|</span>
<span>
<b>责任</b>
</span>
<span>右上角标 责任人</span>
</div>
</>
);
}
/** 只读壳:流程包 → 图 → 画(任务页 / 工作流实例页用) */
export function FlowFigure({
pkg,
instances,
onNodeClick,
}: {
pkg: ProcessPackage;
instances: readonly InstanceMark[];
onNodeClick?: (nodeId: string) => void;
}) {
const graph = packageToGraph(pkg, instances);
return (
<div className="process-desk">
<div className="process-doc">
<FlowDiagram
graph={graph}
variant="business"
ariaLabel={`流程图 · ${pkg.name}`}
{...(onNodeClick === undefined ? {} : { onNodeClick })}
/>
</div>
</div>
);
}