antv x6使用(思维导图、鱼骨图、持节点排序、新增节点、编辑节点、删除节点、选中节点)

项目需要实现如下效果图表,功能包括节点排序、新增节点、编辑节点、删除节点、选中节点等

思维导图: 

鱼骨图: 

组织结构图:

树图:

html部分如下: 

<template>
  <div class="MindMapContent">
    <el-dropdown split-button size="small" style="margin-right: 10px" @command="handleCommand">
      {{ mindTypeText }}
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item command="mindMap">思维导图</el-dropdown-item>
          <el-dropdown-item command="ishikawa">鱼骨图</el-dropdown-item>
          <el-dropdown-item command="organize">组织结构图</el-dropdown-item>
          <el-dropdown-item command="tree">树形图</el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
    <el-button size="small" @click="addNode">新增节点</el-button>
    <el-button size="small" @click="updateNode">编辑节点</el-button>
    <el-button size="small" type="danger" plain @click="removeNode">删除节点</el-button>
    <div id="mindContent" style="height: 300px">
      <div id="container"></div>
    </div>

    <el-dialog v-model="visible" :title="pageType == 'edit' ? '编辑' : '新增'">
      <div>
        <el-form class="search-form" ref="formData" size="small" label-width="120px" :model="formData">
          <el-form-item label="节点名称" prop="label" :rules="[{required: true, message: '请输入节点名称',trigger: 'blur'}]">
            <el-input v-model="formData.label" style="width: 60%"></el-input>
          </el-form-item>
        </el-form>
      </div>
      <span slot="footer" class="dialog-footer">
          <el-button @click="cancelDialog">返回</el-button>
          <el-button type="primary" @click="submitData">提交</el-button>
      </span>
    </el-dialog>
  </div>
</template>

定义数据如下:

  data(){
    return{
      graph: null, // 画布实例对象
      objData:{},  //
      visible: false,
      pageType: '',   //弹窗的类型,编辑、新增
      formData: {},  //当前节点
      dagreLayout: null,   //思维导图布局
      organizeLayout: null,   //组织结构布局
      mindType: 'mindMap',   //图标类型
      nowData: {},         //点击选中的数据
    }
  },
  computed: {
    mindTypeText(){
      let map = {
        mindMap: '思维导图',
        ishikawa: '鱼骨图',
        organize: '组织结构图',
        tree: '树形图'
      }
      return map[this.mindType]
    }
  },

需要后端返回的数据格式如下:

