vue简单使用antv/x6 的demo

概要

这是一个基于 Vue 和 AntV/X6 的流程图配置 Demo,支持拖拽添加自定义节点悬浮删除节点/边画布缩放数据保存,适合作为可视化流程编排工具的入门实现。

一、主要功能
  1. 界面布局:采用左右布局设计,左侧为组件菜单,右侧为画布区域;支持通过 +/- 按钮控制画布缩放比例。

  2. 节点管理:使用自定义 Vue 组件作为节点样式,实现高度可定制化;通过拖拽左侧菜单项到画布添加流程节点;支持鼠标悬浮显示删除按钮,可删除单个节点。

  3. 边管理:支持在节点间创建连接线;鼠标悬浮在边上显示删除按钮,可删除连接关系。

  4. 数据持久化:实现保存功能,可将当前画布中的节点和边信息保存;数据结构可后续加载还原流程图。

  5. 交互体验:拖拽添加操作直观简便。

二、主页展示

(1)主页面:菜单组件添加拖拽

<template>
  <div class="visual-box">
    <!-- 左侧组件菜单区域 -->
    <div class="component-menu" v-loading="menuLoading">
      <el-menu
        class="menu-box"
        default-active="1"
        @dragstart.native="handleDragStart"
        :unique-opened="true"
      >
        <menu-item
          v-for="(item, index) in menuDataList"
          :key="index"
          :menu-data="item"
        />
      </el-menu>
    </div>
    <!-- 主画布区域 -->
    <div class="mian-container" ref="mian-container">
      <!-- 监听和处理拖拽事件 -->
      <div id="container" @dragover="handleDragOver" @drop="handleDrop"></div>
    </div>
  </div>
</template>

(2)处理拖拽事件并添加节点到画布

 methods: {
    // 处理拖拽开始事件
    handleDragStart(event) {
      // 从拖拽数据中获取菜单数据
      const menuData = JSON.parse(
        event.dataTransfer.getData("application/json")
      );
      this.draggedNodeLabel = menuData; // 保存拖拽的节点数据
    },
    // 处理拖拽经过事件
    handleDragOver(event) {
      event.preventDefault();
    },
    // 处理放置事件
    handleDrop(event) {
      event.preventDefault(); // 阻止默认行为
      // 获取鼠标位置并转换为画布坐标
      const { clientX, clientY } = event;
      const { x, y } = this.graph.clientToLocal(clientX, clientY);
      // 创建新节点配置
      const newNode = {
        id: this.generateRandomCode(), // 生成唯一ID
        shape: "custom-node", // 节点类型
        x: x, // x坐标
        y: y, // y坐标
        width: 120, // 宽度
        height: 36, // 高度
        data: {
          ...this.draggedNodeLabel, // 节点数据
        },
        ports: {
          items: [
            {
              id: this.generateAlphaCode(), // 生成连接桩ID
              group: "top",
            },
            {
              id: this.generateAlphaCode(),
              group: "bottom",
            },
          ],
        },
      };
      // 添加节点到画布
      this.graph.addNode(newNode);
    },
  },

(3)删除节点或者边

initGraph() {
      // 创建X6图实例
      const graph = new Graph({
        container: document.getElementById("container"), // 容器元素
         //  其他配置...
      });

      // 节点鼠标进入事件 - 显示删除工具
      graph.on("node:mouseenter", ({ e, node }) => {
        node.addTools([
          {
            name: "button-remove", // 删除按钮工具
            args: {
              x: 100, // x位置
              y: 0, // y位置
              offset: { x: 10, y: 10 }, // 偏移量
            },
          },
        ]);
      });

      // 节点鼠标离开事件 - 隐藏工具
      graph.on("node:mouseleave", ({ e, node }) => {
        node.removeTools();
      });
      
      // 边鼠标进入事件 - 显示删除工具
      graph.on("edge:mouseenter", ({ edge }) => {
        edge.addTools([{ name: "button-remove", args: { distance: 20 } }]);
      });

      // 边鼠标离开事件 - 隐藏工具
      graph.on("edge:mouseleave", ({ edge }) => {
        edge.removeTools();
      });
    },

(4)完整的主页代码

