antv/x6@2.x + vue3 开发环境配置

官方文档地址:x6.antv.antgroup.com/

本篇示例地址:github.com/Chnsome/ant...

前言

2.x 与 1.x 的不同

1、@2.x 将 @1.x 中画布的每个格外的功能独立成了单独的包,比如框选功能、快捷键功能等

2、@2.x 的 API 文档、代码演示更清晰易懂(不再需要去 sandbox 找代码),代码风格向 tsx、vue-composition-api 看齐,对于 react、vue3 项目更友好

搭建开发环境

一、安装

shell 复制代码
npm i @antv/x6 @antv/x6-plugin-history @antv/x6-plugin-selection @antv/x6-plugin-snapline @antv/x6-plugin-transform @antv/x6-vue-shape -D
json 复制代码
// package.json
{
  "@antv/x6": "^2.0.0",
  "@antv/x6-plugin-clipboard": "^2.0.0", // 如果使用剪切板功能,需要安装此包
  "@antv/x6-plugin-history": "^2.0.0", // 如果使用撤销重做功能,需要安装此包
  "@antv/x6-plugin-keyboard": "^2.0.0", // 如果使用快捷键功能,需要安装此包
  "@antv/x6-plugin-minimap": "^2.0.0", // 如果使用小地图功能,需要安装此包
  "@antv/x6-plugin-scroller": "^2.0.0", // 如果使用滚动画布功能,需要安装此包
  "@antv/x6-plugin-selection": "^2.0.0", // 如果使用框选功能,需要安装此包
  "@antv/x6-plugin-snapline": "^2.0.0", // 如果使用对齐线功能,需要安装此包
  "@antv/x6-plugin-dnd": "^2.0.0", // 如果使用 dnd 功能,需要安装此包
  "@antv/x6-plugin-stencil": "^2.0.0", // 如果使用 stencil 功能,需要安装此包
  "@antv/x6-plugin-transform": "^2.0.0", // 如果使用图形变换功能,需要安装此包
  "@antv/x6-plugin-export": "^2.0.0", // 如果使用图片导出功能,需要安装此包
  "@antv/x6-react-components": "^2.0.0", // 如果使用配套 UI 组件,需要安装此包
  "@antv/x6-react-shape": "^2.0.0", // 如果使用 react 渲染功能,需要安装此包
  "@antv/x6-vue-shape": "^2.0.0" // 如果使用 vue 渲染功能,需要安装此包
}

二、初始化画布 + 使用插件

vue 复制代码
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Graph, Shape } from "@antv/x6";

// 引入插件
import { Selection } from '@antv/x6-plugin-selection'
import { Snapline } from '@antv/x6-plugin-snapline'
import { History } from '@antv/x6-plugin-history'
import { Transform } from '@antv/x6-plugin-transform'
    
const graph = ref<Graph>()

onMounted(() => {
  setTimeout(() => {
    graph.value = initGraph()
  })
})
    
