Vue2项目 使用AntV X6 实现拖拽,自定义组件,数据重渲染,连线自定义等功能

前言

因公司业务要求,需要实现一个类似于拓扑图的功能,能够实现拖拽,组件自定义,弹窗提示,自动连线区分状态,数据保存重新渲染,状态更新等功能,查阅文档发现@antv/x6的灵活性比较高,能够满足这些需求
文章借鉴 先知demonsvue项目中使用antvX6新手教程,附demo案例讲解,功能大都也都有实现,所以也抽空整理了一下文档

功能预览

antv x6

功能实现

页面布局什么的就不赘述了直接怼用到的功能做描述了

1.下载引入插件

直接 npm install *** 相关依赖就行了

import { Graph } from '@antv/x6'
import insertCss from 'insert-css' // 连接线的动画效果
import { register } from '@antv/x6-vue-shape' // 自定义组件 vue的版本

连接线的其他效果可以参考 边-样式

2.界面

2.1初始化拓扑图

    // 初始化拓扑图
    initGraph () {
      const container = document.getElementById('container')

      if (this.graph !== null) this.graph.dispose()
      this.graph = new Graph({
        autoResize: true,
        container: container, // 画布容器
        width: container.offsetWidth, // 画布宽
        height: container.offsetHeight, // 画布高
        background: false, // 背景(透明)
        snapline: true, // 对齐线
        // 配置连线规则
        connecting: {
          snap: true, // 自动吸附
          allowBlank: false, // 是否允许连接到画布空白位置的点
          allowMulti: true, // 是否允许在相同的起始节点和终止之间创建多条边
          allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点
          highlight: true, // 拖动边时,是否高亮显示所有可用的节点
          allowNode: false, // 是否允许边连接到节点(非节点上的连接桩),默认为 true。
          allowEdge: false, // 是否允许边连接到另一个边,默认为 true。
          allowPort: false, // 是否允许边连接到连接桩,默认为 true。
          highlighting: {
            magnetAdsorbed: {
              name: 'stroke',
              args: {
                attrs: {
                  fill: '#5F95FF',
                  stroke: '#5F95FF'
                }
              }
            }
          },
          router: {
            // 对路径添加额外的点
            name: 'orth'
          },
          connector: {
            // 边渲染到画布后的样式
            name: 'rounded',
            args: {
              radius: 10
            }
          }
        },
        panning: {
          enabled: false
        },
        mousewheel: {
          enabled: true, // 支持滚动放大缩小
          zoomAtMousePosition: true,
          modifiers: 'ctrl',
          minScale: 0.5, // 数值越小 看到的内容越多
          maxScale: 1
        },
        grid: {
          type: 'dot',
          size: 20, // 网格大小 10px
          visible: true, // 渲染网格背景
          args: {
            color: '#a0a0a0', // 网格线/点颜色
            thickness: 2 // 网格线宽度/网格点大小
          }
        },
        // 这一步非常重要 设置到能不能拖动节点 
        interacting: function (cellView) {
          if (
            cellView.cell.getData() !== undefined &&
            !cellView.cell.getData().disableMove
          ) {
            return { nodeMovable: false }
          }
          return true
        }
      })
      // 自定义组件 一定要提前注册  不然刷新页面后重新渲染会报错
      register({
        shape: 'custom-vue-node',
        width: 100,
        height: 100,
        component: ItemStyle
      })

      this.obstacles = []

      this.edge = []
      const update = () => {
        const edgeView = this.graph.findViewByCell(this.edge)
        edgeView.update()
      }

      this.obstacles.forEach((obstacle) =>
        obstacle.on('change:position', update)
      )
      this.nodeAddEvent() // 添加事件监听
      this.addSourceNode() // 添加初始图标 就是左侧那个大黑盒
    },
