antv-x6节点vue组件

562 阅读5分钟

前言

一个用于物流行业的一个功能;需求需要一个画布可以拖拽节点(条形码,文字等等信息)组成一个信息图(类似快递上的物流信息),最终可以导出图片

每个节点(条形码,文字等等)可以拖拽编辑,这里使用vue作为antv-x6的节点来实现交互和导出图片

可以对节点拖拽,有容器节点;选中节点可以在右侧对节点一些属性修改,这些是和节点组件可通信的;

这里只是示例,可通过自定义的扩展节点组件和右侧表单字段扩展

git仓库

用到了这些库

{
    "@antv/x6": "^2.18.1",
    "@antv/x6-plugin-export": "^2.1.6",
    "@antv/x6-plugin-stencil": "^2.1.5",
    "@antv/x6-vue-shape": "^2.1.2",
    "monaco-editor": "^0.52.0",
    "vue": "^3.4.37"
}
  • @antv/x6

    主要的库了,提供了基本的画布、默认节点的操作交互

  • @antv/x6-plugin-stencil

    antv/x6的一个扩展--左侧可拖拽到画布中的菜单

  • @antv/x6-plugin-export

    导出各类图片用到的扩展

  • @antv/x6-vue-shape

    将vue组件作为节点的扩展

  • monaco-editor

    一个代码编辑器,可以输入代码格式化等等;vscode同款

一、vue组件注册为节点

x6.antv.antgroup.com/tutorial/in…

@antv/x6-vue-shape扩展库中提供了,getTeleportregister方法

  1. register 将vue组件注册为节点

    import {register} from '@antv/x6-vue-shape'
    import address from "../nodeList/address.vue";
    register({
        shape: 'custom-address',        //具体使用时节点名称/类型
        width: 260,
        height: 32,
        component: address,             //vue组件
    })
    
  2. getTeleport 使用扩展

    在antv实例的vue组件中,加入以下代码

    import { getTeleport } from '@antv/x6-vue-shape';
    const TeleportContainer = getTeleport();
    

    template中使用TeleportContainer

    <template>
      <div class="designer">
        <div class="designer-menu" ref="menuRef"></div>
        <div class="designer-graph" ref="graphRef"></div>
        <div class="designer-form">
          <div class="designer-form-actions">
            <button @click="handleExportPng">导出PNG</button>
            <button @click="handleExportSvg">导出SVG</button>
            <button @click="handleExportBase">获取BASE64</button>
          </div>
          <nodeForm :selectCell="selectCell"></nodeForm>
        </div>
      </div>
      <TeleportContainer />
    </template>
    

二、节点组件的内容,和如何知晓更新了内容(通信)

x6.antv.antgroup.com/tutorial/in… 该文档下方更新节点可查看官方描述

例如上方用到的address组件,它的内容如下

  • nodeBase.ts 通过inject('getGraph') inject('getNode')分别获取画布实例和节点对象

可通过节点事件change:data监听节点的data属性变化,去做一些操作

import {computed, inject, ref} from 'vue';
export default function(){
    const Graph = inject<any>('getGraph')('getGraph');
    const Node = inject<any>('getNode')('getNode');
    const data = ref(Node.data);
    Node.on('change:data', ({current}: any) => {
        data.value = current;
    })
    const selectedToClassName = computed(()=>{
        if(data.value?.selected){
            return {
                'node-list-selected':true,
            }
        }else{
            return {}
        }
    })
    return {
        Graph,
        Node,
        data,
        selectedToClassName
    }
}
  • address.vue
<script setup lang="ts">
import nodeBase from './tool/node-base.ts';

const {Graph, Node, data, selectedToClassName} = nodeBase();
</script>

<template>
  <div class="node-list-address" :class="selectedToClassName">
    <div class="node-list-component-content" :style="data?.styles">
      {{ data.label }}
    </div>
  </div>
</template>

三、添加默认节点到画布中

使用画布实例addNode方法添加一个元素到画布中

该方法中的shape是在[一]当中register-shape字段值

const defaultNodes = () => {
  if (!example.value.graph) return;

  const parent = example.value.graph.addNode({
    x: 100,
    y: 100,
    width: 400,
    height: 400,
    shape: 'custom-parent',
    data: { parent: true },
  });

  parent.addChild(example.value.graph.addNode({
    x: 110,
    y: 110,
    width: 100,
    height: 30,
    shape: 'custom-address',
    data: {
      label: '北京市-朝阳区-xx-xx',
      styles: { color: '#333' }
    }
  }));

  parent.addChild(example.value.graph.addNode({
    x: 110,
    y: 310,
    width: 100,
    height: 30,
    shape: 'custom-box',
    data: {
      label: 'defalut node',
      styles: { color: '#333' }
    }
  }));
};

