import { Graph, FunctionExt, Shape, Addon } from '@antv/x6' import insertCss from 'insert-css' import './shape' ;(insertCss as any)(` @keyframes ant-line { to { stroke-dashoffset: -1000 } } `) export default class FlowGraph { public static graph: Graph private static stencil: Addon.Stencil /** * 初始化方法 * @param {*} dom 画板容器 * @param {*} width 容器宽度 * @param {*} height 容器高度 * @param {*} flag 默认为true,传入false只实例化画板 * @returns */ public static init( dom: HTMLElement, width: number = 1200, height: number = 900, flag: boolean = true ) { // 初始化 流程图画板 this.graph = new Graph({ background: { color: '#e5e5e5' // 设置画布背景颜色 }, container: dom, width: width, height: height, autoResize: true, grid: { size: 10, visible: true, type: 'doubleMesh', args: [ { color: '#cccccc', thickness: 1 }, { color: '#5F95FF', thickness: 1, factor: 4 } ] }, scroller: { enabled: false, pageVisible: false, pageBreak: false, pannable: false }, // 开启画布缩放 mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], minScale: 0.5, maxScale: 2 }, interacting: { nodeMovable: true, //节点是否可以被移动。 edgeMovable: false, //边是否可以被移动。 edgeLabelMovable: false, //边的标签是否可以被移动。 arrowheadMovable: false, //边的起始/终止箭头是否可以被移动 vertexMovable: true, //边的路径点是否可以被移动。 vertexAddable: true, //是否可以添加边的路径点。 vertexDeletable: true //边的路径点是否可以被删除。 }, connecting: { snap: true, // 是否自动吸附 allowMulti: true, // 是否允许在相同的起始节点和终止之间创建多条边 allowNode: false, // 是否允许边链接到节点(非节点上的链接桩) allowBlank: false, // 是否允许连接到空白点 allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点, allowEdge: false, // 是否允许边链接到另一个边 highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点 connectionPoint: 'anchor', // 指定连接点 anchor: 'center', // 指定被连接的节点的锚点 createEdge() { // X6 的 Shape 命名空间中内置 Edge、DoubleEdge、ShadowEdge 三种边 return new Shape.Edge({ attrs: { line: { stroke: '#5F95FF', strokeWidth: 1, targetMarker: { name: 'classic', size: 8 } } }, router: { name: 'manhattan' } }) }, validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) { if (sourceView === targetView) { return false } if (!sourceMagnet) { return false } if (!targetMagnet) { return false } return true } }, highlighting: { magnetAvailable: { name: 'stroke', args: { padding: 4, attrs: { strokeWidth: 4, stroke: 'rgba(223,234,255)' } } } }, // 开启拖拽平移(防止冲突,按下修饰键并点击鼠标才能触发画布拖拽) panning: { enabled: true, modifiers: 'shift' }, resizing: true, rotating: true, selecting: { enabled: true, multiple: true, rubberband: true, movable: true, showNodeSelectionBox: true }, snapline: true, history: true, clipboard: { enabled: true }, keyboard: { enabled: true }, embedding: { enabled: true, findParent({ node }) { const bbox = node.getBBox() return this.getNodes().filter(node => { // 只有 data.parent 为 true 的节点才是父节点 const data = node.getData() if (data && data.parent) { const targetBBox = node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) } } }) if (!flag) { // this.graph.centerContent() this.graph.hideGrid() // 返显渲染的时候 隐藏网格 return this.graph } this.initStencil() this.initShape() // this.initGraphShape() this.initEvent() return this.graph } // 初始化根节点 private static initStencil() { this.stencil = new Addon.Stencil({ target: this.graph, title: '节点搜索', stencilGraphWidth: 250, stencilGraphHeight: 0, placeholder: '请输入节点关键字', notFoundText: '未搜索到结果', search: { rect: true }, collapsable: true, groups: [ { name: 'basic', title: '基础节点' // graphHeight: 180 }, { name: 'logic', title: '逻辑节点' }, { name: 's-pro', title: '子流程节点', layoutOptions: { columns: 1, marginX: 60 } // graphHeight: 260 }, { name: 'combination', title: '组合节点', layoutOptions: { columns: 1, marginX: 60 } // graphHeight: 260 }, { name: 'group', title: '节点组', // graphHeight: 100, layoutOptions: { columns: 1, marginX: 60 } } ] }) const stencilContainer = document.querySelector('#stencil') stencilContainer?.appendChild(this.stencil.container) } // 初始化具体每个根节点下不同类型节点 private static initShape() { const { graph } = this // 基础节点 const r1 = graph.createNode({ shape: 'flow-chart-rect', attrs: { body: { rx: 24, ry: 24 }, text: { text: '开始' } } }) const r2 = graph.createNode({ shape: 'flow-chart-rect', attrs: { text: { text: '流程' } } }) const r3 = graph.createNode({ shape: 'flow-chart-rect', width: 52, height: 52, angle: 45, attrs: { text: { text: '判断', transform: 'rotate(-45deg)' } }, ports: { groups: { top: { position: { name: 'top', args: { dx: -26 } } }, right: { position: { name: 'right', args: { dy: -26 } } }, bottom: { position: { name: 'bottom', args: { dx: 26 } } }, left: { position: { name: 'left', args: { dy: 26 } } } } } }) const r4 = graph.createNode({ shape: 'flow-chart-rect', width: 38, height: 12, attrs: { text: { text: '点' } } }) // 子流程节点 const s1 = graph.createNode({ shape: 'sub-flow-rect', attrs: { title: { text: 'XX子流程' }, text: { text: '测试xx流程' }, lip: { transform: 'scale(0.01)' } } }) const s2 = graph.createNode({ shape: 'sub-flow-rect' }) const s3 = graph.createNode({ shape: 'sub-flow-rect' }) const s4 = graph.createNode({ shape: 'sub-flow-rect' }) const s5 = graph.createNode({ shape: 'sub-flow-rect' }) const L1 = graph.createNode({ shape: 'logic-flow-path' }) // 组合节点 const c1 = graph.createNode({ shape: 'flow-chart-image-rect' }) const c2 = graph.createNode({ shape: 'flow-chart-title-rect' }) const c3 = graph.createNode({ shape: 'flow-chart-animate-text' }) // 节点组 const g1 = graph.createNode({ shape: 'groupNode', attrs: { text: { text: '节点群组' } }, data: { parent: true } }) this.stencil.load([r1, r2, r3, r4], 'basic') this.stencil.load([s1, s2, s3, s4, s5], 's-pro') this.stencil.load([L1], 'logic') this.stencil.load([c1, c2, c3], 'combination') this.stencil.load([g1], 'group') } // 根据json渲染节点和边 public static initGraphShape(gd: any) { this.graph.fromJSON(gd) } // 连接桩显示时机 private static showPorts(ports: NodeListOf, show: boolean) { for (let i = 0, len = ports.length; i < len; i = i + 1) { ports[i].style.visibility = show ? 'visible' : 'hidden' } } // 右键菜单 // public static handleContextmenu = (e: { pageX: any; pageY: any }, cell) => { // const cells = this.graph.getSelectedCells() // ContextMenu.showContextMenu({ // x: e.pageX, // y: e.pageY, // items: [ // { // label: '编辑子流程', // onClick: () => { // if (cell) { // console.log('cell', cell) // } // } // }, // { // label: '删除节点', // onClick: () => { // if (cells.length) { // this.graph.removeCells(cells) // } // } // }, // { // label: '置顶', // onClick: () => { // if (cells.length) { // cells.forEach(item => item.toFront({ deep: true })) // } // } // }, // { // label: '置底', // onClick: () => { // if (cells.length) { // cells.forEach(item => item.toBack({ deep: true })) // } // } // } // ] // }) // } // 事件相关 private static initEvent() { const { graph } = this const container = document.getElementById('container')! // 右键编辑文本 // graph.on('node:contextmenu', ({ cell, view }) => { // console.log(view.container) // const oldText = cell.attr('text/text') as string // cell.attr('text/style/display', 'none') // const elem = view.container.querySelector('.x6-edit-text') as HTMLElement // if (elem) { // elem.innerText = oldText // elem.focus() // } // const onBlur = () => { // cell.attr('text/text', elem.innerText) // cell.attr('text/style/display', 'inline-block') // } // if (elem) { // elem.addEventListener('blur', () => { // onBlur() // elem.removeEventListener('blur', onBlur) // }) // } // }) // 鼠标移入 显示连接桩 graph.on( 'node:mouseenter', FunctionExt.debounce(() => { const ports = container.querySelectorAll('.x6-port-body') as NodeListOf this.showPorts(ports, true) }), 500 ) // 鼠标移出 隐藏连接桩 graph.on('node:mouseleave', () => { const ports = container.querySelectorAll('.x6-port-body') as NodeListOf this.showPorts(ports, false) }) graph.on('node:collapse', ({ node, e }: any) => { e.stopPropagation() node.toggleCollapse() const collapsed = node.isCollapsed() const cells = node.getDescendants() cells.forEach((n: any) => { if (collapsed) { n.hide() } else { n.show() } }) }) graph.on('cell:dblclick', ({ cell, e }) => { const isNode = cell.isNode() const name = cell.isNode() ? 'node-editor' : 'edge-editor' cell.removeTool(name) cell.addTools({ name, args: { event: e, attrs: { backgroundColor: isNode ? '#EFF4FF' : '#FFF' } } }) }) // backspace graph.bindKey('delete', () => { const cells = graph.getSelectedCells() if (cells.length) { graph.removeCells(cells) } }) // graph.on('node:contextmenu', ({ e, x, y, cell, view }) => { // const cells = this.graph.getSelectedCells() // if (!cells.length) { // return // } // this.handleContextmenu(e, cell) // }) // 鼠标动态添加/删除小工具。 graph.on('edge:mouseenter', ({ cell }) => { /** * EdgeTool * vertices 路径点工具,在路径点位置渲染一个小圆点,拖动小圆点修改路径点位置,双击小圆点删除路径点,在边上单击添加路径点。 * segments 线段工具。在边的每条线段的中心渲染一个工具条,可以拖动工具条调整线段两端的路径点的位置。 * boundary 根据边的包围盒渲染一个包围边的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。 * button 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。 * button-remove 在指定的位置处,渲染一个删除按钮,点击时删除对应的边。 * source-arrowhead-和-target-arrowhead 在边的起点或终点渲染一个图形(默认是箭头),拖动该图形来修改边的起点或终点。 * edge-editor 提供边上文本编辑功能。 */ cell.addTools([ { name: 'vertices', args: { attrs: { fill: '#007acc' }, // 移动路径点过程中的吸附半径。当路径点与邻近的路径点的某个坐标 (x, y) 距离在半径范围内时,将当前路径点的对应坐标 (x, y) 吸附到邻居路径的路径点。 snapRadius: 20, // 在边上按下鼠标时,是否可以添加新的路径点。 addable: true, // 是否可以通过双击移除路径点。 removable: true, // 是否自动移除冗余的路径点。 removeRedundancies: true, // 是否阻止工具上的鼠标事件冒泡到边视图上。阻止后鼠标与工具交互时将不会触发边的 mousedown、mousemove 和 mouseup 事件。 stopPropagation: false } } ]) }) graph.on('edge:mouseleave', ({ cell }) => { cell.removeTools() }) } }