2.2添加默认节点

    // 添加默认DTU
    addSourceNode () {
      // 我这里是回显数据 可以忽略
      if (this.uData.topologyData) { 
        const _data = JSON.parse(this.uData.topologyData)
        this.fromJSONData = _data
        this.refresh()
        return
      }
      // 这里是处理大黑盒右边的连接点样式 根据自己的需求处理
      const _protType = this.protocolTypeArray.map((item) => {
        return {
          id: item.dictLabel,// 有id的时候不会重复添加
          group: 'bottom',
          attrs: {
            text: {
              // stroke: item.cssClass,
              // fill: item.cssClass,
              text: item.dictLabel // 连接点文案
            },
            circle: {
              r: 7,
              magnet: true,
              stroke: '#ffffff',
              strokeWidth: 0,
              fill: item.cssClass //节点样式
            }
          },
          tip: item.dictLabel
        }
      })

      this.source = this.graph.addNode({
        id: 'source_DTU',
        shape: 'image',
        x: 40,
        y: 180,
        width: 192,
        height: 240,
        imageUrl: this.sourcePng,// 这里是图片   
        attrs: {
          body: {
            fill: '#f5f5f5',
            stroke: '#d9d9d9'
          },  
        },
        ports: {
          groups: {
            bottom: {
              position: 'right',
              // args: { x: -50 },
              label: {
                position: {
                  name: 'right' // 连接点文案所在方向
                }
                // },
                // attrs: {
                //   circle: {
                //     r: 6,
                //     magnet: true,
                //     stroke: '#ffffff',
                //     strokeWidth: 2,
                //     fill: '#5F95FF'
                //   }
              }
            }
          },
          items: _protType // 这里是你的所有连接点
        },
        data: {
          disableMove: false // true为可拖拽,false不可拖拽   和前面的 interacting 设置有关
        }
      })
    },
2.3 处理事件

    // 节点操作
    nodeAddEvent () {
      // const { graph } = this
      // const container = document.getElementById('container')
      // const changePortsVisible = (visible) => {
      //   const ports = container.querySelectorAll('.x6-port-body')
      //   for (let i = 0, len = ports.length; i < len; i = i + 1) {
      //     ports[i].style.visibility = visible ? 'visible' : 'hidden'
      //   }
      // }
      // this.graph.on('node:mouseenter', () => {
      //   console.log('鼠标进入!!!')
      //   changePortsVisible(true)
      // })
      // this.graph.on('node:mouseleave', () => {
      //   console.log('鼠标离开!!!')
      //   changePortsVisible(false)
      // })
      // 删除
      this.graph.on('cell:removed', ({ cell, index, options }) => {
        // console.log('删除!!!', cell, index, options)
        // 处理气泡显示状态 可以忽略
        // 某个节点被删除时 激活在列表中的状态
        [this.moduleList, this.moduleData].forEach((module) => {
          module.forEach((item) => {
            if (cell.id === item.id) {
              item.uid = null
            }
          })
        })
      })
      this.graph.on('scale', ({ sx, sy, ox, oy }) => {
        // console.log('缩放!!!', sx, sy, ox, oy)
        // this.mapSize = 1
      })
      this.graph.on('node:mousedown', ({ e, x, y, node, view }) => {
        // console.log('鼠标按下!!!', node, node.id)
        // 处理气泡显示状态 可以忽略
        // 有气泡显示时禁止拖动
        if (node.id === 'source_DTU' || this.topPopupShow) {
          node.data.disableMove = false
        }
      })
      this.graph.on('node:mouseup', ({ e, x, y, node, view }) => {
        // console.log('鼠标抬起!!!', node, node.id)
        // 处理气泡显示状态 可以忽略
        // 鼠标抬起 气泡显示
        if (node.id === 'source_DTU') return
        node.data.disableMove = true
      })
      this.graph.on('node:move', ({ e, x, y, node, view }) => {
        // console.log('移动!!!', node)
      })
      this.graph.on('node:embed', ({ e, x, y, node, view }) => {
        // console.log('嵌入!!!', node)
      })
      // 节点绑定点击事件 单击 双击都可以 看业务需要
      this.graph.on('node:dblclick', ({ e, x, y, node, view }) => { 
        // console.log('双击!!!', node)
        if (node.id === 'source_DTU') return
        // 判断是否有选中过节点
        if (this.curSelectNode) {
          // 移除选中状态
          this.curSelectNode.removeTools()
          // 判断两次选中节点是否相同
          if (this.curSelectNode !== node) {
            node.addTools([
              {
                name: 'boundary',
                args: {
                  attrs: {
                    fill: '#16B8AA',
                    stroke: '#2F80EB',
                    strokeWidth: 1,
                    fillOpacity: 0.1
                  }
                }
              },
              {
                name: 'button-remove',
                args: {
                  x: '100%',
                  y: 0,
                  offset: {
                    x: 0,
                    y: 0
                  }
                }
              }
            ])
            this.curSelectNode = node
          } else {
            this.curSelectNode = null
          }
        } else {
          this.curSelectNode = node
          node.addTools([
            {
              name: 'boundary',
              args: {
                attrs: {
                  fill: '#16B8AA',
                  stroke: '#2F80EB',
                  strokeWidth: 1,
                  fillOpacity: 0.1
                }
              }
            },
            {
              name: 'button-remove',
              args: {
                x: '100%',
                y: 0,
                offset: {
                  x: 0,
                  y: 0
                }
              }
            }
          ])
        }
      })
    },