mindData: {
    edgeList: [
        {source: '50',target: '54'},
        {source: '50',target: '61'},
        {source: '54',target: '66'},
        {source: '61',target: '67'},
        {source: '67',target: '69'},
        {source: '50',target: '71'},
    ],
    nodeList: [
        {id: '50', label: '根节点', fjdId: null},
        {id: '54', label: '节点1', fjdId: '50'},
        {id: '61', label: '节点2', fjdId: '50' },
        {id: '71', label: '节点3', fjdId: '50'},
        {id: '66', label: '333', fjdId: '54'},
        {id: '67', label: '444', fjdId: '61'},
        {id: '69', label: '555', fjdId: '67'}
     ]
}

        获取后端返回数据后, 需要为节点和边设置样式,所以需要对数据进行处理。x6图最好只加载一次,后续再进行操作时只需要更新数据即可。因为在项目中可以为某个节点绑定其它属性id,绑定后仍保持选中状态,所以设置selectNodeId,当有selectNodeId时,需要选中node.id为selectNodeId的节点

    //获取节点数据
    getNodeData(bool,selectNodeId){
        this.objData.nodes = (this.mindData.nodeList || []).map(item => {
          return {
            id: item.id, // String,可选,节点的唯一标识
            width: 120,   // Number,可选,节点大小的 width 值
            height: 30,  // Number,可选,节点大小的 height 值
            label: item.label, // String,节点标签
            fjdId: item.fjdId,
            data: {
              portalId: item.portalId || '',
            },
            attrs: {
              body: {
                stroke: 'rgba(238, 238, 238, 1)',
                strokeWidth: 1,
                rx: 5,
                ry: 5,
                style: {
                  filter: 'drop-shadow(0px 0px 8px rgba(0,0,0,0.07))'
                }
              },
              label: {
                fontSize: 12,
                textWrap: {
                  ellipsis: true,
                  width: 105
                }
              }
            }
          }
        })
        this.objData.edges = this.mindData.edgeList || []
        //初始化加载mind,更新数据时不初始化mind
        if(bool){
          this.initGraph()
        }else {
          this.graph.cleanSelection()
          this.nowData = {}
          //更新节点信息后重新布局 
          let gridLayout = new DagreLayout({
            type: 'dagre',
            rankdir: 'LR',
            align: undefined,
            ranksep: 45,
            nodesep: 5,
          })

          this.graph.fromJSON(gridLayout.layout(this.objData))

          //如果有selectNodeId,则选中node.id为selectNodeId的节点
          if (selectNodeId) {
            const node = this.graph.getCellById(selectNodeId)
            if (node) {
              this.graph.resetSelection(node)
              this.nowData = {
                id: node.id,
                label: node.label,
                portalId: node.data.portalId || ''
              }
              //返回选中的数据  
              this.$emit('getData', this.nowData)
            }
          }
        }
    },
 初始化画布
    // 初始化流程图画布
    initGraph() {
      let container = document.getElementById('container')
      this.graph = null
      this.graph = new Graph({
        container,
        width: '100%',
        height: '100%',
        //最大最小缩放比例
        scaling: {
          min: 0.7,
          max: 1.2
        },
        autoResize: true,
        panning: true,
        mousewheel: true,
        background: {
          color: '#ffffff', // 设置画布背景颜色
        },
      })

      this.dagreLayout = new DagreLayout({
        type: 'dagre',
        rankdir: 'LR',
        align: undefined,
        ranksep: 45,
        nodesep: 5,
      })
      this.organizeLayout = new DagreLayout({
        type: 'dagre',
        rankdir: 'TB',
        align: undefined,
        ranksep: 15,
        nodesep: 45,
      })

      //数据上图
      // this.graph.fromJSON(this.dagreLayout.layout(this.objData))
      this.handleCommand('mindMap')

      //使用x6选中插件
      this.graph.use(
          new Selection({
            enabled: true,
            multiple: false,
            movable: false,
            rubberband: false,
            showNodeSelectionBox: true,
            clearSelectionOnBlank: false
          })
      )

      //节点点击选中  
      this.graph.on('node:click', ({ e,node }) => {
        e.stopPropagation()
        tooltip.style.display = 'none'
        this.graph.resetSelection(node)
        this.nowData = {
          id: node.id,
          label: node.label,
          portalId: node.data.portalId || ''
        }
        this.$emit('getData',this.nowData)
      })
      //点击节点外清空点击数据  
      this.graph.on('blank:click', ({ e,node }) => {
        this.graph.cleanSelection()
        this.nowData = {}
      })

      //node节点有宽度限制,label超过宽度时显示...,但是需要tooltip显示完整的label 
      const tooltip = document.createElement('div')
      tooltip.className = 'x6-tooltip'
      tooltip.style.position = "absolute"
      tooltip.style.display = 'none'
      tooltip.style.padding = '6px'
      tooltip.style.borderRadius = '5px'
      tooltip.style.backgroundColor = '#303133'
      tooltip.style.color = '#ffffff'
      tooltip.style.fontSize = '12px'
      let mindContent = document.getElementById('mindContent')
      mindContent.appendChild(tooltip)
      this.graph.on('node:mouseenter', ({ node }) => {
        if(node.label){
          const position = this.graph.localToGraph(node.getBBox().getCenter())
          tooltip.style.display = 'block'
          tooltip.style.left = `${position.x - 60}px`
          tooltip.style.top = `${position.y - 50}px`
          tooltip.textContent = node.label
        }
      })
      this.graph.on('node:mouseleave', ({ node }) => {
        tooltip.style.display = 'none'
      })
    },
