重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-14 18:27:42 +08:00
parent 09fe61e9a6
commit f9633e23b1
3 changed files with 45 additions and 287 deletions

View File

@ -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>
);
};

View File

@ -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',

View File

@ -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;