2.4 添加节点
		<div
            v-for="item in moduleData"
            :key="item.id"
            :draggable=" true"
            @dragend="handleDragEnd($event, item)"
            class="menu-item" 
          >
          右侧可拖动的列表相关样式
          </div>
这里是连接线上的离线图标 根据自己的需求判断是否需要
// 离线图标样式
const offlinesStyle = {
  markup: [
    {
      tagName: 'image',
      attrs: {
        'xlink:href': offlinePng, // 图片地址
        width: 20,
        height: 20,
        y: -10,
        x: -10
      }
    }
  ],
  position: {
    distance: 0.8, // 图标位于这条线的位置
    args: {
      keepGradient: true,
      ensureLegibility: true
    }
  }
}


    // 拖动后松开鼠标触发事件 
    handleDragEnd (e, item) {
      // console.log(e, item, this.boxStyle.top, this.boxStyle.left) // 可以获取到最后拖动后松开鼠标时的坐标和拖动的节点相关信息
      if (item.uid) return // 如果有绑定 忽略
      // 这里处理的是 拖入后的位置 根据你的需求调整 我这里是有动态调整 所以对位置有处理
      this.boxStyle = document
        .getElementsByClassName('antvBox')[0]
        .getBoundingClientRect() 
      // 向左移动一定的距离才判定是移入
      if (e.offsetX < -100) {
        item.uid = new Date().getTime()
        let min = this.boxStyle.left + this.boxStyle.width - 60
        let X = e.pageX - this.boxStyle.left - 30
        let Y = e.pageY - this.boxStyle.top - 30
        this.addHandleNode(Math.min(X, min), Y, item)
      }
    },
    // 添加节点到画布
    addHandleNode (x, y, item) {
      // 添加自定义节点到图表
      const target = this.graph.addNode({
        shape: 'custom-vue-node', // 这里是你的自定义组件名称 和前面 register 定义的保持一致
        id: item.id, // 有ID 不会重复添加 id不可为0
        x,
        y,
        width: 260,
        height: 80,
        data: {
          disableMove: true, // true为可拖拽,false不可拖拽
          ...item
        }
      })


      this.graph.addEdge({
        source: {
          cell: this.source,
          port: item.dictLabel // 连接桩 从哪个桩开始 和前面添加默认点的 id 对应
        },
        target,
        router: { name: 'manhattan' },
        connector: { name: 'rounded' },
        attrs: {
          line: {
            stroke: item.borderColor,
            targetMarker: 'classic',
            strokeDasharray: 10,
            style: {
            // 这里绑定的事你的 连接线 的动画
              animation: item.connectionStatus
                ? 'ant-line 60s infinite linear'
                : ''
            }
          }
        },
        labels: item.connectionStatus ? [] : [offlinesStyle]
      })
    },
