|
@@ -15,6 +15,10 @@ import ReactFlow, {
|
|
|
Handle,
|
|
Handle,
|
|
|
Position,
|
|
Position,
|
|
|
ConnectionMode,
|
|
ConnectionMode,
|
|
|
|
|
+ EdgeLabelRenderer,
|
|
|
|
|
+ BaseEdge,
|
|
|
|
|
+ getStraightPath,
|
|
|
|
|
+ EdgeTypes,
|
|
|
} from 'reactflow';
|
|
} from 'reactflow';
|
|
|
import 'reactflow/dist/style.css';
|
|
import 'reactflow/dist/style.css';
|
|
|
import {
|
|
import {
|
|
@@ -441,7 +445,7 @@ function CustomNode({ data, selected, id }: any) {
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
- className={`relative px-3 py-2.5 rounded-lg shadow-sm border-2 w-auto min-w-[100px] max-w-[200px] bg-white ${
|
|
|
|
|
|
|
+ className={`relative px-4 py-4 rounded-lg shadow-sm border-2 w-[180px] h-auto min-h-[140px] bg-white ${
|
|
|
selected
|
|
selected
|
|
|
? 'border-blue-500 shadow-md ring-1 ring-blue-200'
|
|
? 'border-blue-500 shadow-md ring-1 ring-blue-200'
|
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
@@ -513,20 +517,22 @@ function CustomNode({ data, selected, id }: any) {
|
|
|
isConnectable={true}
|
|
isConnectable={true}
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <div className={`${config?.bgColor || 'bg-gray-50'} ${config?.borderColor || 'border-gray-100'} border p-2 rounded-xl flex-shrink-0 shadow-sm flex items-center justify-center`} style={{ borderRadius: '12px' }}>
|
|
|
|
|
|
|
+ {/* 垂直布局:顶部图标、中间名称、底部ID */}
|
|
|
|
|
+ <div className="flex flex-col items-center justify-between gap-3 h-full">
|
|
|
|
|
+ {/* 顶部:图标 */}
|
|
|
|
|
+ <div className={`${config?.bgColor || 'bg-gray-50'} ${config?.borderColor || 'border-gray-100'} border p-3 rounded-xl shadow-sm flex items-center justify-center flex-shrink-0`} style={{ borderRadius: '12px' }}>
|
|
|
<Icon
|
|
<Icon
|
|
|
- className={`${config?.iconColor || 'text-gray-600'} text-base`}
|
|
|
|
|
|
|
+ className={`${config?.iconColor || 'text-gray-600'} text-2xl`}
|
|
|
style={{ color: config?.iconColorCustom || undefined }}
|
|
style={{ color: config?.iconColorCustom || undefined }}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="flex-1 min-w-0">
|
|
|
|
|
- <div className="font-semibold text-xs text-gray-900 mb-0.5 leading-tight whitespace-nowrap">
|
|
|
|
|
- {data.label || config?.label}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="text-[10px] text-gray-500">
|
|
|
|
|
- ID: {nodeId}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* 中间:节点名称 */}
|
|
|
|
|
+ <div className="font-semibold text-sm text-gray-900 leading-tight text-center break-words w-full flex-1 flex items-center justify-center px-1">
|
|
|
|
|
+ {data.label || config?.label}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* 底部:ID */}
|
|
|
|
|
+ <div className="text-xs text-gray-500 text-center flex-shrink-0">
|
|
|
|
|
+ ID: {nodeId}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -578,6 +584,87 @@ const nodeTypes = {
|
|
|
complete: CompleteNode,
|
|
complete: CompleteNode,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// 全局变量存储删除函数(用于边组件)
|
|
|
|
|
+let globalDeleteEdgeFn: ((id: string) => void) | null = null;
|
|
|
|
|
+
|
|
|
|
|
+// 自定义边组件 - 定义在组件外部以确保稳定引用
|
|
|
|
|
+function CustomEdgeWithDelete({
|
|
|
|
|
+ id,
|
|
|
|
|
+ sourceX,
|
|
|
|
|
+ sourceY,
|
|
|
|
|
+ targetX,
|
|
|
|
|
+ targetY,
|
|
|
|
|
+ selected,
|
|
|
|
|
+ markerEnd,
|
|
|
|
|
+ style,
|
|
|
|
|
+}: any) {
|
|
|
|
|
+ const [edgePath, labelX, labelY] = getStraightPath({
|
|
|
|
|
+ sourceX,
|
|
|
|
|
+ sourceY,
|
|
|
|
|
+ targetX,
|
|
|
|
|
+ targetY,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('边组件渲染,ID:', id, 'selected:', selected, 'labelX:', labelX, 'labelY:', labelY);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <BaseEdge
|
|
|
|
|
+ id={id}
|
|
|
|
|
+ path={edgePath}
|
|
|
|
|
+ markerEnd={markerEnd}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ ...style,
|
|
|
|
|
+ strokeWidth: selected ? 3 : 2,
|
|
|
|
|
+ stroke: selected ? '#3b82f6' : '#94a3b8',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ {selected && (
|
|
|
|
|
+ <EdgeLabelRenderer>
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ position: 'absolute',
|
|
|
|
|
+ left: labelX,
|
|
|
|
|
+ top: labelY,
|
|
|
|
|
+ transform: 'translate(-50%, -50%)',
|
|
|
|
|
+ pointerEvents: 'all',
|
|
|
|
|
+ zIndex: 1000,
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="nodrag nopan"
|
|
|
|
|
+ >
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={(e) => {
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ console.log('删除按钮被点击,连线ID:', id);
|
|
|
|
|
+ if (globalDeleteEdgeFn) {
|
|
|
|
|
+ globalDeleteEdgeFn(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ onMouseDown={(e) => {
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="bg-red-500 hover:bg-red-600 rounded-full p-1.5 shadow-lg transition-colors flex items-center justify-center w-8 h-8"
|
|
|
|
|
+ title="删除连线"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <DeleteOutlined className="text-sm text-white" style={{ color: 'white' }} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </EdgeLabelRenderer>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 自定义边类型映射 - 定义在组件外部
|
|
|
|
|
+const edgeTypes: EdgeTypes = {
|
|
|
|
|
+ straight: CustomEdgeWithDelete,
|
|
|
|
|
+ default: CustomEdgeWithDelete,
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
export default function ProcessDesigner() {
|
|
export default function ProcessDesigner() {
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
|
const location = useLocation();
|
|
const location = useLocation();
|
|
@@ -591,6 +678,7 @@ export default function ProcessDesigner() {
|
|
|
return id ? parseInt(id, 10) : null;
|
|
return id ? parseInt(id, 10) : null;
|
|
|
}, [location.search]);
|
|
}, [location.search]);
|
|
|
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
|
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
|
|
|
|
+ const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
|
|
|
const [activeTabKey, setActiveTabKey] = useState<string>('info');
|
|
const [activeTabKey, setActiveTabKey] = useState<string>('info');
|
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
|
const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
|
|
const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
|
|
@@ -820,7 +908,7 @@ export default function ProcessDesigner() {
|
|
|
target: targetId,
|
|
target: targetId,
|
|
|
sourceHandle: edge.sourceHandle,
|
|
sourceHandle: edge.sourceHandle,
|
|
|
targetHandle: edge.targetHandle,
|
|
targetHandle: edge.targetHandle,
|
|
|
- type: edge.type || 'smoothstep',
|
|
|
|
|
|
|
+ type: edge.type || 'straight',
|
|
|
animated: false,
|
|
animated: false,
|
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
|
};
|
|
};
|
|
@@ -978,7 +1066,7 @@ export default function ProcessDesigner() {
|
|
|
targetHandle: targetHandle || undefined,
|
|
targetHandle: targetHandle || undefined,
|
|
|
animated: false,
|
|
animated: false,
|
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
|
- type: 'smoothstep',
|
|
|
|
|
|
|
+ type: 'straight',
|
|
|
};
|
|
};
|
|
|
// 直接添加到数组,不使用 addEdge(因为 addEdge 可能会过滤掉某些连接)
|
|
// 直接添加到数组,不使用 addEdge(因为 addEdge 可能会过滤掉某些连接)
|
|
|
const newEdges = [...eds, newEdge];
|
|
const newEdges = [...eds, newEdge];
|
|
@@ -1063,7 +1151,15 @@ export default function ProcessDesigner() {
|
|
|
// 节点点击处理
|
|
// 节点点击处理
|
|
|
const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
|
|
const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
|
|
|
setSelectedNode(node);
|
|
setSelectedNode(node);
|
|
|
|
|
+ setSelectedEdge(null); // 点击节点时取消连线选择
|
|
|
setActiveTabKey('info'); // 切换节点时重置到第一个tab
|
|
setActiveTabKey('info'); // 切换节点时重置到第一个tab
|
|
|
|
|
+ // 清除所有边的选中状态
|
|
|
|
|
+ setEdges((eds) =>
|
|
|
|
|
+ eds.map((e) => ({
|
|
|
|
|
+ ...e,
|
|
|
|
|
+ selected: false,
|
|
|
|
|
+ }))
|
|
|
|
|
+ );
|
|
|
// 加载节点配置
|
|
// 加载节点配置
|
|
|
const nodeData = node.data || {};
|
|
const nodeData = node.data || {};
|
|
|
const cache = loadNodeCache(node.id);
|
|
const cache = loadNodeCache(node.id);
|
|
@@ -1112,12 +1208,78 @@ export default function ProcessDesigner() {
|
|
|
notificationPerson: source.notificationPerson || '',
|
|
notificationPerson: source.notificationPerson || '',
|
|
|
notificationTime: source.notificationTime || '',
|
|
notificationTime: source.notificationTime || '',
|
|
|
});
|
|
});
|
|
|
- }, [loadNodeCache, nodes]);
|
|
|
|
|
|
|
+ }, [loadNodeCache, nodes, setEdges]);
|
|
|
|
|
|
|
|
// 画布点击处理(取消选择)
|
|
// 画布点击处理(取消选择)
|
|
|
const onPaneClick = useCallback(() => {
|
|
const onPaneClick = useCallback(() => {
|
|
|
setSelectedNode(null);
|
|
setSelectedNode(null);
|
|
|
- }, []);
|
|
|
|
|
|
|
+ setSelectedEdge(null);
|
|
|
|
|
+ // 清除所有边的选中状态
|
|
|
|
|
+ setEdges((eds) =>
|
|
|
|
|
+ eds.map((e) => ({
|
|
|
|
|
+ ...e,
|
|
|
|
|
+ selected: false,
|
|
|
|
|
+ }))
|
|
|
|
|
+ );
|
|
|
|
|
+ }, [setEdges]);
|
|
|
|
|
+
|
|
|
|
|
+ // 连线点击处理
|
|
|
|
|
+ const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+ console.log('连线被点击:', edge.id, '当前type:', edge.type);
|
|
|
|
|
+ setSelectedEdge(edge);
|
|
|
|
|
+ setSelectedNode(null); // 点击连线时取消节点选择
|
|
|
|
|
+ // 更新边的选中状态,并确保类型是 straight
|
|
|
|
|
+ setEdges((eds) => {
|
|
|
|
|
+ const updatedEdges = eds.map((e) => ({
|
|
|
|
|
+ ...e,
|
|
|
|
|
+ type: 'straight', // 强制设置为 straight 类型
|
|
|
|
|
+ selected: e.id === edge.id,
|
|
|
|
|
+ }));
|
|
|
|
|
+ const clickedEdge = updatedEdges.find(e => e.id === edge.id);
|
|
|
|
|
+ console.log('更新后的边状态:', clickedEdge?.selected, 'type:', clickedEdge?.type);
|
|
|
|
|
+ return updatedEdges;
|
|
|
|
|
+ });
|
|
|
|
|
+ }, [setEdges]);
|
|
|
|
|
+
|
|
|
|
|
+ // 删除连线
|
|
|
|
|
+ const handleDeleteEdge = useCallback((edgeId: string) => {
|
|
|
|
|
+ console.log('删除连线:', edgeId);
|
|
|
|
|
+ setEdges((eds) => {
|
|
|
|
|
+ const newEdges = eds.filter((edge) => edge.id !== edgeId);
|
|
|
|
|
+ console.log('删除后的连线数量:', newEdges.length);
|
|
|
|
|
+ // 保存历史
|
|
|
|
|
+ const newHistory = history.slice(0, historyIndex + 1);
|
|
|
|
|
+ newHistory.push({ nodes: [...nodes], edges: [...newEdges] });
|
|
|
|
|
+ setHistory(newHistory);
|
|
|
|
|
+ setHistoryIndex(newHistory.length - 1);
|
|
|
|
|
+ return newEdges;
|
|
|
|
|
+ });
|
|
|
|
|
+ setSelectedEdge(null);
|
|
|
|
|
+ }, [history, historyIndex, nodes]);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新全局删除函数
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ globalDeleteEdgeFn = handleDeleteEdge;
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ globalDeleteEdgeFn = null;
|
|
|
|
|
+ };
|
|
|
|
|
+ }, [handleDeleteEdge]);
|
|
|
|
|
+
|
|
|
|
|
+ // 确保所有边都使用 'straight' 类型(用于自定义边组件)
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ setEdges((eds) => {
|
|
|
|
|
+ const needsUpdate = eds.some(e => e.type !== 'straight');
|
|
|
|
|
+ if (needsUpdate) {
|
|
|
|
|
+ console.log('统一设置所有边的类型为 straight');
|
|
|
|
|
+ return eds.map(e => ({
|
|
|
|
|
+ ...e,
|
|
|
|
|
+ type: 'straight',
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
|
|
+ return eds;
|
|
|
|
|
+ });
|
|
|
|
|
+ }, []); // 只在组件挂载时执行一次
|
|
|
|
|
|
|
|
// 删除节点
|
|
// 删除节点
|
|
|
const handleDeleteNode = useCallback((nodeId?: string) => {
|
|
const handleDeleteNode = useCallback((nodeId?: string) => {
|
|
@@ -1621,7 +1783,7 @@ export default function ProcessDesigner() {
|
|
|
target: targetId,
|
|
target: targetId,
|
|
|
sourceHandle: edge.sourceHandle,
|
|
sourceHandle: edge.sourceHandle,
|
|
|
targetHandle: edge.targetHandle,
|
|
targetHandle: edge.targetHandle,
|
|
|
- type: edge.type || 'smoothstep',
|
|
|
|
|
|
|
+ type: edge.type || 'straight',
|
|
|
animated: false,
|
|
animated: false,
|
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
|
};
|
|
};
|
|
@@ -1822,18 +1984,20 @@ export default function ProcessDesigner() {
|
|
|
isValidConnection={isValidConnection}
|
|
isValidConnection={isValidConnection}
|
|
|
onNodeClick={onNodeClick}
|
|
onNodeClick={onNodeClick}
|
|
|
onNodeContextMenu={onNodeContextMenu}
|
|
onNodeContextMenu={onNodeContextMenu}
|
|
|
|
|
+ onEdgeClick={onEdgeClick}
|
|
|
onPaneClick={onPaneClick}
|
|
onPaneClick={onPaneClick}
|
|
|
onDrop={onDrop}
|
|
onDrop={onDrop}
|
|
|
onDragOver={onDragOver}
|
|
onDragOver={onDragOver}
|
|
|
deleteKeyCode={['Delete', 'Backspace']}
|
|
deleteKeyCode={['Delete', 'Backspace']}
|
|
|
nodeTypes={nodeTypes as NodeTypes}
|
|
nodeTypes={nodeTypes as NodeTypes}
|
|
|
|
|
+ edgeTypes={edgeTypes}
|
|
|
fitView
|
|
fitView
|
|
|
onInit={setReactFlowInstance}
|
|
onInit={setReactFlowInstance}
|
|
|
onMove={(_, viewport) => setZoom(viewport.zoom)}
|
|
onMove={(_, viewport) => setZoom(viewport.zoom)}
|
|
|
connectionMode={ConnectionMode.Loose}
|
|
connectionMode={ConnectionMode.Loose}
|
|
|
defaultEdgeOptions={{
|
|
defaultEdgeOptions={{
|
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
style: { strokeWidth: 2, stroke: '#94a3b8' },
|
|
|
- type: 'smoothstep',
|
|
|
|
|
|
|
+ type: 'straight',
|
|
|
}}
|
|
}}
|
|
|
edgesUpdatable={true}
|
|
edgesUpdatable={true}
|
|
|
edgesFocusable={true}
|
|
edgesFocusable={true}
|