vue3 + antv/x6实现流程图的全过程
作者:FenceRain
随着互联网的发展,越来越多的应用需要实现流程图的制作,如工作流程图、电路图等,文中通过代码以及图文将实现的过程介绍的非常详细,对大家学习或者工作具有一定的参考借鉴价值,需要的朋友可以参考下
新建流程图
// AddDag.vue <template> <div class="content-main"> <div class="tool-container"> <div @click="undo" class="command" title="后退"> <Icon icon="ant-design:undo-outlined" /> </div> <div @click="redo" class="command" title="前进"> <Icon icon="ant-design:redo-outlined" /> </div> <el-divider direction="vertical" /> <div @click="copy" class="command" title="复制"> <Icon icon="ant-design:copy-filled" /> </div> <div @click="paste" class="command" title="粘贴"> <Icon icon="fa-solid:paste" /> </div> <div @click="del" class="command" title="删除"> <Icon icon="ant-design:delete-filled" /> </div> <el-divider direction="vertical" /> <div @click="save" class="command" title="保存"> <Icon icon="ant-design:save-filled" /> </div> <el-divider direction="vertical" /> <div @click="exportPng" class="command" title="导出PNG"> <Icon icon="ant-design:file-image-filled" /> </div> </div> <div class="content-container" id=""> <div class="content"> <div class="stencil" ref="stencilContainer"></div> <div class="graph-content" id="graphContainer" ref="graphContainer"> </div> <div class="editor-sidebar"> <div class="edit-panel"> <el-card shadow="never"> <template #header> <div class="card-header"> <span>{{ cellFrom.title }}</span> </div> </template> <el-form :model="nodeFrom" label-width="50px" v-if="nodeFrom.show"> <el-form-item label="label"> <el-input v-model="nodeFrom.label" @blur="changeLabel" /> </el-form-item> <el-form-item label="desc"> <el-input type="textarea" v-model="nodeFrom.desc" @blur="changeDesc" /> </el-form-item> </el-form> <el-form :model="cellFrom" label-width="50px" v-if="cellFrom.show"> <el-form-item label="label"> <el-input v-model="cellFrom.label" @blur="changeEdgeLabel" /> </el-form-item> <!-- <el-form-item label="连线方式"> <el-select v-model="cellFrom.edgeType" class="m-2" placeholder="Select" @change="changeEdgeType"> <el-option v-for="item in EDGE_TYPE_LIST" :key="item.type" :label="item.name" :value="item.type" /> </el-select> </el-form-item> --> </el-form> </el-card> </div> <div> <el-card shadow="never"> <template #header> <div class="card-header"> <span>Minimap</span> </div> </template> <div class="minimap" ref="miniMapContainer"></div> </el-card> </div> </div> </div> </div> <div v-if="showMenu" class="node-menu" ref="nodeMenu"> <div class="menu-item" v-for="(item, index) in PROCESSING_TYPE_LIST" :key="index" @click="addNodeTool(item)" > <el-image :src="item.image" style="width: 16px; height: 16px" fit="fill" /> <span>{{ item.name }}</span> </div> </div> </div> </template> <script setup lang="ts"> import { Graph, Path, Edge, StringExt, Node, Cell, Model, DataUri } from '@antv/x6' import { Transform } from '@antv/x6-plugin-transform' import { Selection } from '@antv/x6-plugin-selection' import { Snapline } from '@antv/x6-plugin-snapline' import { Keyboard } from '@antv/x6-plugin-keyboard' import { Clipboard } from '@antv/x6-plugin-clipboard' import { History } from '@antv/x6-plugin-history' import { MiniMap } from '@antv/x6-plugin-minimap' //import { Scroller } from '@antv/x6-plugin-scroller' import { Stencil } from '@antv/x6-plugin-stencil' import { Export } from '@antv/x6-plugin-export' import { ref, onMounted, reactive, toRefs, nextTick, onUnmounted } from 'vue' import '@/styles/animation.less' import { ElMessage, ElCard, ElForm, ElFormItem, ElInput, ElImage, ElDivider } from 'element-plus' const stencilContainer = ref() const graphContainer = ref() const miniMapContainer = ref() let graph: any = null const state = reactive({ cellFrom: { title: 'Canvas', label: '', desc: '', show: false, id: '', edgeType: 'topBottom' }, nodeFrom: { title: 'Canvas', label: '', desc: '', show: false, id: '' }, showMenu: false, data: { nodes: [ { id: 'ac51fb2f-2753-4852-8239-53672a29bb14', position: { x: -340, y: -160 }, data: { name: '诗名', type: 'OUTPUT', desc: '春望' } }, { id: '81004c2f-0413-4cc6-8622-127004b3befa', position: { x: -340, y: -10 }, data: { name: '第一句', type: 'SYNC', desc: '国破山河在' } }, { id: '7505da25-1308-4d7a-98fd-e6d5c917d35d', position: { x: -140, y: 180 }, data: { name: '结束', type: 'INPUT', desc: '城春草木胜' } } ], edges: [ { id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c', shape: 'processing-curve', source: { cell: 'ac51fb2f-2753-4852-8239-53672a29bb14', port: '-out' }, target: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '-in' }, zIndex: -1, data: { source: 'ac51fb2f-2753-4852-8239-53672a29bb14', target: '81004c2f-0413-4cc6-8622-127004b3befa' } }, { id: '8cbce713-54be-4c07-8efa-59c505f74ad7', labels: ['下半句'], shape: 'processing-curve', source: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '-out' }, target: { cell: '7505da25-1308-4d7a-98fd-e6d5c917d35d', port: '-in' }, data: { source: '81004c2f-0413-4cc6-8622-127004b3befa', target: '7505da25-1308-4d7a-98fd-e6d5c917d35d' } } ] }, // 节点状态列表 nodeStatusList: [ { id: 'ac51fb2f-2753-4852-8239-53672a29bb14', status: 'success' }, { id: '81004c2f-0413-4cc6-8622-127004b3befa', status: 'success' } ], // 边状态列表 edgeStatusList: [ { id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c', status: 'success' }, { id: '8cbce713-54be-4c07-8efa-59c505f74ad7', status: 'executing' } ], // 加工类型列表 PROCESSING_TYPE_LIST: [ { type: 'SYNC', name: '数据同步', image: new URL('@/assets/imgs/persimmon.png', import.meta.url).href }, { type: 'INPUT', name: '结束', image: new URL('@/assets/imgs/lime.png', import.meta.url).href } ], //边类型 EDGE_TYPE_LIST: [ { type: 'topBottom', name: '上下' }, { type: 'leftRight', name: '左右' } ] }) const { cellFrom, nodeFrom, showMenu, PROCESSING_TYPE_LIST } = toRefs(state) let nodeMenu = ref() // 节点类型 enum NodeType { INPUT = 'INPUT', // 数据输入 FILTER = 'FILTER', // 数据过滤 JOIN = 'JOIN', // 数据连接 UNION = 'UNION', // 数据合并 AGG = 'AGG', // 数据聚合 OUTPUT = 'OUTPUT', // 数据输出 SYNC = 'SYNC' //数据同步 } // 元素校验状态 // enum CellStatus { // DEFAULT = 'default', // SUCCESS = 'success', // ERROR = 'error' // } // 节点位置信息 interface Position { x: number y: number } function init() { graph = new Graph({ container: graphContainer.value, grid: true, panning: { enabled: true, eventTypes: ['leftMouseDown', 'mouseWheel'] }, mousewheel: { enabled: true, modifiers: 'ctrl', factor: 1.1, maxScale: 1.5, minScale: 0.5 }, highlighting: { magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4 } } } }, connecting: { snap: true, allowBlank: false, allowLoop: false, highlight: true, // sourceAnchor: { // name: 'bottom', // args: { // dx: 0, // }, // }, // targetAnchor: { // name: 'top', // args: { // dx: 0, // }, // }, createEdge() { return graph.createEdge({ shape: 'processing-curve', attrs: { line: { strokeDasharray: '5 5' } }, zIndex: -1 }) }, // 连接桩校验 validateConnection({ sourceMagnet, targetMagnet }) { // 只能从输出链接桩创建连接 if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { return false } // 只能连接到输入链接桩 if (!targetMagnet || targetMagnet.getAttribute('port-group') === 'out') { return false } return true } } }) graph.centerContent() // #region 使用插件 graph .use( new Transform({ resizing: true, rotating: true }) ) .use( new Selection({ rubberband: true, showNodeSelectionBox: true }) ) .use( new MiniMap({ container: miniMapContainer.value, width: 200, height: 260, padding: 10 }) ) .use(new Snapline()) .use(new Keyboard()) .use(new Clipboard()) .use(new History()) .use(new Export()) //.use(new Scroller({ // enabled: true, // pageVisible: true, // pageBreak: false, // pannable: true, // })) // #endregion // #region 初始化图形 const ports = { groups: { in: { position: 'top', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } }, out: { position: 'bottom', attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } }, left: { position: 'left', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } }, right: { position: 'right', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } } } // items: [ // { // id: state.currentCode + '-in', // group: 'top', // }, // { // id: state.currentCode + '-out', // group: 'out', // } // ], } Graph.registerNode( 'custom-node', { inherit: 'rect', width: 140, height: 76, attrs: { body: { strokeWidth: 1 }, image: { width: 16, height: 16, x: 12, y: 6 }, text: { refX: 40, refY: 15, fontSize: 15, 'text-anchor': 'start' }, label: { text: 'Please nominate this node', refX: 10, refY: 30, fontSize: 12, fill: 'rgba(0,0,0,0.6)', 'text-anchor': 'start', textWrap: { width: -10, // 宽度减少 10px height: '70%', // 高度为参照元素高度的一半 ellipsis: true, // 文本超出显示范围时,自动添加省略号 breakWord: true // 是否截断单词 } } }, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'image', selector: 'image' }, { tagName: 'text', selector: 'text' }, { tagName: 'text', selector: 'label' } ], data: {}, relation: {}, ports: { ...ports } }, true ) const stencil = new Stencil({ //新建节点库 title: '数据集成', target: graph, search: false, // 搜索 collapsable: true, stencilGraphWidth: 300, //容器宽度 stencilGraphHeight: 600, //容器长度 groups: [ //分组 { name: 'processLibrary', title: 'dataSource' } ], layoutOptions: { dx: 30, dy: 20, columns: 1, //列数(行内节点数) columnWidth: 130, //列宽 rowHeight: 100 //行高 } }) stencilContainer.value.appendChild(stencil.container) // 控制连接桩显示/隐藏 // eslint-disable-next-line no-undef const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => { for (let i = 0, len = ports.length; i < len; i += 1) { ports[i].style.visibility = show ? 'visible' : 'hidden' } } graph.on('node:mouseenter', () => { const container = graphContainer.value const ports = container.querySelectorAll('.x6-port-body') showPorts(ports, true) }) graph.on('node:mouseleave', () => { const container = graphContainer.value const ports = container.querySelectorAll( '.x6-port-body' // eslint-disable-next-line no-undef ) as NodeListOf<SVGElement> showPorts(ports, false) }) // #region 快捷键与事件 graph.bindKey(['meta+c', 'ctrl+c'], () => { // const cells = graph.getSelectedCells() // if (cells.length) { // graph.copy(cells) // } // return false copy() }) graph.bindKey(['meta+x', 'ctrl+x'], () => { const cells = graph.getSelectedCells() if (cells.length) { graph.cut(cells) } return false }) graph.bindKey(['meta+v', 'ctrl+v'], () => { // if (!graph.isClipboardEmpty()) { // const cells = graph.paste({ offset: 32 }) // graph.cleanSelection() // graph.select(cells) // } // return false paste() }) // undo redo graph.bindKey(['meta+z', 'ctrl+z'], () => { // if (graph.canUndo()) { // graph.undo() // } // return false undo() }) graph.bindKey(['meta+y', 'ctrl+y'], () => { // if (graph.canRedo()) { // graph.redo() // } // return false redo() }) // select all graph.bindKey(['meta+a', 'ctrl+a'], () => { const nodes = graph.getNodes() if (nodes) { graph.select(nodes) } }) // delete graph.bindKey('backspace', () => { // const cells = graph.getSelectedCells() // if (cells.length) { // graph.removeCells(cells) // } del() }) // zoom graph.bindKey(['ctrl+1', 'meta+1'], () => { const zoom = graph.zoom() if (zoom < 1.5) { graph.zoom(0.1) } }) graph.bindKey(['ctrl+2', 'meta+2'], () => { const zoom = graph.zoom() if (zoom > 0.5) { graph.zoom(-0.1) } }) // 节点移入画布事件 graph.on('node:added', ({ node }: any) => { // console.log(node,cell); addNodeInfo(node) }) // 节点单击事件 graph.on('node:click', ({ node }: any) => { // console.log(node,cell) addNodeInfo(node) }) //节点被选中时显示添加节点按钮 graph.on('node:selected', (args: { cell: Cell; node: Node; options: Model.SetOptions }) => { if (NodeType.INPUT != args.node.data.type) { args.node.removeTools() args.node.addTools({ name: 'button', args: { x: 0, y: 0, offset: { x: 160, y: 40 }, markup: [ //自定义的删除按钮样式 { tagName: 'circle', selector: 'button', attrs: { r: 8, stroke: 'rgba(0,0,0,.25)', strokeWidth: 1, fill: 'rgba(255, 255, 255, 1)', cursor: 'pointer' } }, { tagName: 'text', textContent: '+', selector: 'icon', attrs: { fill: 'rgba(0,0,0,.25)', fontSize: 15, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', stroke: 'rgba(0,0,0,.25)' } } ], onClick({ e, view }: any) { // console.log(e,cell); showNodeTool(e, view) } } }) } // code here }) //节点被取消选中时触发。 graph.on('node:unselected', (args: { cell: Cell; node: Node; options: Model.SetOptions }) => { args.node.removeTools() }) // 添加边事件 graph.on('edge:added', ({ edge }: any) => { // console.log(edge); addEdgeInfo(edge) edge.data = { source: edge.source.cell, target: edge.target.cell } }) // 线单击事件 graph.on('edge:click', ({ edge }: any) => { // console.log(node,cell) addEdgeInfo(edge) }) //边选中事件 graph.on('edge:selected', (args: { cell: Cell; edge: Edge; options: Model.SetOptions }) => { args.edge.attr('line/strokeWidth', 3) }) //边被取消选中时触发。 graph.on('edge:unselected', (args: { cell: Cell; edge: Edge; options: Model.SetOptions }) => { args.edge.attr('line/strokeWidth', 1) }) const nodeShapes = [ { label: '开始', nodeType: 'OUTPUT' as NodeType }, { label: '数据同步', nodeType: 'SYNC' as NodeType }, { label: '结束', nodeType: 'INPUT' as NodeType } ] const nodes = nodeShapes.map((item) => { const id = StringExt.uuid() const node = { id: id, shape: 'custom-node', // label: item.label, ports: getPortsByType(item.nodeType, id), data: { name: `${item.label}`, type: item.nodeType }, attrs: getNodeAttrs(item.nodeType) } const newNode = graph.addNode(node) return newNode }) //#endregion stencil.load(nodes, 'processLibrary') } // 根据节点的类型获取ports const getPortsByType = (type: NodeType, nodeId: string) => { let ports = [] as any switch (type) { case NodeType.INPUT: ports = [ { id: `${nodeId}-in`, group: 'in' }, { id: `${nodeId}-left`, group: 'left' }, { id: `${nodeId}-right`, group: 'right' } ] break case NodeType.OUTPUT: ports = [ { id: `${nodeId}-out`, group: 'out' }, { id: `${nodeId}-left`, group: 'left' }, { id: `${nodeId}-right`, group: 'right' } ] break default: ports = [ { id: `${nodeId}-in`, group: 'in' }, { id: `${nodeId}-out`, group: 'out' }, { id: `${nodeId}-left`, group: 'left' }, { id: `${nodeId}-right`, group: 'right' } ] break } return ports } // 注册连线 --上下 Graph.registerConnector( 'curveConnectorTB', (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 ) // 注册连线--左右 Graph.registerConnector( 'curveConnectorLR', (sourcePoint, targetPoint) => { const hgap = Math.abs(targetPoint.x - sourcePoint.x) const path = new Path() path.appendSegment(Path.createSegment('M', sourcePoint.x - 4, sourcePoint.y)) path.appendSegment(Path.createSegment('L', sourcePoint.x + 12, sourcePoint.y)) // 水平三阶贝塞尔曲线 path.appendSegment( Path.createSegment( 'C', sourcePoint.x < targetPoint.x ? sourcePoint.x + hgap / 2 : sourcePoint.x - hgap / 2, sourcePoint.y, sourcePoint.x < targetPoint.x ? targetPoint.x - hgap / 2 : targetPoint.x + hgap / 2, targetPoint.y, targetPoint.x - 6, targetPoint.y ) ) path.appendSegment(Path.createSegment('L', targetPoint.x + 2, targetPoint.y)) return path.serialize() }, true ) Graph.registerEdge( 'processing-curve', { inherit: 'edge', markup: [ { tagName: 'path', selector: 'wrap', attrs: { fill: 'none', cursor: 'pointer', stroke: 'transparent', strokeLinecap: 'round' } }, { tagName: 'path', selector: 'line', attrs: { fill: 'none', pointerEvents: 'none' } } ], connector: { name: 'smooth' }, //curveConnectorTB attrs: { wrap: { connection: true, strokeWidth: 10, strokeLinejoin: 'round' }, line: { connection: true, stroke: '#A2B1C3', strokeWidth: 1, targetMarker: { name: 'classic', size: 6 } } } }, true ) // Graph.registerEdge( // 'processing-curve-lr', // { // inherit: 'edge', // markup: [ // { // tagName: 'path', // selector: 'wrap', // attrs: { // fill: 'none', // cursor: 'pointer', // stroke: 'transparent', // strokeLinecap: 'round', // }, // }, // { // tagName: 'path', // selector: 'line', // attrs: { // fill: 'none', // pointerEvents: 'none', // }, // }, // ], // connector: { name: 'curveConnectorLR' }, // attrs: { // wrap: { // connection: true, // strokeWidth: 10, // strokeLinejoin: 'round', // }, // line: { // connection: true, // stroke: '#A2B1C3', // strokeWidth: 1, // targetMarker: { // name: 'classic', // size: 6, // }, // }, // }, // }, // true, // ) //保存 function save() { console.log('save') const graphData = graph.toJSON() console.log(graphData) } //撤销 function undo() { if (graph.canUndo()) { graph.undo() } return false } //取消撤销 function redo() { if (graph.canRedo()) { graph.redo() } return false } //复制 function copy() { const cells = graph.getSelectedCells() if (cells.length) { graph.copy(cells) } return false } //粘贴 function paste() { if (!graph.isClipboardEmpty()) { const cells = graph.paste({ offset: 32 }) graph.cleanSelection() graph.select(cells) } return false } //删除 function del() { const cells = graph.getSelectedCells() if (cells.length) { graph.removeCells(cells) } } //导出PNG function exportPng() { graph.toPNG( (dataUri: string) => { // 下载 DataUri.downloadDataUri(dataUri, 'chart.png') }, { padding: { top: 20, right: 20, bottom: 20, left: 20 } } ) //graph.exportPNG('a.png',{padding:'20px'}); } function addNodeInfo(node: any) { state.nodeFrom.title = 'Node' state.nodeFrom.label = node.label state.nodeFrom.desc = node.attrs.label.text state.nodeFrom.show = true state.nodeFrom.id = node.id state.cellFrom.show = false } function addEdgeInfo(edge: any) { state.nodeFrom.show = false state.cellFrom.title = 'Edge' if (edge.labels[0]) { state.cellFrom.label = edge.labels[0].attrs.label.text } else { state.cellFrom.label = '' } state.cellFrom.edgeType = edge.data ? edge.data.edgeType : '' state.cellFrom.show = true state.cellFrom.id = edge.id } //修改文本 function changeLabel() { const nodes = graph.getNodes() nodes.forEach((node: any) => { if (state.nodeFrom.id == node.id) { node.label = state.nodeFrom.label } }) } //修改描述 function changeDesc() { const nodes = graph.getNodes() nodes.forEach((node: any) => { if (state.nodeFrom.id == node.id) { node.attr('label/text', state.nodeFrom.desc) } }) } //修改边文本 function changeEdgeLabel() { const edges = graph.getEdges() edges.forEach((edge: any) => { if (state.cellFrom.id == edge.id) { edge.setLabels(state.cellFrom.label) console.log(edge) } }) } //修改边的类型 // function changeEdgeType() { // const edges = graph.getEdges() // edges.forEach((edge: any) => { // if (state.cellFrom.id == edge.id) { // // console.log(state.cellFrom.edgeType); // if (state.cellFrom.edgeType == 'topBottom') { // edge.setConnector('curveConnectorTB') // } else { // edge.setConnector('curveConnectorLR') // // console.log(edge); // } // edge.data.edgeType = state.cellFrom.edgeType // } // }) // } const getNodeAttrs = (nodeType: string) => { let attr = {} as any switch (nodeType) { case NodeType.INPUT: attr = { image: { 'xlink:href': new URL('@/assets/imgs/lime.png', import.meta.url).href }, //左侧拖拽样式 body: { fill: '#b9dec9', stroke: '#229453' }, text: { text: '结束', fill: '#229453' } } break case NodeType.SYNC: attr = { image: { 'xlink:href': new URL('@/assets/imgs/persimmon.png', import.meta.url).href }, //左侧拖拽样式 body: { fill: '#edc3ae', stroke: '#f9723d' }, text: { text: '数据同步', fill: '#f9723d' } } break case NodeType.OUTPUT: attr = { image: { 'xlink:href': new URL('@/assets/imgs/rice.png', import.meta.url).href }, //左侧拖拽样式 body: { fill: '#EFF4FF', stroke: '#5F95FF' }, text: { text: '开始', fill: '#5F95FF' } } break } return attr } //加载初始节点 function getData() { let cells = [] as any const location = state.data location.nodes.map((node) => { let attr = getNodeAttrs(node.data.type) if (node.data.desc) { attr.label = { text: node.data.desc } } if (node.data.name) { let temp = attr.text if (temp) { temp.text = node.data.name } } cells.push( graph.addNode({ id: node.id, x: node.position.x, y: node.position.y, shape: 'custom-node', attrs: attr, ports: getPortsByType(node.data.type as NodeType, node.id), data: node.data }) ) }) location.edges.map((edge) => { cells.push( graph.addEdge({ id: edge.id, source: edge.source, target: edge.target, zIndex: edge.zIndex, shape: 'processing-curve', // connector: { name: 'curveConnector' }, labels: edge.labels, attrs: { line: { strokeDasharray: '5 5' } }, data: edge.data }) ) }) graph.resetCells(cells) } // 开启边的运行动画 const excuteAnimate = (edge: any) => { edge.attr({ line: { stroke: '#3471F9' } }) edge.attr('line/strokeDasharray', 5) edge.attr('line/style/animation', 'running-line 30s infinite linear') } // 显示边状态 const showEdgeStatus = () => { state.edgeStatusList.forEach((item) => { const edge = graph.getCellById(item.id) if (item.status == 'success') { edge.attr('line/strokeDasharray', 0) edge.attr('line/stroke', '#52c41a') } else if ('error' == item.status) { edge.attr('line/stroke', '#ff4d4f') } else if ('executing' == item.status) { excuteAnimate(edge) } }) } // 显示添加按钮菜单 function showNodeTool(e: any, _view: any) { // console.log(view); state.showMenu = true nextTick(() => { nodeMenu.value.style.top = e.offsetY + 60 + 'px' nodeMenu.value.style.left = e.offsetX + 210 + 'px' }) } // 点击添加节点按钮 function addNodeTool(item: any) { // console.log(item); createDownstream(item.type) state.showMenu = false } /** * 根据起点初始下游节点的位置信息 * @param node 起始节点 * @param graph * @returns */ const getDownstreamNodePosition = (node: Node, graph: Graph, dx = 250, dy = 100) => { // 找出画布中以该起始节点为起点的相关边的终点id集合 const downstreamNodeIdList: string[] = [] graph.getEdges().forEach((edge) => { const originEdge = edge.toJSON()?.data console.log(node) if (originEdge.source === node.id) { downstreamNodeIdList.push(originEdge.target) } }) // 获取起点的位置信息 const position = node.getPosition() let minX = Infinity let maxY = -Infinity graph.getNodes().forEach((graphNode) => { if (downstreamNodeIdList.indexOf(graphNode.id) > -1) { const nodePosition = graphNode.getPosition() // 找到所有节点中最左侧的节点的x坐标 if (nodePosition.x < minX) { minX = nodePosition.x } // 找到所有节点中最x下方的节点的y坐标 if (nodePosition.y > maxY) { maxY = nodePosition.y } } }) return { x: minX !== Infinity ? minX : position.x + dx, y: maxY !== -Infinity ? maxY + dy : position.y } } // 创建下游的节点和边 const createDownstream = (type: NodeType) => { // console.log(graph.getSelectedCells()); const cells = graph.getSelectedCells() if (cells.length == 1) { const node = cells[0] //console.log(node,"node"); if (graph) { // 获取下游节点的初始位置信息 const position = getDownstreamNodePosition(node, graph) // 创建下游节点 const newNode = createNode(type, graph, position) const source = node.id const target = newNode.id // 创建该节点出发到下游节点的边 createEdge(source, target, graph) } } else { ElMessage({ message: '请选择一个节点', type: 'warning' }) } } const createNode = (type: NodeType, graph: Graph, position?: Position): Node => { let newNode = {} as Node const typeName = state.PROCESSING_TYPE_LIST?.find((item) => item.type === type)?.name const id = StringExt.uuid() const node = { id, shape: 'custom-node', x: position?.x, y: position?.y, ports: getPortsByType(type, id), data: { name: `${typeName}`, type }, attrs: getNodeAttrs(type) } newNode = graph.addNode(node) return newNode } const createEdge = (source: string, target: string, graph: Graph) => { const edge = { id: StringExt.uuid(), shape: 'processing-curve', source: { cell: source // port: `${source}-out`, }, target: { cell: target // port: `${target}-in`, }, zIndex: -1, data: { source, target }, attrs: { line: { strokeDasharray: '5 5' } } } // console.log(edge); if (graph) { graph.addEdge(edge) } } onMounted(() => { init() // graph.fromJSON(state.data); getData() showEdgeStatus() }) onUnmounted(() => { graph.dispose() }) </script> <style lang="less" scoped> .content-main { display: flex; width: 100%; flex-direction: column; height: calc(100vh - 85px - 40px); background-color: #ffffff; position: relative; .tool-container { padding: 8px; display: flex; align-items: center; color: rgba(0, 0, 0, 0.45); .command { display: inline-block; width: 27px; height: 27px; margin: 0 6px; padding-top: 6px; text-align: center; cursor: pointer; } } } .content-container { position: relative; width: 100%; height: 100%; .content { width: 100%; height: 100%; position: relative; min-width: 400px; min-height: 600px; display: flex; border: 1px solid #dfe3e8; flex-direction: row; // flex-wrap: wrap; flex: 1 1; .stencil { width: 250px; height: 100%; border-right: 1px solid #dfe3e8; position: relative; :deep(.x6-widget-stencil) { background-color: #fff; } :deep(.x6-widget-stencil-title) { background-color: #fff; } :deep(.x6-widget-stencil-group-title) { background-color: #fff !important; } } .graph-content { width: calc(100% - 180px); height: 100%; } .editor-sidebar { display: flex; flex-direction: column; border-left: 1px solid #e6f7ff; background: #fafafa; z-index: 9; .el-card { border: none; } .edit-panel { flex: 1 1; background-color: #fff; } :deep(.x6-widget-minimap-viewport) { border: 1px solid #8f8f8f; } :deep(.x6-widget-minimap-viewport-zoom) { border: 1px solid #8f8f8f; } } } } :deep(.x6-widget-transform) { margin: -1px 0 0 -1px; padding: 0px; border: 1px solid #239edd; } :deep(.x6-widget-transform > div) { border: 1px solid #239edd; } :deep(.x6-widget-transform > div:hover) { background-color: #3dafe4; } :deep(.x6-widget-transform-active-handle) { background-color: #3dafe4; } :deep(.x6-widget-transform-resize) { border-radius: 0; } :deep(.x6-widget-selection-inner) { border: 1px solid #239edd; } :deep(.x6-widget-selection-box) { opacity: 0; } .topic-image { visibility: hidden; cursor: pointer; } .x6-node:hover .topic-image { visibility: visible; } .x6-node-selected rect { stroke-width: 2px; } .node-menu { position: absolute; box-shadow: var(--el-box-shadow-light); background: var(--el-bg-color-overlay); border: 1px solid var(--el-border-color-light); padding: 5px 0px; .menu-item { display: flex; align-items: center; white-space: nowrap; list-style: none; line-height: 22px; padding: 5px 16px; margin: 0; font-size: var(--el-font-size-base); color: var(--el-text-color-regular); cursor: pointer; outline: none; box-sizing: border-box; } .menu-item .el-image { margin-right: 5px; } .menu-item:hover { background-color: var(--el-color-primary-light-9); color: var(--el-color-primary); } } </style>
显示流程图
<template> <div class="content-main"> <div class="content-container" id=""> <div class="content"> <div class="graph-content" id="graphContainer" ref="graphContainer"></div> </div> </div> </div> </template> <script setup lang="ts"> import { Graph, Path, Edge } from '@antv/x6' import { ref, onMounted, reactive } from 'vue' import '@/styles/animation.less' const graphContainer = ref() let graph: any = null const state = reactive({ data: { nodes: [ { id: 'ac51fb2f-2753-4852-8239-53672a29bb14', x: -340, y: -160, ports: [ { id: 'ac51fb2f-2753-4852-8239-53672a29bb14_out', group: 'out' } ], data: { name: '数据输入_1', type: 'OUTPUT', checkStatus: 'sucess' }, attrs: { body: { fill: '#EFF4FF', stroke: '#5F95FF' }, image: { 'xlink:href': 'http://localhost:20002/src/assets/imgs/rice.png' }, label: { text: '春望' }, text: { fill: '#5F95FF', text: '开始' } } }, { id: '81004c2f-0413-4cc6-8622-127004b3befa', x: -340, y: -10, ports: [ { id: '81004c2f-0413-4cc6-8622-127004b3befa_in', group: 'in' }, { id: '81004c2f-0413-4cc6-8622-127004b3befa_out', group: 'out' } ], data: { name: '数据输入_1', type: 'SYAN', checkStatus: 'sucess' }, attrs: { body: { fill: '#edc3ae', stroke: '#f9723d' }, image: { 'xlink:href': 'http://localhost:20002/src/assets/imgs/persimmon.png' }, label: { text: '国破山河在' }, text: { fill: '#f9723d', text: '数据同步' } } }, { id: '7505da25-1308-4d7a-98fd-e6d5c917d35d', x: -140, y: 180, ports: [ { id: '7505da25-1308-4d7a-98fd-e6d5c917d35d_in', group: 'in' } ], data: { name: '数据输入_1', type: 'INPUT', checkStatus: 'sucess' }, attrs: { body: { fill: '#b9dec9', stroke: '#229453' }, image: { 'xlink:href': 'http://localhost:20002/src/assets/imgs/lime.png' }, label: { text: '城春草木胜' }, text: { fill: '#229453', text: '结束' } } } ], edges: [ { attrs: { line: { strokeDasharray: '5 5' } }, connector: { name: 'curveConnector' }, id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c', shape: 'data-processing-curve', source: { cell: 'ac51fb2f-2753-4852-8239-53672a29bb14', port: '_out' }, target: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '_in' }, zIndex: -1 }, { attrs: { line: { strokeDasharray: '5 5' } }, connector: { name: 'curveConnector' }, id: '8cbce713-54be-4c07-8efa-59c505f74ad7', labels: ['下半句'], shape: 'data-processing-curve', source: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '_out' }, target: { cell: '7505da25-1308-4d7a-98fd-e6d5c917d35d', port: '_in' } } ] }, // 节点状态列表 nodeStatusList: [ { id: 'ac51fb2f-2753-4852-8239-53672a29bb14', status: 'success' }, { id: '81004c2f-0413-4cc6-8622-127004b3befa', status: 'success' } ], // 边状态列表 edgeStatusList: [ { id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c', status: 'success' }, { id: '8cbce713-54be-4c07-8efa-59c505f74ad7', status: 'executing' } ] }) // const { data } = toRefs(state) // // 节点类型 // enum NodeType { // INPUT = 'INPUT', // 数据输入 // FILTER = 'FILTER', // 数据过滤 // JOIN = 'JOIN', // 数据连接 // UNION = 'UNION', // 数据合并 // AGG = 'AGG', // 数据聚合 // OUTPUT = 'OUTPUT' // 数据输出 // } function init() { graph = new Graph({ container: graphContainer.value, interacting: function () { return { nodeMovable: false } }, grid: true, panning: { enabled: false, eventTypes: ['leftMouseDown', 'mouseWheel'] }, mousewheel: { enabled: true, modifiers: 'ctrl', factor: 1.1, maxScale: 1.5, minScale: 0.5 }, highlighting: { magnetAdsorbed: { name: 'stroke', args: { attrs: { fill: '#fff', stroke: '#31d0c6', strokeWidth: 4 } } } }, connecting: { snap: true, allowBlank: false, allowLoop: false, highlight: true, sourceAnchor: { name: 'bottom', args: { dx: 0 } }, targetAnchor: { name: 'top', args: { dx: 0 } }, createEdge() { return graph.createEdge({ shape: 'data-processing-curve', attrs: { line: { strokeDasharray: '5 5' } }, zIndex: -1 }) }, // 连接桩校验 validateConnection({ sourceMagnet, targetMagnet }) { // 只能从输出链接桩创建连接 if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { return false } // 只能连接到输入链接桩 if (!targetMagnet || targetMagnet.getAttribute('port-group') === 'out') { return false } return true } } }) graph.centerContent() // #region 初始化图形 const ports = { groups: { in: { position: 'top', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } }, out: { position: 'bottom', attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } }, left: { position: 'left', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } }, right: { position: 'right', attrs: { circle: { r: 4, magnet: true, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff', style: { visibility: 'hidden' } } } } } // items: [ // { // id: state.currentCode + '_in', // group: 'top', // }, // { // id: state.currentCode + '_out', // group: 'out', // } // ], } Graph.registerNode( 'custom-node', { inherit: 'rect', width: 140, height: 76, attrs: { body: { strokeWidth: 1 }, image: { width: 16, height: 16, x: 12, y: 6 }, text: { refX: 40, refY: 15, fontSize: 15, 'text-anchor': 'start' }, label: { text: 'Please nominate this node', refX: 10, refY: 30, fontSize: 12, fill: 'rgba(0,0,0,0.6)', 'text-anchor': 'start', textWrap: { width: -10, // 宽度减少 10px height: '70%', // 高度为参照元素高度的一半 ellipsis: true, // 文本超出显示范围时,自动添加省略号 breakWord: true // 是否截断单词 } } }, markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'image', selector: 'image' }, { tagName: 'text', selector: 'text' }, { tagName: 'text', selector: 'label' } ], data: {}, relation: {}, ports: { ...ports } }, true ) // 注册连线 Graph.registerConnector( 'curveConnector', (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 ) } Edge.config({ markup: [ { tagName: 'path', selector: 'wrap', attrs: { fill: 'none', cursor: 'pointer', stroke: 'transparent', strokeLinecap: 'round' } }, { tagName: 'path', selector: 'line', attrs: { fill: 'none', pointerEvents: 'none' } } ], connector: { name: 'curveConnector' }, attrs: { wrap: { connection: true, strokeWidth: 10, strokeLinejoin: 'round' }, line: { connection: true, stroke: '#A2B1C3', strokeWidth: 1, targetMarker: { name: 'classic', size: 6 } } } }) Graph.registerEdge('data-processing-curve', Edge, true) function getData() { let cells = [] as any const location = state.data location.nodes.map((node) => { cells.push( graph.addNode({ id: node.id, x: node.x, y: node.y, shape: 'custom-node', attrs: node.attrs, ports: node.ports, data: node.data }) ) }) location.edges.map((edge) => { cells.push( graph.addEdge({ id: edge.id, source: edge.source, target: edge.target, zIndex: edge.zIndex, shape: 'data-processing-curve', connector: { name: 'curveConnector' }, labels: edge.labels, attrs: edge.attrs }) ) }) graph.resetCells(cells) } // 开启边的运行动画 const excuteAnimate = (edge: any) => { edge.attr({ line: { stroke: '#3471F9' } }) edge.attr('line/strokeDasharray', 5) edge.attr('line/style/animation', 'running-line 30s infinite linear') } // 显示边状态 const showEdgeStatus = () => { state.edgeStatusList.forEach((item) => { const edge = graph.getCellById(item.id) if (item.status == 'success') { edge.attr('line/strokeDasharray', 0) edge.attr('line/stroke', '#52c41a') } else if ('error' == item.status) { edge.attr('line/stroke', '#ff4d4f') } else if ('executing' == item.status) { excuteAnimate(edge) } }) } onMounted(() => { init() // graph.fromJSON(state.data); getData() showEdgeStatus() }) </script> <style lang="less" scoped> .content-main { display: flex; width: 100%; flex-direction: column; height: calc(100vh - 85px - 40px); background-color: #ffffff; position: relative; } .content-container { position: relative; width: 100%; height: 100%; .content { width: 100%; height: 100%; position: relative; min-width: 400px; min-height: 600px; display: flex; border: 1px solid #dfe3e8; flex-direction: row; // flex-wrap: wrap; flex: 1 1; .graph-content { width: calc(100%); height: 100%; } } } :deep(.x6-widget-transform) { margin: -1px 0 0 -1px; padding: 0px; border: 1px solid #239edd; } :deep(.x6-widget-transform > div) { border: 1px solid #239edd; } :deep(.x6-widget-transform > div:hover) { background-color: #3dafe4; } :deep(.x6-widget-transform-active-handle) { background-color: #3dafe4; } :deep(.x6-widget-transform-resize) { border-radius: 0; } :deep(.x6-widget-selection-inner) { border: 1px solid #239edd; } :deep(.x6-widget-selection-box) { opacity: 0; } .topic-image { visibility: hidden; cursor: pointer; } .x6-node:hover .topic-image { visibility: visible; } .x6-node-selected rect { stroke-width: 2px; } </style>
总结
到此这篇关于vue3 + antv/x6实现流程图的文章就介绍到这了,更多相关vue3+antv/x6流程图内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!