function initGraph() {
  const container = document.getElementById('container')!;
  const graph = new Graph({
    container,
    autoResize: true, // 自动变化画布大小
    // 是否显示网格
    grid: {
      size: 5,
      visible: true,
    },
    // 自定义交互限制
    interacting: {
      nodeMovable: true,  // 节点是否可以被移动。
      edgeMovable: true,  // 边是否可以被移动。
      edgeLabelMovable: true,  // 边的标签是否可以被移动。
      magnetConnectable: true,  // 当在具有 magnet 属性的元素上按下鼠标开始拖动时,是否触发连线交互。
      arrowheadMovable: true,  // 边的起始/终止箭头是否可以被移动。
      vertexMovable: true,  // 边的路径点是否可以被移动。
      vertexAddable: true,  // 是否可以添加边的路径点。
      vertexDeletable: true,  // 边的路径点是否可以被删除。
    },
    // 是否限制节点移动范围
    translating: {
        restrict: true,
    },
    // 定义点击节点链接桩后的连接线
    connecting: {
      allowBlank: true, // 是否允许连接到画布空白位置的点
      // 自定义连接器
      connector: {
        name: 'rounded',
        args: {
          radius: 20,
        },
      },
      // 自定义路由
      router: {
        name: 'orth',
        args: {
          padding: 1,
        },
      },
      // 是否开启连线过程中自动吸附
      snap: {
        radius: 20, // 吸附半径
        anchor: 'center',
      },
      // 自定义新建边样式
      createEdge() {
        return new Shape.Edge({
          markup: [
            {
              tagName: 'path',
              selector: 'fill',
            },
          ],
          attrs: {
            fill: {
              connection: true,
              strokeWidth: 2,
              fill: 'none',
              stroke: {
                type: 'linearGradient',
                stops: [
                  { offset: '0%', color: '#006EFF' },
                  { offset: '100%', color: '#A42FD6' },
                ],
              },
              // 箭头
              targetMarker: {
                name: 'block',
                fill: '#A42FD6',
                offset: 0,
                width: 5,
                height: 10,
              },
            },
          },
        });
      },
      // 在移动边的时候判断连接是否有效
      validateConnection({ targetMagnet }) {
        return !!targetMagnet;
      },
    },
    // 连接到连接桩时的高亮显示
    highlighting: {
      magnetAdsorbed: {
        name: 'stroke',
        args: {
          attrs: {
            fill: '#5F95FF',
            stroke: '#5F95FF',
          },
        },
      },
    },
  })
  
  // 挂载插件
  .use(
    new Selection({
      enabled: true,
      multiple: true,
      rubberband: true,
      movable: true,
      showNodeSelectionBox: true,
    }),
  )
  .use(
    new History({
      enabled: true,
    }),
  )
  .use(
    new Snapline({
      enabled: true,
    }),
  )
  .use(
    new Transform({
      resizing: true,
      rotating: true,
    }),
  )

  return graph
}
</script>

<template>
    <div class="outer">
        <div id="container"></div>
    </div>
</template>

<style lang="less" scoped>
.outer {
  margin: 0 auto;
  width: 95vw;
  height: 95vh;

  #container {
    width: 100%;
    height: 100%;
  }
}
</style>

三、注册自定义节点

官网中的提示是:

在选择渲染方式时我们推荐:

  • 如果节点内容比较简单,而且需求比较固定,使用 SVG 节点
  • 其他场景,都推荐使用当前项目所使用的框架来渲染节点
typescript 复制代码
// registerCells.ts
import { register } from '@antv/x6-vue-shape'
import Foo from './vue-nodes/Foo.vue'
import Bar from './vue-nodes/Bar.vue'

// #region 初始化连接桩
const ports = {
  // 输出链接桩群组定义
  groups: {
    top: {
      position: 'top',
      attrs: {
        circle: {
          r: 10,
          magnet: true,
          stroke: '#5F95FF',
          strokeWidth: 1,
          fill: '#fff',
          style: {
            visibility: 'hidden',
          },
        },
      },
    },
    bottom: {
      position: 'bottom',
      attrs: {
        circle: {
          r: 10,
          magnet: true,
          strokeWidth: 1,
          stroke: '#5F95FF',
          fill: '#fff',
          style: {
            visibility: 'hidden',
          },
        },
      },
    },
    left: {
      position: 'left',
      attrs: {
        circle: {
          r: 10,
          fill: '#fff',
          magnet: true,
          stroke: '#5F95FF',
          strokeWidth: 1,
          style: {
            visibility: 'hidden',
          },
        },
      },
    },
    right: {
      position: 'right',
      attrs: {
        circle: {
          style: {
            visibility: 'hidden',
          },
          r: 10,
          magnet: true,
          stroke: '#5F95FF',
          strokeWidth: 1,
          fill: '#fff',
        },
      },
    },
  },
  // 可以自定义每一条边有多少个连接桩
  items: [
    {
      id: "top",
      group: "top"
    },
    {
      id: "right1",
      group: "right"
    },
    {
      id: "right2",
      group: "right"
    },
    {
      id: "right3",
      group: "right"
    },
    {
      id: "right4",
      group: "right"
    },
    {
      id: "right5",
      group: "right"
    },
    {
      id: "bottom",
      group: "bottom"
    },
    {
      id: "left1",
      group: "left"
    },
    {
      id: "left2",
      group: "left"
    },
    {
      id: "left3",
      group: "left"
    },
    {
      id: "left4",
      group: "left"
    },
    {
      id: "left5",
      group: "left"
    },
  ]
}
// #endregion

