diff --git a/index.html b/index.html deleted file mode 100644 index 5dce107..0000000 --- a/index.html +++ /dev/null @@ -1,1335 +0,0 @@ -
---相信你们在开发中更多的需求是需要自定义拖拽源,毕竟自定义的功能扩展性高一些,而且可以根据你的业务需求灵活设置。自定义拖拽的优点就是:万物皆可成为拖拽源,不管你使用的是html标签,还是第三方的ui框架,或者树形列表,……这些都可以设置成拖拽源,只有你想不到的,没有官方做不到的,来吧,开整。
-
在线体验(国内镜像):https://sxdpanda.gitee.io/antv-admin/#/antv/demo (gitee的pages服务凉凉了)
-在线体验(备用地址):https://zlecheng.github.io/antv-page/#/home
- --
// 全局graph变量
-let graph = null;
-// 先定义个全局的dnd变量
-let dnd = null;
-// 在mounted中对dnd进行初始化(在graph之后初始化)
-graph = new Graph({
- container: proxy.$refs.container,
- width: '100%',
- ...tempConfig // 这个是我的画布配置
-});
-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>
-
--今天在群里有个小伙伴提出了这么个问题:如何在框选完成后给框选的区域添加一个右键菜单的功能,我看到了这个问题后也是有点懵,心里想着怎么还有这个需求,直接快捷键删除不是更好吗,谁知这位小伙伴也是这么写的,奈何客户要添加右键菜单的功能,所以说,客户最大。既然人家都提出这个需求呢,那就说明大概率情况下是可以做的,只是看自己想不想做了,下面我先来分析下我的思路,仅供参考,不过这个方案应该是能解决大部分的业务场景了
-
这里为了保险起见,我给框选插件的配置项添加了一个自定义的class类名,我可以根据我这个唯一的class去获取框选的容器
-
开始玩dom,先获取到我自定义的这个class,然后再根据这个父级去获取真正的框选容器(框选容器的class需要自行打开浏览器的f12去找哈)
-// 我自定义的class
-const parent = document.getElementsByClassName('cu-selected-container')[0];
-// 框选的容器
-const selectInner = parent.getElementsByClassName('x6-widget-selection-inner')[0];
-// 打印下看是个啥
-console.log('seleeeee >>>', selectInner);
-
没错了,我获取到了,看下图
-
经过前面两个步骤的处理,步骤三就轻松多了,步骤三主要做的事情就是监听框选完成的事件,看过官网的小伙伴都知道,官方大大给我们提供的事件还是挺多的事件,那小伙伴在这里可能就犯嘀咕了,我到底该用哪个呢?其实吧,我觉得这里用哪个事件更多取决你的业务场景,我这里为了节约时间,就直接使用selection:changed
这个事件了。
-:::warning
-Tip:如果你框选的节点个数是0的话需要特殊处理下,不然控制台会报框选的容器不存在,我这里是根据selected的长度进行判断,不满足条件直接提前返回
-:::
graph.on('selection:changed', ({ selected }) => {
- if (selected.length === 0) return;
- const parent = document.getElementsByClassName('cu-selected-container')[0];
- const selectInner = parent.getElementsByClassName('x6-widget-selection-inner')[0];
- selectInner.style.pointerEvents = 'unset';
- console.log('seleeeee >>>', selectInner);
- selectInner.addEventListener('contextmenu', event => {
- event.preventDefault();
- alert(1);
- });
-});
-
最后再看下效果吧,菜单的内容后面我再更新吧……
-
原以为到第三个步骤就结束了,没想到还有个问题,就是我一直去框选,但是这期间我不去触发右击事件,直到最后我再右键,这时你再去点击alert的确定按钮会发现完全关不掉,我想着完了完了,是不是进入死循环了?于是我就多实验了几次,发现这个alert的次数是和我框选的次数是有关联的,于是就在想是哪里除了问题,经过一番排查后发现是事件的问题,好像是每次框选完后没有清空掉dom,于是我就从事件这里下手解决,决定在触发右键菜单之前先移除一下右键的事件(排他法,不管你有没有,先清空再说),果然问题得到了完美的解决
-const handleContextMenu = event => {
- event.preventDefault();
- alert(1);
-};
-graph.on('selection:changed', ({ selected }) => {
- if (selected.length === 0) return;
- const parent = document.getElementsByClassName('cu-selected-container')[0];
- const selectInner = parent.getElementsByClassName('x6-widget-selection-inner')[0];
- selectInner.style.pointerEvents = 'unset';
- console.log('seleeeee >>>', selectInner);
- selectInner.removeEventListener('contextmenu', handleContextMenu);
- selectInner.addEventListener('contextmenu', handleContextMenu);
-});
-
问题分析很关键,代码只是个工具,具体怎么走还是要我们自己去制定,所以在这一行待的时间久了,你会发现分析问题和关键时刻解决问题的能力是有多重要。顺便讲一下这个小功能我也是趁着下班前15分钟搞出来的,如果我直接跳过分析问题的步骤,我估计到节后也不一定能想到解决方案。好了,这个小问题的总结就到这吧,有空再更新菜单坐标的问题的解决思路吧。😜demo也同步更新了,想看效果的可以直接看 国内镜像 / 备用地址
-先看看问题图片,在点击节点的时候会出现这么个玩意,但是我想要的效果是点击的时候不显示,只有在框选的时候才会去显示选区
-
点击节点的时候清空掉选择的选区,这这样子就没有问题了
- graph.on('node:click', ({ node }) => {
- graph.cleanSelection();
- });
-
--首先需要了解的知识点是js的键盘事件,但是我们不能只考虑方向键的触发时机,还要考虑键盘是否是按下的状态
-
准备一个节点,在创建节点的时候把节点存储到一个全局变量中,方便后续获取节点的位置
-let testNode = null;
- // 将节点添加到画布中
- testNode = graph.addNode({
- shape: "circle",
- x: 100,
- y: 200,
- width: 100,
- height: 100,
- label: "矩形",
- attrs: {
- body: {
- fill: "#fff",
- stroke: "#007aff",
- strokeWidth: 5,
- },
- label: {
- refX: "0.5",
- refY: "100%",
- refY2: 4,
- textAnchor: "middle",
- textVerticalAnchor: "top",
- },
- },
- });
-
准备一个开关按钮,用于开启节点的移动,如果不存在节点,那我们可以直接提前return;然后我们给键盘添加监听事件,判断四个方向键对应的keyCode,最后再通过node.position()
这个api设置节点的移动步幅
const handleMove = () => {
- if (!testNode) return;
- document.addEventListener("keydown", (event) => {
- const keyCode = event.keyCode;
- const position = testNode.position();
- switch (keyCode) {
- case 37: // left arrow
- testNode.position(position.x - 10, position.y);
- break;
- case 38: // up arrow
- testNode.position(position.x, position.y - 10);
- break;
- case 39: // right arrow
- testNode.position(position.x + 10, position.y);
- break;
- case 40: // down arrow
- testNode.position(position.x, position.y + 10);
- break;
- }
- });
-};
-
--今天在搞自定义拖拽和鼠标移入节点显示连接桩的时候,所有的步骤都是按照官网的demo来走的,代码也是全部复制的官网的,只有html节点那里是手动写的,这个东西本来如果不出错的话10来分钟就能写完了,谁知道就这个小东西竟然搞了我半个多小时,想想都气人……
-
--在鼠标移入节点的时候显示节点上的连接桩,鼠标离开节点就隐藏连接桩。但是我发现在鼠标移入节点下方(一直到画布可视区域内这段距离)都能触发鼠标移入事件。
-
把body的宽高设置成100%就可以了
---一般情况下,出现这种问题基本上是因为事件冒泡导致的,所以这个问题的解决方法也显而易见,阻止事件传播就行了
-
graph.addEdge({
- event: "edge:click",
- ……
-})
-
graph.on("edge:customevent", ({ name, view, e }) => {
- if (name === "edge:click") {
- e.stopPropagation();
- }
-});
-
好了,到这里的话说明我们已经成功阻止事件传播了,在点击label的时候就不会再触发边的点击事件了
-先看看官方的导出文档:
-图片导出
--由于业务需要,需要把画布上的节点保存成图片供其它模块展示,如果你的后端返回的数据格式是前端想要的,那么大不必搞图片的形式,直接把官网的快速上手代码拿过来循环一下就好了……,这里就拿
-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坐标转成纯数字的即可,不然拖动节点的时候会报错的
-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)
--现在画布上有5个节点,每个节点之间都有边进行相连,当我在删除第一个节点和第二个节点之间的边的时候,要获取到第二个节点的信息以及和第二个节点相连接的所有的子孙节点,包括直接或者间接连接的节点
-
--使用
-getConnectedEdges
这个方法获取所有相连接的边,然后通过边的起始和结束字段去获取起始节点和结束节点,嗯,发现这个方法行不通;开始第二套方案:根据当前节点获取它的所有孩子节点,也就是getChildren
这个方法,但是还不行,它只能获取到直接的子节点,间接的节点就获取不到了;继续第三套方案:去大海里捞针(在1.x的文档里面找到的一个api),getSuccessors(node)
,嗯,挺好,这个可以获取了
graph.on('edge:removed', ({ edge }) => {})
-
graph.getCellById(edge?.target?.cell);
-
graph.getSuccessors(node);
-
// 边被删除的时候触发
- graph.on('edge:removed', ({ edge }) => {
- // 获取边的结束节点信息 unshift
- const node = graph.getCellById(edge?.target?.cell);
- // 这里需要拦截下node的值是不是null,因为在删除节点的时候会同时删除边的,也就是说你不管是删除节点还是删除边,都会进这个方法的
- if (node == null) return;
- // https://antv-x6.gitee.io/zh/docs/api/graph/model/#getsuccessors
- let childNodes = graph.getSuccessors(node);
- // 这里把本身的节点也添加进去,如果不需要就不用push了,直接使用childNodes就行了
- childNodes.unshift(node);
- console.log('子孙节点不包含本身 >>>', childNodes);
- });
-
attrs: {
- line: {
- stroke: '#028FA6',
- strokeWidth: 1,
- targetMarker: {
- name: '',
- width: 12,
- height: 8
- }
- }
- },
-
调用 removeEdge
或者手动删除 edge 时都会触发 edge:removed
事件,那怎么区分两种行为呢?在调用 removeEdge
的时候可以传入第二个参数 options,在 options 里面定义自己的属性,在edge:remove
事件参数中能获取到自定义的属性。
graph.removeEdge(edge, {
- triggerByFunction: true
-})
-graph.on("edge:removed", ({ options }) => {
- if (options.triggerByFunction) {
- // 调用removeEdge删除边
- }
-});
-
/**
- * 显示节点上的连接桩
- * @param {*} ports
- * @param {*} show
- */
-export const showPorts = (ports, show) => {
- for (let i = 0, len = ports.length; i < len; i = i + 1) {
- ports[i].style.visibility = show ? 'visible' : 'hidden';
- }
-};
---------------------------------------------------------------------
-// 节点鼠标移入事件
-graph.on('node:mouseenter', () => {
- const container = document.getElementById('container');
- const ports = container.querySelectorAll('.x6-port-body');
- showPorts(ports, true);
-});
-// 节点鼠标离开事件
-graph.on('node:mouseleave', () => {
- const container = document.getElementById('container');
- const ports = container.querySelectorAll('.x6-port-body');
- showPorts(ports, false);
-});
--------------------------------------------------------------------
-// 连接桩配置示例
-groups:{
- top: {
- position: 'top',
- attrs: {
- circle: {
- r: 3,
- magnet: true,
- stroke: '#028FA6',
- strokeWidth: 1,
- fill: '#fff',
- style: {
- visibility: 'hidden' // 这个一定要配置!!!
- }
- }
- }
- }
-}
-
-
这是通过getNodes
获取到的节点实例
-
通过JSON.parse(JSON.stringify(graph.getNodes()))
获取,坐标放在了position对象里面
-
使用node.toJSON()
方法把节点转成json
数据
const allNodes = graph.getNodes().map(item => {
- return item.toJSON();
-});
-
使用node.position()
方法获取节点位置,然后赋值到data
里面,后面使用的时候就可以直接从data
里面获取
let allNodes = graph.getNodes().forEach(item => {
- const {x, y} = item.position();
- item.data.x = x;
- item.data.y = y;
- });
-
场景来源于系统换肤,我的系统有换肤这个功能,一个是深色模式,一个是浅色模式,不同的模式要显示不同风格的节点配置,比如浅色模式下edge的颜色要为黑色,节点的图标也要换成浅色模式的……
-// 使用计算属性获取当前系统模式(light/dark)
-const getTheme = computed(() => {
- return theme.value;
-})
-
-// watch 监听主题变化重新初始化画布,initGraph 方法里面就是x6的那些七七八八的配置
-watch(theme, val => {
- initGraph(val);
- // if (route.query.id) {
- // getModelInfo(route.query.id);
- // }
-});
-
-// initGraph的方法 currentTheme就是当前监听到的主题模式
-const initGraph = currentTheme => {
- let tempConfig = currentTheme === 'light' ? graphConfigLight : graphConfig;
- // 这里必须要先销毁下再初始化
- if (graph) {
- graph.dispose();
- }
- graph = null;
- graph = new Graph({
- container: proxy.$refs.container,
- width: '100%',
- ...tempConfig
- });
- // 如果后面还有使用插件的地方就继续往下写
-}
-
--如果不通过重载画布的方法,那我们就不需要定义两种配置了,只需要考虑怎么把所有的节点和线的配置给更新掉就行了。这里先说一下线的更改方法,因为线更新比较简单
-
// 首先你得有监听主题色变化的方法
-watch(theme, val => {
- // 先获取画布上的所有线
- const edges = graph.getEdges();
- // 对线进行遍历,根据主题动态设置线的颜色
- edges.forEach(item => {
- item.attr('line/stroke', val === 'light' ? '#4160ff' : '#028FA6'); // 将边的颜色设置为红色
- });
-});
-
--虽然现在把画布上所有线的颜色更新了,但是还有个问题:就是从连接桩拉出来的那个线的颜色还没有改,由于这个拉出来线的配置我是写在了一个单独的配置文件中,要想改这个配置那就必须得再重新加载画布配置,那岂不是又把问题引到通过初始化画布的方法去改样式了吗?于是乎开始在社区摇人,问问别人有没有遇到过类似的问题,果然有位大佬说她大概知道我的需求
-
监听从连接桩拉取线的事件,然后对这个edge进行更新颜色就行了
-graph.on('edge:changed', ({ edge }) => {
- edge.attr('line/stroke', theme.value === 'light' ? '#4160ff' : '#028FA6');
-});
-
线的问题解决了,现在开始讲节点如何更新。我最初的想法是遍历所有的节点,然后把每一个节点data里面的图片字段更新下就行了,然后再重新渲染下节点;然而这个方法并没有奏效,反而还报错了……下面是我写的垃圾代码
-watch(theme, val => {
- const nodes = graph.getNodes();
- nodes.forEach(item => {
- const { img } = item.data;
- if(val === 'light') {
- item.data.img = item.data.img && item.data.img.replace('/operator/','/operator-light/')
- }else {
- item.data.img = item.data.img && item.data.img.replace('/operator-light/','/operator/')
- }
- });
-
- // 再重新渲染节点
- graph.fromJSON({nodes,edges})
-});
-
既然不行,那就开始百度吧,于是就发现了一个新的api方法:resetCells,试了之后行是行了,但是没办法更新线,更新线后也是报错,更新node又不报错……
-watch(theme, val => {
- const nodes = graph.getNodes();
- nodes.forEach(item => {
- const { img } = item.data;
- if(val === 'light') {
- item.data.img = item.data.img && item.data.img.replace('/operator/','/operator-light/')
- }else {
- item.data.img = item.data.img && item.data.img.replace('/operator-light/','/operator/')
- }
- });
-
- // 再重新渲染节点
- graph.resetCells(nodes);
- graph.resetCells(edges); // 开始报错了
-
-});
-
最后我又把页面代码重新看了一遍,看到了之前写的一段更新节点的代码,到此才恍然大悟,自己一直想的是更新节点、更新节点、更新节点,但是就没想到用节点更新的api来做,唉……,于是就有了下面这段终极代码
-watch(theme, val => {
- const edges = graph.getEdges();
- edges.forEach(item => {
- item.attr('line/stroke', val === 'light' ? '#4160ff' : '#028FA6'); // 将边的颜色设置为红色
- });
- const nodes = graph.getNodes();
- nodes.forEach(item => {
- const { img } = item.data;
- const themeIcon =
- img &&
- img.replace(
- val === 'light' ? '/operator/' : '/operator-light/',
- val === 'light' ? '/operator-light/' : '/operator/'
- );
- item.updateData({ img: themeIcon });
- });
-});
-
问题发生的源头还是换肤,切换到浅色模式要显示浅色风格的画布和节点;如果问题到这就结束了也还好,那我直接加载不同的配置就行了,然而问题并没有如此简单……
-我的业务场景:在保存模型的时候,前端会顺带着一个模型的截图给后端,而这个截图的作用仅仅是列表展示使用,其它一无是处,但是换肤的时候得把这个截图换成浅色模式的截图,而且截图里面的节点也得是浅色模式的,问题到这里是不是觉得有点荒唐,是的,就是这么荒唐,此时截图已经生成保存在服务器上了,我要插上翅膀去改服务器上的资源吗?不不不~
直接渲染多个画布,也就是说后端返回多少的列表,我这里就渲染多少个画布,不过这里要说下,每一条数据对应的节点数据要后端返回给前端,而不是前端拿着每一条记录的id去挨个查询
-<div v-for="(item, index) in list" :key="index">
- <!-- 这个是画布容器,记得设置宽高 -->
- <div :id="'graph-' + item.id" class="model-img"></div>
-</div>
-
/**
- * 画布初始化配置
- * @param {*} ids:这个是我从后端列表里面取的id唯一值给画布容器id赋值的
- */
-let allGraph = [];
-const initGraph = ids => {
- ids.forEach(item => {
- let form = new Graph({
- container: document.getElementById('graph-' + item),
- width: '100%',
- ...graphConfig // 这个就是有关画布的一些基础配置
- });
- allGraph.push(form);
- });
- console.log('graph >>>', allGraph);
-};
-
// 节点渲染到对应的画布上
-const handleNodeData = nodes => {
- for (let n = 0; n < nodes.length; n++) {
- // 获取每一个模型对应的节点数据
- const nodeList = nodes[n].nodeVoList;
- console.log('nodeList >>>>', nodeList);
- let tempNodes = [];
- let tempEdges = [];
- for (let i = 0; i < nodeList.length; i++) {
- let item = nodeList[i];
- tempNodes.push({
- id: item.id, // 节点id
- shape: 'cu-data-node', // 自定义节点的名称
- x: Number(item.xindex), // x轴坐标
- y: Number(item.yindex), // y轴坐标
- width: 100, // 默认宽度
- height: 104, // 默认高度
- data: {
- name: item.name, // 节点名称
- desc: '', // 数据量
- opType: item.opType, // 算子类型
- type: item.type, // 1数据源 2算子
- img: getNodeImg(item.opType) // 图标
- },
- // 节点的连接桩
- ports: {
- ...port,
- items: [
- { group: 'left', id: 'port-left' },
- { group: 'right', id: 'port-right' }
- ]
- }
- });
-
- // 连线数据处理
- let preIds = [];
- if (item.preNodeIds && item.preNodeIds != '') {
- preIds = item.preNodeIds.split(',');
- }
- let nextIds = [];
- if (item.nextNodeIds && item.nextNodeIds != '') {
- nextIds = item.nextNodeIds.split(',');
- }
- if (preIds.length > 0) {
- tempEdges = Array.from(new Set(tempEdges.map(JSON.stringify)), JSON.parse);
- preIds.map(v => {
- tempEdges.push({
- source: { cell: v, port: 'port-right' },
- target: { cell: item.id, port: 'port-left' },
- attrs: edgeAttrs
- });
- });
- }
- if (nextIds.length > 0) {
- tempEdges = Array.from(new Set(tempEdges.map(JSON.stringify)), JSON.parse);
- nextIds.map(v => {
- tempEdges.push({
- source: { cell: item.id, port: 'port-right' },
- target: { cell: v, port: 'port-left' },
- attrs: edgeAttrs
- });
- });
- }
- allGraph[n].fromJSON(useDagLayout(tempNodes, tempEdges));
- // allGraph[n].fromJSON({ nodes: tempNodes, edges: tempEdges });
- allGraph[n].zoomTo(0.1);
- allGraph[n].centerContent();
- }
- console.log('处理后的节点数据 >>>', tempNodes);
- console.log('处理后的edge数据 >>>', tempEdges);
- }
-};
-
--终究问题原因还是某方懒的要死,本身能点开的东西,非要再搞一个预览的框,而且这个功能就是个画蛇添足般的存在。但是功能还是要做,由于每个运算节点都不一致,导致我还得拆分成不同的预览组件去展示不同节点的数据(数据格式也是不一致的)
-
--这个方案仅演示我业务里面的,其它的应该都大同小异
-
因为是多个组件,这里需要使用动态组件的标签<component/>
/**
-showPageView:控制预览组件的显示/隐藏
-pos:节点的坐标
-config-data:传递给预览组件的数据
-viewDom:你引入的预览页面的组件(数组类型,比如:[ViewComponent1,ViewComponent2……])
-viewCurrent:当前预览的节点(节点的类型)
-viewMouseMove:监听鼠标是否移入了预览的tootip框里面了
-*/
-
-<component
- :is="viewDom[viewCurrent]"
- v-if="showPageView"
- :pos="cardPos"
- :config-data="configData"
- @mousemove="viewMouseMove"
- @mouseleave="
- showPageView = false;
- isMoveView = false;
-">
-</component>
-
节点鼠标移入移出的写法
- // 节点鼠标移入事件
- graph.on('node:mouseenter', ({ e, node }) => {
- const pos = node.position();
- // 计算当前节点的类型
- viewCurrent.value = parseInt(opType, 10) - 1;
- // 获取坐标
- const pos1 = graph.localToPage(pos.x, pos.y);
- console.log('pos1pos1pos1 >>>', pos1);
- // 传递坐标数据给预览的子组件
- cardPos.value = { left: pos1.x, top: pos1.y };
- // 这个是传递给预览组件的数据
- configData.value = { ...operatorConfiguration };
- // 移入就展示对应的tooltip
- showPageView.value = true;
- });
-
- // 节点鼠标离开事件
- graph.on('node:mouseleave', () => {
- // 添加延时关闭,解决鼠标可以移入到tooltip提示框内部
- setTimeout(() => {
- if (!isMoveView.value) {
- showPageView.value = false;
- }
- }, 20);
- });
-
viewMouseMove 的写法
-/**
- * 鼠标移入预览内容区域
- */
-const viewMouseMove = () => {
- isMoveView.value = true;
-};
-
:::warning
-注意:以上方案是手写tooltip实现,不同分辨率下tooltip的位置可能会存在偏移,所以最稳妥的方法还是借助一些插件或者第三方的ui组件库配合vue节点的方式进行实现
-:::
Shape
插件import { Shape } from '@antv/x6'
effect:['data']
属性的作用是监听节点的data数据变化后及时更新html的内容
-对于我这里为什么把节点图片地址转成base64的问题可以看下这篇文档传送门
// 注册自定义节点 图标+标题+描述
- Shape.HTML.register({
- shape: 'cu-data-node',
- width: 'auto',
- height: 104,
- zIndex: 1,
- effect: ['data'],
- html(cell) {
- // 获取节点传递过来的数据
- const { name, img, desc, opType } = cell.getData();
- // 创建自定义的节点容器
- const container = document.createElement('div');
- container.setAttribute('class', 'cu-container');
-
- // 图片根据不同的类型进行切换,可以是后端返回的图标,也可以是自己本地的图标,如果是后端返回就通过节点的data传进来
- 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的格式;这么做的目的就是解决节点拖拽到画布上会出现短暂的边框闪动问题,如果你要复现这个边框,可以把下面这一行代码注掉(不是必现)
- // const icon = img ? img : remoteImgUrl.value;
- container_img.src = img;
- // 把图片转成base64方便存储到后端
- DataUri.imageToDataUri(img, function (nu, url) {
- // 第一个参数无效,用的只是第二个参数,但是第一个参数不写由不行
- container_img.src = url;
- });
- // container_img.src = img ? img : `${import.meta.env.VITE_IMG_PREFIX}/zfc-images/operator/datasouce.png`;
-
- const container_title = document.createElement('div');
- container_title.innerText = name;
- container_title.setAttribute('class', 'cu-container-title');
- if (name.length > 5) container_title.setAttribute('title', name);
-
- const container_desc = document.createElement('div');
- container_desc.setAttribute('class', 'cu-container-desc');
- // 算子配置不需要描述信息
- container_desc.innerText = opType === '' ? '(共' + desc + '条)' : '';
-
- container.appendChild(container_img);
- container.appendChild(container_title);
- container.appendChild(container_desc);
-
- return container;
- }
- });
-
const node = graph.createNode({
- shape: 'cu-data-node',
- width: 100,
- height: 104,
- label: data?.name,
- id: data?.id,
- // 所有节点的数据源头都在这里设置,需要哪些字段自行添加即可
- data: {
- name: data?.name, // 节点的名称
- img: data?.img || remoteImgUrl.value, // 图标
- desc: data?.dataNum || 0, // 总数据描述
- ……
- },
- /**
- * 连接桩位置判断逻辑
- * 1、数据源类型的连接桩只显示右侧
- * 2、算子类型的连接桩显示左右两侧
- * 3、算子类型-关联回填的连接桩显示左侧
- */
- ports: {
- ...port
- }
- });
-
--本篇文档只讲解vue3中如何使用,vue2的可以参考下官方文档
-
@antv/x6-vue-shape
--既然使用vue节点,那么我们就需要准备一个vue的组件,这个组件就是节点的一些样式,根据你们的ui自行写代码即可
-
<template>
- <div>节点名称</div>
- <div>节点描述</div>
- ……
-</template>
-
import { register, getTeleport } from '@antv/x6-vue-shape';
register({
- shape: 'custom-vue-node',
- width: 'auto',
- height: 104,
- component: vueNode // 这个就是你定义的vue组件
-});
-
import { getTeleport } from '@antv/x6-vue-shape';
-const TeleportContainer = defineComponent(getTeleport());
-
-// template 中添加标签,和你的画布容器平级
-<div id="graphDom"></div>
-<TeleportContainer />
-
const node = graph.createNode({
- shape: 'custom-vue-node',
- width: 100,
- height: 104,
- label: data?.name,
- id: data?.id,
- // 所有节点的数据源头都在这里设置,需要哪些字段自行添加即可
- data: {
- name: data?.name, // 节点的名称
- img: data?.img || remoteImgUrl.value, // 图标
- desc: data?.dataNum || 0, // 总数据描述
- ……
- },
- /**
- * 连接桩位置判断逻辑
- * 1、数据源类型的连接桩只显示右侧
- * 2、算子类型的连接桩显示左右两侧
- * 3、算子类型-关联回填的连接桩显示左侧
- */
- ports: {
- ...port
- }
- });
-
const getNodeData = inject('getNode');
-onMounted(() => {
- const currentNode = getNodeData();
- // 监听当前节点数据发生了变化
- currentNode.on('change:data', ({ current }) => {
- console.log('节点数据是否发生变化了 >>>', current);
- });
-})
-
-检查你的vue组件是否是这种结构
<template>
- <div>内容:{{ dataNode.name }}</div>
- <n-badge>
- <n-avatar :src="vueIco"></n-avatar>
- </n-badge>
-</template>
-
需要改成下面这种的结构(需要用根节点进行包裹)
-<div>
- <div>内容:{{ dataNode.name }}</div>
- <n-badge>
- <n-avatar :src="vueIco"></n-avatar>
- </n-badge>
-</div>
-
--因为我用的是vue类型的节点,所以这里就按照vue节点来进行讲解,其它的节点(React、Angular、Html)这些都是通用的。
-
--在vue节点内部的某个元素上需要执行一个点击事件,但是在执行本事件的时候不能去触发
-node:click
的事件、在执行node:click
事件的时候不能触发vue节点的点击事件,也就是两边的事件都是独立的,谁也不能影响谁,而且vue节点内的点击事件在点击的时候还得获取当前节点信息
直接给vue的点击事件添加stop
修饰符,阻止事件传递,然后在node:click
的时候再阻止下,但是结果下来确是不行……
// vue节点的事件
-@click.stop = test
-
-// 父页面的节点节点事件
-graph.on('node:click',{e} => {
- e.stopPropagation()
-})
-
采用群里小伙伴的方案,阻止节点鼠标按下或者鼠标抬起的事件,这样可以实现在点击vue节点的时候不触发节点本身的node:click
事件,虽然可以实现阻止的功能,但是不好操作节点的数据,我是需要获取当前节点的数据的
通过获取click事件的点击区域进行判断,如果是点击了vue节点内的点击事件区域,就直接在node:click的时候阻止掉就行了
-graph.on('node:click',{e} => {
- // 判断target的className或者id,或者你定义的一些自定义属性,
- // 反正只要你能知道当前点击的区域是属于谁的就行
- // 我在vue节点点击事件的标签上加了个class
- if(e.target.className == 'cu-class') return
-})
-
提的issues:https://github.com/antvis/X6/issues/4323 (这里面有vue2的解决方案)
---这个问题还待解决,官方暂时没有任何答复,短期内只能根据我的业务需求用野路子实现,如果有其它的可以留言你的需求
-
vue节点内部有一个复选框,用于勾选节点,选中后要给当前节点添加一个是否选中的属性,由于节点的数据更新只能在父页面进行更新,所以必须得把复选框绑定的值传递给父页面
---这个方案属于野路子,不是很灵活,如果不是复选框那基本凉凉了
-
// vue节点内正常写复选框绑定的逻辑
-const checked2 = ref(false);
-<el-checkbox v-model="checked2" size="large" @change="checkChange"></el-checkbox>
-
-
-
-// 父组件监听节点的点击事件
-graph.on('node:click',({e,node}) => {
- let state = node.data.checkState ?? false;
- // 这个判断是为了解决复选框的点击事件和节点的点击事件冲突的问题
- if (e.target.className == 'el-checkbox__inner') {
- // 给节点添加一个checkState属性,标识是否选中
- node.updateData({ checkState: !state }, { ignoreHistory: true });
- return;
- }
-})
-
-
-// 最后点击保存按钮的时候获取下节点checkState为true的数据
-const save = () => {
- const allNodes = graph.getNodes();
- // 我这里是取的id属性,如果你们需要其它的可以自行组装
- checkedOps.value = allNodes.filter(item => item.data.checkState).map(item => item.id);
- console.log('checkedOps >>>', checkedOps.value);
-}
-
--这个方案就可以随便玩了,不再局限于我自己的需求,如果还要在节点上加其它的控件都可以完美的把数据传递到父组件,其灵感来源于github的小伙伴qw123gz,问官方交流群的群主,问了半天也没有给出方案……
-
<el-checkbox v-model="checked2" size="large" @change="checkChange"></el-checkbox>
-
-
-const checked2 = ref(false);
-const emits = defineEmits(['getCheckVal']);
-const checkChange = val => {
- emits('getCheckVal', val);
-};
-
register({
- shape: 'custom-vue-node',
- width: 'auto',
- height: 104,
- // component: vueNode 这个是官方提供的注册方式
- component: {
- // 使用vue3的render渲染组件,并添加自定义事件
- render() {
- return h(vueNode, {
- // 事件名称前面必须添加 `on`
- onGetCheckVal: val => getMyCheckVal(val)
- });
- }
- }
-});
-
至此,数据反向传递就完成了,至于怎么使用传递过来的数据就看你们的业务需求了
---有的场景下我们可能需要在vue子组件内拿到画布的实例,如果靠从父组件传递下来,可能不太明智,其实翻看@antv/x6-vue-shape的源码就可以发现它暴露两个provide,一个是官方文档提到的getNode,另外一个就是getGraph
-
const getNodeData = inject('getNode');
-const getGra = inject('getGraph');
-
graph.setSelectionFilter(node => {
- return node.data.checkState == true;
-});
-
graph.clearCells()
graph.zoom(-0.5)
graph.zoom()
graph.getNodes()
graph.getEdges()
translating: {
- restrict: true
-},
-
-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)
-
edge.getSourceNode();
node.toJSON()
Dnd插件
-Stencil插件
-图片导出
-1.x常见问题
-坐标转换
-Transform
-Model
-getSuccessors
-X6 Vue3 Components
-自定义拖拽demo
「AntV」x6 自定义拖拽 | 「AntV」x6 框选添加右键菜单 | 「AntV」x6 图片导出问题汇总 | 「AntV」X6 自定义html节点 | 「AntV」X6 自定义vue节点(vue3)
-关系图谱(国内镜像)
-关系图谱(备用地址)
-AntV x6 自定义拖拽demo
-AntV X6开发实践:踩过的坑与解决方案