TaskChartEditor.jsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import React from 'react'
  2. import styled from 'styled-components'
  3. import { Graph, Addon, Path } from '@antv/x6'
  4. import AlgoNode from './AlgoNode'
  5. const EditorWrapper = styled.div`
  6. margin-top: 20px;
  7. height: 450px;
  8. background: #f0f2f5;
  9. border-radius: 2px;
  10. border: 1px solid #c8d3e9;
  11. .task {
  12. font-family: sans-serif;
  13. padding: 0;
  14. display: flex;
  15. position: relative;
  16. height: 100%;
  17. }
  18. .task-stencil {
  19. width: 200px;
  20. position: relative;
  21. border-right: 1px solid #c8d3e9;
  22. }
  23. .task-content {
  24. flex: 3;
  25. height: 100%;
  26. position: relative;
  27. }
  28. .task-graph {
  29. height: 100% !important;
  30. width: auto !important;
  31. }
  32. .x6-graph-scroller {
  33. border: 1px solid #f0f0f0;
  34. margin-left: -1px;
  35. }
  36. `
  37. //起始节点
  38. const beginNodes = [
  39. {
  40. shape: 'task-node',
  41. height: 50,
  42. width: 180,
  43. data: {
  44. id: '0',
  45. label: '开始',
  46. type: 'begin',
  47. },
  48. ports: {
  49. items: [{ id: 'bottomPort', group: 'bottom' }],
  50. },
  51. },
  52. ]
  53. // 作业节点
  54. const workNodes = [
  55. {
  56. shape: 'task-node',
  57. data: {
  58. id: '1',
  59. label: '每日供应商销量预测作业',
  60. type: 'work',
  61. },
  62. ports: {
  63. items: [
  64. { id: 'topPort', group: 'top' },
  65. { id: 'bottomPort', group: 'bottom' },
  66. ],
  67. },
  68. },
  69. {
  70. shape: 'task-node',
  71. data: {
  72. id: '2',
  73. label: 'HR材料周更新作业',
  74. type: 'work',
  75. },
  76. ports: {
  77. items: [
  78. { id: 'topPort', group: 'top' },
  79. { id: 'bottomPort', group: 'bottom' },
  80. ],
  81. },
  82. },
  83. ]
  84. // 注册节点
  85. Graph.registerNode(
  86. 'task-node',
  87. {
  88. inherit: 'react-shape',
  89. width: 180,
  90. height: 50,
  91. component: <AlgoNode />,
  92. ports: {
  93. groups: {
  94. top: {
  95. position: 'top',
  96. attrs: {
  97. circle: {
  98. r: 4,
  99. magnet: true,
  100. stroke: '#C2C8D5',
  101. strokeWidth: 1,
  102. fill: '#fff',
  103. },
  104. },
  105. },
  106. bottom: {
  107. position: 'bottom',
  108. attrs: {
  109. circle: {
  110. r: 4,
  111. magnet: true,
  112. stroke: '#C2C8D5',
  113. strokeWidth: 1,
  114. fill: '#fff',
  115. },
  116. },
  117. },
  118. },
  119. },
  120. },
  121. true
  122. )
  123. // 注册边
  124. Graph.registerEdge(
  125. 'task-edge',
  126. {
  127. inherit: 'edge',
  128. attrs: {
  129. line: {
  130. stroke: '#C2C8D5',
  131. strokeWidth: 1,
  132. targetMarker: 'block',
  133. },
  134. },
  135. },
  136. true
  137. )
  138. // 注册连接
  139. Graph.registerConnector(
  140. 'algo-connector',
  141. (s, e) => {
  142. const offset = 4
  143. const deltaY = Math.abs(e.y - s.y)
  144. const control = Math.floor((deltaY / 3) * 2)
  145. const v1 = { x: s.x, y: s.y + offset + control }
  146. const v2 = { x: e.x, y: e.y - offset - control }
  147. return Path.normalize(
  148. `M ${s.x} ${s.y}
  149. L ${s.x} ${s.y + offset}
  150. C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
  151. L ${e.x} ${e.y}
  152. `
  153. )
  154. },
  155. true
  156. )
  157. // 侧边栏UI组件
  158. const { Stencil } = Addon
  159. // 存储边
  160. const taskEdges = []
  161. export default class TaskChartEditor extends React.Component {
  162. // 容器DIV
  163. container
  164. // 侧边栏UI容器
  165. stencilContainer
  166. // 构造函数
  167. constructor(props) {
  168. super(props)
  169. this.state = {
  170. taskGraph: null,
  171. selectedNodeData: {},
  172. contextMenu: null,
  173. }
  174. }
  175. //挂载
  176. componentDidMount() {
  177. this.props.onRef(this)
  178. //创建图
  179. const graph = new Graph({
  180. //容器
  181. container: this.container,
  182. background: {
  183. color: '#f0f2f5',
  184. },
  185. // 拖动
  186. panning: {
  187. enabled: true,
  188. eventTypes: ['leftMouseDown', 'mouseWheel'],
  189. },
  190. // 放大缩小
  191. mousewheel: {
  192. enabled: true,
  193. modifiers: 'ctrl',
  194. factor: 1.1,
  195. maxScale: 1.5,
  196. minScale: 0.5,
  197. },
  198. // 交互触发高亮
  199. highlighting: {
  200. magnetAdsorbed: {
  201. name: 'stroke',
  202. args: {
  203. attrs: {
  204. fill: '#fff',
  205. stroke: '#31d0c6',
  206. strokeWidth: 4,
  207. },
  208. },
  209. },
  210. },
  211. // 连接
  212. connecting: {
  213. snap: true,
  214. allowBlank: false,
  215. allowLoop: false,
  216. highlight: true,
  217. connector: 'algo-connector',
  218. connectionPoint: 'anchor',
  219. anchor: 'center',
  220. // 验证
  221. validateMagnet({ magnet }) {
  222. return magnet.getAttribute('port-group') !== 'top'
  223. },
  224. validateConnection({ targetPort, sourceCell, targetCell }) {
  225. const jugeEdge = taskEdges.find(
  226. item =>
  227. item?.source === sourceCell?.id && item?.target === targetCell?.id
  228. )
  229. if (jugeEdge) {
  230. return false
  231. }
  232. return targetPort === 'topPort'
  233. },
  234. createEdge() {
  235. return graph.createEdge({
  236. shape: 'task-edge',
  237. attrs: {
  238. line: {
  239. strokeDasharray: '5 5',
  240. },
  241. },
  242. zIndex: -1,
  243. })
  244. },
  245. },
  246. // 选择
  247. selecting: {
  248. enabled: true,
  249. multiple: true,
  250. rubberEdge: true,
  251. rubberNode: true,
  252. modifiers: 'shift',
  253. rubberband: true,
  254. },
  255. // 快捷键
  256. keyboard: {
  257. enabled: true,
  258. global: true,
  259. },
  260. })
  261. // 删除节点
  262. graph.bindKey(['delete', 'backspace'], () => {
  263. graph.getSelectedCells().forEach(item => {
  264. item.remove()
  265. })
  266. })
  267. // 监听边连接
  268. graph.on('edge:connected', ({ edge }) => {
  269. taskEdges.push({
  270. source: edge.getSourceCell()?.id,
  271. target: edge.getTargetCell()?.id,
  272. })
  273. edge.attr({
  274. line: {
  275. strokeDasharray: '',
  276. },
  277. })
  278. })
  279. graph.on('edge:removed', ({ edge }) => {
  280. const sourceNodeId = edge.store.data.source.cell
  281. const targetNodeId = edge.store.data.target.cell
  282. const index = taskEdges.indexOf({
  283. source: sourceNodeId,
  284. target: targetNodeId,
  285. })
  286. taskEdges.splice(index, 1)
  287. })
  288. // 内容居中
  289. graph.centerContent()
  290. // 创建侧边栏
  291. const stencil = new Stencil({
  292. title: '作业检索',
  293. target: graph,
  294. // 搜索
  295. search(cell, keyword) {
  296. return cell.data.label.indexOf(keyword) !== -1
  297. },
  298. placeholder: '搜索作业',
  299. notFoundText: '未找到相关作业',
  300. scaled: true,
  301. // animation: true,
  302. // 侧边栏项宽高
  303. stencilGraphWidth: 200,
  304. stencilGraphHeight: 200,
  305. layoutOptions: {
  306. columns: 1,
  307. dx: 55,
  308. rowHeight: 'compact',
  309. },
  310. // 侧边栏项目分组
  311. groups: [
  312. { name: 'begin', title: '开始' },
  313. {
  314. name: 'work',
  315. title: '作业',
  316. },
  317. ],
  318. // 拖拽事件 处理节点数据
  319. getDropNode(draggingNode) {
  320. const newNode = draggingNode.clone()
  321. return newNode
  322. },
  323. })
  324. // 添加到侧边栏容器内
  325. this.stencilContainer.appendChild(stencil.container)
  326. // 将可拖拽项加载到侧边栏中
  327. stencil.load(beginNodes, 'begin')
  328. stencil.load(workNodes, 'work')
  329. stencil.resizeGroup('begin', {
  330. width: 200,
  331. height: beginNodes.length * 80,
  332. })
  333. stencil.resizeGroup('work', {
  334. width: 200,
  335. height: workNodes.length * 80,
  336. })
  337. // 设置图
  338. this.setState({ taskGraph: graph })
  339. }
  340. // 卸载
  341. componentWillUnmount() {
  342. this.state.taskGraph.dispose()
  343. }
  344. // 容器
  345. refContainer = container => {
  346. this.container = container
  347. }
  348. // 侧边栏
  349. refStencil = container => {
  350. this.stencilContainer = container
  351. }
  352. render() {
  353. return (
  354. <EditorWrapper>
  355. <div className="task">
  356. {/* 侧边栏 */}
  357. <div className="task-stencil" ref={this.refStencil} />
  358. {/* 容器 */}
  359. <div className="task-content">
  360. <div className="task-graph" ref={this.refContainer} />
  361. </div>
  362. </div>
  363. </EditorWrapper>
  364. )
  365. }
  366. }