【vue2 + Antv X6】实现可拖拽可视化流程图

发布于:2024-04-25 ⋅ 阅读:(13) ⋅ 点赞:(0)

image.png

ANTV X6 简介

X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。

使用过程

  • 首先我们把ANTV X6 安装到我们的项目中

    npm install @antv/x6 --save

顶部工具栏的实现

HTML部分
<div class="top-box">
    <div class="tools-box">
        <div v-for="tool in tools"
             :key="tool.key"
             class="tool"
             @click="handleTrigger(tool.key)">
            <img :src="require(`@/assets/images/${tool.iconClass}.png`)" alt="">
            <div class="word">{{tool.title}}</div>
        </div>
    </div>
</div>
  • 这里的图标用了阿里的图标库大家可以自己去找

data数据部分
tools: [
   {
      title: '保存',
      iconClass: 'save',
      key: 'save',
   },
   {
      title: '撤销',
      iconClass: 'left',
      key: 'onUndo',
   },
   {
      title: '前进',
      iconClass: 'right',
      key: 'onRedo',
   },
   {
      title: '放大',
      iconClass: 'zoomIn',
      key: 'zoomIn',
   },
   {
      title: '缩小',
      iconClass: 'zoomOut',
      key: 'zoomOut',
   },
   {
      title: '居中',
      iconClass: 'center',
      key: 'centerContent',
   },
   {
      title: '预览',
      iconClass: 'view',
      key: 'view',
   },
   {
      title: '开启框选',
      iconClass: 'select',
      key: 'select',
      status: false,
   },
   {
      title: '开启平移',
      iconClass: 'move',
      key: 'move',
      status: false,
   },
   {
      title: '退出',
      iconClass: 'exit',
      key: 'exit',
   }
]
js部分
handleTrigger (command) {
   switch (command) {
   case 'save':
      this.handleSave()
      break
   case 'onUndo':
      this.graph.undo()
      break
   case 'onRedo':
      this.graph.redo()
      break
   case 'zoomIn':
      this.graph.zoom(0.2)
      break
   case 'zoomOut':
      this.graph.zoom(-0.2)
      break
   case 'centerContent':
      this.graph.centerContent()
      break
   case 'view':
      this.exportJson()
      break
   case 'select':
      this.changeRubberband(command)
      break
   case 'move':
      this.changePann(command)
      break
   case 'exit':
      this.$router.push('/')
      break
   default:
      break
   }
},
// 保存的方法 根据业务需要达到数据处理成想要的
handleSave () {
   const data = this.graph.toJSON(); // 可以拿到画完图的数据
   const nodeArr = data.cells;
   const filterCell = nodeArr.filter(item => item.shape !== 'edge');// 这里过滤我们需要的数据,可以根据自己的业务需要来做
   const rulesNodeDTOList = [];
   for (const item of filterCell) {
      const nodeAttribute = item.data ? item.data.nodeAttribute : {}
      if (nodeAttribute) {
         rulesNodeDTOList.push(nodeAttribute);
      }

   }
}
// 预览的方法,根据业务我这里预览转成了G6
exportJson () {
   const data = this.graph.toJSON()
   this.$store.dispatch('g6/setG6data', data)
   console.log(JSON.stringify(data))
   console.log(data)
   // this.$router.push('g6')
   this.dialogTableVisible = true
}
// 开/关框选的方法
changeRubberband (key) {
   this.tools.forEach(item => {
      if (item.key === key) {
         item.status = !item.status;
         item.status ? item.title = '关闭框选' : item.title = '开启框选'
         // this.graph.toggleSelection(item.status)
         this.graph.toggleRubberband(item.status)
         this.graph.toggleStrictRubberband(item.status)
         this.graph.cleanSelection(item.status)
      }
   })
}
// 开/关画布平移的方法
changePann (key) {
   this.tools.forEach(item => {
      if (item.key === key) {
         item.status = !item.status;
         item.status ? item.title = '关闭平移' : item.title = '开启平移'
         this.graph.togglePanning(item.status)
      }
   })
   // graph.togglePanning(val)
   // val ? graph.enablePanning() : graph.disablePanning();
}

