React中antv X6 2.0的使用体验(含demo)

BB一下🫧:写这antv X6系列文章的初衷是发现antv X6 2.0版本的相关文章不多,大多都是老版本的一些操作,2.0版本有一些升级,当我遇到问题时能够参考的资料非常有限,同时也是为了记录自己的学习到的一些知识点。

本次X6使用体验的功能包括:

1、节点右键菜单;

2、节点自动布局;

3、根据列表动态渲染节点;

4、节点之间的连线;

5、模版引用时的预览功能;

6、使用拖拽组件添加节点(包含2种样式 及群组的添加);

仓库地址https://github.com/Tipchak5/antvX6_2.0_React.git


一、节点右键菜单

参考了官方文档写法后自己的的右键菜单功能( 官方案例:https://x6.antv.antgroup.com/zh/examples/edge/tool/#context-menu),个人感觉就是注册一个带有右键菜单功能的自定义节点。在初始化画布时注册一个菜单组件,并将你定义的节点返回出来(返回出来的节点要被Dropdown组件包裹),最后 再把你注册的这个右键菜单作为component放进注册的自定义节点中。 具体如下:

首先在init函数中去把你需要的组件定义好然后注册,我这里是CustomComponent,label是这个节点中要显示的内容,color是这个节点要动态显示的颜色(你可以在生成节点时去定义你要的字段),考虑到可能会有使用动态图片的情况,也在节点中添加了img,是否需要图标可以自行判断。

	const CustomComponent = ({ node }) => {
			const label = node.prop('label');
			const color = node.prop('color');

			const boder = node.store?.data?.attrs?.body?.stroke;
			return (
				<Dropdown
					menu={{
						items: [
							{
								key: 'add',
								label: 'addNode',
								onClick: () => {
									console.log('addNode!!!');
								},
							},
						],
					}}
					trigger={['contextMenu']}
				>
					<div
						className='custom-react-node'
						style={{
							background: label === '开始' ? '#7AA874' : color,
							border: `3px solid ${boder}`,
						}}
					>
						<img className='img' src={male} alt='Icon' />
						{label}
					</div>
				</Dropdown>
			);
		};

		register({
			shape: 'custom-react-node', // 后续生成的节点shap只要是这个 就会有右键菜单
			width: 100,
			height: 40,
			attrs: {
				label: {
					textAnchor: 'left',
					refX: 8,
					textWrap: {
						ellipsis: true,
					},
				},
			},
			component: CustomComponent,
		});

当组件注册好了之后,再引入注册自定义节点用的插件 import { register } from '@antv/x6-react-shape'; 
注册的时候有一个shape字段 ,当你后续生成的节点shap只要和注册的节点shap一致就会有右键菜单


当对某一节点右键操作后,想要获取改节点信息可以使用:

   graph.on('node:contextmenu', ({ node }) => {
            setNodeInfo(node); // 获取点击了右键的节点信息
            console.log(node, '我是被右键点击的节点!');
        });

完整代码 

import { useEffect, useRef, useState } from 'react';
import { register } from '@antv/x6-react-shape';
import { Graph } from '@antv/x6';
import { Export } from '@antv/x6-plugin-export';
import { Selection } from '@antv/x6-plugin-selection';
import { Snapline } from '@antv/x6-plugin-snapline';
import { Keyboard } from '@antv/x6-plugin-keyboard';
import { Clipboard } from '@antv/x6-plugin-clipboard';
import { History } from '@antv/x6-plugin-history';
import { Transform } from '@antv/x6-plugin-transform';
import { reset, showPorts } from '../../utils/method';
import './nodeFlow.less';

const male =
	'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*kUy8SrEDp6YAAAAAAAAAAAAAARQnAQ'; // icon

const ports = {}; // 连接桩 此处省略 可参考demo 或官网

/** 函数组件 */
function Flow(props) {
	let graph;
	const nodeInfoRef = useRef(null);
	const [newGraph, setNewGraph] = useState(null); // 画布

	const [nodeInfo, setNodeInfo] = useState(null); // 节点信息

	useEffect(() => {
		nodeInfoRef.current = nodeInfo;
		// console.log(nodeInfoRef.current);
	}, [nodeInfo]);


	useEffect(() => {
		init(); // 初始
		graph.centerContent();
		graph
			.use(
				new Snapline({
					enabled: true,
				})
			)
			.use(
				new Selection({
					enabled: true,
				})
			)
			.use(
				new Keyboard({
					enabled: true,
				})
			)
			.use(
				new Clipboard({
					enabled: true,
				})
			)
			.use(
				new History({
					enabled: true,
				})
			)
			.use(
				new Transform({
					resizing: true,
					rotating: true,
					enabled: true,
				})
			)
			.use(new Export());

		graph.on('node:click', ({ node }) => {
			console.log(node);
		});

		graph.on('edge:click', ({ edge }) => {
			reset(graph);
			edge.attr('line/stroke', 'orange');
		});

		/** 右键操作 */
		graph.on('node:contextmenu', ({ node }) => {
			setNodeInfo(node); // 获取点击了右键的节点信息
			console.log(node, '我是被右键点击的节点!');
		});

		graph.bindKey(['ctrl+1', 'meta+1'], () => {
			const zoom = graph.zoom();
			if (zoom < 1.5) {
				graph.zoom(0.1);
			}
		});

		graph.bindKey(['ctrl+2', 'meta+2'], () => {
			const zoom = graph.zoom();
			if (zoom > 0.5) {
				graph.zoom(-0.1);
			}
		});

		// 删除处理
		graph.bindKey('backspace', () => {
			const cells = graph.getSelectedCells();
			const cellsId = cells[0].id;
			if (cellsId) {
				graph.removeCells(cells);
				// 删除节点信息 接口
			}
		});

		graph.zoomTo(0.8);

		return () => {
			graph.dispose(); // 销毁画布
		};
	}, []);

	return (
		<div className='FlowManage'>
			<div className='content'>
				<div className='graphBox'>
					<div className='react-shape-app graph'>
						<div id='graph-container' className='app-content' style={{ flex: 1 }}></div>
					</div>
				</div>
			</div>
		</div>
	);

	/** 初始化画布 */
	function init() {
		// 右键菜单
		const CustomComponent = ({ node }) => {
			const label = node.prop('label');
			const color = node.prop('color');

			const boder = node.store?.data?.attrs?.body?.stroke;
			return (
				<Dropdown
					menu={{
						items: [
							{
								key: 'add',
								label: 'addNode',
								onClick: () => {
									console.log('addNode!!!');
								},
							},
						],
					}}
					trigger={['contextMenu']}
				>
					<div
						className='custom-react-node'
						style={{
							background: label === '开始' ? '#7AA874' : color,
							border: `3px solid ${boder}`,
						}}
					>
						{label === '开始' ? null : <img className='img' src={male} alt='Icon' />}
						{label}
					</div>
				</Dropdown>
			);
		};

		register({
			shape: 'custom-react-node',
			width: 100,
			height: 40,
			attrs: {
				label: {
					textAnchor: 'left',
					refX: 8,
					textWrap: {
						ellipsis: true,
					},
				},
			},
			component: CustomComponent,
		});

		graph = new Graph({
			container: document.getElementById('graph-container'),
			grid: true,
			panning: true,
			mousewheel: {
				enabled: true,
				zoomAtMousePosition: true,
				modifiers: 'ctrl',
				minScale: 0.4,
				maxScale: 3,
			},
			connecting: {
				snap: true,
				router: 'manhattan', // 路由模式
				highlight: true,
			},
			scroller: true,
		});

		graph.addNode({
			shape: 'custom-react-node',
			id: -1,
			label: '开始',
			ports: { ...ports },
		});

		graph.centerContent();

		graph.on('node:mouseenter', () => {
			const container = document.getElementById('graph-container');
			const ports = container.querySelectorAll('.x6-port-body');
			showPorts(ports, true);
		});
		graph.on('node:mouseleave', () => {
			const container = document.getElementById('graph-container');
			const ports = container.querySelectorAll('.x6-port-body');
			showPorts(ports, false);
		});
		setNewGraph(graph);
	}
}

export default Flow;

二、节点自动布局

未自动布局:

自动布局后:

自动布局官方也有推荐(使用的是antv/layout插件,但是我这边和官方有点差别,我这里是需要安装dagre,使用的"dagre": "^0.8.5"版本,在使用的地方引入:import dagre from 'dagre';

 具体代码如下(你只用在需要的时候直接调用这个函数即可,如:新增节点后调用):

import dagre from 'dagre';

	function layout() {
		const g = graph ? graph : newGraph;
		const layout = new dagre.graphlib.Graph();
		layout.setGraph({ rankdir: 'LR', ranksep: 50, nodesep: 50, controlPoints: true });
		layout.setDefaultEdgeLabel(() => ({}));

		g?.getNodes()?.forEach((node) => {
			layout.setNode(node.id, { width: node.size().width, height: node.size().height });
		});

		g?.getEdges()?.forEach((edge) => {
			layout.setEdge(edge.getSourceCell()?.id, edge.getTargetCell()?.id);
		});

		dagre.layout(layout);

		g?.getNodes()?.forEach((node) => {
			const pos = layout.node(node.id);
			node.position(pos.x, pos.y);
		});
		g?.centerContent();
	}

当然,你可以根据官方文档来实现(网址:x6.antv.antgroup.com/temp/layout#布局流程


三、根据列表动态渲染节点

最开始我想的是画布上渲染的内容由前端导出成json给后端,当进入页面又让后端返回给前端,这样来渲染,加上当时新增的节点ID和后端是两套ID,而且每次有节点修改都需要后端去json里面改了又返给我,这样对后端不是很友好,在我这个项目中也不算很合理,尤其是一些操作上的处理会很麻烦,考虑到这些问题,最后领导推荐根据列表数据去动态渲染节点,只需要在列表里,把每个节点的信息定义好,比如:节点的名称、颜色、源节点的id,然后再通过遍历去将节点添加到画布中。

具体代码如下:

	/** 根据列表渲染节点 */
	function refreshGraph() {
		const g = graph ? graph : newGraph;
		g?.clearCells(); // 清除先前的数据

		graph.addNode({
			shape: 'custom-react-node',
			id: -1,
			label: '开始',
			ports: { ...ports },
		}); // 原节点

		treeList[0].children?.forEach((i) => { // treeList节点列表
			let newNodeOptions = null;

			newNodeOptions = {
				shape: 'custom-react-node',
				id: i.key,
				label: i.title,
				color: i.color,
				ports: { ...ports },
			};

			// 如果存在父节点 连接两个节点
			let newNode = null;
			if (i.parents && g) {
				// const node = g.getCellById(i.parent);
				newNode = g?.addNode(newNodeOptions);
				i.parents.forEach((id) => {
					// 根据父id 连接子
					g?.addEdge({
						source: id,
						target: i.key,
						router: {
							name: 'manhattan',
						},
					});
				});
			} else {
				// 如果没有父节点
				g?.addEdge({
					source: -1,
					target: i.key,
					router: {
						name: 'manhattan',
					},
				});
				g?.addNode(newNodeOptions);
			}

			autoLayout(g); // 自动布局 抽成的公共方法 记得的引入
		});
	}

四、节点连线时的操作

在useEffect中加入这段代码,当你连接两个节点时,就能获取到源节点 和 目标节点的信息,比如:ID、Label等。

具体代码如下:

	graph.on('edge:connected', ({ isNew, edge }) => {
			if (isNew) {
				// 如果连接节点成功
				const source = edge.getSourceNode(); // 源节点
				const target = edge?.getTargetNode(); // 目标节点
				console.log(`源节点`, source, `目标节点`, target);
			}
		});

五、模版插入时的预览功能

antv X6 的模版预览比较简单,这边是抽成一个组件,只需要从父组件把要预览的数据导出之后,传递到模版预览组件就好了。

具体代码如下:

import { Graph } from '@antv/x6';
import { useState, useEffect, useRef } from 'react';

const templateGraph = (props) => {
	const graphRef = useRef(null);
	const [graph, setGraph] = useState(null);

	useEffect(() => {
		// 画布初始化
		if (graphRef.current) {
			const newGraph = new Graph({
				container: graphRef.current,
				width: '100%',
				height: 200,
				grid: true,
				panning: true,
				mousewheel: {
					enabled: true,
					zoomAtMousePosition: true,
					modifiers: 'ctrl',
					minScale: 0.4,
					maxScale: 3,
				},
				scroller: true,
				node: {
					draggable: false,
				},
			});
			newGraph.centerContent();
			newGraph.zoomTo(0.5); // 画布缩放
			setGraph(newGraph);
			return () => {
				newGraph.dispose(); // 销毁画布
			};
		}
	}, [graphRef]);

	useEffect(() => {
		// 渲染父组件传来的画布数据
		if (props.data && graph) {
			tempate();
		}
	}, [props.data, graph]);

	return (
		<div className='graphBox' style={{ width: '100%', height: '200px' }}>
			<div className='react-shape-app' style={{ width: '100%' }}>
				<div ref={graphRef} style={{ width: '100%', height: '100%' }}></div>
			</div>
		</div>
	);

	/** 模版渲染 */
	async function tempate() {
		graph.removeCells(graph.getCells()); // 清除先前的数据
		await new Promise((resolve) => setTimeout(resolve, 100));
		graph.fromJSON(props.data).centerContent(); // 节点内容渲染与剧中
	}
};

export default templateGraph;

六、使用拖拽组件添加节点 (两种样式)

我这里的写法是根据自己的需要改的,可以直接使用官方的(使用场景 | X6 (antgroup.com)

记得安装dnd插件 然后引入 import { Dnd } from '@antv/x6-plugin-dnd';

        样式一:(代码详情看demo,内含添加节点群组的方法 写的比较简单)

        样式二 :

 这里比较重要的一点是在list中想要使用拖拽的这个插件功能只能在原生的标签中使用(这里用的是ul和li标签),如果想要在antd的组件中使用需要自行去修改源码。

注意:在如果每个节点的img不一样那么就需要在init方法中把每个样式都作为一个自定义的节点去注册一遍!

代码太多,这边就放一小部分,具体的可以看demo

		nodeArr[0]?.children?.forEach((node) => {
			const { key, title, img } = node;
			const shape = `custom-node${key}`;
			register({
				shape,
				width: 100,
				height: 40,
				attrs: {
					image: {
						'xlink:href': img,
					},
				},
				component: (props) => <CustomComponent {...props} image={img} label={title} />,
			});
		}); // 注册每一个节点
const nodeArr = [
	{
		title: '其他节点',
		key: 'myTool',
		children: [
			{
				key: 1,
				title: 'Node1',
				img:'',
			},
			{
				key: 2,
				title: 'Node2',
				img:'',
			},
		],
	},
	{
		title: '通用节点',
		key: 'publicTool',
		children: [],
	},
];


<ul>
	<li>
		其他节点
		<ul>
			{nodeArr[0].children.map((i) => {
			return (
			<div style={{ marginTop: '10px' }} key={i.key} onMouseDown={(e)=> {
				startDrag(e, i);
				}}
				>
				{i.title}
			</div>
			);
			})}
		</ul>
	</li>
	<li style={{ marginTop: '10px' }}>通用节点</li>
</ul>


/** 拖拽节点到画布 */
function startDrag(e, i) {
	const g = graph ? graph : newGraph;
	// console.log(id);
	const nodeTypes = {
		Node1: `custom-node${i.key}`,
		Node2: `custom-node${i.key}`,
		//其他节点类型
	};

	const node = g?.createNode({
		label: i.title,
		ports: { ...ports },
		color: '',
		shape: nodeTypes[i.title],
	});
	dndRef.current?.start(node, e?.nativeEvent);
}

项目差不多也要进入测试阶段了,抽空把自己在antv X6 2.0版本中遇到的一些难点和值得记录的地方写了出来,欢迎大家共同交流,一起进步!👏👏👏~

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用AntV X6的人工智能建模DAG图需要以下步骤: 1. 安装AntV X6库 可以使用npm进行安装,命令如下: ``` npm install @antv/x6 --save ``` 2. 创建画布 使用X6创建一个画布,将其添加到页面。代码如下: ```javascript import { Graph } from '@antv/x6'; const graph = new Graph({ container: document.getElementById('container'), width: 800, height: 600 }); ``` 3. 创建节点 使用X6创建一个节点,并设置其参数。代码如下: ```javascript const node = graph.addNode({ x: 40, y: 40, width: 80, height: 40, label: 'AI', shape: 'rect', attrs: { body: { rx: 5, ry: 5, fill: '#a0d911', stroke: '#096dd9', strokeWidth: 1, }, label: { textAnchor: 'left', refX: 10, fill: '#fff', fontSize: 14, fontFamily: 'Microsoft YaHei', }, }, }); ``` 4. 创建连线 使用X6创建两个节点之间的连线。需要设置起点、终点、样式等参数。代码如下: ```javascript const edge = graph.addEdge({ source: { cell: node1 }, target: { cell: node2 }, attrs: { line: { stroke: '#1890ff', strokeWidth: 1, targetMarker: { name: 'classic', size: 8, }, }, }, }); ``` 5. 添加交互 可以使用X6添加节点、连线的拖动、缩放等交互。代码如下: ```javascript graph.addNodeTool('my-custom-tool', MyCustomTool); graph.addEdgeTool('my-custom-tool', MyCustomTool); graph.bindKey('backspace', () => { const cells = graph.getSelectedCells(); if (cells.length) { graph.removeCells(cells); } }); graph.on('node:click', ({ e, x, y, node }) => { console.log(`click on node: ${node.id}`); }); ``` 以上就是使用AntV X6库创建人工智能建模DAG图的基本步骤。需要根据实际需求进行调整和完善。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值