重构前端逻辑

This commit is contained in:
dengqichen 2025-11-10 15:55:25 +08:00
parent fcdf005e22
commit f14aa984b1
5 changed files with 259 additions and 27 deletions

View File

@ -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 { 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'; import { convertToDisplayName } from '@/utils/workflow/variableConversion';
/** /**
* *
* hover高亮和样式优化 * hover高亮
*/ */
const CustomEdge: React.FC<EdgeProps> = ({ const CustomEdge: React.FC<EdgeProps> = ({
id, id,
@ -19,28 +19,107 @@ const CustomEdge: React.FC<EdgeProps> = ({
markerEnd, markerEnd,
label, label,
selected, selected,
data,
}) => { }) => {
const [isHovered, setIsHovered] = useState(false); 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 转换为节点名 // 将 label 中的 UUID 转换为节点名
const displayLabel = useMemo(() => { const displayLabel = useMemo(() => {
if (!label || typeof label !== 'string') { if (!label || typeof label !== 'string') {
return label; return label;
} }
const allNodes = getNodes() as FlowNode[]; const allNodes = getNodes() as FlowNode[];
return convertToDisplayName(label, allNodes); return convertToDisplayName(label, allNodes);
}, [label, getNodes]); }, [label, getNodes]);
const [edgePath, labelX, labelY] = getSmoothStepPath({ // 计算三个拐点vertices优先用数据中的 vertices否则从旧的 controlPoint 推导最后用默认1/4、1/2、3/4
sourceX, const vertices: ControlPoint[] = useMemo(() => {
sourceY, const dataVertices = (data as any)?.vertices as ControlPoint[] | undefined;
sourcePosition, if (Array.isArray(dataVertices) && dataVertices.length === 3) return dataVertices;
targetX, const cp = customControlPoint;
targetY, if (cp) {
targetPosition, 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 = { const edgeStyle = {
@ -74,11 +153,39 @@ const CustomEdge: React.FC<EdgeProps> = ({
fill="none" fill="none"
stroke="transparent" stroke="transparent"
strokeWidth={20} strokeWidth={20}
style={{ cursor: 'pointer' }} style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} 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 && ( {displayLabel && (
<EdgeLabelRenderer> <EdgeLabelRenderer>

View File

@ -47,6 +47,109 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
const [nodes, , onNodesStateChange] = useNodesState(initialNodes); const [nodes, , onNodesStateChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesStateChange] = useEdgesState(initialEdges); 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( const onConnect = useCallback(
(params: Connection | Edge) => { (params: Connection | Edge) => {
@ -142,7 +245,7 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
// 处理拖拽放置 // 处理拖拽放置
const handleDrop = useCallback((event: React.DragEvent) => { const handleDrop = useCallback((event: React.DragEvent) => {
event.preventDefault(); event.preventDefault();
if (onDrop) { if (onDrop) {
onDrop(event); onDrop(event);
} }
@ -152,7 +255,7 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
const handleDragOver = useCallback((event: React.DragEvent) => { const handleDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault(); event.preventDefault();
event.dataTransfer.dropEffect = 'move'; event.dataTransfer.dropEffect = 'move';
if (onDragOver) { if (onDragOver) {
onDragOver(event); onDragOver(event);
} }
@ -204,19 +307,19 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
elevateEdgesOnSelect elevateEdgesOnSelect
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
> >
<Background <Background
variant={BackgroundVariant.Dots} variant={BackgroundVariant.Dots}
gap={20} gap={20}
size={1} size={1}
color="#e5e7eb" color="#e5e7eb"
/> />
<Controls <Controls
position="bottom-right" position="bottom-right"
showZoom showZoom
showFitView showFitView
showInteractive showInteractive
/> />
<MiniMap <MiniMap
position="bottom-left" position="bottom-left"
nodeColor="#3b82f6" nodeColor="#3b82f6"
maskColor="rgba(0, 0, 0, 0.2)" maskColor="rgba(0, 0, 0, 0.2)"
@ -228,7 +331,7 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
borderRadius: '8px', borderRadius: '8px',
}} }}
/> />
{/* 状态面板 */} {/* 状态面板 */}
<Panel position="top-left"> <Panel position="top-left">
<div style={{ <div style={{

View File

@ -81,7 +81,12 @@ export const useWorkflowLoad = () => {
// ⚠️ 不再设置默认值,边线初始状态为 undefined由用户主动配置 // ⚠️ 不再设置默认值,边线初始状态为 undefined由用户主动配置
const condition = edgeConfig.condition; const condition = edgeConfig.condition;
const label = condition?.expression || edge.name || ''; 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 { return {
id: edge.id, id: edge.id,
source: edge.from, // 后端使用from字段作为source source: edge.from, // 后端使用from字段作为source
@ -99,7 +104,9 @@ export const useWorkflowLoad = () => {
label, label,
data: { data: {
label, label,
condition condition,
vertices,
controlPoint
} }
}; };
}); });

View File

@ -107,7 +107,12 @@ export const useWorkflowSave = () => {
// ⚠️ 并行网关的边线不传递条件配置BPMN 规范不允许) // ⚠️ 并行网关的边线不传递条件配置BPMN 规范不允许)
condition: isFromParallelGateway ? undefined : edge.data?.condition 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) }]
: [])
}; };
}) })
}; };

View File

@ -20,6 +20,12 @@ export interface FlowNodeData extends Record<string, unknown> {
nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition; nodeDefinition?: import('./nodes/types').WorkflowNodeDefinition;
} }
// 控制点类型(用于可拖动的连接线)
export interface ControlPoint {
x: number;
y: number;
}
// React Flow 边数据 - 添加索引签名以满足React Flow的类型约束 // React Flow 边数据 - 添加索引签名以满足React Flow的类型约束
export interface FlowEdgeData extends Record<string, unknown> { export interface FlowEdgeData extends Record<string, unknown> {
label?: string; label?: string;
@ -29,6 +35,10 @@ export interface FlowEdgeData extends Record<string, unknown> {
script?: string; script?: string;
priority: number; priority: number;
}; };
// 兼容旧数据:单个控制点
controlPoint?: ControlPoint;
// 新增多个拐点当前需求为3个
vertices?: ControlPoint[];
} }
// 扩展的React Flow节点类型 // 扩展的React Flow节点类型