G6预览

image.png

画完图的json数据 image.png

到这里 顶部工具基本功都已经实现,基本方法API在官网可以找到

左边拖拽区域的实现

HTML部分
<div id="stencil">
    <div>
        <div class="dnd-circle dnd-start" @mousedown="startDrag('start',$event)"></div>
        <span>开始</span>
    </div>
    <div>
        <div class="dnd-rect" @mousedown="startDrag('rect',$event)"></div>
        <span>节点1</span>
    </div>
    <div>
        <div class="dnd-polygon" @mousedown="startDrag('polygon',$event)"></div>
        <span>节点2</span>
    </div>
    <div>
        <div class="dnd-circle" @mousedown="startDrag('end',$event)"></div>
        <span>结束</span>
    </div>

</div>
js部分
//这里用到了X6的拖拽,所以我们需要安装一下
// npm install @antv/x6-plugin-dnd --save
// import { Dnd } from '@antv/x6-plugin-dnd'
startDrag (type, e) {
   this.startDragToGraph(this.graph, type, e)
},
startDragToGraph (graph, type, e) {
   const startNode = this.graph.createNode({
      shape: 'custom-circle-start',
      width: 38,
      height: 38,
      attrs: {
         body: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: '#ffffff',
            rx: 10,
            ry: 10,
         },
      },
   })
   const polygonNode = this.graph.createNode({
      shape: 'custom-polygon',
      width: 80,
      height: 60,
      attrs: {
         body: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: '#ffffff',
            rx: 10,
            ry: 10,
         },
         label: {
            fontSize: 13,
            fontWeight: 'bold',
         },
      },

   })
   const rectNode = this.graph.createNode({
      shape: 'custom-rect',
      width: 80,
      height: 60,
      attrs: {
         body: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: '#ffffff',
            rx: 10,
            ry: 10,
         },
         label: {
            fontSize: 13,
            fontWeight: 'bold',
         },
      },
   })
   const endNode = this.graph.createNode({
      shape: 'custom-circle-start',
      width: 38,
      height: 38,
      key: 'end',
      attrs: {
         body: {
            strokeWidth: 4,
            stroke: '#000000',
            fill: '#ffffff',
            rx: 10,
            ry: 10,
         },
         label: {
            fontSize: 13,
            fontWeight: 'bold',
         },
      },
   })
   let dragNode;
   if (type === 'start') {
      dragNode = startNode
   } else if (type === 'end') {
      dragNode = endNode
   } else if (type === 'rect') {
      dragNode = rectNode
   } else if (type === 'polygon') {
      dragNode = polygonNode
   }

   this.dnd.start(dragNode, e)
}

中间画布区域的实现

html部分
<div id="container">
    <div id="graph-container"></div>
</div>
js部分

首先引入我们需要的一些依赖(顶部工具栏中需要的)

import { Graph, Shape } from '@antv/x6'
import { Stencil } from '@antv/x6-plugin-stencil'
import { Transform } from '@antv/x6-plugin-transform'
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 { MiniMap } from '@antv/x6-plugin-minimap'
import { Dnd } from '@antv/x6-plugin-dnd'
import { History } from '@antv/x6-plugin-history'
import { register } from '@antv/x6-vue-shape'
import insertCss from 'insert-css'

然后我们初始化一些画布的配置