register({
  shape: 'foo',
  component: Foo,
  ports: { ...ports },
})
register({
  shape: 'bar',
  component: Bar,
  ports: { ...ports },
})

四、绑定事件

typescript 复制代码
function addEvents() {
  if (!graph.value) {
    return
  }
  // 控制连接桩显示/隐藏
  const showPorts = (ports: NodeListOf<HTMLElement>, show: boolean) => {
    for (let i = 0, len = ports.length; i < len; i = i + 1) {
      ports[i].style.visibility = show ? "visible" : "hidden";
    }
  };
  graph.value.on("node:mouseenter", () => {
    const container = document.getElementById("container")!;
    const ports = container.querySelectorAll(".x6-port-body") as NodeListOf<HTMLElement>;
    showPorts(ports, true);
  });
  graph.value.on("node:mouseleave", () => {
    const container = document.getElementById("container")!;
    const ports = container.querySelectorAll(".x6-port-body") as NodeListOf<HTMLElement>;
    showPorts(ports, false);
  });

  // 添加连接线时附带辅助工具
  graph.value.on("edge:mouseenter", ({ cell }) => {
    cell.addTools("vertices", "onhover");
  });
  graph.value.on("edge:mouseleave", ({ cell }) => {
    if (cell.hasTools("onhover")) {
      cell.removeTools();
    }
  });
}

五、添加工具按钮

vue 复制代码
<script setup lang="ts">
function showJSON() {
  console.log(graph.value?.toJSON())
}

function undo() {
  graph.value?.undo()
}

function zoom() {
  graph.value?.zoomToFit({ maxScale: 1 })
}
</script>

<template>
    <button @click="showJSON">Show JSON</button>
    <button @click="undo">Undo</button>
    <button @click="zoom">Zoom</button>
    <div class="outer">
        <div id="container"></div>
    </div>
</template>

开发

一、添加节点

typescript 复制代码
function addNodes() {
  graph.value?.addNodes([
    {
      id: 'foo',
      shape: 'rect',
      width: 200,
      height: 200,
      x: 0,
      y: 0,
      attrs: {
        body: {
          fill: "#191919",
        }
      }
    },
    {
      id: 'bar',
      shape: 'rect',
      width: 100,
      height: 100,
      x: 500,
      y: 500,
      attrs: {
        body: {
          fill: "#000",
        }
      }
    }
  ])
}

二、通过连接桩进行连线

三、保存节点

点击【节点转JSON(保存)】按钮,查看控制台的打印信息,并且保存在 json 文件中

四、展示已保存的节点

这样就可以实时保存自己画的图了

typescript 复制代码
import { onMounted, ref } from "vue";
import { Graph, type Cell } from "@antv/x6";
import cells from './cells.json'

const graph = ref<Graph>()

onMounted(() => {
  graph.value = initGraph()

  // 加载已保存节点
  const res: Cell[] = []
  cells.forEach(i => {
    if (['edge'].includes(i.shape)) {
      res.push(graph.value?.createEdge(i)!)
    } else {
      res.push(graph.value?.createNode(i)!)
    }
  })
  graph.value.resetCells(res as Cell[])
  // graph.value?.centerContent() // 建议画好后再开启
  // graph.value?.zoomToFit({ padding: 10, maxScale: 16 }) // 建议画好后再开启

  // 继续添加节点
  addNodes()
    
  // 绑定事件
  addEvents()
})

五、将画布居中对齐、等比缩放

typescript 复制代码
graph.value?.centerContent()
graph.value?.zoomToFit({ padding: 10, maxScale: 16 })

六、响应式方案

shell 复制代码
npm i @vueuse/core -D
vue 复制代码
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useResizeObserver } from '@vueuse/core'
import { Graph } from "@antv/x6";

const graph = ref<Graph>()

// 导航图响应式显示
const outer = ref(null)

useResizeObserver(outer, (entries) => {
  const [entry] = entries
  const { width, height } = entry.contentRect
  graph.value && resizeGraph(graph.value, width, height)
})
const resizeGraph = (graph: Graph, width: number, height: number) => {
  graph.resize(width, height)
  // 图表居中显示
  graph.centerContent()
  graph.zoomToFit({ maxScale: 10 })
}
</script>

