@antv/xflow绘制流程图总结

1,597 阅读4分钟

背景

工作流编排,主体分三个部分,左侧是可拖拽节点区域,中间是画布编排区,右侧是节点和连接线配置面板 image.png

重点模块实现

自定义节点拖拽区域

由于官方示例 NodeDndPanel 节点拖拽面板,不能满足需求(多级菜单,样式,自定义搜索等)。可通过 WorkspacePanel 自定义组件,结合 antd Input、Menu实现,关键点是拖拽的实现,使用了xflow源码里的 useGraphDnd (位置在packages/xflow-extension/src/canvas-collapse-panel/panel-body/dnd-hook.tsx)给需要拖拽的元素绑定返回的 onMouseDown

画布区

基于 FlowchartCanvas 开发,因为比较喜欢它的锚点,鼠标移入才显示,移除隐藏的这点吧哈哈

连线虚线改实线

在 useCmdConfig 中 addEdge、updateEdge 如下设置

res.edgeCell.setAttrs({
  line: {
    stroke: '#A2B1C3',
    targetMarker: {
      name: 'block',
      width: 12,
      height: 8
    },
    strokeWidth: 1,
    strokeDasharray: 'unset' // 不要用虚线
  }
})

直接拖拽线的端点完成修改节点与节点间的连接

A_C3LbRodorDwAAAAAAAAAAAAAARQnAQ.gif 参考 antv-x6.gitee.io/zh/examples… 具体实现:

    graph.on('edge:mouseenter', async e => {
      const { mode } = await MODELS.GRAPH_META.useValue(app.modelService)
      if (mode === Operate.Status.CHECK) return
      const { cell, edge } = e
      changePortsVisible(true, e)
      cell.addTools([
        {
          name: 'source-arrowhead',
          args: {
            attrs: {
              d: 'M 12 -5 -1 0 12 5 Z',
              fill: '#379E0E',
              stroke: '#379E0E'
            }
          }
        },
        {
          name: 'target-arrowhead',
          args: {
            attrs: {
              d: 'M -12 -5 1 0 -12 5 Z',
              fill: '#379E0E',
              stroke: '#379E0E'
            }
          }
        }
      ])
    })

    graph.on('edge:mouseleave', async e => {
      const { mode } = await MODELS.GRAPH_META.useValue(app.modelService)
      if (mode === Operate.Status.CHECK) return
      const { cell, edge } = e
      changePortsVisible(false, e)
      const source = edge.getSource()
      const target = edge.getTarget()

      const _edge = await getEdgeById(edge.id)
      if (!_edge) return
      commandService.executeCommand<NsEdgeCmd.UpdateEdge.IArgs>(XFlowEdgeCommands.UPDATE_EDGE.id, {
        edgeConfig: {
          ..._edge.data,
          source,
          sourcePort: source.port,
          sourcePortId: source.port,
          target,
          targetPort: target.port,
          targetPortId: target.port,
          data: {
            ..._edge.data?.data,
            source: source.cell,
            sourcePortId: source.port,
            target: target.cell,
            targetPortId: target.port
          }
        },
        options: {}
      })
      cell.removeTools()
    })

其中 changePortsVisible 是控制显示锚点的方法

  const getContainer = e => {
    let currentNode = e?.e?.currentTarget
    if (!currentNode) {
      return document.getElementsByClassName('xflow-canvas-root')
    }
    let containter = null
    while (!containter) {
      const current = currentNode.getElementsByClassName('xflow-canvas-root')
      if (current?.length > 0) {
        containter = current
      }
      currentNode = currentNode.parentNode
    }
    return containter
  }
    const changePortsVisible = (visible: boolean, e?: any) => {
      const containers = getContainer(e)
      Array.from(containers).forEach((container: HTMLDivElement) => {
        const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>
        // 选中中节点时不展示链接桩
        const isSelectedNode = graph.getSelectedCells()?.[0]?.isNode()
        for (let i = 0, len = ports.length; i < len; i = i + 1) {
          ports[i].style.visibility = !isSelectedNode && visible ? 'visible' : 'hidden'
        }
      })
    }

一键美化功能

    commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(XFlowGraphCommands.SAVE_GRAPH_DATA.id, {
      saveGraphDataService: async (meta, graph) => {
        const res = await commandService.executeCommand<NsGraphCmd.GraphLayout.IArgs, NsGraphCmd.GraphLayout.IResult>(
          XFlowGraphCommands.GRAPH_LAYOUT.id,
          {
            layoutType: 'dagre',
            layoutOptions: {
              type: 'dagre',
              /** 布局方向 */
              rankdir: 'TB',
              /** x 节点间距 */
              nodesep: 300,
              /** y 层间距 */
              ranksep: 40
            },
            graphData: graph
          }
        )

        const { graphData } = res.contextProvider().getResult()
        // render
        await commandService.executeCommand<NsGraphCmd.GraphRender.IArgs>(XFlowGraphCommands.GRAPH_RENDER.id, {
          graphData: graphData
        })
        // 居中
        await commandService.executeCommand<NsGraphCmd.GraphZoom.IArgs>(XFlowGraphCommands.GRAPH_ZOOM.id, {
          factor: 'real'
        })

        return { err: null, data: graph, meta }
      }
    })

