Leo před 2 roky
rodič
revize
f4d5951b1b

+ 2 - 0
package.json

@@ -5,6 +5,8 @@
   "dependencies": {
     "@ant-design/compatible": "^1.1.2",
     "@ant-design/pro-components": "^1.1.15",
+    "@antv/x6": "^1.34.1",
+    "@antv/x6-react-shape": "^1.6.1",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.3.0",
     "@testing-library/user-event": "^13.5.0",

+ 0 - 1
src/App.jsx

@@ -48,7 +48,6 @@ export default function Sidebar() {
     }
   })
   const list = routesInfo.concat(...childrenList)
-  console.log('routes', list)
 
   return (
     <SidebarDiv>

+ 57 - 0
src/module/taskmgmt/component/AlgoNode.jsx

@@ -0,0 +1,57 @@
+import React from 'react'
+import styled from 'styled-components'
+import '@antv/x6-react-shape'
+
+const NodeWrapper = styled.div`
+  .node {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    background-color: #fff;
+    border: 1px solid #c2c8d5;
+    border-left: 4px solid #5f95ff;
+    border-radius: 4px;
+    box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
+  }
+  .node .label {
+    display: inline-block;
+    flex-shrink: 0;
+    width: 104px;
+    margin-left: 8px;
+    color: #666;
+    font-size: 12px;
+  }
+  .node .status {
+    flex-shrink: 0;
+  }
+`
+
+export class AlgoNode extends React.Component {
+  shouldComponentUpdate() {
+    const { node } = this.props
+    if (node) {
+      if (node.hasChanged('data')) {
+        return true
+      }
+    }
+    return false
+  }
+
+  render() {
+    const { node } = this.props
+    const data = node?.getData()
+    const { label, status = 'default', type, uri } = data
+
+    return (
+      <NodeWrapper>
+        <div className={`node ${status}`}>
+          <span className="label">{label}</span>
+          {type === 'work' && <span className="label">{uri}</span>}
+        </div>
+      </NodeWrapper>
+    )
+  }
+}
+
+export default AlgoNode

+ 386 - 0
src/module/taskmgmt/component/TaskChartEditor.jsx

@@ -0,0 +1,386 @@
+import React from 'react'
+import styled from 'styled-components'
+import { Graph, Addon, Path } from '@antv/x6'
+import AlgoNode from './AlgoNode'
+
+const EditorWrapper = styled.div`
+  margin-top: 20px;
+  height: 450px;
+  background: #f0f2f5;
+  border-radius: 2px;
+  border: 1px solid #c8d3e9;
+
+  .task-stencil {
+    width: 200px;
+    position: relative;
+  }
+
+  .task-content {
+    flex: 3;
+    height: 100%;
+    position: relative;
+  }
+
+  .task-graph {
+    height: 100% !important;
+    width: auto !important;
+  }
+`
+
+//起始节点
+
+const beginNodes = [
+  {
+    id: '0',
+    shape: 'task-node',
+    height: 80,
+    width: 180,
+    data: {
+      label: '开始',
+      type: 'begin',
+    },
+    ports: {
+      items: [{ id: 'bottomPort', group: 'bottom' }],
+    },
+  },
+]
+
+// 作业节点
+const workNodes = [
+  {
+    id: '1',
+    shape: 'task-node',
+    data: {
+      label: '每日供应商销量预测作业',
+      type: 'work',
+      uri: 'xxx.com:get_data',
+    },
+    ports: {
+      items: [
+        { id: 'topPort', group: 'top' },
+        { id: 'bottomPort', group: 'bottom' },
+      ],
+    },
+  },
+  {
+    id: '2',
+    shape: 'task-node',
+    data: {
+      label: 'HR材料周更新作业',
+      type: 'work',
+      uri: 'xxx.com:update_data',
+    },
+    ports: {
+      items: [
+        { id: 'topPort', group: 'top' },
+        { id: 'bottomPort', group: 'bottom' },
+      ],
+    },
+  },
+]
+
+// 注册节点
+Graph.registerNode(
+  'task-node',
+  {
+    inherit: 'react-shape',
+    width: 180,
+    height: 36,
+    component: <AlgoNode />,
+    ports: {
+      groups: {
+        top: {
+          position: 'top',
+          attrs: {
+            circle: {
+              r: 4,
+              magnet: true,
+              stroke: '#C2C8D5',
+              strokeWidth: 1,
+              fill: '#fff',
+            },
+          },
+        },
+        bottom: {
+          position: 'bottom',
+          attrs: {
+            circle: {
+              r: 4,
+              magnet: true,
+              stroke: '#C2C8D5',
+              strokeWidth: 1,
+              fill: '#fff',
+            },
+          },
+        },
+      },
+    },
+  },
+  true
+)
+
+// 注册边
+Graph.registerEdge(
+  'task-edge',
+  {
+    inherit: 'edge',
+    attrs: {
+      line: {
+        stroke: '#C2C8D5',
+        strokeWidth: 1,
+        targetMarker: 'block',
+      },
+    },
+  },
+  true
+)
+
+// 注册连接
+Graph.registerConnector(
+  'algo-connector',
+  (s, e) => {
+    const offset = 4
+    const 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
+)
+
+// 侧边栏UI组件
+const { Stencil } = Addon
+// 存储边
+const taskEdges = []
+export default class TaskChartEditor extends React.Component {
+  // 容器DIV
+  container
+  // 侧边栏UI容器
+  stencilContainer
+  // 构造函数
+  constructor(props) {
+    super(props)
+    this.state = {
+      taskGraph: null,
+      selectedNodeData: {},
+      contextMenu: null,
+    }
+  }
+  //挂载
+  componentDidMount() {
+    //创建图
+    const graph = new Graph({
+      //容器
+      container: this.container,
+      background: {
+        color: '#f0f2f5',
+      },
+      // 拖动
+      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,
+        connector: 'algo-connector',
+        connectionPoint: 'anchor',
+        anchor: 'center',
+        // 验证
+        validateMagnet({ magnet }) {
+          return magnet.getAttribute('port-group') !== 'top'
+        },
+        validateConnection({ targetPort, sourceCell, targetCell }) {
+          const jugeEdge = taskEdges.find(
+            item =>
+              item?.source === sourceCell?.id && item?.target === targetCell?.id
+          )
+          if (jugeEdge) {
+            return false
+          }
+          return targetPort === 'topPort'
+        },
+        createEdge() {
+          return graph.createEdge({
+            shape: 'task-edge',
+            attrs: {
+              line: {
+                strokeDasharray: '5 5',
+              },
+            },
+            zIndex: -1,
+          })
+        },
+      },
+      // 选择
+      selecting: {
+        enabled: true,
+        multiple: true,
+        rubberEdge: true,
+        rubberNode: true,
+        modifiers: 'shift',
+        rubberband: true,
+      },
+      // 快捷键
+      keyboard: {
+        enabled: true,
+        global: true,
+      },
+    })
+    // 删除节点
+    graph.bindKey(['delete', 'backspace'], () => {
+      graph.getSelectedCells().forEach(item => {
+        item.remove()
+      })
+    })
+
+    // 监听边连接
+    graph.on('edge:connected', ({ edge }) => {
+      taskEdges.push({
+        source: edge.getSourceCell()?.id,
+        target: edge.getTargetCell()?.id,
+      })
+      const targetNodeData = edge.getTargetCell()?.data
+      targetNodeData.inputNumber += 1
+      edge.attr({
+        line: {
+          strokeDasharray: '',
+        },
+      })
+    })
+
+    graph.on('edge:removed', ({ edge }) => {
+      const sourceNodeId = edge.store.data.source.cell
+      const targetNodeId = edge.store.data.target.cell
+
+      const index = taskEdges.indexOf({
+        source: sourceNodeId,
+        target: targetNodeId,
+      })
+
+      const targetNode = this.state.taskGraph.getCellById(targetNodeId)
+
+      if (targetNode) {
+        targetNode.data.inputNumber -= 1
+      }
+
+      taskEdges.splice(index, 1)
+    })
+
+    // 内容居中
+    graph.centerContent()
+
+    // 创建侧边栏
+    const stencil = new Stencil({
+      title: '作业检索',
+      target: graph,
+      // 搜索
+      search(cell, keyword) {
+        return cell.data.label.indexOf(keyword) !== -1
+      },
+      placeholder: '搜索作业',
+      notFoundText: '未找到相关作业',
+      scaled: true,
+      // animation: true,
+      // 侧边栏项宽高
+      stencilGraphWidth: 200,
+      stencilGraphHeight: 200,
+      layoutOptions: {
+        columns: 1,
+        dx: 55,
+        rowHeight: 'compact',
+      },
+      // 侧边栏项目分组
+      groups: [
+        { name: 'begin', title: '开始' },
+        {
+          name: 'work',
+          title: '作业',
+        },
+      ],
+      // 拖拽事件 处理节点数据
+      getDropNode(draggingNode) {
+        const newNode = draggingNode.clone()
+        newNode.data.uri = undefined
+        return newNode
+      },
+    })
+
+    // 添加到侧边栏容器内
+    this.stencilContainer.appendChild(stencil.container)
+    // 将可拖拽项加载到侧边栏中
+    stencil.load(beginNodes, 'begin')
+    stencil.load(workNodes, 'work')
+    stencil.resizeGroup('begin', {
+      width: 200,
+      height: beginNodes.length * 120,
+    })
+    stencil.resizeGroup('work', {
+      width: 200,
+      height: beginNodes.length * 60,
+    })
+    // 设置图
+    this.setState({ taskGraph: graph })
+    console.log('2222', graph, this.state.taskGraph)
+  }
+
+  // 卸载
+  componentWillUnmount() {
+    this.state.taskGraph.dispose()
+  }
+  // 容器
+  refContainer = container => {
+    this.container = container
+  }
+
+  // 侧边栏
+  refStencil = container => {
+    this.stencilContainer = container
+  }
+
+  render() {
+    return (
+      <EditorWrapper>
+        {/* 侧边栏 */}
+        <div className="task-stencil" ref={this.refStencil} />
+        {/* 容器 */}
+        <div className="task-content">
+          <div className="task-graph" ref={this.refContainer} />
+        </div>
+      </EditorWrapper>
+    )
+  }
+}

+ 7 - 7
src/module/taskmgmt/component/TaskCreaterView.jsx

@@ -1,6 +1,7 @@
 import React, { useState } from 'react'
 import { Form, Select } from 'antd'
 import TaskForm from './TaskForm'
+import TaskChartEditor from './TaskChartEditor'
 import styled from 'styled-components'
 
 const TaskCreater = styled.div`
@@ -38,14 +39,9 @@ const { Option } = Select
 const TaskCreaterView = () => {
   const [taskType, setTaskType] = useState(null)
   const [taskTypeForm] = Form.useForm()
-  const [singleTaskForm] = Form.useForm()
+  const [taskForm] = Form.useForm()
   const onTaskTypeChange = value => {
     setTaskType(value)
-    console.log(
-      value,
-      singleTaskForm.getFieldValue(),
-      taskTypeForm.getFieldValue()
-    )
   }
   return (
     <TaskCreater>
@@ -65,7 +61,11 @@ const TaskCreaterView = () => {
         </FormItem>
       </Form>
       <div className="tasktype_label">配置任务信息:</div>
-      <TaskForm singleTaskForm={singleTaskForm} taskType={taskType} />
+      <TaskForm taskForm={taskForm} taskType={taskType} />
+      {taskType === 'multitasking' && (
+        <div className="tasktype_label">作业编排:</div>
+      )}
+      {taskType === 'multitasking' && <TaskChartEditor />}
     </TaskCreater>
   )
 }

+ 47 - 0
yarn.lock

@@ -246,6 +246,23 @@
     lodash "^4.17.21"
     resize-observer-polyfill "^1.5.1"
 
+"@antv/x6-react-shape@^1.6.1":
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/@antv/x6-react-shape/-/x6-react-shape-1.6.1.tgz#cf029ac6580300eaffa7539555cffa6723e8f199"
+  integrity sha512-EkcDoIlfbQC69DGgwznFRsi78fW+apTO0OW5J6tnKpMW3r6zB5zr5QM06hzq5UEUgGheMUxNh3wrTd0jnCwR3A==
+
+"@antv/x6@^1.34.1":
+  version "1.34.1"
+  resolved "https://registry.yarnpkg.com/@antv/x6/-/x6-1.34.1.tgz#33b17ad82a494ff50a74d918c1feba596df68183"
+  integrity sha512-4dNE9h//SY5ID8W+9YU5dE58d0+V9lCXlg0CiI6+4jFCud3RfLkPjni1dpmUo+HDWtrQ0wB80o42HLat9+FYZA==
+  dependencies:
+    csstype "^3.0.3"
+    jquery "^3.5.1"
+    jquery-mousewheel "^3.1.13"
+    lodash-es "^4.17.15"
+    mousetrap "^1.6.5"
+    utility-types "^3.10.0"
+
 "@apideck/better-ajv-errors@^0.3.1":
   version "0.3.6"
   resolved "https://registry.npmmirror.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097"
@@ -3923,6 +3940,11 @@ csstype@^3.0.2:
   resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
   integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
 
+csstype@^3.0.3:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
+  integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
+
 damerau-levenshtein@^1.0.8:
   version "1.0.8"
   resolved "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -6256,6 +6278,16 @@ jest@^27.4.3:
     import-local "^3.0.2"
     jest-cli "^27.5.1"
 
+jquery-mousewheel@^3.1.13:
+  version "3.1.13"
+  resolved "https://registry.yarnpkg.com/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz#06f0335f16e353a695e7206bf50503cb523a6ee5"
+  integrity sha512-GXhSjfOPyDemM005YCEHvzrEALhKDIswtxSHSR2e4K/suHVJKJxxRCGz3skPjNxjJjQa9AVSGGlYjv1M3VLIPg==
+
+jquery@^3.5.1:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.1.tgz#fab0408f8b45fc19f956205773b62b292c147a16"
+  integrity sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==
+
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -6484,6 +6516,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash-es@^4.17.15:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -6718,6 +6755,11 @@ moment@^2.24.0, moment@^2.27.0, moment@^2.29.2, moment@^2.29.4:
   resolved "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
   integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
 
+mousetrap@^1.6.5:
+  version "1.6.5"
+  resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
+  integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
+
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -9697,6 +9739,11 @@ utila@~0.4:
   resolved "https://registry.npmmirror.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
   integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==
 
+utility-types@^3.10.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
+  integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
+
 utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"