<template>
    <div ref="outer" class="outer">
        <div id="container"></div>
    </div>
</template>

<style lang="less" scoped>
.outer {
  margin: 0 auto;
  width: 95vw;
  height: 95vh;

  #container {
    width: 100%;
    height: 100%;
  }
}
</style>

附录

自定义 VUE 节点模板

vue 复制代码
<script setup lang="ts">
import { inject, onMounted, ref } from 'vue'
import { Node } from '@antv/x6'

const getNode = inject('getNode', Function, true)

const dataSource = ref({
  title: '',
})

onMounted(() => {
  const node = getNode() as Node
  const nodeData = node.getData()
  Object.entries(nodeData).forEach(([key, value]) => {
    dataSource.value[key as keyof typeof dataSource.value] = value as string
  })
})

</script>

<template>
  <div class="template-class">
    {{ dataSource.title }}
  </div>
</template>

<style lang="less" scoped>
.template-class {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-weight: bold;
  font-size: 14px;
  line-height: 14px;
}
</style>

完整模板

vue 复制代码
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { Graph, Shape, type Cell } from '@antv/x6';
import { useResizeObserver } from '@vueuse/core'
import './registerCells'
import cells from './cells.json'

// 引入插件
import { Selection } from '@antv/x6-plugin-selection'
import { Snapline } from '@antv/x6-plugin-snapline'
import { History } from '@antv/x6-plugin-history'
import { Transform } from '@antv/x6-plugin-transform'

// 导航图响应式显示
const outer = ref(null)

useResizeObserver(outer, (entries) => {
  const [entry] = entries
  const { width, height } = entry.contentRect
  graph.value && resizeGraph(graph.value, width, height)
})
const resizeGraph = (graph: Graph, width: number, height: number) => {
  graph.resize(width, height)
  // 图表居中显示
  // graph.centerContent() // 建议画好后再开启
  // graph.zoomToFit({ maxScale: 1 }) // 建议画好后再开启
}

const graph = ref<Graph>()

onMounted(() => {
  graph.value = initGraph()

  // 加载已保存节点
  const res: Cell[] = []
  cells.forEach(i => {
    if (['edge'].includes(i.shape)) {
      res.push(graph.value?.createEdge(i)!)
    } else {
      res.push(graph.value?.createNode(i)!)
    }
  })
  graph.value.resetCells(res as Cell[])
  // graph.value?.centerContent() // 建议画好后再开启
  // graph.value?.zoomToFit({ maxScale: 1 }) // 建议画好后再开启

  // 继续添加节点
  // addNodes()

  // 绑定事件
  addEvents()
})

