作为一个90后的老码农,有着自己的倔强,虽然市面上有很多优秀的流程引擎,但是还是想自己也造个轮子,趁着疫情期间,基于jsplumb简单开发了一个简易的流程引擎,下面是前端编辑效果。

基于jsplumb的流程引擎开发_jsplumb

我的思路比较简单:

1.流程节点只有三种类型,开始节点,流程节点和结束节点。

2.每个流程可以设置对应的表和主键,然后后台就可以取到对应表上的所有数据。

3.每个节点之间可以绘制连线,每条连线可以增加判断条件来指引流程该往哪个节点流向。

4.每个流程节点可以设置对应的审批人员以及对应的页面路径和推送提醒。

webservice流程接口:

后端编写了webservice接口,可以获取任务,启动流程以及运转流程。

基于jsplumb的流程引擎开发_工作流_02

以下是比较复杂一点的流程:

1.多分支流程

基于jsplumb的流程引擎开发_jsplumb_03

2.多条件流程

基于jsplumb的流程引擎开发_流程图_04

前端核心代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>流程设计</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="Gang Tao">
<link href="~/Content/css/bootstrap.min.css?v=3.3.6" rel="stylesheet">
<link href="~/Content/css/font-awesome.min.css?v=4.4.0" rel="stylesheet">
<link href="~/Content/css/style.css?v=4.1.0" rel="stylesheet">
<script src="~/Content/js/jquery.min.js?v=2.1.4"></script>
<script src="~/Content/js/bootstrap.min.js?v=3.3.6"></script>
<script src="~/Content/js/plugins/jquery-ui/jquery-ui.min.js" type="text/javascript"></script>
<script src="~/Content/js/plugins/flowchart-builder/js/jsplumb-2.12.9.js" type="text/javascript"></script>
<script src="~/Content/js/plugins/layer/layer.min.js"></script>
<script src="~/Content/js/shui-framework.js" type="text/javascript"></script>
<style type="text/css">
.pageBox
{
margin:0;
padding:0;
}
.menuBox
{
width:100px;
float:left;
height:600px;
}
.menuItems
{
margin:8px;
z-index:999;
}
.flowBox
{
width: 100%;
height:550px;
background-color:#ccc;
position:absolute;
}
.mainBox
{
margin-left:100px;
width: calc(100% - 100px);
height:600px;
border-left:1px solid #e7eaec;
}
.flowBtnBox
{
width: 100%;
height:50px;
padding:8px;
}
.nodeBox {
position: absolute;
width: 200px;
background-color: transparent;

}
.nodeBox:hover{
box-shadow:#66a6e0 0px 0px 12px 0px;
}
.flow-node-header {
background-color: #66a6e0;
height: 25px;
cursor: pointer;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.flow-node-header a {
text-decoration: none;
line-height: 25px;
vertical-align: middle;
}
.flow-node-body {
background-color: beige;
background-color: white;
text-align: center;
cursor: pointer;
height: 25px;
line-height: 25px;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.flow-node-drag {
margin-left:6px;
color:#000;
}
.flow-node-operate{
position: absolute;
top: 0px;
right: 0px;
line-height: 25px;
}
.labelClass {
background-color: white;
padding: 5px;
opacity: 0.7;
border: 1px solid #346789;
border-radius: 2px;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
</head>
<body>
<div class="pageBox">
<div class="menuBox">
<div class="menuItems">
<div data_id="startNode" data_nodeTypes="1" data_name="开始节点" class="btn btn-primary" >开始节点</div>
</div>
<div class="menuItems">
<div data_id="startNode" data_nodeTypes="2" data_name="流程节点" class="btn btn-primary">流程节点</div>
</div>
<div class="menuItems">
<div data_id="endNode" data_nodeTypes="3" data_name="结束节点" class="btn btn-primary" >结束节点</div>
</div>
</div>
<div class="mainBox">
<div class="flowBtnBox">
<div class="btn btn-white" onclick="BtnSave()" >保存</div>
<div class="btn btn-white" onclick="BtnNodeInfo()" >流程信息</div>
<div class="btn btn-white" onclick="BtnWorkflowList()" >流程列表</div>
<div class="btn btn-white" onclick="BtnWorkflowExport()" >导出</div>
<div class="btn btn-white" onclick="BtnWorkflowImport()" >导入</div>
</div>
<div id="flowBox" class="flowBox"></div>
</div>

</div>
<script>
var workflowInfo = {};
var dataNodeList = [];
var dataLineList = [];
var loadEasyFlowFinish = false;
var jsplumbSetting = {
// 动态锚点、位置自适应
Anchors: ['Top', 'TopCenter', 'TopRight', 'TopLeft', 'Right', 'RightMiddle', 'Bottom', 'BottomCenter', 'BottomRight', 'BottomLeft', 'Left', 'LeftMiddle'],
Container: 'flowBox',
// 连线的样式 StateMachine、Flowchart
Connector: 'Flowchart',
// 鼠标不能拖动删除线
ConnectionsDetachable: false,
// 删除线的时候节点不删除
DeleteEndpointsOnDetach: false,
// 连线的端点
// Endpoint: ["Dot", {radius: 5}],
Endpoint: ["Rectangle", {height: 10, width: 10}],
// 线端点的样式
EndpointStyle: { fill: 'rgba(255,255,255,0)', outlineWidth: 1},
LogEnabled: true,//是否打开jsPlumb的内部日志记录
// 绘制线
PaintStyle: { stroke: 'black', strokeWidth: 3 },
// 绘制箭头
Overlays: [['Arrow', {width: 12, length: 12, location: 1}]],
RenderMode: "svg"
};
// jsplumb连接参数
var jsplumbConnectOptions = {
isSource: true,
isTarget: true,
// 动态锚点、提供了4个方向 Continuous、AutoDefault
anchor: "Continuous"
};
var jsplumbSourceOptions = {
/*"span"表示标签,".className"表示类,"#id"表示元素id*/
filter: ".flow-node-drag",
filterExclude: false,
anchor: "Continuous",
allowLoopback: false
};
var jsplumbTargetOptions = {
/*"span"表示标签,".className"表示类,"#id"表示元素id*/
filter: ".flow-node-drag",
filterExclude: false,
anchor: "Continuous",
allowLoopback: false
};

$(function () {
$(".menuItems").draggable({
helper: 'clone',
scope: 'ss'
})
$("#flowBox").droppable({
scope: 'ss',
drop: function (event, ui) {
var $btn = $(ui.draggable[0]).find(".btn");
var node = {};
node.id = "node" + Math.random().toString(36).substr(3, 10);
node.name = $btn.attr("data_name");
node.nodeTypes = $btn.attr("data_nodeTypes");
node.userType = 1;
node.pushType = 1;
node.show = true;
node.left = (ui.position.left - 100) + "px";
node.top = (ui.position.top - 50) + "px";
addNode(node);
}
})

jsPlumb.ready(main);

function main() {
jsPlumb.importDefaults(jsplumbSetting);
jsPlumb.setContainer('flowBox');
jsPlumb.setSuspendDrawing(false, true);

if (!!GetQuery('id')) {
var id = GetQuery('id');

getAjax("/WorkflowModule/Design/GetWorkflowInfo?id=" + id, {}, function (rs) {
var rs = eval("(" + rs + ")");
if (rs.status == "ok") {
workflowInfo = rs.data;
dataNodeList = rs.data.WrokflowNodeList;
dataLineList = rs.data.WrokflowLinesList;
loadEasyFlow();
}
});
}

// 单点击了连接线,
jsPlumb.bind('click', function (conn, originalEvent) {
console.log(conn);
var _layer = Getlayer();
_layer.open({
btn: ['保存', '删除'], //按钮
yes: function (index, layero) {
var fromData = parent.$("#layui-layer-iframe" + index)[0].contentWindow.GetData();
conn.setLabel({
label: fromData.name,
cssClass: 'labelClass'
});
for (var i = 0; i < dataLineList.length; i++) {
if (dataLineList[i].conn_id == conn.id) {
dataLineList[i].labelName = fromData.name;
dataLineList[i].WrokflowLineConditList = fromData.WrokflowLineConditList;
dataLineList[i].labelCss = 'labelClass';
break;
}
};
_layer.close(index);
}, btn2: function (index, layero) {
jsPlumb.deleteConnection(conn)
_layer.close(index);
},
type: 2,
title: '连线信息',
area: ['650px', '540px'],
shadeClose: true, //开启遮罩关闭
content: '/WorkflowModule/Design/LinesForm',
success: function (layero, index) {
var data = {};
for (var i = 0; i < dataLineList.length; i++) {
if (dataLineList[i].conn_id == conn.id) {
data.name = dataLineList[i].labelName;
data.WrokflowLineConditList = dataLineList[i].WrokflowLineConditList;
break;
}
}
data.condition = [];
parent.$("#layui-layer-iframe" + index)[0].contentWindow.SetData(data);
}
});
});
// 连线
jsPlumb.bind("connection", function (evt) {
var from = evt.source.id;
var to = evt.target.id;
if (loadEasyFlowFinish) {
dataLineList.push({ from: from, to: to })
}
});
// 删除连线回调
jsPlumb.bind("connectionDetached", function (evt) {
deleteLine(evt.sourceId, evt.targetId)
})
}
})
// 删除线
function deleteLine(from,to) {
dataLineList = dataLineList.filter(function (line) {
if (line.from == from && line.to == to) {
return false
}
return true
})
}
function loadEasyFlow() {
$("#flowBox").empty();
// 初始化节点
for (var i = 0; i < dataNodeList.length; i++) {
var node = dataNodeList[i];
drawNode(node);
// 设置源点,可以拖出线连接其他节点
jsPlumb.makeSource(node.id, jsplumbSourceOptions);
// 设置目标点,其他源点拖出的线可以连接该节点
jsPlumb.makeTarget(node.id, jsplumbTargetOptions);

jsPlumb.draggable(node.id, {
containment: 'parent'
});
}

// 初始化连线
for (var i = 0; i < dataLineList.length; i++) {
var line = dataLineList[i];
var curline = jsPlumb.connect({
source: line.from,
target: line.to
}, jsplumbConnectOptions);
dataLineList[i].conn_id = curline.id;
if (line.labelName != "") {
curline.setLabel({
label: line.labelName,
cssClass: line.labelCss
})
}
}
setInterval(function () {
loadEasyFlowFinish = true;
}, 500);
}
function changeNodeSite(e) {
var data = {};
data.nodeId = $(e).attr("id");
data.left = $(e).css("left");
data.top = $(e).css("top");
for (var i = 0; i < dataNodeList.length; i++) {
var node = dataNodeList[i];
if (node.id == data.nodeId) {
node.left = data.left;
node.top = data.top;
}
}
}
function onmouseoverNode(e) {
$(e).find(".flow-node-operate").show();
}
function onmouseoutNode(e) {
$(e).find(".flow-node-operate").hide();
}
function editNode(e) {
var _layer = Getlayer();
var currentNode = $(e).parents(".nodeBox");
var nodeId = currentNode.attr("id");
_layer.open({
btn: ['确认', '取消'], //按钮
yes: function (index, layero) {
var PostData = parent.$("#layui-layer-iframe" + index)[0].contentWindow.GetWebControls("#form1");

for (var i = 0; i < dataNodeList.length; i++) {
if (dataNodeList[i].id == nodeId) {
dataNodeList[i].name = PostData.name;
dataNodeList[i].nodeTypes = PostData.nodeTypes;
dataNodeList[i].userType = PostData.userType;
dataNodeList[i].userid = PostData.userid;
dataNodeList[i].userSql = PostData.userSql;
dataNodeList[i].userSqlParam = PostData.userSqlParam;
dataNodeList[i].pushType = PostData.pushType;
dataNodeList[i].pushContent = PostData.pushContent;
dataNodeList[i].pushContentParam = PostData.pushContentParam;
dataNodeList[i].viewUrl = PostData.viewUrl;
dataNodeList[i].dataBase = PostData.dataBase;
dataNodeList[i].weChatViewUrl = PostData.weChatViewUrl;
dataNodeList[i].appViewUrl = PostData.appViewUrl;
currentNode.find(".flow-node-body").html(PostData.name);
break;
}
}
_layer.close(index);
}, btn2: function (index, layero) {
_layer.close(index);
},
type: 2,
title: '节点信息',
area: ['650px', '540px'],
shadeClose: true, //开启遮罩关闭
content: '/WorkflowModule/Design/NodeForm?Id=' + nodeId,
success: function (layero, index) {
var data = {};
for (var i = 0; i < dataNodeList.length; i++) {
if (dataNodeList[i].id == nodeId) {
data.name = dataNodeList[i].name;
data.nodeTypes = dataNodeList[i].nodeTypes;
data.userType = dataNodeList[i].userType;
data.userid = dataNodeList[i].userid;
data.userSql = dataNodeList[i].userSql;
data.userSqlParam = dataNodeList[i].userSqlParam;
data.pushType = dataNodeList[i].pushType;
data.pushContent = dataNodeList[i].pushContent;
data.pushContent = dataNodeList[i].pushContent;
data.pushContentParam = dataNodeList[i].pushContentParam;
data.viewUrl = dataNodeList[i].viewUrl;
data.dataBase = dataNodeList[i].dataBase;
data.weChatViewUrl = dataNodeList[i].weChatViewUrl;
data.appViewUrl = dataNodeList[i].appViewUrl;
break;
}
}
var PostData = parent.$("#layui-layer-iframe" + index)[0].contentWindow.SetData(data);
}
});
}
function deleteNode(e) {
var _layer = Getlayer();
_layer.confirm('确定删除该节点吗?', {
btn: ['确认', '取消'] //按钮
}, function (index) {
var nodeId = $(e).parents(".nodeBox").attr("id");
dataNodeList = dataNodeList.filter(function (node) {
if (node.id === nodeId) {
// 伪删除,将节点隐藏,否则会导致位置错位
node.show = false;
$("#" + nodeId).hide();
}
return true
});
setInterval(function () {
jsPlumb.removeAllEndpoints(nodeId);
}, 50);
_layer.close(index);

}, function () {

});
}
function drawNode(node) {
var node = ['<div id="' + node.id + '" class="nodeBox" onmouseup="changeNodeSite(this)" onmouseover="onmouseoverNode(this)" onmouseout="onmouseoutNode(this)" style="left:' + node.left + ';top:' + node.top + '">',
'<div class="flow-node-header">',
'<span class="fa fa-bars flow-node-drag"></span>',
'<div class="flow-node-operate" style="display:none;">',
'<span onclick="editNode(this)" >编辑</span>&nbsp;',
'<span onclick="deleteNode(this)" >删除</span>&nbsp;',
'</div>',
'</div>',
'<div class="flow-node-body">' + node.name + '</div>',
'</div>'].join('');
$("#flowBox").append(node);
}
function addNode(node) {
dataNodeList.push(node);
drawNode(node);
// 设置源点,可以拖出线连接其他节点
jsPlumb.makeSource(node.id, jsplumbSourceOptions);
// 设置目标点,其他源点拖出的线可以连接该节点
jsPlumb.makeTarget(node.id, jsplumbTargetOptions);

jsPlumb.draggable(node.id, {
containment: 'parent'
});
}

function BtnNodeInfo() {
var _layer = Getlayer();
_layer.open({
btn: ['保存', '取消'], //按钮
yes: function (index, layero) {
var PostData = parent.$("#layui-layer-iframe" + index)[0].contentWindow.GetWebControls("#form1");
workflowInfo = PostData;
_layer.close(index);
}, btn2: function (index, layero) {
_layer.close(index);
},
type: 2,
title: '流程信息',
area: ['650px', '540px'],
shadeClose: true, //开启遮罩关闭
content: '/WorkflowModule/Design/WorkflowInfo',
success: function (layero, index) {
parent.$("#layui-layer-iframe" + index)[0].contentWindow.SetData(workflowInfo);
}
})
}

function BtnSave() {
var _layer = Getlayer();
var postData = {};
postData = workflowInfo;
postData.WrokflowNodeList = dataNodeList;
postData.WrokflowLinesList = dataLineList;
getAjax("/WorkflowModule/Design/WorkflowSave", "data=" + JSON.stringify(postData), function (rs) {
var rs = eval("(" + rs + ")");
if (rs.status == "ok") {
_layer.msg("保存成功")
} else {
_layer.msg("保存失败")
}
});
}
function BtnWorkflowList() {
parent.openTab('/WorkflowModule/Design/WorkflowList', '流程列表');
}
function BtnWorkflowExport() {
var data = {}
data.workflowInfo = workflowInfo;
data.dataNodeList = dataNodeList;
data.dataLineList = dataLineList;
var _layer = Getlayer();
_layer.open({
type: 1,
title: '流程信息',
area: ['420px', '280px'], //宽高
content: '<textarea class="form-control" id="workflowInfoText" name="workflowInfoText" rows="10">' + JSON.stringify(data) + '</textarea>'
});
}
function BtnWorkflowImport() {
var _layer = Getlayer();
_layer.open({
btn: ['保存', '取消'], //按钮
yes: function (index, layero) {
var workflowInfoText = parent.$("#layui-layer" + index).find("#workflowInfoText").val();
var data = eval("(" + workflowInfoText + ")");

//重置ID
if (!!GetQuery('id')) {
data.workflowInfo.id = GetQuery('id');
} else {
data.workflowInfo.id = "";
}


workflowInfo = data.workflowInfo;
dataNodeList = data.dataNodeList;
dataLineList = data.dataLineList;
loadEasyFlowFinish = false;
loadEasyFlow();
_layer.close(index);
}, btn2: function (index, layero) {
_layer.close(index);
},
type: 1,
title: '流程信息',
shadeClose: true, //开启遮罩关闭
area: ['420px', '310px'], //宽高
content: '<textarea class="form-control" id="workflowInfoText" name="workflowInfoText" rows="10"></textarea>'
})
}
</script>
</body>
</html>

官方文档:​​jsPlumb Toolkit Documentation​