节点操作
    //删除节点
    removeNode(){
      if(!this.nowData.id){
        this.$message.error('请选择需要删除的节点')
      }else{
        this.mindData.nodeList = this.mindData.nodeList.filter(item => item.id != this.nowData.id)
        this.mindData.edgeList = this.mindData.edgeList.filter(item => item.target != this.nowData.id)
        this.getNodeData(false)
      }
    },
    //新增节点
    addNode(){
      this.formData = {}
      this.pageType = 'add'
      if (this.objData.nodes.length == 0){
        this.visible = true
      } else{
        if(!this.nowData.id){
          this.$message.error('请选择父节点')
        }else{
          this.visible = true
        }
      }
    },
    //编辑节点
    updateNode(){
      this.formData = {}
      this.pageType = 'edit'
      if(!this.nowData.id){
        this.$message.error('请选择编辑的节点')
      }else{
        this.formData = this.nowData
        this.visible = true
      }
    },

新增节点和编辑节点弹窗操作

    //cancelDialog
    cancelDialog(){
      this.visible = false
    },
    submitData(){
      // 新增的时候,formData就是新增本身,nowData就是父节点
      // 编辑的时候,获取到nowData,赋值给formData
      this.$refs.formData.validate(valid => {
        if(valid){
          if (this.pageType == 'edit'){
            let obj = this.mindData.nodeList.find(item => item.id == this.formData.id)
            obj.label = this.formData.label
            this.visible = false
            this.getNodeData(false)
          }else{
            let id = Math.random().toString(36).substring(2, 4)
            this.mindData.nodeList.push({
              id, label: this.formData.label,
            })
            this.mindData.edgeList.push({
              target: id, source: this.nowData.id,
            })
            this.visible = false
            this.getNodeData(false)
          }
        }
      })
    },

涉及的样式

<style scoped>
.MindMapContent{
  padding: 10px 25px;
  height: 700px;
  background-color: #ffffff;
}
#mindContent{
  position: relative;
}
</style>
数据上图

根据下拉菜单选中的图表类型展示不同布局的图表,先清空图表的元素,然后根据mindType选择不同的布局样式:

    //修改图类型
    handleCommand(e){
      this.mindType = e
      if(this.graph){
        this.graph.clearCells()
      }
      setTimeout(() => {
        let attrs = {
          line: {
            stroke: '#1d6ee4'
          }
        }
        if(this.mindType == 'mindMap'){
          this.objData.edges = (this.objData.edges || []).map(item => {
            return{
              source: item.source, // String,必须,起始节点 id
              target: item.target, // String,必须,目标节点 id
              router: {
                name: 'manhattan',
                args: {
                  startDirections: ['right'],
                  endDirections: ['left']
                }
              },
              attrs
            }
          })
          this.graph.fromJSON(this.dagreLayout.layout(this.objData))
        }
        if(this.mindType == 'organize'){
          this.objData.edges = (this.objData.edges || []).map(item => {
            return{
              source: item.source, // String,必须,起始节点 id
              target: item.target, // String,必须,目标节点 id
              router: {
                name: 'manhattan',
                args: {
                  startDirections: ['bottom'],
                  endDirections: ['top']
                }
              },
              attrs
            }
          })
          this.graph.fromJSON(this.organizeLayout.layout(this.objData))
        }
      },1000)
    },
鱼骨图

在 handleCommand 方法体里面添加代码(根据根节点计算根节点下子节点坐标,然后使用递归逐级绘制该子节点下的所有层级节点):

        if(this.mindType == 'ishikawa'){
          let data = JSON.parse(JSON.stringify(this.objData))
          console.log('data',data);
          data.edges = []
          //根节点的坐标为[100,100],根节点下第一个子节点为根节点x坐标偏移150
          let fristX = 100 + 150
          let fristY = 100
          let fristId = ''
          //根节点信息
          data.nodes[0].x = 100
          data.nodes[0].y = 100
          fristId = data.nodes[0].id

          let isTop = true
          data.nodes.forEach((item, index) => {
            //计算根节点下子节点坐标
            if(item.fjdId == fristId){
              item.x = fristX
              data.edges.push({
                source: {x: item.x + 60, y: fristY + 15},
                target: item.id,
                attrs
              })
              if(isTop){
                item.y = fristY - 60
              }else{
                item.y = fristY + 60
              }
              //递归展示当前节点下的所有节点
              this.buildFish(data,item,isTop)
              //根据上一个子节点的深度,计算子节点所有层级的宽度,下一个子节点在此基础上计算x坐标  
              let nodeWidth = ((this.getNodeDepth(item,data) || 1) * 150)
              fristX += nodeWidth
              isTop = !isTop
            }
          })
          data.edges.push({
            source: fristId,
            target: {x: fristX + 150, y: fristY + 15 },
            attrs
          })
          this.graph.fromJSON(data)
        }

