重构消息通知弹窗
This commit is contained in:
parent
09fe61e9a6
commit
f9633e23b1
@ -1,29 +1,24 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath, useReactFlow } from '@xyflow/react';
|
||||
import type { FlowNode, ControlPoint } from '../types';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { SmartStepEdge as BaseSmartEdge } from '@tisoap/react-flow-smart-edge';
|
||||
import type { FlowNode } from '../types';
|
||||
import { convertToDisplayName } from '@/utils/workflow/variableConversion';
|
||||
|
||||
/**
|
||||
* 自定义边组件
|
||||
* 使用简单贝塞尔曲线 + 单控制点,支持 hover 高亮和拖动调整路径
|
||||
* 自定义边组件 - 智能避障
|
||||
* 使用 SmartStepEdge 实现智能路由避障,采用直角路径
|
||||
*/
|
||||
const CustomEdge: React.FC<EdgeProps> = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
markerEnd,
|
||||
label,
|
||||
selected,
|
||||
data,
|
||||
}) => {
|
||||
const CustomEdge: React.FC<any> = (props) => {
|
||||
const {
|
||||
style = {},
|
||||
markerEnd,
|
||||
label,
|
||||
selected,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { getNodes, setEdges, screenToFlowPosition } = useReactFlow();
|
||||
const { getNodes } = useReactFlow();
|
||||
|
||||
// 将 label 中的 UUID 转换为节点名
|
||||
const displayLabel = useMemo(() => {
|
||||
@ -35,111 +30,6 @@ const CustomEdge: React.FC<EdgeProps> = ({
|
||||
return convertToDisplayName(label, allNodes);
|
||||
}, [label, getNodes]);
|
||||
|
||||
// 计算单个控制点:优先使用保存的 controlPoint,否则使用智能默认位置
|
||||
const controlPoint: ControlPoint = useMemo(() => {
|
||||
const savedCP = (data as any)?.controlPoint as ControlPoint | undefined;
|
||||
if (savedCP) return savedCP;
|
||||
|
||||
// 智能默认位置:根据源和目标位置关系计算
|
||||
const dx = targetX - sourceX;
|
||||
const dy = targetY - sourceY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 控制点在中点位置,垂直偏移距离的 20%,方向取决于节点位置关系
|
||||
const midX = (sourceX + targetX) / 2;
|
||||
const midY = (sourceY + targetY) / 2;
|
||||
|
||||
// 计算垂直方向的偏移(顺时针 90 度)
|
||||
const offsetRatio = 0.2;
|
||||
const offsetX = -dy / distance * distance * offsetRatio;
|
||||
const offsetY = dx / distance * distance * offsetRatio;
|
||||
|
||||
return {
|
||||
x: midX + offsetX,
|
||||
y: midY + offsetY,
|
||||
};
|
||||
}, [data, sourceX, sourceY, targetX, targetY]);
|
||||
|
||||
// 若数据中缺少 controlPoint,则在首次渲染时写回
|
||||
React.useEffect(() => {
|
||||
if (!(data as any)?.controlPoint) {
|
||||
setEdges((eds) => eds.map(ed =>
|
||||
ed.id === id ? { ...ed, data: { ...ed.data, controlPoint } } : ed
|
||||
));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 使用 Catmull-Rom 样条曲线,确保曲线严格经过控制点且平滑
|
||||
const [edgePath, labelX, labelY] = useMemo(() => {
|
||||
// Catmull-Rom 样条曲线会经过所有关键点
|
||||
// 转换为三次贝塞尔曲线:需要计算切线
|
||||
|
||||
// 点序列:起点 -> 控制点 -> 终点
|
||||
const p0 = { x: sourceX, y: sourceY };
|
||||
const p1 = controlPoint;
|
||||
const p2 = { x: targetX, y: targetY };
|
||||
|
||||
// 为了使用 Catmull-Rom,需要虚拟的前后点(用于计算切线)
|
||||
// 虚拟前点:起点的镜像延伸
|
||||
const p_1 = {
|
||||
x: p0.x - (p1.x - p0.x) * 0.3,
|
||||
y: p0.y - (p1.y - p0.y) * 0.3,
|
||||
};
|
||||
|
||||
// 虚拟后点:终点的镜像延伸
|
||||
const p3 = {
|
||||
x: p2.x + (p2.x - p1.x) * 0.3,
|
||||
y: p2.y + (p2.y - p1.y) * 0.3,
|
||||
};
|
||||
|
||||
// Catmull-Rom 转贝塞尔:第一段(p0 -> p1)
|
||||
// 切线: t0 = (p1 - p_1) / 2, t1 = (p2 - p0) / 2
|
||||
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;
|
||||
|
||||
// Catmull-Rom 转贝塞尔:第二段(p1 -> p2)
|
||||
// 切线: t0 = (p2 - p0) / 2, t1 = (p3 - p1) / 2
|
||||
const c3x = p1.x + (p2.x - p0.x) / 6;
|
||||
const c3y = p1.y + (p2.y - p0.y) / 6;
|
||||
const c4x = p2.x - (p3.x - p1.x) / 6;
|
||||
const c4y = p2.y - (p3.y - p1.y) / 6;
|
||||
|
||||
// 拼接两段三次贝塞尔曲线
|
||||
const path = `M ${p0.x},${p0.y} C ${c1x},${c1y} ${c2x},${c2y} ${p1.x},${p1.y} C ${c3x},${c3y} ${c4x},${c4y} ${p2.x},${p2.y}`;
|
||||
|
||||
// 标签显示在控制点位置
|
||||
return [path, controlPoint.x, controlPoint.y] as [string, number, number];
|
||||
}, [controlPoint, sourceX, sourceY, targetX, targetY]);
|
||||
|
||||
// 拖动控制点处理
|
||||
const handleControlPointMouseDown = useCallback((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 =>
|
||||
ed.id === id
|
||||
? { ...ed, data: { ...ed.data, controlPoint: { x: flowPosition.x, y: flowPosition.y } } }
|
||||
: ed
|
||||
));
|
||||
};
|
||||
|
||||
const handleUp = () => {
|
||||
setIsDragging(false);
|
||||
window.removeEventListener('mousemove', handleMove);
|
||||
window.removeEventListener('mouseup', handleUp);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMove);
|
||||
window.addEventListener('mouseup', handleUp);
|
||||
}, [id, setEdges, screenToFlowPosition]);
|
||||
|
||||
|
||||
// 根据状态确定样式
|
||||
const edgeStyle = {
|
||||
...style,
|
||||
@ -156,81 +46,37 @@ const CustomEdge: React.FC<EdgeProps> = ({
|
||||
return markerEnd;
|
||||
})();
|
||||
|
||||
// 自定义标签样式
|
||||
const labelStyle = {
|
||||
fill: selected ? '#3b82f6' : '#64748b',
|
||||
fontWeight: 500,
|
||||
fontSize: 12,
|
||||
};
|
||||
|
||||
const labelBgStyle = {
|
||||
fill: 'white',
|
||||
fillOpacity: 1,
|
||||
};
|
||||
|
||||
const labelBgPadding = [4, 8] as [number, number];
|
||||
const labelBgBorderRadius = 6;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
<g
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<BaseSmartEdge
|
||||
{...restProps}
|
||||
label={displayLabel}
|
||||
style={edgeStyle}
|
||||
markerEnd={markerEndStyle as any}
|
||||
labelStyle={labelStyle}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
/>
|
||||
|
||||
{/* 增加点击区域 */}
|
||||
<path
|
||||
d={edgePath}
|
||||
fill="none"
|
||||
stroke="transparent"
|
||||
strokeWidth={20}
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'pointer' }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
/>
|
||||
|
||||
{/* 单个可拖动控制点 */}
|
||||
{(selected || isHovered) && (
|
||||
<g>
|
||||
<circle
|
||||
cx={controlPoint.x}
|
||||
cy={controlPoint.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={handleControlPointMouseDown}
|
||||
/>
|
||||
<circle
|
||||
cx={controlPoint.x}
|
||||
cy={controlPoint.y}
|
||||
r={15}
|
||||
fill="transparent"
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'grab', pointerEvents: 'all' }}
|
||||
onMouseDown={handleControlPointMouseDown}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* 边标签 */}
|
||||
{displayLabel && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
color: selected ? '#3b82f6' : '#64748b',
|
||||
background: 'white',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '6px',
|
||||
border: `1px solid ${selected ? '#3b82f6' : '#e5e7eb'}`,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{displayLabel}
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ import '@xyflow/react/dist/style.css';
|
||||
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
import { nodeTypes } from '../nodes';
|
||||
import SmartEdge from './SmartEdge';
|
||||
import CustomEdge from './CustomEdge';
|
||||
import { generateEdgeId } from '../utils/idGenerator';
|
||||
|
||||
interface FlowCanvasProps {
|
||||
@ -179,7 +179,7 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
onDragOver={handleDragOver}
|
||||
onMove={onViewportChange}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={{ smoothstep: SmartEdge }}
|
||||
edgeTypes={{ smoothstep: CustomEdge }}
|
||||
isValidConnection={isValidConnection}
|
||||
defaultEdgeOptions={{
|
||||
type: 'smoothstep',
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { EdgeLabelRenderer, EdgeProps, useReactFlow } from '@xyflow/react';
|
||||
import { SmartStepEdge as BaseSmartEdge } from '@tisoap/react-flow-smart-edge';
|
||||
import type { FlowNode } from '../types';
|
||||
import { convertToDisplayName } from '@/utils/workflow/variableConversion';
|
||||
|
||||
/**
|
||||
* 智能边组件 - 自动避开节点(基于 SmartStepEdge)
|
||||
* 使用 react-flow-smart-edge 实现智能路由避障,采用直角路径
|
||||
*/
|
||||
const SmartEdge: React.FC<EdgeProps> = (props) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
selected,
|
||||
style = {},
|
||||
markerEnd,
|
||||
} = props;
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { getNodes } = useReactFlow();
|
||||
|
||||
// 将 label 中的 UUID 转换为节点名
|
||||
const displayLabel = useMemo(() => {
|
||||
if (!label || typeof label !== 'string') {
|
||||
return label;
|
||||
}
|
||||
|
||||
const allNodes = getNodes() as FlowNode[];
|
||||
return convertToDisplayName(label, allNodes);
|
||||
}, [label, getNodes]);
|
||||
|
||||
// 增强样式
|
||||
const enhancedStyle = {
|
||||
...style,
|
||||
stroke: selected ? '#3b82f6' : isHovered ? '#64748b' : '#94a3b8',
|
||||
strokeWidth: selected ? 3 : isHovered ? 2.5 : 2,
|
||||
transition: 'all 0.2s ease',
|
||||
};
|
||||
|
||||
const enhancedMarkerEnd = (() => {
|
||||
if (!markerEnd || typeof markerEnd !== 'object') return markerEnd;
|
||||
const markerObj = markerEnd as Record<string, any>;
|
||||
if (selected) return { ...markerObj, color: '#3b82f6' };
|
||||
if (isHovered) return { ...markerObj, color: '#64748b' };
|
||||
return markerEnd;
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 使用 SmartStepEdge 作为基础(直角路径) */}
|
||||
<BaseSmartEdge
|
||||
{...props}
|
||||
style={enhancedStyle}
|
||||
markerEnd={enhancedMarkerEnd as any}
|
||||
/>
|
||||
|
||||
{/* 自定义标签(如果有) */}
|
||||
{displayLabel && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
data-edge-id={id}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
color: selected ? '#3b82f6' : '#64748b',
|
||||
background: 'white',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '6px',
|
||||
border: `1px solid ${selected ? '#3b82f6' : '#e5e7eb'}`,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{displayLabel}
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmartEdge;
|
||||
Loading…
Reference in New Issue
Block a user