调用该函数集合添加已经注册的节点到画布中,这里用到parent.addChild是父子节点

我是通过data:{parent:true}来表明这是一个可以加入子节点的容器

四、父子节点的嵌套

需要有嵌套关系(拖动父节点,其中的子节点相对的移动),需要在实例画布时加入以下内容:

embedding配置中 看到是通过node.getData().parent来判断是否嵌套

export const installGraph = (dom:HTMLElement) => {
    const graph = new Graph({
        container: dom,
        grid: true,
        mousewheel: {
            enabled: true,
            zoomAtMousePosition: true,
            modifiers: 'ctrl',
            minScale: 0.5,
            maxScale: 3,
        },
        panning:true,
        // 是否嵌套节点
        embedding:{
            enabled: true,
            findParent({ node }) {
                //! 用来区分是否是父节点的,通过设置 节点的data:{parent:true}得知
                const bbox = node.getBBox()
                return this.getNodes().filter((node) => {
                    const data = node.getData<{ parent: boolean }>()
                    if (data && data.parent) {
                        const targetBBox = node.getBBox()
                        return bbox.isIntersectWithRect(targetBBox)
                    }
                    return false
                })
            },
        },
    });
    graph.use(new Export())
    return graph;
}

五、更新节点尺寸,data,zindex

给画布添加节点按下事件cell:mousedown,我们来获取选中节点;

let selectCell = ref();
const nodeSelectedFn = (cell: Cell) => {
  if (selectCell.value && selectCell.value.setData) {
    selectCell.value.setData({ selected: false });
  }
  selectCell.value = graph.getCellById(cell.id);
  selectCell.value.setData({ selected: true });
};
graph.on('cell:mousedown', ({ cell }) => {
  nodeSelectedFn(cell);
});

假如有一按钮点击后修改data中的label

const click = () =>{
    selectCell.value.setData({
        label:+new Date()
    })
}

修改尺寸size

//cell.size()返回宽高信息
const click = () =>{
    selectCell.value.size({
        width:100,
        height:selectCell.value.size().height
    })
}

更多查看文档

六、使用导出图片扩展

在【四】中看到有一段代码graph.use(new Export()) 我们引入Export即可

import { Export } from '@antv/x6-plugin-export';

具体使用

// 导出png
const handleExportPng = () => {
  example.value.graph?.exportPNG();
};
// 导出svg
const handleExportSvg = () => {
  example.value.graph?.exportSVG();
};

function base64ToFile(base64: string, filename: string): File {
  const arr = base64.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bstr = atob(arr[1]);
  const n = bstr.length;
  const u8arr = new Uint8Array(n);

  for (let i = 0; i < n; i++) {
    u8arr[i] = bstr.charCodeAt(i);
  }

  return new File([u8arr], filename, { type: mime });
}

// 转为base64,再将base64转为file等等可用来接口提交
const handleExportBase = () => {
  example.value.graph?.toPNG((a: string) => {
    console.log(a);
    const file = base64ToFile(a, 'x6.png');
    console.log(file);
  });
}

七、拖拽菜单

1. 注册菜单实例

传入菜单的容器元素,菜单可拖拽的目标实例

export const installMenu = (dom:HTMLElement, target:Graph) => {
    const stencil = new Stencil({
        search: true,
        target: target,
        title: '自定义的菜单以及节点',
        stencilGraphWidth: 260,
        stencilGraphPadding: 0,
        layoutOptions: {
            columns: 1,
            dx: 0,
            dy: 3,
            columnWidth: 'compact',
            rowHeight: 'compact'
        },
        groups: [
            {
                title: '基础流程图',
                name: 'group1',
            },
        ],
    })
    dom.appendChild(stencil.container);
    return stencil;
}

2. 添加菜单选项

传入画布实例,菜单实例

export const installMenuOptions = (graph:Graph, menu:Stencil) => {
    const r1 = graph.createNode({
        shape: 'custom-address',
        data: {
            label: 'vue-component-01',
            styles:{
                background:'#ccc',
                fontSize:'16px'
            }
        }
    })
    const r2 = graph.createNode({
        shape: 'custom-box',
        data: {
            label:'vue-component-02',
            styles:{
                background:'#ccc',
                fontSize:'16px'
            }
        }
    })
    const r3 = graph.createNode({
        shape: 'custom-parent',
        data: {
            parent:true,
            label:'vue-component-parent',
            styles:{
                background:'#ccc',
                fontSize:'16px',
                lineHeight:'32px'
            }
        }
    })
    menu.load([r1, r2, r3], 'group1')
}