重构前端逻辑
This commit is contained in:
parent
fcdf005e22
commit
f14aa984b1
@ -1,11 +1,11 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useReactFlow } from '@xyflow/react';
|
||||
import type { FlowNode } from '../types';
|
||||
import type { FlowNode, ControlPoint } from '../types';
|
||||
import { convertToDisplayName } from '@/utils/workflow/variableConversion';
|
||||
|
||||
/**
|
||||
* 自定义边组件
|
||||
* 支持hover高亮和样式优化
|
||||
* 支持hover高亮、样式优化和拖动改变路径
|
||||
*/
|
||||
const CustomEdge: React.FC<EdgeProps> = ({
|
||||
id,
|
||||
@ -19,9 +19,14 @@ const CustomEdge: React.FC<EdgeProps> = ({
|
||||
markerEnd,
|
||||
label,
|
||||
selected,
|
||||
data,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { getNodes } = useReactFlow();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { getNodes, setEdges, screenToFlowPosition } = useReactFlow();
|
||||
|
||||
// 从 data 中获取自定义的控制点
|
||||
const customControlPoint = data?.controlPoint as ControlPoint | undefined;
|
||||
|
||||
// 将 label 中的 UUID 转换为节点名
|
||||
const displayLabel = useMemo(() => {
|
||||
@ -33,14 +38,88 @@ const CustomEdge: React.FC<EdgeProps> = ({
|
||||
return convertToDisplayName(label, allNodes);
|
||||
}, [label, getNodes]);
|
||||
|
||||
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
// 计算三个拐点(vertices):优先用数据中的 vertices;否则从旧的 controlPoint 推导;最后用默认1/4、1/2、3/4
|
||||
const vertices: ControlPoint[] = useMemo(() => {
|
||||
const dataVertices = (data as any)?.vertices as ControlPoint[] | undefined;
|
||||
if (Array.isArray(dataVertices) && dataVertices.length === 3) return dataVertices;
|
||||
const cp = customControlPoint;
|
||||
if (cp) {
|
||||
return [
|
||||
{ x: (sourceX + cp.x) / 2, y: (sourceY + cp.y) / 2 },
|
||||
{ x: cp.x, y: cp.y },
|
||||
{ x: (cp.x + targetX) / 2, y: (cp.y + targetY) / 2 },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ x: sourceX + (targetX - sourceX) * 0.25, y: sourceY + (targetY - sourceY) * 0.25 },
|
||||
{ x: (sourceX + targetX) / 2, y: (sourceY + targetY) / 2 },
|
||||
{ x: sourceX + (targetX - sourceX) * 0.75, y: sourceY + (targetY - sourceY) * 0.75 },
|
||||
];
|
||||
}, [data, customControlPoint, sourceX, sourceY, targetX, targetY]);
|
||||
|
||||
// 若数据中缺少 vertices,则在首次渲染时写回,确保后续保存能持久化
|
||||
React.useEffect(() => {
|
||||
const hasDataVertices = Array.isArray((data as any)?.vertices) && (data as any).vertices.length === 3;
|
||||
if (!hasDataVertices) {
|
||||
setEdges((eds) => eds.map(ed => ed.id === id ? { ...ed, data: { ...ed.data, vertices } } : ed));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 生成路径:使用 Catmull-Rom -> Cubic Bezier 插值,曲线严格经过拐点
|
||||
const [edgePath, labelX, labelY] = useMemo(() => {
|
||||
// 拼接点序列:起点、3个拐点、终点
|
||||
const pts = [
|
||||
{ x: sourceX, y: sourceY },
|
||||
vertices[0],
|
||||
vertices[1],
|
||||
vertices[2],
|
||||
{ x: targetX, y: targetY },
|
||||
];
|
||||
// Catmull-Rom 转三次贝塞尔:C1 = P0 + (P1 - P_{-1})/6, C2 = P1 - (P2 - P0)/6
|
||||
const segs: string[] = [];
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p_1 = i === 0 ? pts[i] : pts[i - 1];
|
||||
const p0 = pts[i];
|
||||
const p1 = pts[i + 1];
|
||||
const p2 = i + 2 < pts.length ? pts[i + 2] : p1;
|
||||
const c1x = p0.x + (p1.x - p_1.x) / 6;
|
||||
const c1y = p0.y + (p1.y - p_1.y) / 6;
|
||||
const c2x = p1.x - (p2.x - p0.x) / 6;
|
||||
const c2y = p1.y - (p2.y - p0.y) / 6;
|
||||
segs.push(`C ${c1x},${c1y} ${c2x},${c2y} ${p1.x},${p1.y}`);
|
||||
}
|
||||
const path = `M ${pts[0].x},${pts[0].y} ${segs.join(' ')}`;
|
||||
// 标签显示在第二个拐点
|
||||
return [path, vertices[1].x, vertices[1].y] as [string, number, number];
|
||||
}, [vertices, sourceX, sourceY, targetX, targetY]);
|
||||
|
||||
// 拖动处理(支持第 idx 个拐点)
|
||||
const handleVertexMouseDown = useCallback((idx: number, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
|
||||
const handleMove = (moveEvent: MouseEvent) => {
|
||||
const flowPosition = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.clientY });
|
||||
setEdges((eds) => eds.map(ed => {
|
||||
if (ed.id !== id) return ed;
|
||||
const old = (ed as any).data?.vertices as ControlPoint[] | undefined;
|
||||
const base = Array.isArray(old) && old.length === 3 ? [...old] : vertices.slice();
|
||||
base[idx] = { x: flowPosition.x, y: flowPosition.y };
|
||||
return { ...ed, data: { ...ed.data, vertices: base } } as any;
|
||||
}));
|
||||
};
|
||||
|
||||
const handleUp = () => {
|
||||
setIsDragging(false);
|
||||
window.removeEventListener('mousemove', handleMove);
|
||||
window.removeEventListener('mouseup', handleUp);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMove);
|
||||
window.addEventListener('mouseup', handleUp);
|
||||
}, [id, setEdges, screenToFlowPosition, vertices]);
|
||||
|
||||
// 根据状态确定样式
|
||||
const edgeStyle = {
|
||||
@ -74,11 +153,39 @@ const CustomEdge: React.FC<EdgeProps> = ({
|
||||
fill="none"
|
||||
stroke="transparent"
|
||||
strokeWidth={20}
|
||||
style={{ cursor: 'pointer' }}
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
/>
|
||||
|
||||
{/* 控制点(三个拐点,直接在SVG中渲染,可拖动) */}
|
||||
{(selected || isHovered) && (
|
||||
<g>
|
||||
{vertices.map((pt, idx) => (
|
||||
<g key={`v-${idx}`}>
|
||||
<circle
|
||||
cx={pt.x}
|
||||
cy={pt.y}
|
||||
r={8}
|
||||
fill={isDragging ? '#2563eb' : '#3b82f6'}
|
||||
stroke="white"
|
||||
strokeWidth={2}
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'grab', filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))', pointerEvents: 'all' }}
|
||||
onMouseDown={(e) => handleVertexMouseDown(idx, e)}
|
||||
/>
|
||||
<circle
|
||||
cx={pt.x}
|
||||
cy={pt.y}
|
||||
r={15}
|
||||
fill="transparent"
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'grab', pointerEvents: 'all' }}
|
||||
onMouseDown={(e) => handleVertexMouseDown(idx, e)}
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* 边标签 */}
|
||||
{displayLabel && (
|
||||
<EdgeLabelRenderer>
|
||||
|
||||
@ -47,6 +47,109 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
const [nodes, , onNodesStateChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesStateChange] = useEdgesState(initialEdges);
|
||||
|
||||
// --- Auto adjust edge vertices when nodes move (preserve shape, avoid excessive length) ---
|
||||
useEffect(() => {
|
||||
// helper: get node position by id
|
||||
const getNodePos = (id: string) => {
|
||||
const n = nodes.find((nn) => nn.id === id);
|
||||
return n?.position;
|
||||
};
|
||||
|
||||
// geometry helpers
|
||||
const recomputeVertices = (
|
||||
oldVerts: Array<{ x: number; y: number }> | undefined,
|
||||
oldA: { x: number; y: number } | undefined,
|
||||
oldB: { x: number; y: number } | undefined,
|
||||
newA: { x: number; y: number },
|
||||
newB: { x: number; y: number }
|
||||
) => {
|
||||
// default three vertices placed along the baseline
|
||||
const defaultThree = () => [
|
||||
{ x: newA.x + (newB.x - newA.x) * 0.25, y: newA.y + (newB.y - newA.y) * 0.25 },
|
||||
{ x: (newA.x + newB.x) / 2, y: (newA.y + newB.y) / 2 },
|
||||
{ x: newA.x + (newB.x - newA.x) * 0.75, y: newA.y + (newB.y - newA.y) * 0.75 },
|
||||
];
|
||||
|
||||
// if no old endpoints or old vertices, return default
|
||||
if (!oldA || !oldB || !oldVerts || oldVerts.length !== 3) {
|
||||
return defaultThree();
|
||||
}
|
||||
|
||||
const projOnLine = (p: { x: number; y: number }, a: { x: number; y: number }, b: { x: number; y: number }) => {
|
||||
const dx = b.x - a.x, dy = b.y - a.y;
|
||||
const len2 = dx * dx + dy * dy || 1;
|
||||
const t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));
|
||||
const px = a.x + t * dx, py = a.y + t * dy;
|
||||
const offx = p.x - px, offy = p.y - py;
|
||||
// sign: which side of baseline (using cross product sign)
|
||||
const cross = dx * (p.y - a.y) - dy * (p.x - a.x);
|
||||
const sign = cross >= 0 ? 1 : -1;
|
||||
const offMag = Math.hypot(offx, offy);
|
||||
return { t, offMag, sign };
|
||||
};
|
||||
|
||||
const mapToNew = (t: number, offMag: number, sign: number, a: { x: number; y: number }, b: { x: number; y: number }) => {
|
||||
const dx = b.x - a.x, dy = b.y - a.y;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
// unit normal for new baseline
|
||||
const nx = -dy / len, ny = dx / len;
|
||||
// clamp offset to avoid huge bulges (auto shrink if nodes far apart)
|
||||
const maxOffset = Math.max(20, 0.35 * len);
|
||||
const useOff = Math.min(offMag, maxOffset);
|
||||
const baseX = a.x + t * dx, baseY = a.y + t * dy;
|
||||
return { x: baseX + sign * useOff * nx, y: baseY + sign * useOff * ny };
|
||||
};
|
||||
|
||||
// Compute mapping (t, offset) from old baseline, then apply to new baseline
|
||||
const tOff = oldVerts.map((v) => projOnLine(v, oldA, oldB));
|
||||
// enforce monotonic t order to keep vertex order stable
|
||||
tOff.sort((a, b) => a.t - b.t);
|
||||
const mapped = tOff.map((m) => mapToNew(m.t, m.offMag, m.sign, newA, newB));
|
||||
// ensure exactly 3 vertices
|
||||
if (mapped.length === 3) return mapped as Array<{ x: number; y: number }>;
|
||||
// fallback
|
||||
return defaultThree();
|
||||
};
|
||||
|
||||
setEdges((eds) => {
|
||||
return eds.map((ed) => {
|
||||
const s = getNodePos(ed.source);
|
||||
const t = getNodePos(ed.target);
|
||||
if (!s || !t) return ed;
|
||||
|
||||
const dataAny = (ed as any).data || {};
|
||||
const last = dataAny._lastEndpoints as { sx: number; sy: number; tx: number; ty: number } | undefined;
|
||||
const current = { sx: s.x, sy: s.y, tx: t.x, ty: t.y };
|
||||
|
||||
// Initialize vertices if missing
|
||||
const curVerts = (dataAny.vertices as any) as Array<{ x: number; y: number }> | undefined;
|
||||
let vertices = curVerts;
|
||||
let changed = false;
|
||||
|
||||
if (!Array.isArray(vertices) || vertices.length !== 3) {
|
||||
vertices = recomputeVertices(undefined, undefined, undefined, { x: current.sx, y: current.sy }, { x: current.tx, y: current.ty });
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Recompute when endpoints moved
|
||||
if (!last || last.sx !== current.sx || last.sy !== current.sy || last.tx !== current.tx || last.ty !== current.ty) {
|
||||
vertices = recomputeVertices(vertices, last ? { x: last.sx, y: last.sy } : undefined, last ? { x: last.tx, y: last.ty } : undefined, { x: current.sx, y: current.sy }, { x: current.tx, y: current.ty });
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) return ed;
|
||||
return {
|
||||
...ed,
|
||||
data: {
|
||||
...ed.data,
|
||||
vertices,
|
||||
_lastEndpoints: current,
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
});
|
||||
}, [nodes, setEdges]);
|
||||
|
||||
// 处理连接
|
||||
const onConnect = useCallback(
|
||||
(params: Connection | Edge) => {
|
||||
|
||||
@ -81,6 +81,11 @@ export const useWorkflowLoad = () => {
|
||||
// ⚠️ 不再设置默认值,边线初始状态为 undefined,由用户主动配置
|
||||
const condition = edgeConfig.condition;
|
||||
const label = condition?.expression || edge.name || '';
|
||||
// 从后端读取保存的拐点(支持多个)
|
||||
const verticesArr = (edge as any).vertices as Array<{ x: number; y: number }> | undefined;
|
||||
const vertices = Array.isArray(verticesArr) ? verticesArr.map(p => ({ x: p.x, y: p.y })) : undefined;
|
||||
// 兼容旧数据:若有旧的单点,用作中间点
|
||||
const controlPoint = vertices && vertices.length > 0 ? undefined : (undefined as any);
|
||||
|
||||
return {
|
||||
id: edge.id,
|
||||
@ -99,7 +104,9 @@ export const useWorkflowLoad = () => {
|
||||
label,
|
||||
data: {
|
||||
label,
|
||||
condition
|
||||
condition,
|
||||
vertices,
|
||||
controlPoint
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@ -107,7 +107,12 @@ export const useWorkflowSave = () => {
|
||||
// ⚠️ 并行网关的边线不传递条件配置(BPMN 规范不允许)
|
||||
condition: isFromParallelGateway ? undefined : edge.data?.condition
|
||||
},
|
||||
vertices: [] // 暂时为空数组
|
||||
// 优先保存多个拐点;若无则回退到单个控制点;否则为空
|
||||
vertices: Array.isArray((edge as any)?.data?.vertices) && (edge as any).data.vertices.length > 0
|
||||
? (edge as any).data.vertices.map((p: any) => ({ x: Math.round(p.x), y: Math.round(p.y) }))
|
||||
: ((edge as any)?.data?.controlPoint
|
||||
? [{ x: Math.round((edge as any).data.controlPoint.x), y: Math.round((edge as any).data.controlPoint.y) }]
|
||||
: [])
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
@ -20,6 +20,12 @@ export interface FlowNodeData extends Record<string, unknown> {
|
||||
nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition;
|
||||
}
|
||||
|
||||
// 控制点类型(用于可拖动的连接线)
|
||||
export interface ControlPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// React Flow 边数据 - 添加索引签名以满足React Flow的类型约束
|
||||
export interface FlowEdgeData extends Record<string, unknown> {
|
||||
label?: string;
|
||||
@ -29,6 +35,10 @@ export interface FlowEdgeData extends Record<string, unknown> {
|
||||
script?: string;
|
||||
priority: number;
|
||||
};
|
||||
// 兼容旧数据:单个控制点
|
||||
controlPoint?: ControlPoint;
|
||||
// 新增:多个拐点(当前需求为3个)
|
||||
vertices?: ControlPoint[];
|
||||
}
|
||||
|
||||
// 扩展的React Flow节点类型
|
||||
|
||||
Loading…
Reference in New Issue
Block a user