流程设计是信息系统中常用的功能,下面就是如何用Vue3+AntV X6来打造一个流程设计器。先上效果图与代码,之后再对核心代码片段进行说明。
流程设计器:究极死胖兽/sps-flow-design (gitee.com)
注:系统基于笔者自己写的模板 究极死胖兽/sps-vite-simple (gitee.com)开发。
整体布局
组件分为四个区域:
- 头部区域:标题与工具栏,可拓展更多的操作按钮。
- 主体左侧:节点库,可拓展更多类型的节点
- 主体中部:流程面板
- 主体右侧:流程/节点/操作配置,可拓展更多的配置项 设计器的实现的功能为,从节点库中将所需节点拖拽入流程面板中,在面板中可进行删除,移动,连线等操作,选中节点/操作后,可对其data中挂载的内容进行配置,未选中时对流程自身属性进行配置,并可将流程及其节点/操作以Json格式输出。
<a-layout class="sps-flow-design h-full w-full">
{/* 标题与工具栏 */}
<a-layout-header class="flex">
<div class="flex-1">Sps-Flow-Design</div>
<div>
<a-button onClick={ modalShow }>Json</a-button>
</div>
</a-layout-header>
{/* 设计器主体 */}
<a-layout>
<a-layout-content class="flex">
{/* 节点库 */}
<div ref={ stencilContanerRef } class="w-60 h-full relative border-r-2 border-gray-300"></div>
{/* 流程面板 */}
<div ref={ graphContanerRef } class="h-full flex-1" style={{ height: '100%' }}></div>
</a-layout-content>
{/* 流程/节点/操作 配置 */}
<a-layout-sider width={ 300 }>
{
//@ts-ignore
flowState.currentCell ? <CellConfig onSubmit={ updateCell } /> : <FlowConfig />
}
</a-layout-sider>
</a-layout>
</a-layout>
流程面板初始化
graph = new Graph({
container: graphContainer,
grid: true, // 显示网格线
connecting: {
router: 'manhattan', // 连接线路由算法
connector: {
name: 'rounded', // 连接线圆角
args: {
radius: 20
}
},
snap: {
radius: 50 // 锚点吸附半径
},
allowBlank: false, // 禁用连线到空处
allowMulti: false, // 禁用在同一对节点中多条连线
allowLoop: false, // 禁用自循环连线
connectionPoint: 'anchor' // 连接点为锚点
},
selecting: {
enabled: true, // 节点/边 可选中
showNodeSelectionBox: true, // 节点选中后边框
showEdgeSelectionBox: true // 边选中后边框
},
snapline: true, // 启用对齐线
keyboard: true, // 启用键盘事件
clipboard: true, // 启用粘贴板
history: true // 启用历史记录
})
节点库初始化
使用x6中的Stencil插件:
const stencil = new Addon.Stencil({
title: '流程节点',
target: graph, // 绑定流程面板
stencilGraphWidth: 200, // 节点库宽度
stencilGraphHeight: 600, // 节点库高度
layoutOptions: {
columns: 2, // 每行节点数
rowhHeight: 40 // 行高
}
})
stencilContainer.appendChild(stencil.container)
自定义节点
注册自定义节点基类
export const registerNode = () => {
// 注册自定义圆形
Graph.registerNode('sps-circle', {
inherit: 'circle',
width: 45,
height: 45,
attrs,
ports: createPorts() // 锚点配置
})
// 注册自定义矩形
Graph.registerNode('sps-rect', {
inherit: 'rect',
width: 66,
height: 36,
attrs,
ports: createPorts()
})
}
根据自定义节点基类创建节点:
const createNodes = (graph: Graph) => {
const startNode = graph.createNode({
shape: 'sps-circle',
label: '开始'
})
const eventNode = graph.createNode({
shape: 'sps-rect',
label: '事件',
attrs: {
body: {
rx: 20,
ry: 20
}
}
})
const endNode = graph.createNode({
shape: 'sps-circle',
label: '结束'
})
return {
startNode,
eventNode,
endNode
}
}
将创建的自定义节点加入到节点库中:
const { startNode, eventNode, endNode } = createNodes(graph)
stencil.load([startNode, eventNode, endNode])
自定义事件
键盘事件
export default function registerKeyboardEvent (graph: Graph) {
// #region 复制 剪切 粘贴
graph.bindKey(_createCtrlKey('c'), () => {
const cells = graph.getSelectedCells()
if (cells.length) {
graph.copy(cells)
}
return false
})
graph.bindKey(_createCtrlKey('x'), () => {
const cells = graph.getSelectedCells()
if (cells.length) {
graph.cut(cells)
}
return false
})
graph.bindKey(_createCtrlKey('v'), () => {
if (!graph.isClipboardEmpty()) {
const cells = graph.paste({ offset: 32 })
graph.cleanSelection()
graph.select(cells)
}
return false
})
// #endregion
// #region 撤销 重做
graph.bindKey(_createCtrlKey('z'), () => {
if (graph.history.canUndo()) {
graph.history.undo()
}
return false
})
graph.bindKey(_createCtrlKey('z', true), () => {
if (graph.history.canRedo()) {
graph.history.redo()
}
return false
})
// #endregion
// 删除
graph.bindKey('backspace', () => {
const cells = graph.getSelectedCells()
if (cells.length) {
graph.removeCells(cells)
}
})
}
节点/边事件
export default function registerNodeEvent (graph: Graph, container: HTMLElement, state: FlowState) {
// #region 控制连接桩显示/隐藏
const showPorts = (ports: any, show: boolean) => {
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? 'visible' : 'hidden'
}
}
graph.on('node:mouseenter', () => {
const ports = container.querySelectorAll(
'.x6-port-body'
)
showPorts(ports, true)
})
graph.on('node:mouseleave', () => {
const ports = container.querySelectorAll(
'.x6-port-body'
)
showPorts(ports, false)
})
// #endregion
// #region 节点/边 选中/取消选中
graph.on('cell:selected', ({ cell }) => {
state.currentCell = {
id: cell.id,
...cell.data,
isNode: cell.isNode()
}
})
graph.on('cell:unselected', () => {
state.currentCell = null
})
// #endregion
}
状态管理
本组件采用provide/inject的方式来进行状态管理
详见:Vue3+TS 优雅地使用状态管理 - 掘金 (juejin.cn)
export interface FlowState {
currentCell: any
flow: any
}
export const createFlowState = () => {
const state: FlowState = reactive({
currentCell: null,
flow: {
id: v4()
}
})
return state
}
export const flowStateKey: InjectionKey<FlowState> = Symbol('FlowState')
export const useFlowState = () => {
const state = inject(flowStateKey)!
return {
state
}
}
配置
在节点配置组件与流程配置组件中通过useFlowState实现与根组件的交互。
节点配置
目前的配置项仅作为示例,在完善的系统中,审批人应当与用户角色模块相结合。后续还可将节点与表单进行绑定。
export default defineComponent({
name: 'CellConfig',
emits: ['submit'],
setup (_, { emit }) {
const { state } = useFlowState()
const submit = () => {
emit('submit', state.currentCell!)
}
/* render 函数 */
return () => {
const cell = state.currentCell!
const text = cell.isNode ? '节点' : '操作'
const nodeConfig = (
<a-form-item label="审批人">
<a-input v-model={[cell.audit, 'value']} />
</a-form-item>
)
return (
<a-form class="p-1" layout="vertical" model={ cell }>
<a-form-item label={`${text}ID`}>{ cell.id }</a-form-item>
<a-form-item label={`${text}名称`}>
<a-input v-model={[cell.name, 'value']} />
</a-form-item>
{ cell.isNode && nodeConfig }
<a-form-item>
<a-button type="primary" onClick={ submit }><i class='far fa-save'></i> 保存</a-button>
</a-form-item>
</a-form>
)
}
}
})
流程配置
export default defineComponent({
name: 'FlowConfig',
setup () {
const { state } = useFlowState()
/* render 函数 */
return () => {
const flow = state.flow
return (
<a-form class="p-1" layout="vertical" model={ flow }>
<a-form-item label="流程ID">{ flow.id }</a-form-item>
<a-form-item label="流程名称">
<a-input v-model={[flow.name, 'value']} />
</a-form-item>
</a-form>
)
}
}
})
Json输出
将流程相关内容转化为JSON,在完善的系统中,可将其内容存到数据库中,后续就可使用graph.fromJSON(data)方法生成流程图。
const graphToJson = () => {
return {
...state.flow,
nodes: graph.getNodes().map(item => {
// @ts-ignore
const { id, shape, label, ports: { items }, data } = item
const { x, y } = item.position()
return {
id,
shape,
label,
x,
y,
ports: {
items
},
data
}
}),
edges: graph.getEdges().map(item => ({
target: item.target,
source: item.source,
data: item.data
}))
}
}