递归展示当前节点下的所有 节点(使用递归计算当前节点下的节点个数,算出当前节点的高度,当前节点的平级节点的高度便在此基础上偏移)

    buildFish(data,item,isTop){
      let nodes = data.nodes
      let edges = data.edges
      let secendX = item.x
      let secendY = item.y
      nodes.filter(_item => _item.fjdId == item.id).forEach(e => {
        // let childNum = nodes.filter(child => child.fjdId == e.id).length + 1
        //使用递归计算当前节点下的节点个数,算出当前节点的高度,当前节点的平级节点的高度便在此基础上偏移
        let childNum = this.getNodeNum(e, data) + 1
        let args = {}
        if(isTop){
          e.x = secendX + 130
          e.y = secendY - 40
          args = {
            startDirections: ['top'],
            endDirections: ['left']
          }
          secendY -= (50 * childNum)
        }else{
          e.x = secendX + 130
          e.y = secendY + 40
          args = {
            startDirections: ['bottom'],
            endDirections: ['left']
          }
          secendY += (50 * childNum)
        }
        edges.push({
          source: item.id,
          target: e.id,
          router: {
            name: 'manhattan',
            args,
          },
          attrs: {
            line: {
              stroke: '#1d6ee4'
            }
          }
        })
        this.buildFish(data,e,isTop)
      })
    },

 获取节点下所有节点的个数

    //获取节点下的节点个数
    getNodeNum(item,data){
      const childNode = data.nodes.filter(node => node.fjdId == item.id)
      if(childNode.length == 0) return 0
      let maxNum = childNode.length
      childNode.forEach(e => {
        maxNum += this.getNodeNum(e,data)
      })
      return maxNum
    },

  获取节点的深度

    //获取节点深度
    getNodeDepth(item,data, currentDepth = 0){
      const childNode = data.nodes.filter(node => node.fjdId == item.id)
      if(childNode.length == 0) return currentDepth
      let maxDepth = currentDepth
      childNode.forEach(e => {
        const depth = this.getNodeDepth(e, data, currentDepth + 1)
        if(depth > maxDepth) maxDepth = depth
      })
      return maxDepth
    },
树形图

在 handleCommand 方法体里面添加代码(根据根节点计算根节点下子节点坐标,然后使用递归逐级绘制该子节点下的所有层级节点

          if(this.mindType == 'tree'){
              let data = JSON.parse(JSON.stringify(this.objData))
              console.log('data',data);
              data.edges = []
              //根节点信息
              let fristX = 100
              let fristY = 100 + 40
              let fristId = ''
              data.nodes[0].x = 100
              data.nodes[0].y = 100
              fristId = data.nodes[0].id

              data.nodes.forEach((item, index) => {
                  if(item.fjdId == fristId){
                      item.x = fristX + 80
                      item.y = fristY
                      data.edges.push({
                          source: fristId,
                          target: item.id,
                          router: {
                              name: 'manhattan',
                              args: {
                                  startDirections: ['bottom'],
                                  endDirections: ['left']
                              }
                          },
                          attrs
                      })
                      this.buildTree(data,item)
                      let nodeHeight = ((this.getNodeNum(item,data) + 1) * 40)
                      fristY += nodeHeight
                  } 
              })
              this.graph.fromJSON(data)
          }

 //计算二级后所有节点的位置(树图)

    //计算二级后所有节点的位置(树图)
    buildTree(data,item){
      let nodes = data.nodes
      let edges = data.edges
      let secendX = item.x
      let secendY = item.y
      nodes.filter(_item => _item.fjdId == item.id).forEach(e => {
        let childNum = this.getNodeNum(e, data)
        e.x = secendX + 80
        e.y = secendY + 40
        secendY += (childNum + 1) * 40

        edges.push({
          source: item.id,
          target: e.id,
          router: {
            name: 'manhattan',
            args: {
              startDirections: ['bottom'],
              endDirections: ['left']
            }
          },
          attrs: {
            line: {
              stroke: '#1d6ee4'
            }
          }
        })
        this.buildTree(data,e)
      })
    },

项目地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值