长期更新版文档请移步语雀(「AntV」X6开发实践:踩过的坑与解决方案 (yuque.com))
相信你们在开发中更多的需求是需要自定义拖拽源,毕竟自定义的功能扩展性高一些,而且可以根据你的业务需求灵活设置。自定义拖拽的优点就是:万物皆可成为拖拽源,不管你使用的是html标签,还是第三方的ui框架,或者树形列表,……这些都可以设置成拖拽源,只有你想不到的,没有官方做不到的,来吧,开整。
这里使用的是Dnd插件,因为我看官方介绍说Stencil插件内部也是依靠Dnd实现的,索性就直接使用Dnd来搞吧
// 先定义个全局的dnd变量 let dnd = null; // 在mounted中对dnd进行初始化(在graph之后初始化) dnd = new Dnd({ target: graph, scaled: false, dndContainer: proxy.$refs.dndContainer });
/** * 自定义拖拽源事件 * @param {*} e * @param {*} treeNode 根据需要传入要添加的参数 * @param {*} data 根据需要传入要添加的参数 * 这里使用的是elementPlus的tree组件 */ const startDrag = (e, treeNode, data) => { console.log('eee', e); console.log('treeNode', treeNode); console.log('data', data); const node = graph.createNode({ shape: 'cu-data-node', width: 150, height: 104, label: data?.label, // 传递给自定义节点的数据 data: { label: data?.label, img: data?.img, desc: data?.desc }, ports: { ...port, items: [ { group: 'top' } ] } }); dnd.start(node, e); };
// 注册自定义节点 图标+标题+描述 Shape.HTML.register({ shape: 'cu-data-node', width: 'auto', height: 104, effect: ['data'], html(cell) { // 获取节点传递过来的数据 const { label, img, desc } = cell.getData(); // 创建自定义的节点容器 const container = document.createElement('div'); container.setAttribute('class', 'cu-container'); // 图片根据不同的类型进行切换,可以是后端返回的图标,也可以是自己本地的图标,如果是后端返回就通过节点的data传进来 const container_img = document.createElement('img'); container_img.src = currentTab.value === 0 ? '/src/assets/images/operator/datasouce.png' : img; container_img.setAttribute('class', 'cu-container-img'); const container_title = document.createElement('div'); container_title.innerText = label; container_title.setAttribute('class', 'cu-container-title'); const container_desc = document.createElement('div'); container_desc.setAttribute('class', 'cu-container-desc'); container_desc.innerText = desc || '描述信息'; container.appendChild(container_img); container.appendChild(container_title); container.appendChild(container_desc); return container; } });
<!-- $event必传,后面的参数根据你的业务需求动态添加 --> <div @mousedown="startDrag($event, node, data)">拖拽的节点</div>
先看看官方的导出文档:
图片导出
由于业务需要,需要把画布上的节点保存成图片供其它模块展示,如果你的后端返回的数据格式是前端想要的,那么大不必搞图片的形式,直接把官网的快速上手代码拿过来循环一下就好了……,这里就拿
toPng
的方法来讲解
toPng
拿到画布的base64数据然而事情却没有这么简单,第一步就遇到了一堆的坑,由于官网上导出的都是它内置的节点,所以导出都没啥问题,但是我使用的是html节点,导出的时候,我节点的图片就死活导不出来,而且导出的样式也是乱的(样式错乱问题)
一句话:图片必须得是base64格式的导出才会有图片,不然无法导出节点的图片
需要调用 imageToDataUri把图片地址转成base64的数据,这个方法我也是摸索了好久才找到的,官方文档完全没有提及这个方法,如果需要查看其它方法,请打印
DataUri
这个对象
DataUri.imageToDataUri('/images/operator/datasouce.png', function (nu, url) { // 第一个参数无效,用的只是第二个参数,但是第一个参数不写不行 container_img.src = url; // 给图片标签赋值 } );
toPng
生成base64数据这个步骤中要处理的问题:导出后样式不正确,导出的时候页面闪动
graph.toPNG( dataUri => { console.log('dataUri >>>>', dataUri); // 这个就是base的图片地址 }, { width: 526, height: 268, backgroundColor: 'rgba(25, 87, 121, 0.18)', quality: 1, // 图片质量 取值范围:0-1,默认0.92 // copyStyles: false, // 自定义样式表,为了解决导出后节点样式丢失的问题,暂时官方还没有修复这个bug stylesheet: ` .cu-container { display: flex; flex-direction: column; align-items: center; justify-content: center; } .cu-container-title { color: #d3e6f3; } .cu-container-img { width: 53px; height: 53px; margin-bottom: 4px; } .cu-container-desc { color: rgba(211, 230, 243, 0.7); margin-top: 3px; } .cu-container-title, .cu-container-desc { font-size: 14px; font-weight: 400; line-height: 20px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; } ` } );
:::warning
Tip:图片导出样式和原节点的样式不一致问题:如果你遇到了这个问题,目前最好的方法是不断调整stylesheet
中的样式,直到导出的样式和原节点的样式几乎一致即可。当然,如果你的甲方不重视页面交互,咱们完全可以使用copyStyles:true
这个属性就行了,这样就不用设置 stylesheet
了
:::
:::tips
昨天才把html节点中的图片转成base格式的,今天就发现一个用户体验的问题;那么是啥呢?就是我从左侧的树形菜单中拖拽节点的时候(鼠标按下也是同样问题),发现节点的图片区域那里会出现一个边框,持续时间不是很长,就几毫秒的时间,但是当你连续拖拽几个不同节点的时候就会发现这个边框竟然又消失不见了,如果此时重新进入页面,再开始拖动节点,图片的边框又出现了。
:::
经过上面四个方案的尝试后,我大概知道了问题的源头在哪边了,那就是我自定义html节点中图片地址赋值的地方,由于
DataUri.imageToDataUri
这个方法是个异步执行的,所以才会导致在渲染的时候会出现短暂的视觉差
DataUri.imageToDataUri('随便写个参数名',url)
的回调中再把图片的src替换成base64的const container_img = document.createElement('img'); container_img.setAttribute('class', 'cu-container-img'); container_img.setAttribute('alt', '节点ico'); container_img.style.cursor = 'pointer'; // 先用远程图片地址给图片的src赋值,然后再重新赋值成base64的格式;这么做的目的就是解决节点拖拽到画布上会出现短暂的边框闪动问题,如果你要复现这个边框,可以把下面这一行代码注掉(不是必现) container_img.src = img; // 把图片转成base64方便存储到后端 DataUri.imageToDataUri(img, function (nu, url) { // 第一个参数无效,用的只是第二个参数,但是第一个参数不写由不行 container_img.src = url; });
前端需要把画布上的节点保存到后端,然后前端在获取详情接口的时候要把节点进行居中处理
在不调用后端接口的情况下使用centerContent()
是没得问题的;但是在动态获取节点数据后就会存在异步加载的问题,也就是先将内容居中了,之后再设置节点到画布中去,此时centerContent
的时机已经过去了,节点还是更具自身的位置进行排列
由于作者用的vue技术栈,所以这里的解决方法主要以vue为主
方案1:使用nextTick
等待dom全部渲染完成
nextTick(() => { graph.centerContent(); });
方案2:直接在接口中使用
getDataView({ size: -1, name: item.tableMetaName }).then(res => { if (res.code === 0) { // 缩放 graph.zoom(-0.1); // 画布居中 graph.centerContent(); } });
原本的写法是节点右键的时候通过node.position()
的方法获取节点的坐标,然后再把节点的坐标绑到右键菜单的dom上,但是发现对画布进行平移的时候,右键菜单的位置还停留在第一次的位置,原因就是画布平移和节点没啥关系,节点的坐标并不会因为画布平移了就自动更改自身的坐标
对鼠标的坐标进行转换
,这也是1.9版本中新增的一个方法,关键是在官方文档中还找不到这个方法,只能死马当活马医了 坐标转换,果然问题解决了
graph.on('node:contextmenu', ({ e, node }) => { const pos = graph.clientToGraph(e.clientX, e.clientY);// 核心代码就是这一行 createMenuDom({ x: pos.x, y: pos.y, node, type: 0 }); });
这里需要对javascript的dom有点基础,不过这只是我创建dom的方法,如果你们想用其它的方法也是可以的哈
let divMenuContainer = null; const createMenuDom = ({ x, y, node, edge, type }) => { if (divMenuContainer) { // 如果存在了菜单,就先移除再创建,不然你的页面上会多出来好多菜单的 document.getElementById('container').removeChild(divMenuContainer); } divMenuContainer = document.createElement('div'); divMenuContainer.setAttribute('class', 'div-menu-container'); divMenuContainer.style.left = x + 30 + 'px'; divMenuContainer.style.top = y + 'px'; const divMenuItem = document.createElement('div'); divMenuItem.setAttribute('class', 'div-menu-item'); divMenuItem.innerText = type === 0 ? '删除节点' : '删除边'; divMenuItem.addEventListener('click', () => { type === 0 ? graph.removeNode(node) : graph.removeEdge(edge); divMenuContainer.style.display = 'none'; }); divMenuContainer.appendChild(divMenuItem); document.getElementById('container').appendChild(divMenuContainer); document.body.addEventListener('click', () => { if (divMenuContainer) { divMenuContainer.style.display = 'none'; } }); };
graph.on('node:contextmenu', ({ e, node }) => { // 坐标转换 const pos = graph.clientToGraph(e.clientX, e.clientY); // 调用创建dom的方法,把坐标和节点信息传递进去 createMenuDom({ x: pos.x, y: pos.y, node, type: 0 }); });
常见问题
也是下面这个问题的解决方案
这是官方的demo
连线 undo - CodeSandbox
new Graph({ history: { enabled: true, beforeAddCommand(event, args: any) { // 忽略历史变更 if (args.options.ignoreHistory) { return false } }, }, })
graph.on('edge:connected', ({ edge }) => { // 传入自定义的 ignoreHistory 选项来忽略历史变更 edge.attr('line/strokeDasharray', null, { ignoreHistory: true }) })
new History({ enabled: true, beforeAddCommand(event, args) { if (args.options.ignoreHistory) { return false; } } }) node.setData({ tableMeta: res.data.records, desc: res?.data?.records?.length || 0 }, { ignoreHistory: true });
直接把x,y坐标转成纯数字的即可,不然拖动节点的时候会报错的
Transform
new Graph({ translating: { restrict: true } })
此方法会返回所有的输入和输出边,如果只要输入边的节点或者输出边的节点信息,请看这里链接
const getParentNodes = node => { const nodeId = node.id; const connectedNodes = []; // 如果需要其他方法,请看下方的具体配置,根据自己的需要修改这里的代码即可 const edges = graph.getConnectedEdges(node, { deep: true }); for (const edge of edges) { const sourceNode = edge.getSourceNode(); const targetNode = edge.getTargetNode(); if (sourceNode.id !== nodeId) { connectedNodes.push(sourceNode); } if (targetNode.id !== nodeId) { connectedNodes.push(targetNode); } } return connectedNodes; }; // 具体的配置 const edges = graph.getConnectedEdges(node) // 返回输入和输出边 const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: true }) // 返回输入和输出边 const edges = graph.getConnectedEdges(node, { incoming: true }) // 返回输入边 const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: false }) // 返回输入边 const edges = graph.getConnectedEdges(node, { outgoing: true }) // 返回输出边 const edges = graph.getConnectedEdges(node, { incoming:false, outgoing: true }) // 返回输出边 const edges = graph.getConnectedEdges(node, { deep: true }) // 返回输入和输出边,包含链接到所有子孙节点/边的输入和输出边 const edges = graph.getConnectedEdges(node, { deep: true, incoming: true }) // 返回输入边,包含链接到所有子孙节点/边的输入边 const edges = graph.getConnectedEdges(node, { deep: true, enclosed: true }) // 返回输入和输出边,同时包含子孙节点/边之间相连的边 const edges = graph.getConnectedEdges(node, { indirect: true }) // 返回输入和输出边,包含间接连接的边
(in promise) Error: Node with name 'cu-port' already registered.
下面这个报错是群友发的,但是问题和我之前遇到的是一类问题,所以就直接告诉群友问题所在了😀
Graph.registerNode(name,options)
Graph.registerNode(name,options,true)
graph.clearCells()
graph.zoom(-0.5)
graph.zoom()
graph.getNodes()
graph.getEdges()
translating: { restrict: true },
node.position(x,y)
allowMulti: false
graph.on('edge:connected', ({ isNew, edge, currentCell }) => { // 回调的参数:https://antv-x6.gitee.io/zh/docs/tutorial/intermediate/events/#%E8%BE%B9%E8%BF%9E%E6%8E%A5%E5%8F%96%E6%B6%88%E8%BF%9E%E6%8E%A5 console.log('被连接的节点详细参数', currentCell); if (currentCell.data['type'] === 0) { proxy.$modal.msgError('数据源无法作为输出节点'); // 移除连接的边 graph.removeEdge(edge?.id); } });
const node = graph.getCellById('node1') const connectedEdges = graph.getConnectedEdges(node)
附:参考文档
Dnd插件
Stencil插件
图片导出
1.x常见问题
坐标转换
Transform
Model