2.5 节点页面

在你的父页面 注册好页面

import ItemStyle from './ItemStyle.vue'

components: { ItemStyle },
 

数据在这一步会携带过去

	initGraph () {
      ......
      // 自定义组件 一定要提前注册  不然刷新页面后重新渲染会报错
      register({
        shape: 'custom-vue-node',
        width: 100,
        height: 100,
        component: ItemStyle
      })
	  ......
    },
   // 添加节点到画布
    addHandleNode (x, y, item) {
      // 添加自定义节点到图表
      const target = this.graph.addNode({
       ......
        data: {
          disableMove: true, // true为可拖拽,false不可拖拽
          ...item  // 你的数据
        }
      })

		......
    },

子页面接收数据


  created () {
    const node = this.getNode()
    this.dataInfo = node.data
    // console.log('node===', node, this.dataInfo)
  },

3.保存 复现

这里可以直接看文档 数据处理 很详细

// 导出
save () { 
		// 内容很明了 点开基本就知道什么是什么了
        const _data = this.graph.toJSON()  
        // 处理你的业务需求
        ......
    },
// 导入
refresh () { 
		// 如果有需要 可以直接更新 _data 数据
        this.graph.fromJSON(_data) Ï
    }

在这里插入图片描述

要在Vue3中使用AntV X6实现右键菜单功能,你可以按照以下步骤操作: 1. 安装依赖:在Vue项目中安装AntV X6ant-design-vue库。 ``` npm install @antv/x6 ant-design-vue --save ``` 2. 创建右键菜单组件:创建一个Vue组件,作为X6图形编辑器右键菜单的容器。 ```vue <template> <a-dropdown :trigger="['contextmenu']" @visible-change="onVisibleChange"> <a-menu slot="overlay" :style="{ width: '120px' }"> <a-menu-item @click="deleteNode">删除节点</a-menu-item> </a-menu> <div class="x6-contextmenu" ref="container"></div> </a-dropdown> </template> <script> import { defineComponent } from 'vue'; import { Dropdown, Menu, message } from 'ant-design-vue'; export default defineComponent({ name: 'X6ContextMenu', components: { Dropdown, Menu, MenuItem: Menu.Item, }, emits: ['deleteNode'], mounted() { this.menu = this.$refs.container; this.menu.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); }); }, methods: { onVisibleChange(visible) { if (visible) { this.$emit('contextmenu', this.menu); } }, deleteNode() { this.$emit('deleteNode'); }, }, }); </script> ``` 3. 在X6图形编辑器中添加右键菜单:在X6图形编辑器中添加右键菜单功能。 ```vue <template> <div class="x6-editor"> <x6-contextmenu @contextmenu="onContextMenu" @deleteNode="deleteNode"></x6-contextmenu> <div class="x6-graph" ref="container"></div> </div> </template> <script> import { defineComponent } from 'vue'; import { Graph, Node } from '@antv/x6'; import X6ContextMenu from './X6ContextMenu.vue'; export default defineComponent({ name: 'X6Editor', components: { X6ContextMenu, }, data() { return { graph: null, }; }, mounted() { this.graph = new Graph({ container: this.$refs.container, grid: true, // 其他配置 }); // 添加节点 const node = this.graph.addNode({ // 节点配置 }); // 右键菜单事件 this.graph.on('contextmenu', ({ x, y }) => { this.$refs.contextmenu.show(x, y); }); }, methods: { onContextMenu(menu) { // 清空菜单 menu.innerHTML = ''; // 添加菜单项 const deleteMenuItem = document.createElement('a-menu-item'); deleteMenuItem.innerHTML = '删除节点'; deleteMenuItem.addEventListener('click', () => { this.deleteNode(); }); menu.appendChild(deleteMenuItem); }, deleteNode() { // 删除节点 this.graph.removeNode(node); }, }, }); </script> ``` 这样就可以在Vue3中使用AntV X6实现右键菜单功能了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值