123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- 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<SVGAElement>, 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<SVGAElement>
- this.showPorts(ports, true)
- }),
- 500
- )
- // 鼠标移出 隐藏连接桩
- graph.on('node:mouseleave', () => {
- const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>
- 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()
- })
- }
- }
|