|
@@ -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>
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+}
|