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.
@@ -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
- // connectionLineComponent={edgeTypes.connectionLine}
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
- setNodes([...layoutedNodes, ...hiddenNodes]);
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
- // 如果布局改变了边的路径(比如使用了 elkjs 或 dagre 有些配置会改边),也要更新边
205
- // setEdges([...layoutedEdges, ...hiddenEdges]); // 根据你的 getLayoutedElements 返回值决定
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, // 明确告诉 fitView 只关注刚才排版过的可见节点
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, Collapse, Form, Input, Modal, Space, Typography, message } from 'antd';
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 { Modal, Table, Tag, Typography } from 'antd';
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, Card, Modal, Typography, message } from 'antd';
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
- /** 派生展示边:保留原 handle / 曲线,仅标记轨迹 */
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 passed = new Set(overlay!.passedCodes);
80
- const waiting = new Set(overlay!.waitingCodes);
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 onTrail = Boolean(
93
- sourceCode
94
- && targetCode
95
- && passed.has(sourceCode)
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: Boolean(onTrail && (waiting.has(targetCode) || targetCode === current)),
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
  });
@@ -1,6 +1,6 @@
1
1
  import { createElement } from 'react';
2
2
  import type { Edge, Node } from '@xyflow/react';
3
- import { Modal } from 'antd';
3
+ import { Modal } from '../../../ui/antd';
4
4
 
5
5
  export type FlowIssue = {
6
6
  level: 'error' | 'warning';
@@ -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, Dropdown, Form, Input, Modal, Popover, Tag, message, Typography } from 'antd';
6
- import type { MenuProps } from 'antd';
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';
@@ -1,6 +1,6 @@
1
1
  // PopoverContext.tsx
2
2
  import React, { createContext, useContext, useRef, useState, useEffect } from 'react';
3
- import { Popover } from 'antd';
3
+ import { Popover } from '../../ui/antd';
4
4
 
5
5
  interface PopoverInfo {
6
6
  content: React.ReactNode;
@@ -1,4 +1,4 @@
1
- import { Popover } from "antd";
1
+ import { Popover } from '../../../ui/antd';
2
2
 
3
3
  export const FlowPopover = (props: any) => {
4
4
  const { children, content, open, onOpenChange } = props;
@@ -1,4 +1,4 @@
1
- import { Tooltip, TooltipProps } from "antd";
1
+ import { Tooltip, type TooltipProps } from '../../../ui/antd';
2
2
 
3
3
  export const FlowToolTip = (props: TooltipProps) => {
4
4
  const { children, ...restProps } = props;