function initGraph() {
  const container = document.getElementById('container')!;
  const graph = new Graph({
    container,
    autoResize: true, // 自动变化画布大小
   	// 是否显示网格
    grid: {
      size: 5,
      visible: true,
    },
    // 自定义交互限制
    interacting: {
      nodeMovable: true,  // 节点是否可以被移动。
      edgeMovable: true,  // 边是否可以被移动。
      edgeLabelMovable: true,  // 边的标签是否可以被移动。
      magnetConnectable: true,  // 当在具有 magnet 属性的元素上按下鼠标开始拖动时,是否触发连线交互。
      arrowheadMovable: true,  // 边的起始/终止箭头是否可以被移动。
      vertexMovable: true,  // 边的路径点是否可以被移动。
      vertexAddable: true,  // 是否可以添加边的路径点。
      vertexDeletable: true,  // 边的路径点是否可以被删除。
    },
    // 是否限制节点移动范围
    translating: {
      restrict: true,
    },
    // 定义点击节点链接桩后的连接线
    connecting: {
      allowBlank: true, // 是否允许连接到画布空白位置的点
      // 自定义连接器
      connector: {
        name: 'rounded',
        args: {
          radius: 20,
        },
      },
      // 自定义路由
      router: {
        name: 'orth',
        args: {
          padding: 1,
        },
      },
      // 是否开启连线过程中自动吸附
      snap: {
        radius: 20, // 吸附半径
        anchor: 'center',
      },
      // 自定义新建边样式
      createEdge() {
        return new Shape.Edge({
          markup: [
            {
              tagName: 'path',
              selector: 'fill',
            },
          ],
          attrs: {
            fill: {
              connection: true,
              strokeWidth: 2,
              fill: 'none',
              stroke: {
                type: 'linearGradient',
                stops: [
                  { offset: '0%', color: '#006EFF' },
                  { offset: '100%', color: '#A42FD6' },
                ],
              },
              // 箭头
              targetMarker: {
                name: 'block',
                fill: '#A42FD6',
                offset: 0,
                width: 5,
                height: 10,
              },
            },
          },
        });
      },
      // 在移动边的时候判断连接是否有效
      validateConnection({ targetMagnet }) {
        return !!targetMagnet;
      },
    },
    // 连接到连接桩时的高亮显示
    highlighting: {
      magnetAdsorbed: {
        name: 'stroke',
        args: {
          attrs: {
            fill: '#5F95FF',
            stroke: '#5F95FF',
          },
        },
      },
    },
  })

  // 挂载插件
  .use(
    new Selection({
      enabled: true,
      multiple: true,
      rubberband: true,
      movable: true,
      showNodeSelectionBox: true,
    }),
  )
  .use(
    new History({
      enabled: true,
    }),
  )
  .use(
    new Snapline({
      enabled: true,
    }),
  )
  .use(
    new Transform({
      resizing: true,
      rotating: true,
    }),
  )

  return graph
}

function addEvents() {
  if (!graph.value) {
    return
  }
  // 控制连接桩显示/隐藏
  const showPorts = (ports: NodeListOf<HTMLElement>, show: boolean) => {
    for (let i = 0, len = ports.length; i < len; i = i + 1) {
      ports[i].style.visibility = show ? "visible" : "hidden";
    }
  };
  graph.value.on("node:mouseenter", () => {
    const container = document.getElementById("container")!;
    const ports = container.querySelectorAll(".x6-port-body") as NodeListOf<HTMLElement>;
    showPorts(ports, true);
  });
  graph.value.on("node:mouseleave", () => {
    const container = document.getElementById("container")!;
    const ports = container.querySelectorAll(".x6-port-body") as NodeListOf<HTMLElement>;
    showPorts(ports, false);
  });

  // 添加连接线时附带辅助工具
  graph.value.on("edge:mouseenter", ({ cell }) => {
    cell.addTools("vertices", "onhover");
  });
  graph.value.on("edge:mouseleave", ({ cell }) => {
    if (cell.hasTools("onhover")) {
      cell.removeTools();
    }
  });
}

function addNodes() {
  graph.value?.addNodes([
    {
      id: 'foo',
      shape: 'foo',
      width: 200,
      height: 200,
      x: 0,
      y: 0,
      data: {
        title: 'i am foo'
      }
    },
    {
      id: 'bar',
      shape: 'bar',
      width: 100,
      height: 100,
      x: 500,
      y: 500,
      data: {
        title: 'i am bar'
      }
    }
  ])
}


function showJSON() {
  console.log(graph.value?.toJSON())
}

function undo() {
  graph.value?.undo()
}

function zoom() {
  graph.value?.zoomToFit({ maxScale: 1 })
}
</script>

<template>
  <button @click="showJSON">Show JSON</button>
  <button @click="undo">Undo</button>
  <button @click="zoom">Zoom</button>
  <div ref="outer" class="outer">
    <div id="container"></div>
  </div>
</template>

<style lang="less" scoped>
.outer {
  margin: 0 auto;
  width: 95vw;
  height: 95vh;

  #container {
    width: 100%;
    height: 100%;
  }
}
</style>
相关推荐
王解3 分钟前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里4 分钟前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱4 分钟前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster23 分钟前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python
baozhengw25 分钟前
UniAPP快速入门教程(一)
前端·uni-app
nameofworld35 分钟前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重
帅比九日1 小时前
【HarmonyOS NEXT】实战——登录页面
前端·学习·华为·harmonyos
摇光931 小时前
promise
前端·面试·promise
麻花20131 小时前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习
.5481 小时前
提取双栏pdf的文字时 输出文件顺序混乱
前端·pdf