背景
工作流编排,主体分三个部分,左侧是可拖拽节点区域,中间是画布编排区,右侧是节点和连接线配置面板
重点模块实现
自定义节点拖拽区域
由于官方示例 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' // 不要用虚线
}
})
直接拖拽线的端点完成修改节点与节点间的连接
参考 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代表选择器 等等约定好
}
]
流程图的全局变量管理
需求
如下:
- 开始节点 可以定义任意数量的变量,这些变量可以在任何节点,任何连接线使用
- 某个节点可以使用从流程开始节点定义的变量 和 他之前的节点输出变量,如图,把最上边的图简化成
首先我们在开始节点定义两个变量args01、args02
那么 A节点只能使用 args01、args02
那么 B节点能使用 args01、args02,和A节点的输出o1、o2
那么 C节点能使用 args01、args02,和A节点的输出o1、o2 和 B节点的o4
那么 D节点能使用 args01、args02,和A节点的输出o1、o2
获取选项
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 有奇效!!!