<template>
  <div class="visual-box">
    <!-- 左侧组件菜单区域 -->
    <div class="component-menu" v-loading="menuLoading">
      <!-- 组件库菜单 -->
      <el-menu
        class="menu-box"
        default-active="1"
        @dragstart.native="handleDragStart"
        :unique-opened="true"
      >
        <!-- 递归渲染菜单项 -->
        <menu-item
          v-for="(item, index) in menuDataList"
          :key="index"
          :menu-data="item"
        />
      </el-menu>
    </div>
    
    <!-- 主画布区域 -->
    <div class="mian-container" ref="mian-container">
      <!-- 顶部工具栏 -->
      <div class="tools" :style="toolRightStyle">
        <span>
          <!-- 放大按钮 -->
          <i class="el-icon-plus pointer mr-8" @click="handleZoomIn"></i>
          {{ graphSize }}%
          <!-- 缩小按钮 -->
          <i class="el-icon-minus pointer ml-8" @click="handleZoomOut"></i>
        </span>
        <el-divider direction="vertical"></el-divider>
        <!-- 保存按钮 -->
        <div id="toSave" @click="handleSave" class="save-btn">
          <img
            src="@/assets/VisualImg/save.svg"
            alt="保存"
            class="svg-icon"
          />保存
        </div>
      </div>
      
      <!-- X6画布容器 -->
      <div id="container" @dragover="handleDragOver" @drop="handleDrop"></div>
    </div>
  </div>
</template>