配置面板

数据的保存和更新

同样的方式,通过 WorkspacePanel 实现自定义组件,里边可以调用

const { commandService, getEdgeById, getNodeById } = useXFlowApp()

思路是 通过 getNodeById 方法拿到原数据nodeData,调用form拿到新值formValue,合并更新

更新节点信息

  commandService.executeCommand<NsNodeCmd.UpdateNode.IArgs>(XFlowNodeCommands.UPDATE_NODE.id, {
    nodeConfig: {
      ...nodeData,
      label: formValue.display,
      data: {
        ...nodeData.data,
        ...formValue
      }
    }
  })

更新连接点信息

    commandService.executeCommand<NsEdgeCmd.UpdateEdge.IArgs>(XFlowEdgeCommands.UPDATE_EDGE.id, {
    edgeConfig: {
      ...edgeData,
      label: formValue.display,
      data: {
        ...edgeData.data,
        ...formValue
      }
    }
    })

动态配置面板

这个需要加在获取左侧所有节点的接口里,比如节点信息有个input字段

input: [
    {
        label: '姓名',
        name: 'name',
        mode:'0' // 0 代表输入框 1代表选择器 等等约定好
    }
]

流程图的全局变量管理

需求

如下:

  1. 开始节点 可以定义任意数量的变量,这些变量可以在任何节点,任何连接线使用
  2. 某个节点可以使用从流程开始节点定义的变量 和 他之前的节点输出变量,如图,把最上边的图简化成

image.png

image.png 首先我们在开始节点定义两个变量args01、args02

image.png

那么 A节点只能使用 args01、args02

image.png

那么 B节点能使用 args01、args02,和A节点的输出o1、o2

image.png

那么 C节点能使用 args01、args02,和A节点的输出o1、o2 和 B节点的o4

image.png

那么 D节点能使用 args01、args02,和A节点的输出o1、o2

image.png

获取选项

const { getNodeById, getGraphData } = useXFlowApp()
// 获取开始节点的变量
const getStartNodeOptions = getNodeById('start').then(node => {
  const startNodeData = node.data
  const startNodeOptions =
    startNodeData.data.variables.length > 0
      ? [
          {
            label: '开始节点',
            value: 'start',
            // disabled: true,
            children: startNodeData.data.variables.map(({ display, name }) => ({
              label: display,
              value: name
            }))
          }
        ]
      : []
  return [...startNodeOptions]
})
const getPrevNodesOptions = (nodeData, nodeIdMap, edgeIdMap) => {
  const getPrevNodeIds = nodeData => {
    let prevNodeIds: string[] = []

    if (!nodeData.incomingEdges) return []
    for (const prevEdges of nodeData.incomingEdges) {
      // 删除向量,与该向量的节点并未同步,还保留该incomingEdges
      if (!edgeIdMap.get(prevEdges.id)) continue
      const prevNodeId = prevEdges.data.data.source
      const prevNode = nodeIdMap.get(prevNodeId)
      const renderKey = prevNode.renderKey
      if (renderKey !== FlowNode.RenderKey.TERMINAL && !prevNodeIds.includes(prevNodeId)) {
        if (renderKey !== FlowNode.RenderKey.DECISION) {
          prevNodeIds.push(prevNodeId)
        }
        const prevNode = nodeIdMap.get(prevNodeId)
        prevNodeIds = [...prevNodeIds, ...getPrevNodeIds(prevNode)]
      }
    }
    return [...new Set(prevNodeIds)] // 有重复 ,待优化
  }
  const prevNodeIds = getPrevNodeIds(nodeData)
  const prevNodesOptions = prevNodeIds.map(prevNodeId => {
    const nodeData = nodeIdMap.get(prevNodeId)
    return {
      label: nodeData.data.display,
      value: /* nodeData.data.name */ prevNodeId, // 防止标识相同
      // disabled: true,
      children: nodeData.data.output
    }
  })
  return Promise.resolve(prevNodesOptions)
}
const getGlobalVariablesOption = nodeData => {
  return getGraphData()
    .then(({ nodes, edges }) => {
      const nodeIdMap = new Map()
      for (const node of nodes) {
        nodeIdMap.set(node.id, node)
      }
      const edgeIdMap = new Map()
      for (const edge of edges) {
        edgeIdMap.set(edge.id, edge)
      }
      return {
        nodeIdMap,
        edgeIdMap
      }
    })
    .then(({ nodeIdMap, edgeIdMap }) => {
      return Promise.all([getStartNodeOptions, getPrevNodesOptions(nodeData, nodeIdMap, edgeIdMap)]).then(
        ([startNodeOptions, prevNodesOptions]) => {
          return [...startNodeOptions, ...prevNodesOptions]
        }
      )
    })
}

上级变量修改或者删除,同步给下级使用的节点

就是说,上方变量被删了,下边节点的输入框里就显示一堆id字符串了,因为没有这个选项了 思路:在每次保存节点或者连接线的数据时,在被使用的变量处保存一个被使用的字段如useid来保存哪个节点使用该变量,当该变量被修改删除时,同步修改useid执行的对应节点的值

结语

将 graph实例 保存到 window 有奇效!!!