Flow design is a common function in the information system, the following is how to use Vue3+AntV X6 to create a flow designer. Renderings and code will be shown first, followed by core code snippets.
SVG Designer + Icon Library: SPS-flow-Design
Note: The system is developed based on the template SPS-viet-Simple written by the author.
The overall layout
Components are divided into four areas:
- Header area: title and toolbar, more operation buttons can be expanded.
- To the left of the main body: node library for more types of nodes
- Middle of body: Process panel
- On the right side of the main body: Process/node/operation configuration, you can expand more configuration items
For the realization of the designer’s function, will be required from the node drag and drop into the process panel, can be removed in the panel, mobile, attachment, such as operation, the selected node/after the operation, can be mounted in its data configuration of the content, not selected configuration properties on the process itself, and can process and its output node/operation in Json format.
<a-layout class="sps-flow-design h-full w-full">
{/* Title and toolbar */}
<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>
{/* Designer body */}
<a-layout>
<a-layout-content class="flex">{/* Node library */}<div ref={ stencilContanerRef } class="w-60 h-full relative border-r-2 border-gray-300"></div>{/* Process panel */}<div ref={ graphContanerRef } class="h-full flex-1" style={{ height: '100'}} % ></div>
</a-layout-content>
{/* Process/node/operation configuration */}
<a-layout-sider width={ 300} > {//@ts-ignore
flowState.currentCell ? <CellConfig onSubmit={ updateCell} / > : <FlowConfig />
}
</a-layout-sider>
</a-layout>
</a-layout>
Copy the code
The process panel is initialized
graph = new Graph({
container: graphContainer,
grid: true.// Display grid lines
connecting: {
router: 'manhattan'.// Link routing algorithm
connector: {
name: 'rounded'.// Connect the line with rounded corners
args: {
radius: 20}},snap: {
radius: 50 // Anchor point adsorption radius
},
allowBlank: false.// Disable connections to empty Spaces
allowMulti: false.// Disable multiple connections on the same node pair
allowLoop: false.// Disable self-loop connections
connectionPoint: 'anchor' // Join points are anchors
},
selecting: {
enabled: true.// Nodes/edges can be selected
showNodeSelectionBox: true.// Select the back border of the node
showEdgeSelectionBox: true // Select the back border
},
snapline: true.// Enable alignment lines
keyboard: true.// Enable keyboard events
clipboard: true.// Enable the paste board
history: true // Enable history
})
Copy the code
The node library is initialized
Using Stencil in X6:
const stencil = new Addon.Stencil({
title: 'Process node'.target: graph, // Bind the process panel
stencilGraphWidth: 200.// Node library width
stencilGraphHeight: 600.// Node library height
layoutOptions: {
columns: 2.// Number of nodes per row
rowhHeight: 40 / / line height
}
})
stencilContainer.appendChild(stencil.container)
Copy the code
Custom node
Registers custom node base classes
export const registerNode = () = > {
// Register custom circles
Graph.registerNode('sps-circle', {
inherit: 'circle'.width: 45.height: 45,
attrs,
ports: createPorts() // Anchor point configuration
})
// Register custom rectangles
Graph.registerNode('sps-rect', {
inherit: 'rect'.width: 66.height: 36,
attrs,
ports: createPorts()
})
}
Copy the code
Create a node based on the custom node base class:
const createNodes = (graph: Graph) = > {
const startNode = graph.createNode({
shape: 'sps-circle'.label: '开始'
})
const eventNode = graph.createNode({
shape: 'sps-rect'.label: 'events'.attrs: {
body: {
rx: 20.ry: 20}}})const endNode = graph.createNode({
shape: 'sps-circle'.label: 'the end'
})
return {
startNode,
eventNode,
endNode
}
}
Copy the code
Add the created custom node to the node library:
const { startNode, eventNode, endNode } = createNodes(graph)
stencil.load([startNode, eventNode, endNode])
Copy the code
Custom events
Keyboard events
export default function registerKeyboardEvent (graph: Graph) {
// #region copy, cut and paste
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 undo redo
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
/ / delete
graph.bindKey('backspace'.() = > {
const cells = graph.getSelectedCells()
if (cells.length) {
graph.removeCells(cells)
}
})
}
Copy the code
Node/side events
export default function registerNodeEvent (graph: Graph, container: HTMLElement, state: FlowState) {
// #region control link pile show/hide
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 node/side selected/unselected
graph.on('cell:selected'.({ cell }) = > {
state.currentCell = {
id: cell.id, ... cell.data,isNode: cell.isNode()
}
})
graph.on('cell:unselected'.() = > {
state.currentCell = null
})
// #endregion
}
Copy the code
State management
This component provides/Inject status management
See: Vue3+TS for elegant use of state management
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
}
}
Copy the code
configuration
The interaction with the root component is implemented through useFlowState in the node configuration component and process configuration component.
Node configuration
The current configuration items are only examples; in a full-blown system, approvers should be combined with user role modules. You can then bind the node to the form.
export default defineComponent({
name: 'CellConfig'.emits: ['submit'],
setup (_, { emit }) {
const { state } = useFlowState()
const submit = () = > {
emit('submit', state.currentCell!)
}
/* render function */
return () = > {
const cell = state.currentCell!
const text = cell.isNode ? 'nodes' : 'operation'
const nodeConfig = (
<a-form-item label="Approver">
<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={` ${textName `}} >
<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>save</a-button>
</a-form-item>
</a-form>)}}})Copy the code
The process configuration
export default defineComponent({
name: 'FlowConfig',
setup () {
const { state } = useFlowState()
/* render function */
return () = > {
const flow = state.flow
return (
<a-form class="p-1" layout="vertical" model={ flow} >
<a-form-item label="The process ID">{ flow.id }</a-form-item>
<a-form-item label="Process Name">
<a-input v-model={[flow.name, 'value']} / >
</a-form-item>
</a-form>)}}})Copy the code
Json output
Transform the relevant content of the process into JSON. In a full-blown system, the content can be stored in a database and then flow charts can be generated using the graph.fromjson (data) method.
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
}))
}
}
Copy the code