feat: 流程编辑器 bpmn-js 画布模式 + 引擎多出口/退回/移交/或签前端接线
build / build (push) Successful in 4m6s

- def-editor 新增画布模式(默认):bpmn-js 内存画布(精简 palette/汉化/撤销重做/缩放/出口命名/导出 BPMN),
  toBpmn/fromBpmn 双向序列化,contentJson 仍为持久格式(+layout 坐标);卡片模式保留回退
- 新增 utils/flowLayout.ts 分层布局(设计器与查看器共用)
- TaskHandle:多出口节点出口选择下拉;退回/移交按钮与弹窗;轨迹显示已退回/已移交及所选出口
- FlowDiagram:优先使用保存坐标;边画出口名标签;办件状态着色/当前节点高亮
- api/flow:approve 带 edgeName,新增 returnBack/transfer
This commit is contained in:
chenxing
2026-09-16 22:22:26 +08:00
parent 842a4cfb14
commit e52c4324f6
11 changed files with 1177 additions and 80 deletions
+68
View File
@@ -0,0 +1,68 @@
/**
* 流程图自动布局(从 FlowDiagram.vue 提取的设计器/查看器共用实现):
* Kahn 拓扑分层 depth(to)=max(depth(from))+1,从左到右布列、同列按定义顺序纵排。
* 设计器(contentJson 无 layout 坐标时算初始坐标)与查看器(无坐标回退)共用同一口径。
*/
export const SLOT_W = 200; // 节点槽位宽
export const SLOT_H = 88; // 节点槽位高
export const COL_GAP = 72; // 列间距(留给正交折线走线)
export const ROW_GAP = 24; // 行间距
export const PAD = 24;
export const NODE_BOX: Record<string, { w: number; h: number }> = {
START: { w: 104, h: 40 },
END: { w: 104, h: 40 },
APPROVAL: { w: 184, h: 64 },
PARALLEL_SPLIT: { w: 128, h: 72 },
PARALLEL_JOIN: { w: 128, h: 72 },
};
export interface Pos {
x: number;
y: number;
w: number;
h: number;
depth: number;
row: number;
}
/** 拓扑分层布局:入参 {nodes,edges},返回 nodeId → 位置 */
export function layout(data: any): Record<string, Pos> {
const nodes: any[] = data?.nodes || [];
const edges: any[] = data?.edges || [];
if (!nodes.length) return {};
const inDeg: Record<string, number> = {};
nodes.forEach((n) => { inDeg[n.id] = 0; });
edges.forEach((e) => { if (inDeg[e.to] !== undefined) inDeg[e.to]++; });
const depth: Record<string, number> = {};
const queue = nodes.filter((n) => !inDeg[n.id]).map((n) => n.id);
queue.forEach((id) => { depth[id] = 0; });
const order = [...queue];
while (order.length) {
const id = order.shift()!;
edges.forEach((e) => {
if (e.from !== id || inDeg[e.to] === undefined) return;
depth[e.to] = Math.max(depth[e.to] ?? 0, (depth[id] ?? 0) + 1);
if (--inDeg[e.to] === 0) order.push(e.to);
});
}
const rowsIn: Record<number, number> = {};
const pos: Record<string, Pos> = {};
nodes.forEach((n) => {
const d = depth[n.id] ?? 0;
depth[n.id] = d;
const row = rowsIn[d] ?? 0;
rowsIn[d] = row + 1;
const box = NODE_BOX[n.type] || NODE_BOX.APPROVAL;
pos[n.id] = {
x: PAD + d * (SLOT_W + COL_GAP) + (SLOT_W - box.w) / 2,
y: PAD + row * (SLOT_H + ROW_GAP) + (SLOT_H - box.h) / 2,
w: box.w,
h: box.h,
depth: d,
row,
};
});
return pos;
}