<script>
import { Graph, Path } from "@antv/x6";
import "@antv/x6-vue-shape";
// 引入自定义组件
import CustomNode from "./components/CustomNode.vue";
import MenuItem from "./components/menuItem.vue";
import menuData from "./menu.json";
export default {
  name: "Construction",
  components: {
    CustomNode,
    MenuItem,
  },
  data() {
    return {
      graph: null, 
      draggedNodeLabel: "", 
      menuLoading: false, 
      menuDataList: menuData, 
      graphSize: 100, // 画布缩放比例
      nodeData: {}, // 节点数据集合
      saveLoading: false, // 保存加载状态
    };
  },
  mounted() {
    this.initGraph();
  },
  methods: {
    initGraph() {
      // 注册自定义节点类型
      Graph.registerNode(
        "custom-node",
        {
          inherit: "vue-shape", // 继承vue-shape基础类型
          width: 120, // 默认宽度
          height: 36, // 默认高度
          component: CustomNode, // 使用的Vue组件
          ports: {
            groups: {
              // 顶部连接桩配置
              top: {
                position: "top",
                attrs: {
                  circle: {
                    magnet: true, // 可连接
                    stroke: "#C9CDD4", // 边框颜色
                    r: 3, // 半径
                  },
                },
              },
              bottom: {
                position: "bottom",
                attrs: {
                  circle: {
                    magnet: true,
                    stroke: "#C9CDD4",
                    r: 3,
                  },
                },
              },
            },
          },
        },
        true // 覆盖已存在的同名节点
      );
      
      // 注册自定义边类型
      Graph.registerEdge(
        "dag-edge",
        {
          inherit: "edge",
          attrs: {
            line: {
              stroke: "#C2C8D5", 
              strokeWidth: 2,
              targetMarker: {
                name: "block", // 箭头类型
                width: 12, // 箭头宽度
                height: 8, // 箭头高度
              },
            },
          },
        },
        true 
      );
      
      // 注册自定义连接器
      Graph.registerConnector(
        "algo-connector",
        (s, e) => {
          // 自定义连接路径算法
          const offset = 4;
          const deltaY = Math.abs(e.y - s.y);
          const control = Math.floor((deltaY / 3) * 2);
          const v1 = { x: s.x, y: s.y + offset + control };
          const v2 = { x: e.x, y: e.y - offset - control };
          return Path.normalize(
            `M ${s.x} ${s.y}
             L ${s.x} ${s.y + offset}
             C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
             L ${e.x} ${e.y}
            `
          );
        },
        true 
      );
      // 创建X6图实例
      const graph = new Graph({
        container: document.getElementById("container"), // 容器元素
        panning: true, // 启用平移
        autoResize: true, // 自动调整大小
        mousewheel: true, // 启用鼠标滚轮缩放
        background: {
          color: "#F5F7FA", 
        },
        grid: {
          size: 10, 
          visible: true, 
        },
        highlighting: {
          // 高亮配置
          magnetAdsorbed: {
            name: "stroke",
            args: {
              attrs: {
                fill: "#fff",
                stroke: "#2155f4",
                strokeWidth: 4,
              },
            },
          },
        },
        connecting: {
          // 连接配置
          snap: true, // 自动吸附
          allowBlank: false, // 不允许连接到空白处
          allowLoop: false, // 不允许自环连接
          highlight: true, // 高亮显示可连接点
          connector: "algo-connector", // 使用的连接器
          connectionPoint: "anchor", // 连接点类型
          anchor: "center", // 锚点位置
          validateMagnet() {
            // 验证连接点是否有效
            return true;
          },
          createEdge() {
            // 创建边时的回调
            return graph.createEdge({
              shape: "dag-edge", // 边类型
              attrs: {
                line: {
                  strokeDasharray: "5 5", // 虚线样式
                  targetMarker: {
                    name: "block",
                    width: 12,
                    height: 8,
                  },
                },
              },
              zIndex: -1, // 层级
            });
          },
        },
        selecting: {
          // 选择配置
          enabled: true, // 启用选择
          multiple: false, // 是否允许多选
          rubberEdge: true, // 橡皮筋选择边
          rubberNode: true, // 橡皮筋选择节点
          modifiers: "shift", // 修改键
          rubberband: true, // 启用框选
        },
      });
      
      this.graph = graph; 
      // 节点鼠标进入事件 - 显示删除工具
      graph.on("node:mouseenter", ({ e, node }) => {
        node.addTools([
          {
            name: "button-remove", // 删除按钮工具
            args: {
              x: 100, // x位置
              y: 0, // y位置
              offset: { x: 10, y: 10 }, // 偏移量
            },
          },
        ]);
      });
    
      // 节点鼠标离开事件 - 隐藏工具
      graph.on("node:mouseleave", ({ e, node }) => {
        node.removeTools();
      });

      // 画布缩放事件
      graph.on("scale", ({ sx, sy }) => {
        this.graphSize = Math.round(sx * 100); // 更新缩放比例
      });

      // 边鼠标进入事件 - 显示删除工具
      graph.on("edge:mouseenter", ({ edge }) => {
        edge.addTools([{ name: "button-remove", args: { distance: 20 } }]);
      });
      
      // 边鼠标离开事件 - 隐藏工具
      graph.on("edge:mouseleave", ({ edge }) => {
        edge.removeTools();
      });
    },
    handleZoomIn() {
      this.graph.zoom(0.1); 
    },
    handleZoomOut() {
      this.graph.zoom(-0.1);
    },
    
    /**
     * 处理拖拽开始事件
     * @param {Event} event 拖拽事件对象
     */
    handleDragStart(event) {
      // 从拖拽数据中获取菜单数据
      const menuData = JSON.parse(
        event.dataTransfer.getData("application/json")
      );
      this.draggedNodeLabel = menuData; // 保存拖拽的节点数据
    },
    
    /**
     * 处理拖拽经过事件
     * @param {Event} event 拖拽事件对象
     */
    handleDragOver(event) {
      event.preventDefault(); // 阻止默认行为以允许放置
    },
    
    /**
     * 处理放置事件
     * @param {Event} event 拖拽事件对象
     */
    handleDrop(event) {
      this.isChange = true; // 标记有变更
      event.preventDefault(); // 阻止默认行为
      
      // 获取鼠标位置并转换为画布坐标
      const { clientX, clientY } = event;
      const { x, y } = this.graph.clientToLocal(clientX, clientY);
      
      // 创建新节点配置
      const newNode = {
        id: this.generateRandomCode(), // 生成唯一ID
        shape: "custom-node", // 节点类型
        x: x, 
        y: y, 
        width: 120, 
        height: 36, 
        data: {
          ...this.draggedNodeLabel, 
        },
        ports: {
          items: [
            {
              id: this.generateAlphaCode(), // 生成连接桩ID
              group: "top", 
            },
            {
              id: this.generateAlphaCode(),
              group: "bottom", 
            },
          ],
        },
      };
      // 添加节点到画布
      this.graph.addNode(newNode);
    },
    
    /**
     * 生成随机6位字符ID
     * @returns {string} 随机ID
     */
    generateRandomCode() {
      const chars =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
      let result = "";
      for (let i = 0; i < 6; i++) {
        const randomIndex = Math.floor(Math.random() * chars.length);
        result += chars[randomIndex];
      }
      return result;
    },
    
    /**
     * 生成随机6位字母ID
     * @returns {string} 随机字母ID
     */
    generateAlphaCode() {
      return Array.from(
        { length: 6 },
        () => "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)]
      ).join("");
    },
    
    /**
     * 保存流程图
     */
    handleSave() {
      this.saveLoading = true;
      const data = this.graph.toJSON(); // 获取画布JSON数据
      // 这里可以添加数据处理和调用保存接口的逻辑
      // ...
    },
  },
};
</script>

小结

这个 Demo 主要展示了如何使用 Vue 和 AntV/X6 快速构建一个流程图配置工具,后续可以进一步扩展的功能包括:节点属性配置面板、流程图状态动态变化、撤销重做功能等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值