initGraph () {
   const nodeWidth = 80, nodeHeight = 60;

   this.graph = new Graph({
      container: document.getElementById('graph-container'),
      autoResize: true,
      translating: {
         restrict: true,
      },
      mousewheel: {
         enabled: true,
         modifiers: 'Ctrl',
         maxScale: 4,
         minScale: 0.2,
      },
      grid: {
         visible: true,
         type: 'mesh',
         args: [
            {
               color: '#c5c5c5', // 主网格线颜色
               thickness: 1, // 主网格线宽度
            },
         ],
      },
      connecting: {
         snap: true, // 是否自动吸附
         allowMulti: true, // 是否允许在相同的起始节点和终止之间创建多条边
         allowNode: false, // 是否允许边链接到节点(非节点上的链接桩)
         allowBlank: false, // 是否允许连接到空白点
         allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,
         allowEdge: false, // 是否允许边链接到另一个边
         highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点
         router: {
            name: 'manhattan',
            args: {
               startDirections: ['top', 'right', 'bottom', 'left'],
               endDirections: ['top', 'right', 'bottom', 'left'],
            },
         },
         // connector: {
         //     name: 'rounded',
         //     // args: { radius: 10, },
         // },
         anchor: 'center',
         connectionPoint: 'anchor',
         validateConnection ({ targetMagnet }) {
            return !!targetMagnet
         },
      },
      highlighting: {
         // 连接桩可以被连接时在连接桩外围围渲染一个包围框
         magnetAvailable: {
            name: 'stroke',
            args: {
               attrs: {
                  fill: '#fff',
                  stroke: '#A4DEB1',
                  strokeWidth: 4,
               },
            },
         },
         // 连接桩吸附连线时在连接桩外围围渲染一个包围框
         magnetAdsorbed: {
            name: 'stroke',
            args: {
               attrs: {
                  fill: '#fff',
                  stroke: '#31d0c6',
                  strokeWidth: 4,
               },
            },
         },
      },
   })

   this.dnd = new Dnd({
      target: this.graph,
      scaled: false,
   })

   this.graph
      .use(new Selection({ showNodeSelectionBox: true, pointerEvents: 'none' }))
      .use(new Snapline())
      .use(new Keyboard())
      .use(new Clipboard())
      .use(new History())
      .use(new MiniMap({ container: document.getElementById('minimap') }))
   //连接桩配置
   const ports = {
      groups: {
         top: {
            position: 'top',
            attrs: {
               circle: {
                  r: 4,
                  magnet: true,
                  stroke: '#5F95FF',
                  strokeWidth: 2,
                  fill: '#fff',
                  style: {
                     visibility: 'hidden',
                  },
               },
            },
         },
         right: {
            position: 'right',
            attrs: {
               circle: {
                  r: 4,
                  magnet: true,
                  stroke: '#5F95FF',
                  strokeWidth: 2,
                  fill: '#fff',
                  style: {
                     visibility: 'hidden',
                  },
               },
            },
         },
         bottom: {
            position: 'bottom',
            attrs: {
               circle: {
                  r: 4,
                  magnet: true,
                  stroke: '#5F95FF',
                  strokeWidth: 2,
                  fill: '#fff',
                  style: {
                     visibility: 'hidden',
                  },
               },
            },
         },
         left: {
            position: 'left',
            attrs: {
               circle: {
                  r: 4,
                  magnet: true,
                  stroke: '#5F95FF',
                  strokeWidth: 2,
                  fill: '#fff',
                  style: {
                     visibility: 'hidden',
                  },
               },
            },
         },
      },
      items: [
         {
            group: 'top',
         },
         {
            group: 'right',
         },
         {
            group: 'bottom',
         },
         {
            group: 'left',
         },
      ],
   }
   // 控制连接桩显示/隐藏
   const showPorts = (ports, show) => {
      for (let i = 0, len = ports.length; i < len; i += 1) {
         ports[i].style.visibility = show ? 'visible' : 'hidden'
      }
   }

   Graph.registerNode(
      'custom-rect',
      {
         inherit: 'rect',
         // attrs 可自定义
         ports: { ...ports },
      },
      true,
   )

   Graph.registerNode(
      'custom-polygon',
      {
         inherit: 'polygon',
         points: '0,10 10,0 20,10 10,20',
         ports: {
            ...ports,
         },
      },
      true,
   )

   Graph.registerNode(
      'custom-circle-start',
      {
         inherit: 'circle',
         ports: { ...ports },
      },
      true,
   )
   this.graph.bindKey(['meta+c', 'ctrl+c'], () => {
      const cells = this.graph.getSelectedCells()
      if (cells.length) {
         this.graph.copy(cells)
      }
      return false
   })

   this.graph.bindKey(['meta+x', 'ctrl+x'], () => {
      const cells = this.graph.getSelectedCells()
      if (cells.length) {
         this.graph.cut(cells)
      }
      return false
   })

   this.graph.bindKey(['meta+v', 'ctrl+v'], () => {
      if (!this.graph.isClipboardEmpty()) {
         const cells = this.graph.paste({ offset: 32 })
         this.graph.cleanSelection()
         this.graph.select(cells)
      }
      return false
   })

   this.graph.bindKey(['meta+z', 'ctrl+z'], () => {
      if (this.graph.canUndo()) {
         this.graph.undo()
      }
      return false
   })

   this.graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
      if (this.graph.canRedo()) {
         this.graph.redo()
      }
      return false
   })

   this.graph.bindKey(['meta+a', 'ctrl+a'], () => {
      const nodes = this.graph.getNodes()
      if (nodes) {
         this.graph.select(nodes)
      }
   })

   this.graph.bindKey('backspace', () => {
      const cells = this.graph.getSelectedCells()
      if (cells.length) {
         this.graph.removeCells(cells)
      }
   })

   this.graph.bindKey(['ctrl+1', 'meta+1'], () => {
      const zoom = this.graph.zoom()
      if (zoom < 1.5) {
         this.graph.zoom(0.1)
      }
   })

   this.graph.bindKey(['ctrl+2', 'meta+2'], () => {
      const zoom = graph.zoom()
      if (zoom > 0.5) {
         this.graph.zoom(-0.1)
      }
   })

   this.graph.on('cell:mouseenter', ({ cell }) => {
      console.log(cell.isNode(), '123')
      const container = document.getElementById('graph-container')
      const ports = container.querySelectorAll('.x6-port-body')
      showPorts(ports, !cell.attrs.typeName);
      if (cell.isNode()) {
         cell.addTools([
            {
               name: 'button-remove',
               args: {
                  x: 0,
                  y: 0,
                  offset: { x: 10, y: 10 },
               },
            },
         ])
      } else {
         cell.addTools([
            {
               name: 'button-remove',
               args: { distance: -40 },
            },
         ])
      }
   })

   this.graph.on('cell:mouseleave', ({ cell }) => {
      if (cell.hasTool('button-remove')) {
         cell.removeTool('button-remove');
      }
      // const container = document.getElementById('graph-container')
      // const ports = container.querySelectorAll('.x6-port-body')
      // showPorts(ports, false);
   })
   this.graph.on('node:click', ({ x, y, node, cell }) => {
      this.currentCell = cell;
      if (cell.isNode() && !cell.attrs.typeName) {
         // 这可以写一些点击节点时和右侧表单交互的效果
         
      }
      if (cell.hasTool('button')) {
         cell.removeTool('button');
      } else {
         const markup = [
            {
               tagName: 'circle',
               selector: 'button',
               attrs: {
                  r: 10,
                  stroke: '#6d6d6d',
                  'stroke-width': 3,
                  fill: 'white',
                  cursor: 'pointer',
               },
            },
         ];
         const markupDel = [{
            tagName: 'circle',
            selector: 'button',
            attrs: {
               r: 10,
               stroke: '#6d6d6d',
               'stroke-width': 3,
               fill: 'white',
               cursor: 'pointer',
            },
         },
            {
               tagName: 'text',
               textContent: 'x',
               selector: 'label',
               attrs: {
                  fill: '#000000',
                  'font-size': 15,
                  'text-anchor': 'middle',
                  'pointer-events': 'none',
                  y: '0.3em',
               },
            }]
         const markupM = [{
            tagName: 'circle',
            selector: 'button',
            attrs: {
               r: 10,
               stroke: '#6d6d6d',
               'stroke-width': 3,
               fill: 'white',
               cursor: 'pointer',
            },
         },
            {
               tagName: 'text',
               textContent: 'M',
               selector: 'label',
               attrs: {
                  fill: '#000000',
                  'font-size': 10,
                  'text-anchor': 'middle',
                  'pointer-events': 'none',
                  y: '0.3em',
               },
            }]
         if (cell.isNode() && !cell.attrs.typeName && cell.shape !== 'custom-circle-start') {
            cell.addTools([
               // {
               //     name: 'button',
               //     args: {
               //        markup: markup,
               //        x: 40,
               //        y: 80,
               //        onClick: () => {// bottom
               //           const rect = new Shape.Rect({
               //              x: x - nodeWidth / 2,
               //              y: y + 70,
               //              width: nodeWidth,
               //              height: nodeHeight,
               //              ports: { ...node.ports },
               //              attrs: { ...cell.attrs }
               //           })
               //           // 添加节点
               //           this.graph.addNode(rect);
               //           const addNodeCls = (this.graph.addNode(rect));
               //           // 添加边
               //           this.graph.addEdge({
               //              source: { cell: node.id, port: node.port.ports[2].id },
               //              target: { cell: addNodeCls.id, port: addNodeCls.port.ports[0].id },
               //              tools: [
               //                 {
               //                    name: 'edge-editor',
               //                    args: {
               //                       attrs: {
               //                          backgroundColor: '#fff',
               //                       },
               //                    },
               //                 },
               //              ],
               //           })
               //           if (cell.isNode() && cell.hasTool('button-remove')) {
               //              cell.removeTool('button')
               //           }
               //        },
               //     },
               // },
               {
                  name: 'button',
                  args: {
                     markup: markup,
                     x: 110,
                     y: 10,
                     onClick: () => {// right
                        const rect = new Shape.Rect({
                           x: x + 150,
                           y: y - nodeHeight / 2,
                           width: nodeWidth,
                           height: nodeHeight,
                           ports: { ...node.ports },
                           attrs: { ...cell.attrs, text: { ...cell.attrs.text, text: '' } },
                        })
                        // 添加节点
                        this.graph.addNode(rect);
                        const addNodeCls = (this.graph.addNode(rect));
                        console.log(addNodeCls, 'addNodeCls')
                        // 添加边
                        this.graph.addEdge({
                           source: { cell: node.id, port: node.port.ports[1].id },
                           target: { cell: addNodeCls.id, port: addNodeCls.port.ports[3].id },
                        })
                        if (cell.isNode()) {
                           cell.removeTool('button')
                        }
                     },
                  },
               },
               // {
               //     name: 'button',
               //     args: {
               //        markup: markupDel,
               //        x: 110,
               //        y: 70,
               //        onClick: () => {// right
               //           this.graph.removeCell(cell)
               //        },
               //     },
               // },
               // {
               //     name: 'button',
               //     args: {
               //        markup: markup,
               //        x: 40,
               //        y: -20,
               //        onClick: () => { // top
               //           const rect = new Shape.Rect({
               //              x: x - nodeWidth / 2,
               //              y: y - 120,
               //              width: nodeWidth,
               //              height: nodeHeight,
               //              ports: { ...node.ports },
               //              attrs: { ...cell.attrs }
               //           })
               //           // 添加节点
               //           this.graph.addNode(rect);
               //           const addNodeCls = (this.graph.addNode(rect));
               //           // 添加边
               //           this.graph.addEdge({
               //              source: { cell: node.id, port: node.port.ports[0].id },
               //              target: { cell: addNodeCls.id, port: addNodeCls.port.ports[2].id },
               //           })
               //           if (cell.isNode() && cell.hasTool('button-remove')) {
               //              cell.removeTool('button')
               //           }
               //        },
               //     },
               // },
               {
                  name: 'button',
                  args: {
                     markup: markupM,
                     x: 110,
                     y: 40,
                     onClick: () => { // mark
                        const rect = new Shape.Rect({
                           x: x - nodeWidth / 2,
                           y: y - 120,
                           width: nodeWidth + 100,
                           height: nodeHeight,
                           // ports: { ...node.ports },
                           attrs: {
                              typeName: 'mark',
                              label: {
                                 textWrap: {
                                    width: -10,      // 宽度减少 10px
                                    height: '100%',   // 高度为参照元素高度的一半
                                    ellipsis: false,  // 文本超出显示范围时,自动添加省略号
                                    breakWord: true, // 是否截断单词
                                 },
                              },
                           },
                           tools: ['node-editor']

                        })
                        // 添加节点
                        this.graph.addNode(rect);
                        const addNodeCls = (this.graph.addNode(rect));
                        console.log(node, 'addNodeCls')
                        // 添加边
                        this.graph.addEdge({
                           source: { cell: node.id, port: node.port.ports[0].id },
                           target: {
                              cell: addNodeCls.id, anchor: {
                                 name: 'left',
                                 args: {
                                    dy: -10,
                                 },
                              },
                              connectionPoint: 'anchor',
                           },
                           router: { name: 'metro' },
                           attrs: {
                              line: {
                                 stroke: '#1890ff',
                                 strokeDasharray: 5,
                                 targetMarker: 'classic',
                              },
                           },
                        })
                        if (cell.isNode()) {
                           cell.removeTool('button')
                        }
                     },
                  },
               },
               // {
               //     name: 'button',
               //     args: {
               //        markup: markup,
               //        x: -20,
               //        y: 30,
               //        onClick: () => { // left
               //           const rect = new Shape.Rect({
               //              x: x - 150,
               //              y: y - nodeHeight / 2,
               //              width: nodeWidth,
               //              height: nodeHeight,
               //              ports: { ...node.ports },
               //              attrs: { ...cell.attrs }
               //           })
               //           // 添加节点
               //           this.graph.addNode(rect);
               //           const addNodeCls = (this.graph.addNode(rect));
               //           // 添加边
               //           this.graph.addEdge({
               //              source: { cell: node.id, port: node.port.ports[3].id },
               //              target: { cell: addNodeCls.id, port: addNodeCls.port.ports[1].id },
               //              tools: [
               //                 {
               //                    name: 'edge-editor',
               //                    args: {
               //                       attrs: {
               //                          backgroundColor: '#fff',
               //                       },
               //                    },
               //                 },
               //              ],
               //           })
               //           if (cell.isNode() && cell.hasTool('button-remove')) {
               //              cell.removeTool('button')
               //           }
               //        },
               //     },
               // }
            ]);
            const container = document.getElementById('graph-container')
            const ports = container.querySelectorAll('.x6-port-body')
            showPorts(ports, false)
         }
      }
   })

   this.graph.on('blank:click', () => {
      // this.currentCell && this.currentCell.removeTools();
      if (this.currentCell) {
         this.currentCell.removeTool('button');
         this.currentCell.removeTool('button-move');
      }
      this.isClose = true;
      this.isGloable = true;
   })
},

