feat(diagram): 前端竖排镜像——edgePath/标签锚点方向自适应(发布顺序无关)、名单 2 行 tspan+等N人、补 instance 图例、auto-fit 与 375px 适配
build / build (push) Successful in 3m30s

This commit is contained in:
chenxing
2026-10-09 19:42:51 +08:00
parent 8b02f05494
commit 053eb0047b
2 changed files with 195 additions and 61 deletions
+47 -39
View File
@@ -1,23 +1,24 @@
/**
* 流程图自动布局(从 FlowDiagram.vue 提取的设计器/查看器共用实现):
* Kahn 拓扑分层 depth(to)=max(depth(from))+1,从左到右布列、同列按定义顺序纵排。
* 设计器(contentJson 无 layout 坐标时算初始坐标)与查看器(无坐标回退)共用同一口径。
* 流程图自动布局(查看器在后端未下发 positions 时的灰度兜底):Kahn 拓扑分层
* depth(to)=max(depth(from))+1,自上而下布层、同层按定义顺序横排、整层块对竖直中线水平居中。
* 与后端 DiagramLayoutService 同规则同常量(2026-10-09 竖排改造镜像);设计器已是钉钉式
* 竖向卡片流,不再消费本模块。
*/
export const SLOT_W = 200; // 节点槽位宽
export const SLOT_H = 88; // 节点槽位高
export const COL_GAP = 72; // 列间距(留给正交折线走线)
export const ROW_GAP = 24; // 行间距
export const SLOT_W = 280; // 分支槽宽(盒型在槽内水平居中;与后端一致)
export const SLOT_H = 100; // 层槽高(盒型在槽内垂直居中;与后端一致)
export const GAP_X = 44; // 分支列间距(分支间无需走线空间)
export const GAP_Y = 56; // 层间距(留给竖直走线 + 多出口文案)
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 },
NOTIFY: { w: 184, h: 64 },
PARALLEL_SPLIT: { w: 128, h: 72 },
PARALLEL_JOIN: { w: 128, h: 72 },
CONDITION: { w: 128, h: 72 },
START: { w: 116, h: 44 },
END: { w: 116, h: 44 },
APPROVAL: { w: 264, h: 76 },
NOTIFY: { w: 264, h: 76 },
PARALLEL_SPLIT: { w: 132, h: 78 },
PARALLEL_JOIN: { w: 132, h: 78 },
CONDITION: { w: 132, h: 78 },
};
export interface Pos {
@@ -36,7 +37,7 @@ export function layout(data: any): Record<string, Pos> {
return pos;
}
/** Kahn 分层 + 同列按定义顺序纵排(基础网格,保证无重叠) */
/** Kahn 分层 + 同层按定义顺序横排(基础网格,保证无重叠) */
function layered(data: any): Record<string, Pos> {
const nodes: any[] = data?.nodes || [];
const edges: any[] = data?.edges || [];
@@ -56,31 +57,38 @@ function layered(data: any): Record<string, Pos> {
if (--inDeg[e.to] === 0) order.push(e.to);
});
}
const rowsIn: Record<number, number> = {};
const rowsIn: Record<number, any[]> = {};
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 row = (rowsIn[d] = rowsIn[d] || []).length;
rowsIn[d].push(n);
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,
x: PAD + row * (SLOT_W + GAP_X) + (SLOT_W - box.w) / 2,
y: PAD + d * (SLOT_H + GAP_Y) + (SLOT_H - box.h) / 2,
w: box.w,
h: box.h,
depth: d,
row,
};
});
// 整层块对竖直中线水平居中(镜像后端:按实际占宽算块宽,不含尾随 GAP_X)
const maxCols = Math.max(...Object.values(rowsIn).map((a) => a.length));
const axisW = (maxCols - 1) * (SLOT_W + GAP_X) + SLOT_W;
Object.values(rowsIn).forEach((arr) => {
const blockW = (arr.length - 1) * (SLOT_W + GAP_X) + SLOT_W;
const dx = (axisW - blockW) / 2;
arr.forEach((n) => { pos[n.id].x += dx; });
});
return pos;
}
/**
* 并行分支组垂直居中:分支不再从 SPLIT 行向下堆叠,而是以 SPLIT 中心线为轴上下对称展开
* (与钉钉/参考稿一致)。SPLIT↔JOIN 配对沿用编辑器口径(各出边可达集求交,取 BFS 序最早的
* PARALLEL_JOIN),组员 = SPLIT 到 JOIN 之间可达的全部节点;JOIN 中线吸附回 SPLIT 中线。
* 内层组先于外层组处理(嵌套并行整体平移不破坏内层居中);结束统一把最顶行抬回 PAD。
* 并行分支组水平居中:分支不再从 SPLIT 行向右堆叠,而是以 SPLIT 竖直中线为轴左右对称展开
* (2026-10-09 竖排镜像)。SPLIT↔JOIN 配对沿用编辑器口径(各出边可达集求交,取 BFS 序最早的
* PARALLEL_JOIN),组员 = SPLIT 到 JOIN 之间可达的全部节点;JOIN 水平中点吸附回 SPLIT 中线。
* 内层组先于外层组处理(嵌套并行整体平移不破坏内层居中);结束统一把最左列推回 PAD。
*/
function centerParallelGroups(data: any, pos: Record<string, Pos>): void {
const nodes: any[] = data?.nodes || [];
@@ -144,23 +152,23 @@ function centerParallelGroups(data: any, pos: Record<string, Pos>): void {
- Math.max(...a.members.map((m: string) => pos[m].depth)));
groups.forEach((g: any) => {
const axisY = pos[g.split].y + pos[g.split].h / 2;
let top = Infinity;
let bottom = -Infinity;
const axisX = pos[g.split].x + pos[g.split].w / 2;
let left = Infinity;
let right = -Infinity;
g.members.forEach((id: string) => {
top = Math.min(top, pos[id].y);
bottom = Math.max(bottom, pos[id].y + pos[id].h);
left = Math.min(left, pos[id].x);
right = Math.max(right, pos[id].x + pos[id].w);
});
const delta = axisY - (top + bottom) / 2;
g.members.forEach((id: string) => { pos[id].y += delta; });
pos[g.join].y += axisY - (pos[g.join].y + pos[g.join].h / 2);
const delta = axisX - (left + right) / 2;
g.members.forEach((id: string) => { pos[id].x += delta; });
pos[g.join].x += axisX - (pos[g.join].x + pos[g.join].w / 2);
});
// 上移出界的组把整图平移回 PAD(同 delta 平移不破坏各组居中)
let minY = Infinity;
Object.values(pos).forEach((p) => { minY = Math.min(minY, p.y); });
if (minY < PAD) {
const lift = PAD - minY;
Object.values(pos).forEach((p) => { p.y += lift; });
// 左移出界的组把整图平移回 PAD(同 delta 平移不破坏各组居中)
let minX = Infinity;
Object.values(pos).forEach((p) => { minX = Math.min(minX, p.x); });
if (minX < PAD) {
const lift = PAD - minX;
Object.values(pos).forEach((p) => { p.x += lift; });
}
}