index.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. import { Graph, FunctionExt, Shape, Addon } from '@antv/x6'
  2. import insertCss from 'insert-css'
  3. import './shape'
  4. ;(insertCss as any)(`
  5. @keyframes ant-line {
  6. to {
  7. stroke-dashoffset: -1000
  8. }
  9. }
  10. `)
  11. export default class FlowGraph {
  12. public static graph: Graph
  13. private static stencil: Addon.Stencil
  14. /**
  15. * 初始化方法
  16. * @param {*} dom 画板容器
  17. * @param {*} width 容器宽度
  18. * @param {*} height 容器高度
  19. * @param {*} flag 默认为true,传入false只实例化画板
  20. * @returns
  21. */
  22. public static init(
  23. dom: HTMLElement,
  24. width: number = 1200,
  25. height: number = 900,
  26. flag: boolean = true
  27. ) {
  28. // 初始化 流程图画板
  29. this.graph = new Graph({
  30. background: {
  31. color: '#e5e5e5' // 设置画布背景颜色
  32. },
  33. container: dom,
  34. width: width,
  35. height: height,
  36. autoResize: true,
  37. grid: {
  38. size: 10,
  39. visible: true,
  40. type: 'doubleMesh',
  41. args: [
  42. {
  43. color: '#cccccc',
  44. thickness: 1
  45. },
  46. {
  47. color: '#5F95FF',
  48. thickness: 1,
  49. factor: 4
  50. }
  51. ]
  52. },
  53. scroller: {
  54. enabled: false,
  55. pageVisible: false,
  56. pageBreak: false,
  57. pannable: false
  58. },
  59. // 开启画布缩放
  60. mousewheel: {
  61. enabled: true,
  62. modifiers: ['ctrl', 'meta'],
  63. minScale: 0.5,
  64. maxScale: 2
  65. },
  66. interacting: {
  67. nodeMovable: true, //节点是否可以被移动。
  68. edgeMovable: false, //边是否可以被移动。
  69. edgeLabelMovable: false, //边的标签是否可以被移动。
  70. arrowheadMovable: false, //边的起始/终止箭头是否可以被移动
  71. vertexMovable: true, //边的路径点是否可以被移动。
  72. vertexAddable: true, //是否可以添加边的路径点。
  73. vertexDeletable: true //边的路径点是否可以被删除。
  74. },
  75. connecting: {
  76. snap: true, // 是否自动吸附
  77. allowMulti: true, // 是否允许在相同的起始节点和终止之间创建多条边
  78. allowNode: false, // 是否允许边链接到节点(非节点上的链接桩)
  79. allowBlank: false, // 是否允许连接到空白点
  80. allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,
  81. allowEdge: false, // 是否允许边链接到另一个边
  82. highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点
  83. connectionPoint: 'anchor', // 指定连接点
  84. anchor: 'center', // 指定被连接的节点的锚点
  85. createEdge() {
  86. // X6 的 Shape 命名空间中内置 Edge、DoubleEdge、ShadowEdge 三种边
  87. return new Shape.Edge({
  88. attrs: {
  89. line: {
  90. stroke: '#5F95FF',
  91. strokeWidth: 1,
  92. targetMarker: {
  93. name: 'classic',
  94. size: 8
  95. }
  96. }
  97. },
  98. router: {
  99. name: 'manhattan'
  100. }
  101. })
  102. },
  103. validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) {
  104. if (sourceView === targetView) {
  105. return false
  106. }
  107. if (!sourceMagnet) {
  108. return false
  109. }
  110. if (!targetMagnet) {
  111. return false
  112. }
  113. return true
  114. }
  115. },
  116. highlighting: {
  117. magnetAvailable: {
  118. name: 'stroke',
  119. args: {
  120. padding: 4,
  121. attrs: {
  122. strokeWidth: 4,
  123. stroke: 'rgba(223,234,255)'
  124. }
  125. }
  126. }
  127. },
  128. // 开启拖拽平移(防止冲突,按下修饰键并点击鼠标才能触发画布拖拽)
  129. panning: {
  130. enabled: true,
  131. modifiers: 'shift'
  132. },
  133. resizing: true,
  134. rotating: true,
  135. selecting: {
  136. enabled: true,
  137. multiple: true,
  138. rubberband: true,
  139. movable: true,
  140. showNodeSelectionBox: true
  141. },
  142. snapline: true,
  143. history: true,
  144. clipboard: {
  145. enabled: true
  146. },
  147. keyboard: {
  148. enabled: true
  149. },
  150. embedding: {
  151. enabled: true,
  152. findParent({ node }) {
  153. const bbox = node.getBBox()
  154. return this.getNodes().filter(node => {
  155. // 只有 data.parent 为 true 的节点才是父节点
  156. const data = node.getData()
  157. if (data && data.parent) {
  158. const targetBBox = node.getBBox()
  159. return bbox.isIntersectWithRect(targetBBox)
  160. }
  161. return false
  162. })
  163. }
  164. }
  165. })
  166. if (!flag) {
  167. // this.graph.centerContent()
  168. this.graph.hideGrid() // 返显渲染的时候 隐藏网格
  169. return this.graph
  170. }
  171. this.initStencil()
  172. this.initShape()
  173. // this.initGraphShape()
  174. this.initEvent()
  175. return this.graph
  176. }
  177. // 初始化根节点
  178. private static initStencil() {
  179. this.stencil = new Addon.Stencil({
  180. target: this.graph,
  181. title: '节点搜索',
  182. stencilGraphWidth: 250,
  183. stencilGraphHeight: 0,
  184. placeholder: '请输入节点关键字',
  185. notFoundText: '未搜索到结果',
  186. search: { rect: true },
  187. collapsable: true,
  188. groups: [
  189. {
  190. name: 'basic',
  191. title: '基础节点'
  192. // graphHeight: 180
  193. },
  194. {
  195. name: 'logic',
  196. title: '逻辑节点'
  197. },
  198. {
  199. name: 's-pro',
  200. title: '子流程节点',
  201. layoutOptions: {
  202. columns: 1,
  203. marginX: 60
  204. }
  205. // graphHeight: 260
  206. },
  207. {
  208. name: 'combination',
  209. title: '组合节点',
  210. layoutOptions: {
  211. columns: 1,
  212. marginX: 60
  213. }
  214. // graphHeight: 260
  215. },
  216. {
  217. name: 'group',
  218. title: '节点组',
  219. // graphHeight: 100,
  220. layoutOptions: {
  221. columns: 1,
  222. marginX: 60
  223. }
  224. }
  225. ]
  226. })
  227. const stencilContainer = document.querySelector('#stencil')
  228. stencilContainer?.appendChild(this.stencil.container)
  229. }
  230. // 初始化具体每个根节点下不同类型节点
  231. private static initShape() {
  232. const { graph } = this
  233. // 基础节点
  234. const r1 = graph.createNode({
  235. shape: 'flow-chart-rect',
  236. attrs: {
  237. body: {
  238. rx: 24,
  239. ry: 24
  240. },
  241. text: {
  242. text: '开始'
  243. }
  244. }
  245. })
  246. const r2 = graph.createNode({
  247. shape: 'flow-chart-rect',
  248. attrs: {
  249. text: {
  250. text: '流程'
  251. }
  252. }
  253. })
  254. const r3 = graph.createNode({
  255. shape: 'flow-chart-rect',
  256. width: 52,
  257. height: 52,
  258. angle: 45,
  259. attrs: {
  260. text: {
  261. text: '判断',
  262. transform: 'rotate(-45deg)'
  263. }
  264. },
  265. ports: {
  266. groups: {
  267. top: {
  268. position: {
  269. name: 'top',
  270. args: {
  271. dx: -26
  272. }
  273. }
  274. },
  275. right: {
  276. position: {
  277. name: 'right',
  278. args: {
  279. dy: -26
  280. }
  281. }
  282. },
  283. bottom: {
  284. position: {
  285. name: 'bottom',
  286. args: {
  287. dx: 26
  288. }
  289. }
  290. },
  291. left: {
  292. position: {
  293. name: 'left',
  294. args: {
  295. dy: 26
  296. }
  297. }
  298. }
  299. }
  300. }
  301. })
  302. const r4 = graph.createNode({
  303. shape: 'flow-chart-rect',
  304. width: 38,
  305. height: 12,
  306. attrs: {
  307. text: {
  308. text: '点'
  309. }
  310. }
  311. })
  312. // 子流程节点
  313. const s1 = graph.createNode({
  314. shape: 'sub-flow-rect',
  315. attrs: {
  316. title: {
  317. text: 'XX子流程'
  318. },
  319. text: {
  320. text: '测试xx流程'
  321. },
  322. lip: {
  323. transform: 'scale(0.01)'
  324. }
  325. }
  326. })
  327. const s2 = graph.createNode({
  328. shape: 'sub-flow-rect'
  329. })
  330. const s3 = graph.createNode({
  331. shape: 'sub-flow-rect'
  332. })
  333. const s4 = graph.createNode({
  334. shape: 'sub-flow-rect'
  335. })
  336. const s5 = graph.createNode({
  337. shape: 'sub-flow-rect'
  338. })
  339. const L1 = graph.createNode({
  340. shape: 'logic-flow-path'
  341. })
  342. // 组合节点
  343. const c1 = graph.createNode({
  344. shape: 'flow-chart-image-rect'
  345. })
  346. const c2 = graph.createNode({
  347. shape: 'flow-chart-title-rect'
  348. })
  349. const c3 = graph.createNode({
  350. shape: 'flow-chart-animate-text'
  351. })
  352. // 节点组
  353. const g1 = graph.createNode({
  354. shape: 'groupNode',
  355. attrs: {
  356. text: {
  357. text: '节点群组'
  358. }
  359. },
  360. data: {
  361. parent: true
  362. }
  363. })
  364. this.stencil.load([r1, r2, r3, r4], 'basic')
  365. this.stencil.load([s1, s2, s3, s4, s5], 's-pro')
  366. this.stencil.load([L1], 'logic')
  367. this.stencil.load([c1, c2, c3], 'combination')
  368. this.stencil.load([g1], 'group')
  369. }
  370. // 根据json渲染节点和边
  371. public static initGraphShape(gd: any) {
  372. this.graph.fromJSON(gd)
  373. }
  374. // 连接桩显示时机
  375. private static showPorts(ports: NodeListOf<SVGAElement>, show: boolean) {
  376. for (let i = 0, len = ports.length; i < len; i = i + 1) {
  377. ports[i].style.visibility = show ? 'visible' : 'hidden'
  378. }
  379. }
  380. // 右键菜单
  381. // public static handleContextmenu = (e: { pageX: any; pageY: any }, cell) => {
  382. // const cells = this.graph.getSelectedCells()
  383. // ContextMenu.showContextMenu({
  384. // x: e.pageX,
  385. // y: e.pageY,
  386. // items: [
  387. // {
  388. // label: '编辑子流程',
  389. // onClick: () => {
  390. // if (cell) {
  391. // console.log('cell', cell)
  392. // }
  393. // }
  394. // },
  395. // {
  396. // label: '删除节点',
  397. // onClick: () => {
  398. // if (cells.length) {
  399. // this.graph.removeCells(cells)
  400. // }
  401. // }
  402. // },
  403. // {
  404. // label: '置顶',
  405. // onClick: () => {
  406. // if (cells.length) {
  407. // cells.forEach(item => item.toFront({ deep: true }))
  408. // }
  409. // }
  410. // },
  411. // {
  412. // label: '置底',
  413. // onClick: () => {
  414. // if (cells.length) {
  415. // cells.forEach(item => item.toBack({ deep: true }))
  416. // }
  417. // }
  418. // }
  419. // ]
  420. // })
  421. // }
  422. // 事件相关
  423. private static initEvent() {
  424. const { graph } = this
  425. const container = document.getElementById('container')!
  426. // 右键编辑文本
  427. // graph.on('node:contextmenu', ({ cell, view }) => {
  428. // console.log(view.container)
  429. // const oldText = cell.attr('text/text') as string
  430. // cell.attr('text/style/display', 'none')
  431. // const elem = view.container.querySelector('.x6-edit-text') as HTMLElement
  432. // if (elem) {
  433. // elem.innerText = oldText
  434. // elem.focus()
  435. // }
  436. // const onBlur = () => {
  437. // cell.attr('text/text', elem.innerText)
  438. // cell.attr('text/style/display', 'inline-block')
  439. // }
  440. // if (elem) {
  441. // elem.addEventListener('blur', () => {
  442. // onBlur()
  443. // elem.removeEventListener('blur', onBlur)
  444. // })
  445. // }
  446. // })
  447. // 鼠标移入 显示连接桩
  448. graph.on(
  449. 'node:mouseenter',
  450. FunctionExt.debounce(() => {
  451. const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>
  452. this.showPorts(ports, true)
  453. }),
  454. 500
  455. )
  456. // 鼠标移出 隐藏连接桩
  457. graph.on('node:mouseleave', () => {
  458. const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>
  459. this.showPorts(ports, false)
  460. })
  461. graph.on('node:collapse', ({ node, e }: any) => {
  462. e.stopPropagation()
  463. node.toggleCollapse()
  464. const collapsed = node.isCollapsed()
  465. const cells = node.getDescendants()
  466. cells.forEach((n: any) => {
  467. if (collapsed) {
  468. n.hide()
  469. } else {
  470. n.show()
  471. }
  472. })
  473. })
  474. graph.on('cell:dblclick', ({ cell, e }) => {
  475. const isNode = cell.isNode()
  476. const name = cell.isNode() ? 'node-editor' : 'edge-editor'
  477. cell.removeTool(name)
  478. cell.addTools({
  479. name,
  480. args: {
  481. event: e,
  482. attrs: {
  483. backgroundColor: isNode ? '#EFF4FF' : '#FFF'
  484. }
  485. }
  486. })
  487. })
  488. // backspace
  489. graph.bindKey('delete', () => {
  490. const cells = graph.getSelectedCells()
  491. if (cells.length) {
  492. graph.removeCells(cells)
  493. }
  494. })
  495. // graph.on('node:contextmenu', ({ e, x, y, cell, view }) => {
  496. // const cells = this.graph.getSelectedCells()
  497. // if (!cells.length) {
  498. // return
  499. // }
  500. // this.handleContextmenu(e, cell)
  501. // })
  502. // 鼠标动态添加/删除小工具。
  503. graph.on('edge:mouseenter', ({ cell }) => {
  504. /**
  505. * EdgeTool
  506. * vertices 路径点工具,在路径点位置渲染一个小圆点,拖动小圆点修改路径点位置,双击小圆点删除路径点,在边上单击添加路径点。
  507. * segments 线段工具。在边的每条线段的中心渲染一个工具条,可以拖动工具条调整线段两端的路径点的位置。
  508. * boundary 根据边的包围盒渲染一个包围边的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。
  509. * button 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。
  510. * button-remove 在指定的位置处,渲染一个删除按钮,点击时删除对应的边。
  511. * source-arrowhead-和-target-arrowhead 在边的起点或终点渲染一个图形(默认是箭头),拖动该图形来修改边的起点或终点。
  512. * edge-editor 提供边上文本编辑功能。
  513. */
  514. cell.addTools([
  515. {
  516. name: 'vertices',
  517. args: {
  518. attrs: { fill: '#007acc' },
  519. // 移动路径点过程中的吸附半径。当路径点与邻近的路径点的某个坐标 (x, y) 距离在半径范围内时,将当前路径点的对应坐标 (x, y) 吸附到邻居路径的路径点。
  520. snapRadius: 20,
  521. // 在边上按下鼠标时,是否可以添加新的路径点。
  522. addable: true,
  523. // 是否可以通过双击移除路径点。
  524. removable: true,
  525. // 是否自动移除冗余的路径点。
  526. removeRedundancies: true,
  527. // 是否阻止工具上的鼠标事件冒泡到边视图上。阻止后鼠标与工具交互时将不会触发边的 mousedown、mousemove 和 mouseup 事件。
  528. stopPropagation: false
  529. }
  530. }
  531. ])
  532. })
  533. graph.on('edge:mouseleave', ({ cell }) => {
  534. cell.removeTools()
  535. })
  536. }
  537. }