image.png

节点旁边的按钮,上面的代表点击时可以自动增加一个节点,下面的代表点击时出现一个节点可以给当前节点增加备注

image.png

将此方法在mounted中调用

右侧表单部分(可以自行定义,因业务字段较多这里就不贴了)

js部分

    // 获取当前点击选中的元素
   let selectedCell = this.graph.getSelectedCells();
整个CSS
<style lang="less">
    .g6-wrap {
        width: 100%;

        .el-dialog {
            display: flex;
            flex-direction: column;
            margin: 0 !important;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            /*height:600px;*/
            max-height: calc(100% - 30px);
            max-width: calc(100% - 30px);
            border-radius: 2px;
        }

        .el-dialog .el-dialog__body {
            flex: 1;
            overflow: auto;
        }

        position: relative;

        .top-box {
            height: 40px;
            z-index: 999;
            background: #FFFFFF;
            position: absolute;
            top: 0;
            left: 100px;

            .tools-box {
                display: flex;
                align-items: center;

                .tool {
                    width: 50px;
                    margin-right: 5px;
                    cursor: pointer;
                    padding: 2px;
                    text-align: center;

                    img {
                        width: 20px;
                        height: 20px;
                    }

                    .word {
                        font-size: 10px;
                    }
                }

                .tool:hover {
                    background: #d7d7d7;
                    border-radius: 2px;
                }
            }

            .goBack {
                height: 38px;
                line-height: 38px;
            }
        }

        #container {
            display: flex;
            height: 100%;
            margin: 0 10px;

            .x6-widget-selection-box {
                border: 3px dashed #239edd;
                border-radius: 1px;
            }

            .x6-widget-selection-inner {
                border: 2px solid #239edd;
                border-radius: 10px;
            }

            #minimap {
                position: absolute;
                top: 30px;
                right: 30%;
            }

            #stencil {
                width: 100px;
                height: 100%;
                position: relative;
                display: flex;
                flex-direction: column;
                align-items: center;
                border-right: 1px solid #dfe3e8;
                text-align: center;
                font-size: 12px;

                .dnd-rect {
                    width: 50px;
                    height: 30px;
                    line-height: 40px;
                    text-align: center;
                    border: 2px solid #000000;
                    border-radius: 6px;
                    cursor: move;
                    font-size: 12px;
                    margin-top: 30px;
                }

                .dnd-polygon {
                    width: 35px;
                    height: 35px;
                    border: 2px solid #000000;
                    transform: rotate(45deg);
                    cursor: move;
                    font-size: 12px;
                    margin-top: 30px;
                    margin-bottom: 10px;
                }

                .dnd-circle {
                    width: 35px;
                    height: 35px;
                    line-height: 45px;
                    text-align: center;
                    border: 5px solid #000000;
                    border-radius: 100%;
                    cursor: move;
                    font-size: 12px;
                    margin-top: 30px;
                }

                .dnd-start {
                    border: 2px solid #000000;
                }

                .x6-widget-stencil {
                    background-color: #f8f9fb;
                }

                .x6-widget-stencil-title {
                    background: #eee;
                    font-size: 1rem;
                }

                .x6-widget-stencil-group-title {
                    font-size: 1rem !important;
                    background-color: #fff !important;
                    height: 40px !important;
                }

                .x6-widget-transform {
                    margin: -1px 0 0 -1px;
                    padding: 0px;
                    border: 1px solid #239edd;
                }

                .x6-widget-transform > div {
                    border: 1px solid #239edd;
                }

                .x6-widget-transform > div:hover {
                    background-color: #3dafe4;
                }

                .x6-widget-transform-active-handle {
                    background-color: #3dafe4;
                }

                .x6-widget-transform-resize {
                    border-radius: 0;
                }

            }
        }

        .right-box {
            display: block;
            position: absolute;
            top: 0;
            right: 0;
            width: 30%;
            height: 100%;
            border-left: 2px solid #ccc;
            background: #FFFFFF;
            overflow: auto;
            transition: display .3s;
        }

        .close {
            display: none;
        }

        .open {
            position: absolute;
            top: 50%;
            right: 30%;
            width: 20px;
            height: 100px;
            font-size: 25px;
            background: #FFFFFF;
            text-align: center;
            line-height: 100px;
            border: 2px solid #ccc;
            border-right: none;
            border-radius: 2px;
            cursor: pointer;
            transition: right;
        }

        .right0 {
            right: 10px;
        }

        #graph-container {
            width: calc(100% - 220px);
            border-right: 2px solid #ccc;
            /*margin: 45px 10px 10px;*/
        }

        .el-drawer__header {
            margin-bottom: 0;
            font-weight: bold;
            font-size: 20px;
            color: #000;
        }
    }
</style>

最后,整体效果

image.png