Vue3 + antv/x6 实现流程图

新建流程图

在这里插入图片描述

// 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-optionv-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"><divclass="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 = nullconst 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: numbery: 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, // 宽度减少 10pxheight: '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-undefconst 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.valueconst ports = container.querySelectorAll('.x6-port-body')showPorts(ports, true)})graph.on('node:mouseleave', () => {const container = graphContainer.valueconst 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 falsecopy()})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 falsepaste()})// undo redograph.bindKey(['meta+z', 'ctrl+z'], () => {// if (graph.canUndo()) {//   graph.undo()// }// return falseundo()})graph.bindKey(['meta+y', 'ctrl+y'], () => {// if (graph.canRedo()) {//   graph.redo()// }// return falseredo()})// select allgraph.bindKey(['meta+a', 'ctrl+a'], () => {const nodes = graph.getNodes()if (nodes) {graph.select(nodes)}})// deletegraph.bindKey('backspace', () => {// const cells = graph.getSelectedCells()// if (cells.length) {//   graph.removeCells(cells)// }del()})// zoomgraph.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})//#endregionstencil.load(nodes, 'processLibrary')
}// 根据节点的类型获取ports
const getPortsByType = (type: NodeType, nodeId: string) => {let ports = [] as anyswitch (type) {case NodeType.INPUT:ports = [{id: `${nodeId}-in`,group: 'in'},{id: `${nodeId}-left`,group: 'left'},{id: `${nodeId}-right`,group: 'right'}]breakcase NodeType.OUTPUT:ports = [{id: `${nodeId}-out`,group: 'out'},{id: `${nodeId}-left`,group: 'left'},{id: `${nodeId}-right`,group: 'right'}]breakdefault: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 = 4const 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' }, //curveConnectorTBattrs: {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.labelstate.nodeFrom.desc = node.attrs.label.textstate.nodeFrom.show = truestate.nodeFrom.id = node.idstate.cellFrom.show = false
}function addEdgeInfo(edge: any) {state.nodeFrom.show = falsestate.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 = truestate.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 anyswitch (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'}}breakcase 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'}}breakcase 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 anyconst location = state.datalocation.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.textif (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 = truenextTick(() => {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()?.dataconsole.log(node)if (originEdge.source === node.id) {downstreamNodeIdList.push(originEdge.target)}})// 获取起点的位置信息const position = node.getPosition()let minX = Infinitylet maxY = -Infinitygraph.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.idconst target = newNode.id// 创建该节点出发到下游节点的边createEdge(source, target, graph)}} else {ElMessage({message: '请选择一个节点',type: 'warning'})}
}const createNode = (type: NodeType, graph: Graph, position?: Position): Node => {let newNode = {} as Nodeconst typeName = state.PROCESSING_TYPE_LIST?.find((item) => item.type === type)?.nameconst 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 = nullconst 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, // 宽度减少 10pxheight: '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 = 4const 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 anyconst location = state.datalocation.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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/275813.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

初识Python语言-课堂练习【pyhton123题库】

初识Python语言-课堂练习【pyhton123题库】 一、单项选择题 1、Guido van Rossum正式对外发布Python版本的年份是&#xff1a; A 2008B 1998C 1991D 2002 【答案】C 【解析】暂无解析2、下面不是Python语言特点的是&#xff1a;‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪…

无需编程技能:Python爬虫与数据可视化毕业论文代写服务

引言 作为一名在软件技术领域深耕多年的专业人士,我不仅在软件开发和项目部署方面积累了丰富的实践经验,更以卓越的技术实力获得了🏅30项软件著作权证书的殊荣。这些成就不仅是对我的技术专长的肯定,也是对我的创新精神和专业承诺的认可。我的专业知识涵盖了从前端界面设…

Visual Studio 2022之Release版本程序发送到其它计算机运行

目录 1、缺少dll​ 2、应用程序无法正常启动 3、This application failed to start because no Qt platform plugin could be initialized. 代码在Debug模式下正常运行&#xff0c;然后切换到Release模式下&#xff0c;也正常运行&#xff0c;把第三方平台的dll拷贝到exe所在…

honle电源维修UV电源控制器维修EVG EPS60

好乐UV电源控制器维修&#xff1b;honle控制器维修&#xff1b;UV电源维修MUC-Steuermodul 2 LΛmpen D-82166 主要维修型号&#xff1a; EVG EPS 60/120、EVG EPS 100、EVG EPS200、EVG EPS 220、EVG EPS 340、EVG EPS40C-HMI、EVG EPS60 HONLE好乐uv电源维修故障包括&#…

React组件(函数式组件,类式组件)

函数式组件 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>React Demo</title> <!-- 引…

基于FPGA加速的bird-oid object算法实现

导语 今天继续康奈尔大学FPGA 课程ECE 5760的典型案例分享——基于FPGA加速的bird-oid object算法实现。 &#xff08;更多其他案例请参考网站&#xff1a; Final Projects ECE 5760&#xff09; 1. 项目概述 项目网址 ECE 5760 Final Project 模型说明 Bird-oid object …

镭雕机:如何利用激光技术实现高质量的产品标记

镭雕机是一种利用激光技术实现高质量产品标记的设备。它通过激光束在各种不同的物质表面进行精确的打标&#xff0c;可以产生永久性的标记效果&#xff0c;这些标记不仅精美&#xff0c;而且具有高度的精度和清晰度。以下是镭雕机如何利用激光技术实现高质量产品标记的详细过程…

寄存器(内存访问)

文章目录 寄存器&#xff08;内存访问&#xff09;1 内存中字的存储2 DS和[address]3 字的传送4 mov、add、sub指令5 数据段6 栈7 CPU提供的栈机制8 栈顶超界的问题9 push、pop指令10 栈段 寄存器&#xff08;内存访问&#xff09; 1 内存中字的存储 CPU中&#xff0c;用16位寄…

Spring Cloud Gateway如何实现熔断

Spring Cloud Gateway熔断集成 熔断应用&#xff1a; 金融市场中的熔断机制&#xff1a;在金融交易系统中&#xff0c;熔断机制&#xff08;Circuit Breaker&#xff09;是一种市场保护措施&#xff0c;旨在预防市场剧烈波动时可能导致的系统性风险。当某个基准指数&#xff08…

基于Ambari搭建大数据分析平台

一、部署工具简介 1. Hadoop生态系统 Hadoop big data ecosystem in Apache stack 2. Hadoop的发行版本 Hadoop的发行版除了Apache的开源版本之外&#xff0c;国外比较流行的还有&#xff1a;Cloudera发行版(CDH)、Hortonworks发行版&#xff08;HDP&#xff09;、MapR等&am…

【网络安全】-数字证书

数字证书 数字证书是互联网通讯中用于标志通讯各方身份信息的一串数字或数据&#xff0c;它为网络应用提供了一种验证通信实体身份的方式。具体来说&#xff0c;数字证书是由权威的证书授权&#xff08;CA&#xff09;中心签发的&#xff0c;包含公开密钥拥有者信息以及公开密…

linux中将终端Terminal添加到任务栏

问题描述 如题&#xff0c;默认的任务栏中没有终端这一组件&#xff0c;因此&#xff0c;想要打开终端&#xff0c;需要先切换到桌面&#xff0c;影响了使用体验。 解决方法 1.在applications里找到Terminal Emulator。 2.将Terminal Emulator拖动到任务栏&#xff0c;即可…

【完美实现】VITE + VUE3 + SVG图片解析+element-plus开发环境初始化(基于macos)

一、最终效果 废话少说&#xff0c;直接上效果 这是我的初始化程序提供的页面&#xff0c;在这个页面上实现了一下几个功能&#xff1a; 1、vite初始化之后的路由安装和初始化&#xff1b; 2、标准SVG的解析&#xff0c;并可调整大小、颜色&#xff1b; 3、element-plus的安…

Websocket在Asp.net webApi(.net framework)上的应用

之前在写看板部分的web api的时候&#xff0c;都是通过Ajax在规定时间内轮询调用web api&#xff0c;这样简单省事&#xff0c;但是当看板多了&#xff08;并发量上来&#xff09;以后&#xff0c;比较消耗服务器的性能&#xff0c;所以最近研究了websocket&#xff0c;希望使用…

运放的基础知识

运算放大器&#xff08;Operational Amplifier&#xff0c;简称运放&#xff09;是一种直流耦合、差模&#xff08;差动模式&#xff09;输入的高增益电压放大器&#xff0c;通常具有单端输出。它能产生一个相对于输入端电势差大数十万倍的输出电势&#xff08;对地而言&#x…

Jenkins Pipeline实现Golang项目的CI/CD

Jenkins Pipeline实现Golang项目的CI/CD 背景 最近新增了一个Golang实现的项目&#xff0c;需要接入到现有的流水线架构中。 流程图 这边流程和之前我写过的一篇《基于Jenkins实现的CI/CD方案》差不多&#xff0c;不一样的是构建现在是手动触发的&#xff0c;没有配置webho…

蓝桥杯倒计时 36天-DFS练习

文章目录 飞机降落仙境诅咒小怂爱水洼串变换 飞机降落 思路&#xff1a;贪心暴搜。 #include<bits/stdc.h>using namespace std; const int N 10; int t,n; //这题 N 比较小&#xff0c;可以用暴力搜搜复杂度是 TN*N! struct plane{int t,d,l; }p[N]; bool vis[N];//用…

OceanBase中binlog service 功能的试用

OBLogProxy简介 OBLogProxy即OceanBase的增量日志代理服务&#xff0c;它可与OceanBase建立连接并读取增量日志&#xff0c;从而为下游服务提供了变更数据捕获&#xff08;CDC&#xff09;的功能。 关于OBLogProxy的详尽介绍与具体的安装指引&#xff0c;您可以参考这篇官方OB…

RocketMQ快速入门_2. rocketmq 的应用场景、与其他mq的差异

0. 引言 之前我们讲解过rabbitMQ&#xff0c;本期我们将进入吞吐量更加强大的rocketMQ的学习。 1. 基础概念 如果你是刚接触MQ的同学&#xff0c;还不清楚消息队列的基础概念的&#xff0c;可以参考我之前这篇文章&#xff1a; https://wu55555.blog.csdn.net/article/deta…

IOS降级后从高版本到低版本恢复备份

IOS降级后从高版本到低版本恢复备份 此方法只适用于小版本还原&#xff0c;比如17.4->17.3&#xff0c;未验证大版本恢复可行性手机型号&#xff1a;iphone 13pro 系统版本&#xff1a;17.4 降级版本&#xff1a;17.3.1 步骤 通过itunes或者MacOS系统下对当前版本进行备份…