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

2023-11-02 05:30

本文主要是介绍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 (<Dropdownmenu={{items: [{key: 'add',label: 'addNode',onClick: () => {console.log('addNode!!!');},},],}}trigger={['contextMenu']}><divclassName='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'; // iconconst 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 (<Dropdownmenu={{items: [{key: 'add',label: 'addNode',onClick: () => {console.log('addNode!!!');},},],}}trigger={['contextMenu']}><divclassName='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版本中遇到的一些难点和值得记录的地方写了出来,欢迎大家共同交流,一起进步!👏👏👏~

这篇关于React中antv X6 2.0的使用体验(含demo)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/328437

相关文章

使用Python实现图像LBP特征提取的操作方法

《使用Python实现图像LBP特征提取的操作方法》LBP特征叫做局部二值模式,常用于纹理特征提取,并在纹理分类中具有较强的区分能力,本文给大家介绍了如何使用Python实现图像LBP特征提取的操作方... 目录一、LBP特征介绍二、LBP特征描述三、一些改进版本的LBP1.圆形LBP算子2.旋转不变的LB

Maven的使用和配置国内源的保姆级教程

《Maven的使用和配置国内源的保姆级教程》Maven是⼀个项目管理工具,基于POM(ProjectObjectModel,项目对象模型)的概念,Maven可以通过一小段描述信息来管理项目的构建,报告... 目录1. 什么是Maven?2.创建⼀个Maven项目3.Maven 核心功能4.使用Maven H

Python中__init__方法使用的深度解析

《Python中__init__方法使用的深度解析》在Python的面向对象编程(OOP)体系中,__init__方法如同建造房屋时的奠基仪式——它定义了对象诞生时的初始状态,下面我们就来深入了解下_... 目录一、__init__的基因图谱二、初始化过程的魔法时刻继承链中的初始化顺序self参数的奥秘默认

SpringBoot使用GZIP压缩反回数据问题

《SpringBoot使用GZIP压缩反回数据问题》:本文主要介绍SpringBoot使用GZIP压缩反回数据问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot使用GZIP压缩反回数据1、初识gzip2、gzip是什么,可以干什么?3、Spr

HTML5中的Microdata与历史记录管理详解

《HTML5中的Microdata与历史记录管理详解》Microdata作为HTML5新增的一个特性,它允许开发者在HTML文档中添加更多的语义信息,以便于搜索引擎和浏览器更好地理解页面内容,本文将探... 目录html5中的Mijscrodata与历史记录管理背景简介html5中的Microdata使用M

html5的响应式布局的方法示例详解

《html5的响应式布局的方法示例详解》:本文主要介绍了HTML5中使用媒体查询和Flexbox进行响应式布局的方法,简要介绍了CSSGrid布局的基础知识和如何实现自动换行的网格布局,详细内容请阅读本文,希望能对你有所帮助... 一 使用媒体查询响应式布局        使用的参数@media这是常用的

HTML5表格语法格式详解

《HTML5表格语法格式详解》在HTML语法中,表格主要通过table、tr和td3个标签构成,本文通过实例代码讲解HTML5表格语法格式,感兴趣的朋友一起看看吧... 目录一、表格1.表格语法格式2.表格属性 3.例子二、不规则表格1.跨行2.跨列3.例子一、表格在html语法中,表格主要通过< tab

Spring Boot 集成 Quartz并使用Cron 表达式实现定时任务

《SpringBoot集成Quartz并使用Cron表达式实现定时任务》本篇文章介绍了如何在SpringBoot中集成Quartz进行定时任务调度,并通过Cron表达式控制任务... 目录前言1. 添加 Quartz 依赖2. 创建 Quartz 任务3. 配置 Quartz 任务调度4. 启动 Sprin

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Java使用SLF4J记录不同级别日志的示例详解

《Java使用SLF4J记录不同级别日志的示例详解》SLF4J是一个简单的日志门面,它允许在运行时选择不同的日志实现,这篇文章主要为大家详细介绍了如何使用SLF4J记录不同级别日志,感兴趣的可以了解下... 目录一、SLF4J简介二、添加依赖三、配置Logback四、记录不同级别的日志五、总结一、SLF4J