1、效果图:
2、项目技术栈:React +Xflow +Formily
3、项目结构
4、项目整体技术流程
**
**
- 技术方案主要参考xflow流程图解决方案
- 前端设计好流程图及节点的相关属性,保存时需要将原始json数据转换成后端所需要的XML数据,参考bpmn规范
- 点击设计流程后会进入xflow流程设计页面
- 右侧的属性面板为左侧节点拖拽到画布当中点击后的自定义属性,全部采用json-schema的形式
- 保存流程图数据
- 保存时转换成bpmn所需要的XML
5、技术点具体讲解
- 工作流主页面设计
主画布FlowchartCanvas,主画布内置了一些常用的交互事件和默认配置,可通过config进行调整。使用通用流程图组件FlowchartNodePanel,除了内置的常用节点外,提供了便捷的自定义能力,可快速定制业务节点。
css
<XFlowContext.Provider value={{formSchema: formSchema}}> <XFlow className={`flow-user-custom-clz ${pathname === '/previewFlow' ? 'preview-flow-clz' : ''} ${pathname === '/flowDetail' ? 'flow-detail-clz' : ''}`} commandConfig={commandConfig} onLoad={onLoad} meta={currentMeta} > <FlowchartExtension /> {/* 左侧操作栏 */} {/* 查看 预览不展示 */} <FlowchartNodePanel show={true} showOfficial={false} // 是否展示通用面板 registerNode={registerNode} defaultActiveKey={['start-list', 'task-list', 'gateway-list', 'end-list', 'process-list']} position={{ width: (pathname !== '/previewFlow' && pathname !== '/flowDetail') && 220, top: 88, bottom: 0, left: 0 }} /> {/* 最顶层保存、预览栏 */} {/* 预览 保存 编辑流程名称 返回 */} <div className={'top-flow-operate'} style={{ cursor: 'pointer', zIndex: 100, position: 'absolute', top: '3px' }} > <span className={'flow-left-icon'} style={{ position: 'absolute', top: '12px', display: 'inline-block', width: 16, height: 16 }} onClick={() => goBackPage()}> <LeftOutlined /> </span> <CanvasService /> </div> {/* 顶层流程操作栏 */} <CanvasToolbar className='xflow-workspace-toolbar-operate' layout='horizontal' config={toolbarConfigOperate} position={{ top: 0, left: 0, bottom: 0 }} /> {/* 置前、框选等操作栏 */} {/* 预览和查看流程页不展示 */} { pathname !== '/flowDetail' && pathname !== '/previewFlow' && <div className={'flow-customize-header'}> <p style={{ position: 'absolute', top: '10px', left: '10px', zIndex: 100, margin: '0px', fontWeight: 500, fontSize: '14px', lineHeight: '22px' }}>组件库</p> <CanvasToolbar layout='horizontal'//展示的方式 config={toolbarConfig} position={{ top: 0, left: 219, right: 0, bottom: 0 }} style={{ borderLeft: '1px solid rgba(229, 230, 235, 1)' }} /> </div> } {/* 右侧属性操作栏 预览页不展示 */} { pathname !== '/previewFlow' && <FlowchartFormPanel style={{ border: '1px solid rgba(229, 230, 235, 1)', height: '100%' }} position={{ width: 320, top: pathname !== '/flowDetail' ? 93 : 52, bottom: 0, right: 0 }} controlMapService={controlMapService}//注册自定义Form组件 formSchemaService={NsJsonForm.formSchemaService}//控制面板切换逻辑 /> } <KeyBindings config={keybindingConfig} /> {/* 查看详情页不支持画布和节点拖拽 */} <FlowchartCanvas position={{ top: 40, left: 0, right: 0, bottom: 0 }} config={{ interacting: detailPage ? false : true, grid: { visible: true, }, // 是否展示网格 }} > <div className={'bottom-operate-container'}> <CanvasScaleToolbar layout='horizontal' position={{ top: calcHeight, right: 340 }} className={'footer-canvas-operate'} style={{ left: 'auto', }} /> </div> <CanvasContextMenu config={menuConfig} /> <CanvasSnapline color='#faad14' /> <CanvasNodePortTooltip /> </FlowchartCanvas> </XFlow> </XFlowContext.Provider>
- 左侧自定义组件(节点都是自定义)
用空启动事件举例:
arduino
import {NsGraph} from "@antv/xflow-core";import {DefaultNodeConfig, NODE_HEIGHT, NODE_WIDTH, XFLOWTHEME} from "../../constants";import React from "react";/** * 空启动事件 * @param props * @constructor */export const NoneStartNode: NsGraph.INodeRender = (props) => { const { size = { width: NODE_WIDTH, height: NODE_HEIGHT }, data = {} } = props; const { stroke = DefaultNodeConfig.stroke, label = DefaultNodeConfig.label, fill = DefaultNodeConfig.fill, fontFill = DefaultNodeConfig.fontFill, fontSize = DefaultNodeConfig.fontSize, showNodeStyle = true, } = data; const { width, height } = size; return ( <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill={fill === '#FFFFFF' ? 'none' : fill} xmlns="http://www.w3.org/2000/svg"> <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg"> <circle cx="49" cy="24" r="9.9" stroke={stroke} /> </svg> <text x={width / 2} y={height / 1.5} fill={fontFill} textAnchor="middle" alignmentBaseline="middle" fontSize={fontSize} > {label || '空启动事件'} </text> {/* <rect x="0.5" y="0.5" width="97" height="67" rx="1.5" stroke={stroke} opacity=".5" /> */} <rect x="0.5" y="0.5" width="97" height="67" rx="1.5" stroke={stroke} opacity=".5" style={{ display: showNodeStyle ? 'block' : 'none' }} /> </svg> );};
- 右侧自定义属性
右侧的属性面板为左侧节点拖拽到画布当中点击后的自定义属性,全部采用json-schema的形式。
css
/**
* 自定义组件属性表单
* @param nodeConfig
* @param meta
*/
export const createComponentSchema = (nodeConfig: NsGraph.INodeConfig, meta: NsGraph.IGraphMeta):ISchema => {
return {
type: 'object',
properties: {
settingForm: {
type: 'void',
// 组件容器属性,会影响布局效果
// 'x-decorator': 'FormItem',
'x-component': 'FormCollapse',
'x-component-props': {
'accordion': true,
'ghost': true
},
properties: {
styleProp: {
'x-component': 'FormCollapse.CollapsePanel',
'x-component-props': {
header: '样式',
},
...StyleSchema
},
activityProp: {
'type': 'void',
'x-component': 'FormCollapse.CollapsePanel',
'x-component-props': {
header: '组件属性',
},
'properties': {
...BaseSchema,
...getComponentSchema(nodeConfig, meta),
...ListenerSchema
}
},
ListenerProp: {
'x-component': 'FormCollapse.CollapsePanel',
'x-component-props': {
header: '执行监听器',
},
}
}
}
}
};
};
-
保存流程图数据
typescripttoolbarGroup.push({ tooltip: '保存', iconName: 'SaveOutlined', id: TOOLBAR_ITEMS.SAVE_GRAPH_DATA, onClick: async ({ commandService }) => { commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>( TOOLBAR_ITEMS.SAVE_GRAPH_DATA, { saveGraphDataService: async (meta, graphData) => { console.log(graphData); const data = BpmnAdapter.adapterOut({...graphData, meta}); // 下载XML文件 download('x-flow.xml', lfJson2Xml(data)); // 上传和部署流程图 const xmlData = lfJson2Xml(data); try { // 上传流程图 const uploadRes:any = await uploadBpmnXml(xmlData); if (uploadRes.meta.code !== 200) { throw uploadRes.meta.msg; } // 部署流程图 const deployRes: any = await deployMode(uploadRes.data.modelId); if (deployRes.meta.code !== 200) { throw deployRes.meta.msg; } // 保存原始流程数据,用于下次回显 const saveRes: any = await addFlowResources(meta, graphData); if (saveRes.meta.code !== 200) { throw saveRes.meta.msg; } return message.success('流程保存成功!') } catch (err) { return message.error(err); } }, }, ) }, });
-
保存数据转化
保存时转换成bpmn所需要的XML
ruby
adapterOut(data) { const bpmnProcessData = { // '-id': `Process_${getBpmnId()}`, '-id': `${data.meta.id ? data.meta.flowKey : `Process_${getBpmnId()}`}`, '-name': `${data.meta.flowName}`, '-isExecutable': 'true', }; convertLf2ProcessData(bpmnProcessData, data); const bpmnDiagramData = { '-id': 'BPMNPlane_1', '-bpmnElement': bpmnProcessData['-id'], }; convertLf2DiagramData(bpmnDiagramData, data); const bpmnData = { 'bpmn:definitions': { '-id': `Definitions_${getBpmnId()}`, '-xmlns:activiti': 'http://activiti.org/bpmn', '-xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', '-xmlns:bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL', '-xmlns:bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI', '-xmlns:dc': 'http://www.omg.org/spec/DD/20100524/DC', '-xmlns:di': 'http://www.omg.org/spec/DD/20100524/DI', '-targetNamespace': 'http://bpmn.io/schema/bpmn', '-exporter': 'bpmn-js (https://demo.bpmn.io)', '-exporterVersion': '7.3.0', // 转换信号定义 ...signalToJson(data?.meta?.signal), // 转换消息定义 ...messageToJson(data?.meta?.message), 'bpmn:process': bpmnProcessData, 'bpmndi:BPMNDiagram': { '-id': 'BPMNDiagram_1', 'bpmndi:BPMNPlane': bpmnDiagramData, }, }, }; return bpmnData; }