vue项目中使用antvX6(可拖拽,vue3)
参考 先知demons 这位大佬的这篇文章:https://blog.csdn.net/wzy_PROTEIN/article/details/136305034?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-136305034-blog-136032209.235v43pc_blog_bottom_relevance_base2&spm=1001.2101.3001.4242.1&utm_relevant_index=3
以下是我的流程+思路+页面展示(页面较粗糙)
- 左侧:折叠菜单+选项卡
- 右侧:画布
<template><div class="dashboard-container"><!-- 顶部操作按钮 --><div class="butClass"><el-button type="primary" @click="save">保存</el-button></div><!-- 主内容区 --><div class="antvBox"><!-- 左侧节点菜单列表 --><el-tabs type="border-card" class="tabsBoxs"><el-tab-pane label="产品"><el-collapse v-model="activeNames" accordion><!-- 产品、场景、菜单分组 --><el-collapse-item title="产品" name="product"><div class="productClass"><!-- 动态渲染节点项,支持拖拽 --><divdraggable="true"@dragend="handleDragEnd($event, item, activeNames)"v-for="item in productData.data":key="item.name":class="getShapeClass(item.type)">{{ item.name }}</div></div></el-collapse-item><!-- 场景、菜单分组结构类似 --></el-collapse></el-tab-pane></el-tabs><!-- 右侧画布区域 --><div class="canvas-card"><div id="container" /> <!-- AntV X6 画布容器 --></div></div></div>
</template>
- 我写的是css绘制图形拖拽去画布后生成节点(我的节点内容是列表内容渲染的,而且需要多层级)折叠面板只能展开一级,拖拽后自动关闭当前层级,打开下一级,需要修改的可以在handleDragEnd里修改
<script setup lang="ts">import { ref, onMounted } from 'vue';import { Graph } from '@antv/x6';// --------------------------- 状态管理 ---------------------------// 折叠面板当前展开的分组名称(手风琴模式)const activeNames = ref<string>('product');// 当前选中的节点(用于节点选中状态管理)const curSelectNode = ref(null);// 产品节点数据const productData = {data: [{ name: '开始', type: 'start', id: 1 },{ name: '过程', type: 'process', id: 2 },{ name: '可选过程', type: 'optProcess', id: 3 },{ name: '决策', type: 'decisionMaking', id: 4 },{ name: '数据', type: 'nodeData', id: 6 },{ name: '连接', type: 'connect', id: 5 },],};// 场景节点数据const sceneData = {data: [{ name: '场景A', type: 'process', id: 11 },{ name: '场景B', type: 'start', id: 12 },{ name: '场景C', type: 'optProcess', id: 13 },{ name: '场景D', type: 'decisionMaking', id: 14 },{ name: '场景E', type: 'nodeData', id: 15 },{ name: '场景F', type: 'connect', id: 16 },],};// 菜单节点数据const menuData = {data: [{ name: '菜单A', type: 'process', id: 21 },{ name: '菜单B', type: 'start', id: 22 },{ name: '菜单C', type: 'optProcess', id: 23 },{ name: '菜单D', type: 'decisionMaking', id: 24 },{ name: '菜单E', type: 'nodeData', id: 25 },{ name: '菜单F', type: 'connect', id: 26 },],};// X6 图形实例const graph = ref(null);/*** 保存当前画布状态*/const save = () => {// 输出画布完整JSON结构(包含节点、连线、布局等信息)console.log(graph.value.toJSON(), 'graph');// 输出所有节点实例(可用于数据持久化或调试)console.log(graph.value.getNodes(), 'node');};// --------------------------- 节点创建逻辑 ---------------------------/*** 向画布添加节点的核心方法* @param x - 节点在画布中的X坐标(基于容器左上角)* @param y - 节点在画布中的Y坐标(基于容器左上角)* @param item - 节点数据(包含type/name/id等信息)*/const addHandleNode = (x, y, item) => {// 基础样式(所有节点共用的默认配置)const baseStyle = {label: item.name, // 节点文本内容attrs: {// 图形和文本样式body: {// 节点图形样式fill: '#eff4ff', // 填充色stroke: '#6397ff', // 边框色strokeWidth: 1, // 边框宽度},label: {// 节点文本样式text: item.name, // 文本内容(与label重复,AntV X6要求)fill: 'black', // 文本颜色fontSize: 12, // 字体大小},},width: 80, // 节点默认宽度(可被类型配置覆盖)height: 40, // 节点默认高度(可被类型配置覆盖)// 统一添加连接桩配置ports: {groups: {top: {position: 'top' /* 顶部端口 */,attrs: {circle: {r: 4,magnet: true,stroke: '#5F95FF',strokeWidth: 1,fill: '#fff',},},},bottom: {position: 'bottom' /* 底部端口 */,attrs: {circle: {r: 4,magnet: true,stroke: '#5F95FF',strokeWidth: 1,fill: '#fff',},},},left: {position: 'left' /* 左侧端口 */,attrs: {circle: {r: 4,magnet: true,stroke: '#5F95FF',strokeWidth: 1,fill: '#fff',},},},right: {position: 'right' /* 右侧端口 */,attrs: {circle: {r: 4,magnet: true,stroke: '#5F95FF',strokeWidth: 1,fill: '#fff',},},},},items: [// 端口实例(启用四个方向的端口){ group: 'top', id: 'top' },{ group: 'bottom', id: 'bottom' },{ group: 'left', id: 'left' },{ group: 'right', id: 'right' },],},};// 按节点类型配置差异化样式(形状、尺寸、特殊属性)const shapeConfig = {// 开始start: {shape: 'rect', // 形状:矩形(通过圆角模拟椭圆)label: item.name,width: 80, // 宽度height: 40, // 高度attrs: {body: {fill: '#eff4ff', // 填充颜色stroke: '#6397ff', // 边框颜色strokeWidth: 1, // 边框宽度rx: 20, // 圆角半径(实现胶囊形)ry: 26,},label: {textWrap: {// 文本换行设置ellipsis: true, // 允许省略width: -10, // 文本宽度调整},},},},// 过程process: {shape: 'rect', // 普通矩形节点label: item.name,width: 80,height: 40,attrs: {body: {fill: '#eff4ff',stroke: '#6397ff',strokeWidth: 1,},label: {textWrap: {ellipsis: true,width: -10,},},},},// 数据节点(多边形)nodeData: {shape: 'polygon', // 形状:多边形label: item.name,width: 90,height: 40,attrs: {body: {fill: '#eff4ff',stroke: '#6397ff',strokeWidth: 1,refPoints: '10,0 40,0 30,20 0,20', // 多边形顶点坐标},label: {textWrap: {ellipsis: true,width: -10,},},},},// 决策节点(菱形)decisionMaking: {shape: 'polygon', // 形状:多边形label: item.name,width: 90,height: 60,attrs: {body: {fill: '#eff4ff',stroke: '#6397ff',strokeWidth: 1,},},// 菱形顶点坐标(相对于节点中心)points: [[-40, 0],[0, -30],[40, 0],[0, 30],],},// 连接connect: {shape: 'circle',width: 60,height: 60,attrs: {body: {fill: '#eff4ff',stroke: '#6397ff',strokeWidth: 1,},},},// 可选过程optProcess: {shape: 'rect',label: item.name,width: 80,height: 40,attrs: {body: {fill: '#eff4ff',stroke: '#6397ff',strokeWidth: 1,rx: 6,ry: 6,},label: {textWrap: {ellipsis: true,width: -10,},},},ports: {groups: {/* ... */},items: [/* ... */],visible: true, // 默认可见},},};// 坐标校验(防止节点超出画布边界)x = Math.max(0, Math.min(x, graph.value.options.width - 80));y = Math.max(0, Math.min(y, graph.value.options.height - 40));// 创建节点并添加到画布(合并基础样式和类型特定样式)graph.value.addNode({id: item.id, // 节点唯一IDx: x, // 坐标y: y,...baseStyle, // 基础样式...shapeConfig[item.type], // 合并基础样式和类型特定配置});};// --------------------------- 拖拽事件处理 ---------------------------/*** 处理节点拖拽结束事件(计算坐标并创建节点)* @param e - 拖拽事件对象* @param item - 被拖拽的节点数据* @param name - 当前折叠面板分组名称(用于自动切换分组)*/const handleDragEnd = (e, item, name) => {// 获取画布容器及其位置信息const container = document.getElementById('container');const rect = container.getBoundingClientRect();// 计算节点在画布中的坐标(基于容器左上角)const x = e.clientX - rect.left; // X坐标 = 鼠标X坐标 - 容器左边界距离视口左边界的距离const y = e.clientY - rect.top; // Y坐标同理// 校验节点类型是否合法(防止非法类型节点被创建)const validTypes = ['start', 'process', 'optProcess', 'decisionMaking', 'nodeData', 'connect'];if (!validTypes.includes(item.type)) {return;}// 添加节点到画布addHandleNode(x, y, item);if (name == 'product') {activeNames.value = 'scene';} else if (name == 'scene') {activeNames.value = 'menu';}// addHandleNode(e.pageX - 500, e.pageY - 200, item);};// 添加统一显示控制const showPorts = (show: boolean) => {const ports = graph.value?.container?.querySelectorAll('.x6-port-body');ports?.forEach((port) => {port.style.visibility = show ? 'visible' : 'hidden';});};// --------------------------- 节点交互事件 ---------------------------/*** 初始化节点相关交互事件(鼠标进入/离开、点击等)*/const nodeAddEvent = () => {const container = document.getElementById('container');// 鼠标进入节点时显示端口graph.value.on('node:mouseenter', ({ node }) => {showPorts(true); // 显示连接桩// 添加删除按钮工具(位于节点右上角)node.addTools({name: 'button-remove',args: { x: '100%', y: 0 },});});graph.value.on('node:added', ({ node }) => {node.attr('label/text', node.label || node.id);});// 鼠标离开节点时隐藏端口graph.value.on('node:mouseleave', ({ node }) => {showPorts(false);node.removeTools();});// 节点点击事件graph.value.on('node:click', ({ node }) => {if (curSelectNode.value) {// 已有选中节点时curSelectNode.value.removeTools(); // 移除前一个节点的工具if (curSelectNode.value !== node) {// 点击新节点时// 添加选中边框(半透明蓝色背景)和删除按钮node.addTools([{name: 'boundary',args: {attrs: {fill: '#16B8AA',stroke: '#2F80EB',strokeWidth: 1,fillOpacity: 0.1,},},},{name: 'button-remove',args: {x: '100%',y: 0,offset: { x: 0, y: 0 },},},]);curSelectNode.value = node;} else {curSelectNode.value = null;}} else {curSelectNode.value = node;node.addTools([{name: 'boundary',args: {attrs: {fill: '#16B8AA',stroke: '#2F80EB',strokeWidth: 1,fillOpacity: 0.1,},},},{name: 'button-remove',args: {x: '100%',y: 0,offset: { x: 0, y: 0 },},},]);}});// 连线鼠标移入事件graph.value.on('cell:mouseenter', ({ cell }) => {if (cell.shape === 'edge') {// 仅处理连线// 添加删除按钮工具cell.addTools([{name: 'button-remove',args: {x: '100%',y: 0,offset: { x: 0, y: 0 },},},]);cell.setAttrs({line: {stroke: '#409EFF', // 连线颜色变为蓝色},});cell.zIndex = 99; // 提升层级防止被遮挡}});// 连线鼠标移出事件graph.value.on('cell:mouseleave', ({ cell }) => {if (cell.shape === 'edge') {cell.removeTools();cell.setAttrs({line: {stroke: 'black',},});cell.zIndex = 1;}});graph.value.on('edge:added', ({ edge }) => {edge.attr({line: {stroke: '#1890ff',strokeWidth: 1.5,targetMarker: {name: 'block',size: 6,},},});});graph.value.on('edge:added', ({ edge }) => {edge.attr('line/stroke', '#ff4d4f');setTimeout(() => {edge.attr('line/stroke', '#1890ff');}, 1000);});};// --------------------------- 画布初始化 ---------------------------/*** 初始化AntV X6画布实例(核心配置)*/const initGraph = () => {const container = document.getElementById('container');try {// 创建X6图形实例graph.value = new Graph({container, // 绑定画布容器width: container.offsetWidth, // 画布宽度(自适应容器)height: container.offsetHeight, // 画布高度(自适应容器)background: false,snapline: true, // 启用对齐线(节点自动吸附到网格)connecting: {snap: true, // 连线端点自动吸附到端口allowBlank: false,allowMulti: true, // 允许多重连接allowLoop: true, // 允许自环highlight: true, // 高亮显示highlighting: {magnetAdsorbed: {name: 'stroke',args: {attrs: {fill: '#5F95FF',stroke: '#5F95FF',},},},},router: {name: 'orth', // 正交路由},connector: {name: 'rounded', // 圆角连接器args: {radius: 8, // 圆角半径},},validateMagnet({ magnet }) {// 允许从所有连接桩创建连接return true;},validateConnection({ sourceMagnet, targetMagnet }) {// 允许任意两个连接桩之间的连接return true;},},// 平移配置panning: {enabled: false,},// 鼠标滚轮缩放配置mousewheel: {enabled: true,zoomAtMousePosition: true,modifiers: 'ctrl',minScale: 0.5,maxScale: 3,},// 网格配置grid: {type: 'dot', // 点状网格size: 20, // 网格大小visible: true,args: {color: '#a0a0a0',thickness: 2,},},});// 添加节点事件nodeAddEvent();} catch (error) {console.error('Graph initialization failed:', error);}};// 组件挂载时初始化流程图onMounted(() => {initGraph();});const getShapeClass = (type) => {switch (type) {case 'process':return 'shape-rectangle';case 'start':return 'shape-ellipse';case 'optProcess':return 'shape-parallelogram';case 'decisionMaking':return 'shape-decisionMaking';case 'nodeData':return 'shape-nodeData ';case 'connect':return 'shape-circle';default:return 'shape-rectangle';}};
</script>
- css
<style lang="scss" scoped>.dashboard-container {/* 占满视口高度 */height: 100vh;}.antvBox {display: flex; /* 弹性布局实现左右分栏 */width: 100%;height: calc(100vh - 60px);/* 扣除顶部按钮高度 */}.tabsBoxs {width: 25%; /* 左侧菜单占比25% */padding: 10px; /*内边距 */border-right: 1px solid #eee; /*右侧边框*/}.canvas-card {width: 75%; /*右侧画布占比75% */padding: 10px;}#container {width: 100%;height: 100%;/* 虚线边框标识画布区域 */border: 1px dashed #a6a6a6;/* 浅灰色背景 */background: #f8f9fa;}/* --------------------------- 左侧节点样式 --------------------------- */.shape-rectangle {/* 矩形节点:直角矩形 */width: 80px;height: 40px;border-radius: 0;}.shape-ellipse {/* 椭圆节点:通过左右padding和大圆角模拟 - 增加左右内边距 -大圆角实现椭圆效果*/padding: 0 26px;border-radius: 30px;}.shape-decisionMaking {/* 菱形节点:通过clip-path裁剪 */clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);}.shape-circle {/* 圆形节点:正方形+50%圆角 */width: 40px;height: 40px;border-radius: 50%;}.shape-nodeData {/* 倾斜节点:通过transform skew实现 --- 向左倾斜20度 */transform: skew(-20deg);/* 注意:文本需要反向倾斜以保持正立,此处简化处理 */}/* --------------------------- AntV X6 样式覆盖 --------------------------- */:deep(.x6-port-body) {/* 强制显示连接桩(解决默认隐藏问题) */visibility: visible !important;/* 过渡动画 */transition: all 0.3s;&:hover {/* 悬停时端口边框变亮绿色 */stroke: #31d0c6;/* 边框加粗 */stroke-width: 2px;}}
</style>
结束!!!!!