概要
这是一个基于 Vue 和 AntV/X6 的流程图配置 Demo,支持拖拽添加自定义节点、悬浮删除节点/边、画布缩放和数据保存,适合作为可视化流程编排工具的入门实现。
一、主要功能
-
界面布局:采用左右布局设计,左侧为组件菜单,右侧为画布区域;支持通过 +/- 按钮控制画布缩放比例。
-
节点管理:使用自定义 Vue 组件作为节点样式,实现高度可定制化;通过拖拽左侧菜单项到画布添加流程节点;支持鼠标悬浮显示删除按钮,可删除单个节点。
-
边管理:支持在节点间创建连接线;鼠标悬浮在边上显示删除按钮,可删除连接关系。
-
数据持久化:实现保存功能,可将当前画布中的节点和边信息保存;数据结构可后续加载还原流程图。
-
交互体验:拖拽添加操作直观简便。
二、主页展示
(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 快速构建一个流程图配置工具,后续可以进一步扩展的功能包括:节点属性配置面板、流程图状态动态变化、撤销重做功能等。