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.hqwc.cn/news/529716.html

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

相关文章

深耕版本控制、代码质量与安全等领域,龙智荣获“Perforce 2023年度合作伙伴”奖项

在近日举行的Perforce 2024合作伙伴峰会上&#xff0c;龙智被评选为“Perforce 2023年度合作伙伴”。这一奖项不仅是对龙智在中国市场开拓中的进取精神与丰硕成果的高度认可&#xff0c;也是Perforce公司对于龙智持续创新精神及专业技术与服务的表彰。 自2012年成为Perforce中…

PyCharm无代码提示解决

PyCharm无代码提示解决方法 在使用PyCharm工具时&#xff0c;调用方法却无法进行提示&#xff0c;针对PyCharm无代码提示整理下解决方案 1、Python内置语法无智能提示 复现&#xff1a;我这里以urllib库读取网页内容为例&#xff0c;在通过urlopen(&#xff09;之后调用getur…

强烈推荐—GpuMall智算云实例网盘操作详解

实例网盘为实例的 /gm-fs 目录&#xff0c;该目录为实例同一个数据中心的分布式存储&#xff0c;对于较大的文件或者压缩文件有着出色的读写性能&#xff0c;实例网盘不受实例删除/释放影响&#xff0c;采用分布式冗余存储&#xff0c;数据安全性较高&#xff0c;强烈建议使用网…

jvm八股

文章目录 运行时数据区域Java堆对象创建对象的内存布局对象的访问定位句柄直接指针 GC判断对象是否已死引用计数算法可达性分析算法 引用的类别垃圾收集算法分代收集理论标记清除算法标记复制算法标记整理算法 实现细节并发的可达性分析 垃圾收集器serial收集器ParNew收集器Par…

go语言基础 -- 文件操作

基础的文件操作方法 go里面的文件操作封装在os包里面的File结构体中&#xff0c;要用的时候最好去查下官方文档&#xff0c;这里介绍下基本的文件操作。 打开关闭文件 import("os" ) func main() {// Open返回*File指针&#xff0c;后续的操作都通过*File对象操作…

VUE学习第三篇----VUE实例

1、当一个 Vue 实例被创建时&#xff0c;它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时&#xff0c;视图将会产生“响应”&#xff0c;即匹配更新为新的值。 html网页文件如下所示&#xff1a; <html> <head> &…

SpringCloudAlibaba 网关gateway整合sentinel日志默认路径修改

SpringCloudAlibaba 网关gateway整合sentinel 实现网关限流熔断 问题提出 今天运维突然告诉我 在服务器上内存满了 原因是nacos日志高达3G,然后将日志文件发给我看了一下之后才发现是gateway整合sentinel使用了默认日志地址导致日志生成地址直接存在与根路径下而且一下存在多…

论文学习——基于注意力预测策略的动态多目标优化合作差分进化论

论文题目&#xff1a;Cooperative Differential Evolution With an Attention-Based Prediction Strategy for Dynamic Multiobjective Optimization 基于注意力预测策略的动态多目标优化合作差分进化论&#xff08;Xiao-Fang Liu , Member, IEEE, Jun Zhang, Fellow, IEEE, a…

【大厂AI课学习笔记NO.74】人工智能产业技术架构

包括基础层、技术层和应用层。 人工智能的产业技术架构是一个多层次、多维度的复杂系统&#xff0c;它涵盖了从基础硬件和软件设施到高级算法和应用技术的全过程。这个架构通常可以分为三个主要层次&#xff1a;基础层、技术层和应用层。下面我将详细论述这三个层次及其细分内…

rancher是什么

Rancher Labs是制作Rancher的公司。Rancher Labs成立于2014年&#xff0c;是一家专注于企业级容器管理软件的公司。它的产品设计旨在简化在分布式环境中部署和管理容器的过程&#xff0c;帮助企业轻松地采用容器技术和Kubernetes。Rancher Labs提供的Rancher平台支持Docker容器…

pip 和conda 更换镜像源介绍

1、前言 很多深度学习的项目免不了安装库文件、配置环境等等&#xff0c;如果利用官方提供的连接&#xff0c;网速很慢&#xff0c;而且很容易download掉。 所以配置好了虚拟环境&#xff0c;将pip换源属实重要 常见的国内镜像源有清华、中科大、阿里等等... 这里建议用中科…

NIFI从Oracle11G同步数据到Mysql_亲测可用_解决数据重复_数据跟源表不一致的问题---大数据之Nifi工作笔记0065

首先来看一下整体的流程: 可以看到了用到了上面的这些处理器,然后我们主要看看,这里之前 同步的时候,总是出现重复的数据,奇怪. 比如源表中只有166条数据,但是同步过去以后变成了11万条数据了. ${db.table.name:equals(table1):or(${db.table.name:equals(table2)})} 可以看…