yyd-flow-designer 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/designer/flow/Edges/ConnectionLine.tsx +61 -22
- package/src/designer/flow/Edges/CustomEdge.tsx +4 -2
- package/src/designer/flow/Edges/DefaultEdge.tsx +364 -38
- package/src/designer/flow/Edges/edgeRoute.ts +186 -0
- package/src/designer/flow/FlowStage.tsx +2 -1
- package/src/designer/flow/FlowToolbar.tsx +10 -10
- package/src/designer/flow/components/ExecuteTestModal.tsx +9 -1
- package/src/designer/flow/components/ShortcutsModal.tsx +9 -2
- package/src/designer/flow/components/VersionManager.tsx +2 -1
- package/src/designer/flow/utils/trailOverlay.ts +158 -11
- package/src/designer/flow/utils/validateFlowGraph.ts +1 -1
- package/src/designer/index.tsx +3 -2
- package/src/shared/flow/PopoverContext.tsx +1 -1
- package/src/shared/flow/components/FlowPopover.tsx +1 -1
- package/src/shared/flow/components/FlowToolTip.tsx +1 -1
- package/src/shared/flow/utils.ts +166 -37
- package/src/ui/antd.tsx +2 -0
- package/src/viewer/FlowViewer.tsx +11 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Edge, Node } from '@xyflow/react';
|
|
2
|
+
|
|
3
|
+
export type Waypoint = { x: number; y: number };
|
|
4
|
+
|
|
5
|
+
export type EdgeRouteData = {
|
|
6
|
+
/** 中间折点(不含起终点);有值且 manual 时用户可拖 */
|
|
7
|
+
waypoints?: Waypoint[];
|
|
8
|
+
/** true = 用户改过,整理节点前会重算并清掉 */
|
|
9
|
+
manual?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const BACK_RAIL_GAP = 64;
|
|
13
|
+
const BACK_RAIL_STEP = 32;
|
|
14
|
+
const STUB = 28;
|
|
15
|
+
const CORNER = 10;
|
|
16
|
+
const LAYOUT_DEFAULT_W = 180;
|
|
17
|
+
const LAYOUT_DEFAULT_H = 72;
|
|
18
|
+
|
|
19
|
+
const round = (n: number) => Math.round(n * 10) / 10;
|
|
20
|
+
|
|
21
|
+
export const isBackEdgeXY = (sourceX: number, targetX: number) => sourceX > targetX + 20;
|
|
22
|
+
|
|
23
|
+
/** 圆角正交路径:points 含起点与终点 */
|
|
24
|
+
export const waypointsToPath = (points: Waypoint[], radius = CORNER): string => {
|
|
25
|
+
if (points.length < 2) return '';
|
|
26
|
+
const pts = points.map((p) => ({ x: round(p.x), y: round(p.y) }));
|
|
27
|
+
let d = `M ${pts[0].x} ${pts[0].y}`;
|
|
28
|
+
for (let i = 1; i < pts.length - 1; i += 1) {
|
|
29
|
+
const p0 = pts[i - 1];
|
|
30
|
+
const p1 = pts[i];
|
|
31
|
+
const p2 = pts[i + 1];
|
|
32
|
+
const dx1 = p1.x - p0.x;
|
|
33
|
+
const dy1 = p1.y - p0.y;
|
|
34
|
+
const dx2 = p2.x - p1.x;
|
|
35
|
+
const dy2 = p2.y - p1.y;
|
|
36
|
+
const len1 = Math.hypot(dx1, dy1) || 1;
|
|
37
|
+
const len2 = Math.hypot(dx2, dy2) || 1;
|
|
38
|
+
const r = Math.min(radius, len1 / 2, len2 / 2);
|
|
39
|
+
const sx = p1.x - (dx1 / len1) * r;
|
|
40
|
+
const sy = p1.y - (dy1 / len1) * r;
|
|
41
|
+
const ex = p1.x + (dx2 / len2) * r;
|
|
42
|
+
const ey = p1.y + (dy2 / len2) * r;
|
|
43
|
+
d += ` L ${round(sx)} ${round(sy)} Q ${p1.x} ${p1.y} ${round(ex)} ${round(ey)}`;
|
|
44
|
+
}
|
|
45
|
+
const last = pts[pts.length - 1];
|
|
46
|
+
d += ` L ${last.x} ${last.y}`;
|
|
47
|
+
return d;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** 自动折点(不含起终点) */
|
|
51
|
+
export const computeAutoWaypoints = (
|
|
52
|
+
sourceX: number,
|
|
53
|
+
sourceY: number,
|
|
54
|
+
targetX: number,
|
|
55
|
+
targetY: number,
|
|
56
|
+
lane = 0,
|
|
57
|
+
): Waypoint[] => {
|
|
58
|
+
if (isBackEdgeXY(sourceX, targetX)) {
|
|
59
|
+
const railY = Math.max(sourceY, targetY) + BACK_RAIL_GAP + lane * BACK_RAIL_STEP;
|
|
60
|
+
const outX = sourceX + STUB + lane * 10;
|
|
61
|
+
const inX = targetX - STUB - lane * 10;
|
|
62
|
+
const midX = (outX + inX) / 2;
|
|
63
|
+
// 中间多一个「深度点」,选中后可上下拉整段回连轨道高度
|
|
64
|
+
return [
|
|
65
|
+
{ x: outX, y: sourceY },
|
|
66
|
+
{ x: outX, y: railY },
|
|
67
|
+
{ x: midX, y: railY },
|
|
68
|
+
{ x: inX, y: railY },
|
|
69
|
+
{ x: inX, y: targetY },
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
const span = targetX - sourceX;
|
|
73
|
+
if (span < 40) {
|
|
74
|
+
const midY = (sourceY + targetY) / 2 + lane * 12;
|
|
75
|
+
const midX = sourceX + Math.max(24, span / 2);
|
|
76
|
+
return [
|
|
77
|
+
{ x: midX, y: sourceY },
|
|
78
|
+
{ x: midX, y: midY },
|
|
79
|
+
{ x: targetX - 20, y: midY },
|
|
80
|
+
{ x: targetX - 20, y: targetY },
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
// 正向:水平错开折列,多路汇入不叠在一起
|
|
84
|
+
const bendX = sourceX + span * (0.4 + Math.min(lane, 5) * 0.08);
|
|
85
|
+
if (Math.abs(sourceY - targetY) < 8) {
|
|
86
|
+
return []; // 直线,无需折点
|
|
87
|
+
}
|
|
88
|
+
return [
|
|
89
|
+
{ x: bendX, y: sourceY },
|
|
90
|
+
{ x: bendX, y: targetY },
|
|
91
|
+
];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const nodeSize = (node?: Node) => ({
|
|
95
|
+
w: (node as any)?.measured?.width ?? (node as any)?.width ?? LAYOUT_DEFAULT_W,
|
|
96
|
+
h: (node as any)?.measured?.height ?? (node as any)?.height ?? LAYOUT_DEFAULT_H,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const contentOf = (node?: Node) =>
|
|
100
|
+
(node?.data as any)?.nodeConfig?.content ?? {};
|
|
101
|
+
|
|
102
|
+
/** 估算 sourceHandle / targetHandle 的画布坐标 */
|
|
103
|
+
export const estimateHandlePoint = (
|
|
104
|
+
node: Node | undefined,
|
|
105
|
+
handleId: string | null | undefined,
|
|
106
|
+
side: 'source' | 'target',
|
|
107
|
+
): Waypoint => {
|
|
108
|
+
if (!node) return { x: 0, y: 0 };
|
|
109
|
+
const { w, h } = nodeSize(node);
|
|
110
|
+
const x0 = node.position.x;
|
|
111
|
+
const y0 = node.position.y;
|
|
112
|
+
const branches = contentOf(node).branches;
|
|
113
|
+
let yRatio = 0.5;
|
|
114
|
+
if (Array.isArray(branches) && handleId && side === 'source') {
|
|
115
|
+
const idx = branches.findIndex((b: any) => {
|
|
116
|
+
const id = b?.id;
|
|
117
|
+
if (!id) return false;
|
|
118
|
+
return handleId === `${node.id}_gateway_${id}`
|
|
119
|
+
|| handleId === `${node.id}_parallel_${id}`
|
|
120
|
+
|| handleId.endsWith(`_${id}`);
|
|
121
|
+
});
|
|
122
|
+
if (idx >= 0) {
|
|
123
|
+
yRatio = (idx + 1) / (branches.length + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
x: side === 'source' ? x0 + w : x0,
|
|
128
|
+
y: y0 + h * yRatio,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 按当前节点位置为所有边分配车道并写入自动 waypoints(清掉 manual)。
|
|
134
|
+
* 整理节点后调用。
|
|
135
|
+
*/
|
|
136
|
+
export const assignAutoEdgeRoutes = (nodes: Node[], edges: Edge[]): Edge[] => {
|
|
137
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
138
|
+
|
|
139
|
+
// 先算每条边的起终点,区分回连
|
|
140
|
+
type Meta = {
|
|
141
|
+
edge: Edge;
|
|
142
|
+
sx: number;
|
|
143
|
+
sy: number;
|
|
144
|
+
tx: number;
|
|
145
|
+
ty: number;
|
|
146
|
+
back: boolean;
|
|
147
|
+
};
|
|
148
|
+
const metas: Meta[] = edges.map((edge) => {
|
|
149
|
+
const s = estimateHandlePoint(nodeMap.get(edge.source), edge.sourceHandle, 'source');
|
|
150
|
+
const t = estimateHandlePoint(nodeMap.get(edge.target), edge.targetHandle, 'target');
|
|
151
|
+
return {
|
|
152
|
+
edge,
|
|
153
|
+
sx: s.x,
|
|
154
|
+
sy: s.y,
|
|
155
|
+
tx: t.x,
|
|
156
|
+
ty: t.y,
|
|
157
|
+
back: isBackEdgeXY(s.x, t.x),
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 同目标 / 回连目标 分组分配 lane
|
|
162
|
+
const laneOf = new Map<string, number>();
|
|
163
|
+
const groups = new Map<string, Meta[]>();
|
|
164
|
+
metas.forEach((m) => {
|
|
165
|
+
const key = m.back ? `back:${m.edge.target}` : `fwd:${m.edge.target}`;
|
|
166
|
+
(groups.get(key) ?? (groups.set(key, []), groups.get(key)!)).push(m);
|
|
167
|
+
});
|
|
168
|
+
groups.forEach((list) => {
|
|
169
|
+
list.sort((a, b) => a.sy - b.sy || a.edge.id.localeCompare(b.edge.id));
|
|
170
|
+
list.forEach((m, idx) => laneOf.set(m.edge.id, idx));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return metas.map((m) => {
|
|
174
|
+
const lane = laneOf.get(m.edge.id) ?? 0;
|
|
175
|
+
const waypoints = computeAutoWaypoints(m.sx, m.sy, m.tx, m.ty, lane);
|
|
176
|
+
const prev = (m.edge.data ?? {}) as EdgeRouteData;
|
|
177
|
+
return {
|
|
178
|
+
...m.edge,
|
|
179
|
+
data: {
|
|
180
|
+
...prev,
|
|
181
|
+
waypoints,
|
|
182
|
+
manual: false,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
};
|
|
@@ -5,6 +5,7 @@ import { useUpdateEffect } from 'ahooks';
|
|
|
5
5
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
6
6
|
import { AddNode } from './actions/AddNode';
|
|
7
7
|
import Property from './actions/Property';
|
|
8
|
+
import ConnectionLine from './Edges/ConnectionLine';
|
|
8
9
|
import defaultEdgeOptions from './Edges/defaultEdgeOptions';
|
|
9
10
|
import FlowToolbar from './FlowToolbar';
|
|
10
11
|
import { useGlobalPopover } from '../../shared/flow/PopoverContext';
|
|
@@ -269,7 +270,7 @@ const FlowStage = (props: FlowStageProps) => {
|
|
|
269
270
|
nodesConnectable={!lock}
|
|
270
271
|
elementsSelectable={!lock}
|
|
271
272
|
defaultEdgeOptions={defaultEdgeOptions}
|
|
272
|
-
|
|
273
|
+
connectionLineComponent={ConnectionLine}
|
|
273
274
|
onConnect={onConnect}
|
|
274
275
|
// onNodeClick={onNodeClick}
|
|
275
276
|
onSelectionChange={onSelectionChange}
|
|
@@ -20,6 +20,7 @@ import { useGlobalPopover } from '../../shared/flow/PopoverContext';
|
|
|
20
20
|
import { useFlowHistoryStore } from '../../shared/flow/stores/useFlowHistoryStore';
|
|
21
21
|
import { useFlowStore } from '../../shared/flow/stores/useFlowStore';
|
|
22
22
|
import { getLayoutedElements } from '../../shared/flow/utils';
|
|
23
|
+
import { assignAutoEdgeRoutes } from './Edges/edgeRoute';
|
|
23
24
|
import { AddNode } from './actions/AddNode';
|
|
24
25
|
import FlowHotKeys from './components/FlowHotKeys';
|
|
25
26
|
import ShortcutsModal from './components/ShortcutsModal';
|
|
@@ -194,26 +195,25 @@ const FlowToolbar = ({ copyAble, handleSave, isPreview = false, option, selected
|
|
|
194
195
|
);
|
|
195
196
|
|
|
196
197
|
// 4. 只对可见元素进行布局计算
|
|
197
|
-
// 注意:这里返回的 layoutedNodes 只包含可见节点
|
|
198
198
|
const layoutedNodes = getLayoutedElements(visibleNodes, visibleEdges);
|
|
199
199
|
|
|
200
|
-
// 5.
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
// 5. 按新坐标重算所有边的折点(清掉手工折点),多路汇入按深度错开
|
|
201
|
+
const hiddenEdges = allEdges.filter(
|
|
202
|
+
(e) => !visibleNodeIds.has(e.source) || !visibleNodeIds.has(e.target),
|
|
203
|
+
);
|
|
204
|
+
const routedEdges = assignAutoEdgeRoutes(layoutedNodes, visibleEdges);
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
setNodes([...layoutedNodes, ...hiddenNodes]);
|
|
207
|
+
setEdges([...routedEdges, ...hiddenEdges]);
|
|
206
208
|
|
|
207
|
-
// 6. 视图适应
|
|
208
|
-
// 推荐使用 requestAnimationFrame 确保 React 渲染完成
|
|
209
209
|
window.requestAnimationFrame(() => {
|
|
210
210
|
fitView({
|
|
211
|
-
nodes: layoutedNodes,
|
|
211
|
+
nodes: layoutedNodes,
|
|
212
212
|
duration: 300,
|
|
213
213
|
padding: 0.2
|
|
214
214
|
});
|
|
215
215
|
});
|
|
216
|
-
}, [getNodes, getEdges, setNodes, fitView]);
|
|
216
|
+
}, [getNodes, getEdges, setNodes, setEdges, fitView]);
|
|
217
217
|
|
|
218
218
|
// // 保存处理函数(仅触发,不做业务逻辑)
|
|
219
219
|
// const handleSave = useCallback(() => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PlayCircleOutlined } from '@ant-design/icons';
|
|
2
|
-
import { Button,
|
|
2
|
+
import { Button, Form, Input, Space, Typography, message } from 'antd';
|
|
3
|
+
import { Collapse, Modal } from '../../../ui/antd';
|
|
3
4
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
5
|
import { httpPost } from '../../../adapters/http';
|
|
5
6
|
import FlowViewer from '../../../viewer/FlowViewer';
|
|
@@ -66,6 +67,7 @@ const applyResultToForm = (form: ReturnType<typeof Form.useForm>[0], r: any) =>
|
|
|
66
67
|
passedNodes: pretty(st.passedNodes ?? st.PassedNodes ?? []),
|
|
67
68
|
parallelTokens: pretty(st.parallelTokens ?? st.ParallelTokens ?? []),
|
|
68
69
|
joinBarriers: pretty(st.joinBarriers ?? st.JoinBarriers ?? {}),
|
|
70
|
+
cancelledHistory: pretty(st.cancelledHistory ?? st.CancelledHistory ?? []),
|
|
69
71
|
});
|
|
70
72
|
return {
|
|
71
73
|
newTodos: Array.isArray(nextNodes) ? nextNodes.length : 0,
|
|
@@ -118,6 +120,7 @@ const ExecuteTestModal = ({ open, onClose, getFlowData }: ExecuteTestModalProps)
|
|
|
118
120
|
passedNodes: '[]',
|
|
119
121
|
parallelTokens: '[]',
|
|
120
122
|
joinBarriers: '{}',
|
|
123
|
+
cancelledHistory: '[]',
|
|
121
124
|
});
|
|
122
125
|
setResultText('');
|
|
123
126
|
setLastResult(null);
|
|
@@ -144,6 +147,7 @@ const ExecuteTestModal = ({ open, onClose, getFlowData }: ExecuteTestModalProps)
|
|
|
144
147
|
const passedNodes = tryParseJson(values.passedNodes, 'passedNodes') ?? [];
|
|
145
148
|
const parallelTokens = tryParseJson(values.parallelTokens, 'parallelTokens') ?? [];
|
|
146
149
|
const joinBarriers = tryParseJson(values.joinBarriers, 'joinBarriers') ?? {};
|
|
150
|
+
const cancelledHistory = tryParseJson(values.cancelledHistory, 'cancelledHistory') ?? [];
|
|
147
151
|
|
|
148
152
|
if (instanceId && (!Array.isArray(passedNodes) || passedNodes.length === 0)) {
|
|
149
153
|
setAdvancedKeys(['advanced']);
|
|
@@ -175,6 +179,7 @@ const ExecuteTestModal = ({ open, onClose, getFlowData }: ExecuteTestModalProps)
|
|
|
175
179
|
passedNodes,
|
|
176
180
|
parallelTokens,
|
|
177
181
|
joinBarriers,
|
|
182
|
+
cancelledHistory,
|
|
178
183
|
},
|
|
179
184
|
};
|
|
180
185
|
|
|
@@ -310,6 +315,9 @@ const ExecuteTestModal = ({ open, onClose, getFlowData }: ExecuteTestModalProps)
|
|
|
310
315
|
<Form.Item label="joinBarriers" name="joinBarriers">
|
|
311
316
|
<Input.TextArea rows={3} className="font-mono text-xs" />
|
|
312
317
|
</Form.Item>
|
|
318
|
+
<Form.Item label="cancelledHistory(累计作废)" name="cancelledHistory">
|
|
319
|
+
<Input.TextArea rows={3} className="font-mono text-xs" />
|
|
320
|
+
</Form.Item>
|
|
313
321
|
</>
|
|
314
322
|
),
|
|
315
323
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Table, Tag, Typography } from 'antd';
|
|
2
|
+
import { Modal } from '../../../ui/antd';
|
|
2
3
|
import { KeyOutlined } from '@ant-design/icons';
|
|
3
4
|
import { useMemo } from 'react';
|
|
4
5
|
|
|
@@ -78,10 +79,16 @@ const shortcuts: ShortcutItem[] = [
|
|
|
78
79
|
},
|
|
79
80
|
{
|
|
80
81
|
key: 'tidy-up',
|
|
81
|
-
label: '
|
|
82
|
+
label: '整理布局(并重算连线折点)',
|
|
82
83
|
shortcut: `${MOD_KEY} + Alt + A`,
|
|
83
84
|
category: '视图控制'
|
|
84
85
|
},
|
|
86
|
+
{
|
|
87
|
+
key: 'edge-bend',
|
|
88
|
+
label: '选中连线:工具栏改颜色/label;拖折点(Shift 正交);双击加折点;右键删',
|
|
89
|
+
shortcut: '鼠标',
|
|
90
|
+
category: '编辑操作'
|
|
91
|
+
},
|
|
85
92
|
{
|
|
86
93
|
key: 'lock',
|
|
87
94
|
label: '锁定/解锁',
|
|
@@ -2,7 +2,8 @@ import { getAllQueryString, successMessage } from '../../../adapters/utils';
|
|
|
2
2
|
import { useService, useServiceContent } from '../../../adapters/hooks';
|
|
3
3
|
import { ClockCircleOutlined, HistoryOutlined } from '@ant-design/icons';
|
|
4
4
|
import { useUpdateEffect } from 'ahooks';
|
|
5
|
-
import { Button,
|
|
5
|
+
import { Button, Typography, message } from 'antd';
|
|
6
|
+
import { Card, Modal } from '../../../ui/antd';
|
|
6
7
|
import { useEffect, useMemo } from 'react';
|
|
7
8
|
import {
|
|
8
9
|
FlowVersionHistoryAddReq,
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import type { Edge, Node } from '@xyflow/react';
|
|
2
2
|
|
|
3
|
+
export type WaitingToken = {
|
|
4
|
+
nodeCode: string;
|
|
5
|
+
forkNodeCode?: string;
|
|
6
|
+
branchId?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
3
9
|
export type TrailOverlay = {
|
|
4
10
|
passedCodes: string[];
|
|
5
11
|
waitingCodes: string[];
|
|
6
12
|
currentCode?: string;
|
|
13
|
+
/** 有序轨迹编码(相邻对 = 已走过的边);有驳回回路时比 passedCodes 集合更准 */
|
|
14
|
+
passedSequence?: string[];
|
|
15
|
+
/** 挂起令牌(含 fork),用于只高亮「真正进入挂起」的那条入边 */
|
|
16
|
+
waitingTokens?: WaitingToken[];
|
|
7
17
|
} | null;
|
|
8
18
|
|
|
9
19
|
export type TrailStatus = 'idle' | 'passed' | 'waiting' | 'current';
|
|
10
20
|
|
|
11
21
|
const TRAIL_CLASS_RE = /\bexecute-trail--(?:idle|passed|waiting|current)\b/g;
|
|
12
22
|
|
|
23
|
+
const edgeKey = (source: string, target: string) => `${source}\0${target}`;
|
|
24
|
+
|
|
13
25
|
export const collectCodes = (items: any[], keyCandidates: string[]): string[] => {
|
|
14
26
|
const set = new Set<string>();
|
|
15
27
|
if (!Array.isArray(items)) return [];
|
|
@@ -26,6 +38,41 @@ export const collectCodes = (items: any[], keyCandidates: string[]): string[] =>
|
|
|
26
38
|
return [...set];
|
|
27
39
|
};
|
|
28
40
|
|
|
41
|
+
/** 从 passedNodes 抽出有序 nodeCode(保留重复,供相邻对还原真实路径) */
|
|
42
|
+
export const collectSequence = (items: any[], keyCandidates: string[]): string[] => {
|
|
43
|
+
const seq: string[] = [];
|
|
44
|
+
if (!Array.isArray(items)) return seq;
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
if (!item || typeof item !== 'object') continue;
|
|
47
|
+
for (const key of keyCandidates) {
|
|
48
|
+
const v = item[key];
|
|
49
|
+
if (typeof v === 'string' && v.trim()) {
|
|
50
|
+
seq.push(v.trim());
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return seq;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const collectWaitingTokens = (items: any[]): WaitingToken[] => {
|
|
59
|
+
if (!Array.isArray(items)) return [];
|
|
60
|
+
const out: WaitingToken[] = [];
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
if (!item || typeof item !== 'object') continue;
|
|
64
|
+
const nodeCode = String(item.nodeCode ?? item.NodeCode ?? '').trim();
|
|
65
|
+
if (!nodeCode) continue;
|
|
66
|
+
const forkNodeCode = String(item.forkNodeCode ?? item.ForkNodeCode ?? '').trim() || undefined;
|
|
67
|
+
const branchId = String(item.branchId ?? item.BranchId ?? '').trim() || undefined;
|
|
68
|
+
const dedupe = `${nodeCode}\0${forkNodeCode || ''}\0${branchId || ''}`;
|
|
69
|
+
if (seen.has(dedupe)) continue;
|
|
70
|
+
seen.add(dedupe);
|
|
71
|
+
out.push({ nodeCode, forkNodeCode, branchId });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
|
|
29
76
|
const statusOf = (
|
|
30
77
|
code: string,
|
|
31
78
|
passed: Set<string>,
|
|
@@ -43,10 +90,95 @@ const hasActiveTrail = (overlay: TrailOverlay): boolean => {
|
|
|
43
90
|
return Boolean(
|
|
44
91
|
overlay.passedCodes.length
|
|
45
92
|
|| overlay.waitingCodes.length
|
|
46
|
-
|| overlay.currentCode?.trim()
|
|
93
|
+
|| overlay.currentCode?.trim()
|
|
94
|
+
|| (overlay.passedSequence && overlay.passedSequence.length)
|
|
95
|
+
|| (overlay.waitingTokens && overlay.waitingTokens.length),
|
|
47
96
|
);
|
|
48
97
|
};
|
|
49
98
|
|
|
99
|
+
/** 相邻 passedNodes → 已走过边 */
|
|
100
|
+
const buildPassedEdgeKeys = (sequence: string[]): Set<string> => {
|
|
101
|
+
const keys = new Set<string>();
|
|
102
|
+
for (let i = 0; i < sequence.length - 1; i += 1) {
|
|
103
|
+
const a = sequence[i];
|
|
104
|
+
const b = sequence[i + 1];
|
|
105
|
+
if (a && b && a !== b) {
|
|
106
|
+
keys.add(edgeKey(a, b));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return keys;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 挂起/当前节点只高亮「真正到达」的入边:
|
|
114
|
+
* 1) 轨迹末节点能连到该挂起节点 → 用这条(含驳回回路)
|
|
115
|
+
* 2) 否则用 token.forkNodeCode → 挂起节点(并行腿仍挂在 fork 出口)
|
|
116
|
+
* 3) 再否则:图上唯一一条「已走过源 → 挂起」边
|
|
117
|
+
*/
|
|
118
|
+
const buildArrivalEdgeKeys = (
|
|
119
|
+
edges: Edge[],
|
|
120
|
+
codeById: Map<string, string>,
|
|
121
|
+
sequence: string[],
|
|
122
|
+
waitingTokens: WaitingToken[],
|
|
123
|
+
waitingCodes: string[],
|
|
124
|
+
currentCode: string,
|
|
125
|
+
): Set<string> => {
|
|
126
|
+
const keys = new Set<string>();
|
|
127
|
+
const lastPassed = sequence.length ? sequence[sequence.length - 1] : '';
|
|
128
|
+
|
|
129
|
+
const codePairs = edges.map((e) => ({
|
|
130
|
+
source: codeById.get(e.source) ?? '',
|
|
131
|
+
target: codeById.get(e.target) ?? '',
|
|
132
|
+
})).filter((p) => p.source && p.target);
|
|
133
|
+
|
|
134
|
+
const targets = new Set<string>(waitingCodes);
|
|
135
|
+
if (currentCode) targets.add(currentCode);
|
|
136
|
+
|
|
137
|
+
const tokensByCode = new Map<string, WaitingToken[]>();
|
|
138
|
+
for (const t of waitingTokens) {
|
|
139
|
+
const list = tokensByCode.get(t.nodeCode) ?? [];
|
|
140
|
+
list.push(t);
|
|
141
|
+
tokensByCode.set(t.nodeCode, list);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const target of targets) {
|
|
145
|
+
let resolved = false;
|
|
146
|
+
|
|
147
|
+
if (lastPassed && lastPassed !== target) {
|
|
148
|
+
const fromLast = codePairs.some((p) => p.source === lastPassed && p.target === target);
|
|
149
|
+
if (fromLast) {
|
|
150
|
+
keys.add(edgeKey(lastPassed, target));
|
|
151
|
+
resolved = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!resolved) {
|
|
156
|
+
const tokens = tokensByCode.get(target) ?? [];
|
|
157
|
+
for (const t of tokens) {
|
|
158
|
+
if (t.forkNodeCode && t.forkNodeCode !== target) {
|
|
159
|
+
const fromFork = codePairs.some(
|
|
160
|
+
(p) => p.source === t.forkNodeCode && p.target === target,
|
|
161
|
+
);
|
|
162
|
+
if (fromFork) {
|
|
163
|
+
keys.add(edgeKey(t.forkNodeCode, target));
|
|
164
|
+
resolved = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!resolved) {
|
|
171
|
+
const incoming = codePairs.filter((p) => p.target === target);
|
|
172
|
+
const fromPassed = incoming.filter((p) => sequence.includes(p.source));
|
|
173
|
+
if (fromPassed.length === 1) {
|
|
174
|
+
keys.add(edgeKey(fromPassed[0].source, fromPassed[0].target));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return keys;
|
|
180
|
+
};
|
|
181
|
+
|
|
50
182
|
/** 派生展示节点:不改写画布 state,只叠加轨迹 class */
|
|
51
183
|
export const applyTrailToNodes = (nodes: Node[], overlay: TrailOverlay): Node[] => {
|
|
52
184
|
if (!hasActiveTrail(overlay)) {
|
|
@@ -67,7 +199,7 @@ export const applyTrailToNodes = (nodes: Node[], overlay: TrailOverlay): Node[]
|
|
|
67
199
|
});
|
|
68
200
|
};
|
|
69
201
|
|
|
70
|
-
/**
|
|
202
|
+
/** 派生展示边:已过 = passedNodes 相邻对;动画 = 进入挂起/当前的真实入边 */
|
|
71
203
|
export const applyTrailToEdges = (
|
|
72
204
|
edges: Edge[],
|
|
73
205
|
nodes: Node[],
|
|
@@ -76,9 +208,15 @@ export const applyTrailToEdges = (
|
|
|
76
208
|
if (!hasActiveTrail(overlay)) {
|
|
77
209
|
return edges;
|
|
78
210
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
211
|
+
const sequence = overlay!.passedSequence?.length
|
|
212
|
+
? overlay!.passedSequence
|
|
213
|
+
: overlay!.passedCodes;
|
|
214
|
+
const waitingCodes = overlay!.waitingCodes;
|
|
81
215
|
const current = overlay!.currentCode?.trim() || '';
|
|
216
|
+
const waitingTokens = overlay!.waitingTokens?.length
|
|
217
|
+
? overlay!.waitingTokens
|
|
218
|
+
: waitingCodes.map((nodeCode) => ({ nodeCode }));
|
|
219
|
+
|
|
82
220
|
const codeById = new Map(
|
|
83
221
|
nodes.map((n) => [
|
|
84
222
|
n.id,
|
|
@@ -86,22 +224,31 @@ export const applyTrailToEdges = (
|
|
|
86
224
|
]),
|
|
87
225
|
);
|
|
88
226
|
|
|
227
|
+
const passedEdgeKeys = buildPassedEdgeKeys(sequence);
|
|
228
|
+
const arrivalEdgeKeys = buildArrivalEdgeKeys(
|
|
229
|
+
edges,
|
|
230
|
+
codeById,
|
|
231
|
+
sequence,
|
|
232
|
+
waitingTokens,
|
|
233
|
+
waitingCodes,
|
|
234
|
+
current,
|
|
235
|
+
);
|
|
236
|
+
|
|
89
237
|
return edges.map((edge) => {
|
|
90
238
|
const sourceCode = codeById.get(edge.source) ?? '';
|
|
91
239
|
const targetCode = codeById.get(edge.target) ?? '';
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
&& (passed.has(targetCode) || waiting.has(targetCode) || targetCode === current),
|
|
97
|
-
);
|
|
240
|
+
const key = edgeKey(sourceCode, targetCode);
|
|
241
|
+
const onPassedTrail = Boolean(sourceCode && targetCode && passedEdgeKeys.has(key));
|
|
242
|
+
const isArrival = Boolean(sourceCode && targetCode && arrivalEdgeKeys.has(key));
|
|
243
|
+
const onTrail = onPassedTrail || isArrival;
|
|
98
244
|
return {
|
|
99
245
|
...edge,
|
|
100
|
-
animated:
|
|
246
|
+
animated: isArrival,
|
|
101
247
|
className: onTrail ? 'execute-trail-edge--on' : undefined,
|
|
102
248
|
data: {
|
|
103
249
|
...(edge.data || {}),
|
|
104
250
|
trailOn: onTrail,
|
|
251
|
+
trailArrival: isArrival,
|
|
105
252
|
},
|
|
106
253
|
};
|
|
107
254
|
});
|
package/src/designer/index.tsx
CHANGED
|
@@ -2,8 +2,9 @@ import { EllipsisOutlined, ExportOutlined, HistoryOutlined, ImportOutlined, Play
|
|
|
2
2
|
import { useParams } from '../adapters/router';
|
|
3
3
|
import { useDesignerConfig, useIsControlledDesigner, type FlowGraphContent } from '../adapters/designerConfig';
|
|
4
4
|
import { useMount, useUpdateEffect } from 'ahooks';
|
|
5
|
-
import { Button,
|
|
6
|
-
import
|
|
5
|
+
import { Button, Form, Input, Tag, message, Typography } from 'antd';
|
|
6
|
+
import { Dropdown, Modal, Popover } from '../ui/antd';
|
|
7
|
+
import type { MenuProps } from '../ui/antd';
|
|
7
8
|
import { useCallback, useMemo, useRef, useState, type ChangeEvent } from 'react';
|
|
8
9
|
|
|
9
10
|
import Flow from './flow';
|