angular6基於jsplumb的規則引擎流程設計實現
阿新 • • 發佈:2018-12-20
jsPlumb是一個在元素之間繪製連線線的javascript框架,它使用svg技術繪製連線線。
相關資料連結:
前段時間,公司專案需要,用了差不多接近一週時間在angular6中實現了一個規則引擎流程拖拽設計,整體效果如下圖所示:
核心程式碼如下:
1.介面左側規則節點拖拽到右側生成:
//定義左側規則節點拖放函式 public initRuleEngineNodeDrage(): void{ setTimeout(function(oper){ let euleNode = oper.element.nativeElement.querySelector('p-accordion'); $(euleNode).find('.rule-engine-node').draggable({ helper: "clone", scope: "engine", }); $("#ruleEngineJsPlumb").droppable({ scope: "engine", drop: function (event, ui) { // 建立工廠模型到拖拽區 oper.createModel(ui, $(this)); } }); $('#ruleEngineJsPlumb').on('click', 'div.jsplumb-node', function () { if(!$(this).hasClass("jsplumb-node-selected")){ $('div.jsplumb-node').removeClass('jsplumb-node-selected'); $(this).addClass('jsplumb-node-selected'); } // 點選節點控制按鈕 let elemetEndpoints = oper.ruleNodesJsplumb.$jsPlumbInstance.selectEndpoints({element: $(this)}); elemetEndpoints.each(function(endpoint){ const type = endpoint.anchor.type; if(type == 'RightMiddle'){ // 右邊端點為sourceAnchors,定義了overlays節點按鈕 oper.ruleNodesJsplumb._nodeButtonClick(endpoint); } }); // 隱藏連線按鈕 oper.ruleNodesJsplumb._lineButtonShow({flag: false}); // 預載入選中節點的屬性及頁面內容 // 設定當前拖拽節點屬性 oper.ruleNodesJsplumb._setRuleNodeOptions(this); // 預載入節點表單自定義屬性 oper.componentsHandle.loadComponent(oper.ruleNodesJsplumb.$nodeModel.nodeType, oper.ruleNodesJsplumb.$nodeModel.description, oper.ruleNodesJsplumb.$nodeModel.components.options); // 獲取表單資料內容並儲存 oper.saveOptionsForm(); return false; }); },200,this); } //建立模型(引數依次為:drop事件的ui、當前容器) public createModel(ui, selector): string { // 新增規則節點模型及樣式屬性 let nodeId = 'node_' + this.ruleNodesJsplumb._getUUID(12,12); let cloneNode = $(ui.helper).clone(false); cloneNode .attr('id', nodeId) .removeClass('rule-engine-node') .addClass('jsplumb-node'); $(selector).append(cloneNode); var left = parseInt((ui.offset.left - $(selector).offset().left)+""); var top = parseInt((ui.offset.top - $(selector).offset().top + 10)+""); $("#" + nodeId).css("left", left).css("top", top); // 將規則節點新增至jsPlumb let nodeInputObj = cloneNode.find(".rule-engine-port-input"); let nodeOutputObj = cloneNode.find(".rule-engine-port-output"); let nodeInput = nodeInputObj.length > 0 ? true : false; let nodeOutput = nodeOutputObj.length > 0 ? true : false; this.ruleNodesJsplumb._addEndpoints({nodeId: nodeId, nodeInput: nodeInput, nodeOutput: nodeOutput}); cloneNode.click(); return nodeId; };
2.封裝的jsplumb連結及錨點:
constructor( public crudService: CrudService ){ const opers = this; jsPlumb.ready(function () { opers._initInstance(); opers._initEndpoints(); opers._initEvents(); }); } // 獲取uuid唯一資料 _getUUID(len: number, radix: number): string{ var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); var uuid = [], i; radix = radix || chars.length; if (len) { // Compact form for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; } else { // rfc4122, version 4 form var r; // rfc4122 requires these characters uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; uuid[14] = '4'; // Fill in random data. At i==19 set the high bits of clock sequence as // per rfc4122, sec. 4.1.5 for (i = 0; i < 36; i++) { if (!uuid[i]) { r = 0 | Math.random()*16; uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; } } } return uuid.join(''); } // 初始jsPlumb例項物件 public _initInstance(){ let oper = this; oper.$jsPlumbInstance = jsPlumb.getInstance({ // 預設拖拽屬性 DragOptions: { cursor: 'pointer', zIndex: 2000 }, // 箭頭和提示文字定義 ConnectionOverlays: [ // 定義箭頭 [ "Arrow", {location: 0.97, id: "arrow", visible: true, width: 15, length: 15} ], // 定義箭頭的文字 [ "Label", {location: 0.5, id: "label", visible: false, cssClass: "aLabel", events:{ click:function(info) {} } }], [ "Label", {location: 0.5, id: "button_edit", visible: false, cssClass: "aLabel-button aLabel-edit", label:"<i class='fa fa-pencil'></i>", events:{ click:function(info) { let label = info.component.getOverlay("label").getLabel(); oper.$connectorLabel = label; oper.$connectorDisplayDialog = true; } } }], [ "Label", {location: 0.5, id: "button_del", visible: false, cssClass: "aLabel-button aLabel-del", label:"<i class='fa fa-close'></i>", events:{ click:function(info) { oper.crudService.confirmService.confirm({ message: '您確認要刪除選擇的連結嗎?', header: '連結刪除', icon: 'fa fa-question-circle', acceptLabel : '是', rejectLabel : '否', accept: () => { jsPlumb.detach(info.component);// 刪除connection oper.$selectedConnection = null;// 置空connection } }); } } }] ], // 預設情況下連結是否可拆卸(使用滑鼠)。預設為true ConnectionsDetachable: true, // 是否重新連結使用者已使用滑鼠分離然後刪除的連結。預設值為false。 ReattachConnections: true, // 例項所在容器 Container: "ruleEngineJsPlumb" }); } // 初始端點連結樣式 public _initEndpoints(){ let oper = this; this.$paintStyle = { stroke: "transparent",// 端點border顏色 strokeWidth: 1, // 端點border-width fill: "transparent", // 端點背景顏色 radius: 7 }; // 滑鼠懸浮在端點上的樣式 this.$hoverPaintStyle = { stroke: this.$color,// 端點border顏色 strokeWidth: 1,// 端點border-width fill: this.$color,// 端點背景顏色 radius: 7 }; // 基本連結線樣式 this.$connectorStyle = { stroke: this.$color,// 線條顏色 strokeWidth: 2,// 線條大小 joinstyle: "round", outlineStroke: "white",// 線條邊緣顏色 outlineWidth: 1 // 線條邊緣大小 }; // 滑鼠懸浮在連結線上的樣式 this.$connectorHoverStyle = { stroke: this.$color,// 線條顏色 strokeWidth: 3,// 線條大小 outlineStroke: "white",// 線條邊緣顏色 outlineWidth: 2 // 線條邊緣大小 }; // 基本連結線樣式 this.$connectorClickStyle = { stroke: this.$colorClick,// 線條顏色 strokeWidth: 2,// 線條大小 joinstyle: "round", outlineStroke: "white",// 線條邊緣顏色 outlineWidth: 1 // 線條邊緣大小 }; // 滑鼠懸浮在連結線上的樣式 this.$connectorClickHoverStyle = { stroke: this.$colorClick,// 線條顏色 strokeWidth: 3,// 線條大小 outlineStroke: "white",// 線條邊緣顏色 outlineWidth: 2 // 線條邊緣大小 }; // 源端點樣式定義 this.$sourceEndpoint = { // 端點型別大小 endpoint: ["Rectangle", {width: 10, height: 10, cssClass: 'jsplumb-endpoint'}], // 端點基本樣式 paintStyle: this.$paintStyle, // 端點懸浮樣式 hoverPaintStyle: this.$hoverPaintStyle, // 設定連結點最多可以連結幾條線,值-1表示沒有上限 maxConnections: -1, // 是否可以拖動(作為連線起點) isSource: true, // 是否可以放置(作為連線終點) isTarget: false, // 連結線型別 connector: [ "Bezier", { stub: [40, 60], curviness: 150 } ], // 連結線基本樣式 connectorStyle: this.$connectorStyle, // 連結線懸浮樣式 connectorHoverStyle: this.$connectorHoverStyle, // 端點拖拽樣式 dragOptions: {}, // 端點label定義 overlays: [[ "Label", {location: [0, -0.1], id: "node_edit", visible: false, cssClass: "aLabel-button aLabel-edit", label:"<i class='fa fa-pencil'></i>", events:{ click:function(info) { oper.$nodeDisplayDialog = true; } } }], [ "Label", {location: [0, -0.1], id: "node_del", visible: false, cssClass: "aLabel-button aLabel-del", label:"<i class='fa fa-close'></i>", events:{ click:function(info) { let element = info.component.element; let nodeName = $(element).find('.rule-engine-label').text(); oper.crudService.confirmService.confirm({ message: '您確認要刪除選擇的['+ nodeName +']節點嗎?', header: '節點刪除', icon: 'fa fa-question-circle', acceptLabel : '是', rejectLabel : '否', accept: () => { let elemetEndpoints = oper.$jsPlumbInstance.selectEndpoints({element: element}); elemetEndpoints.each(function(endpoint){ oper.$jsPlumbInstance.deleteEndpoint(endpoint);// 端點,連線 }); jsPlumb.remove(element);// 刪除節點元素 oper._deleteRuleNode(element); oper.$selectedEndpoint = null;// 置空端點 oper.$selectedConnection = null;// 置空連線 } }); } } }]] }; // 目標端點樣式定義 this.$targetEndpoint = { // 端點型別大小 endpoint: ["Rectangle", {width: 10, height: 10, cssClass: 'jsplumb-endpoint' }], // 端點基本樣式 paintStyle: this.$paintStyle, // 端點懸浮樣式 hoverPaintStyle: this.$hoverPaintStyle, // 設定連結點最多可以連結幾條線,值-1表示沒有上限 maxConnections: -1, // 是否可以拖動(作為連線起點) isSource: false, // 是否可以放置(作為連線終點) isTarget: true, // 端點拖拽樣式 dropOptions: { hoverClass: "drop-hover", activeClass: "drop-active" } }; } // 初始繫結事件 public _initEvents(){ let oper = this, instance = this.$jsPlumbInstance; // 點選連線觸發顯示連線按鈕,隱藏節點按鈕 instance.bind('click', function (connection, originalEvent) { oper._lineButtonClick(connection); oper._nodeButtonShow({flag: false}); return false; }); // 當連結建立前進行條件判斷 instance.bind('beforeDrop', function (info) { if(!info.connection){ return false; } // 判斷是否已經連結 let result = instance.getConnections(info.connection); if(result && result.length > 0){ // 已連結時連結不會建立,注意,必須是false return false; }else{ if(info.connection.sourceId == info.connection.targetId){ // 與自己端點連結不會建立,注意,必須是false return false; }else{ return true; } } }); } // 動態新增節點端點 public _addEndpoints({nodeId = "", nodeInput = true, nodeOutput = true} = {}){ const sourceAnchors = (nodeOutput ? ['RightMiddle'] : []), targetAnchors = (nodeInput ? ['LeftMiddle'] : []); for (let i = 0; i < sourceAnchors.length; i++) { let sourceUUID = nodeId + this.$split + sourceAnchors[i]; this.$jsPlumbInstance.addEndpoint(nodeId, this.$sourceEndpoint, { anchor: sourceAnchors[i], uuid: sourceUUID }); } for (let j = 0; j < targetAnchors.length; j++) { let targetUUID = nodeId + this.$split + targetAnchors[j]; this.$jsPlumbInstance.addEndpoint(nodeId, this.$targetEndpoint, { anchor: targetAnchors[j], uuid: targetUUID }); } // 使規則節點可以拖拽 this.$jsPlumbInstance.draggable(nodeId, { grid: [20, 20] }); } // 設定端點連線的label public _setConnectLabel({connection = this.$selectedConnection, label = ""} = {}){ if(label){ connection.getOverlay("label").setLabel(label); connection.getOverlay("label").setVisible(true); }else{ connection.getOverlay("label").setLabel(""); connection.getOverlay("label").setVisible(false); } } // 節點上的編輯和刪除按鈕顯示事件 public _nodeButtonShow({endpoint = this.$selectedEndpoint, flag = true} = {}){ if(!endpoint){ return; } // 節點編輯按鈕 let rule_node_edit = endpoint.getOverlay("node_edit"); // 節點刪除按鈕 let rule_node_del = endpoint.getOverlay("node_del"); let elementId = endpoint.anchor.elementId; if(rule_node_edit && rule_node_del){ if(flag){ rule_node_edit.setVisible(true); rule_node_del.setVisible(true); $('#'+ elementId).addClass('jsplumb-node-selected'); }else { this.$selectedEndpoint = null; rule_node_edit.setVisible(false); rule_node_del.setVisible(false); $('#'+ elementId).removeClass('jsplumb-node-selected'); } } } // 節點上的編輯和刪除按鈕點選顯示 public _nodeButtonClick(endpoint){ if(!endpoint){ return; } if(this.$selectedEndpoint){ this._nodeButtonShow({endpoint: this.$selectedEndpoint, flag: false}); this._nodeButtonShow({endpoint: endpoint}); this.$selectedEndpoint = endpoint; }else{ this._nodeButtonShow({endpoint: endpoint}); this.$selectedEndpoint = endpoint; } } // 連線上的編輯和刪除按鈕顯示事件 public _lineButtonShow({connection = this.$selectedConnection, flag = true} = {}){ if(!connection){ return; } // 連線編輯按鈕 let path_button_edit = connection.getOverlay("button_edit"); // 連線刪除按鈕 let path_button_del = connection.getOverlay("button_del"); if(path_button_edit && path_button_del){ if(flag){ connection.setPaintStyle(this.$connectorClickStyle); connection.setHoverPaintStyle(this.$connectorClickHoverStyle); path_button_edit.setVisible(true); path_button_del.setVisible(true); }else { this.$selectedConnection = null; path_button_edit.setVisible(false); path_button_del.setVisible(false); connection.setPaintStyle(this.$connectorStyle); connection.setHoverPaintStyle(this.$connectorHoverStyle); } } } // 連線上的編輯和刪除按鈕點選顯示 public _lineButtonClick(connection){ if(!connection){ return; } if(this.$selectedConnection){ this._lineButtonShow({connection: this.$selectedConnection, flag: false}); this._lineButtonShow({connection: connection}); this.$selectedConnection = connection; }else{ this._lineButtonShow({connection: connection}); this.$selectedConnection = connection; } } // (資料處理)通過節點編輯點選獲取物件元素的屬性 public _setRuleNodeOptions(nodeElement){ if(!nodeElement){ return; } this.$nodeModel.id = $(nodeElement).attr('id');// 節點唯一標識 this.$nodeModel.nodeType = $(nodeElement).attr('nodeType');// 節點型別 // 從dom物件獲取資料 let nodeInputObj = $(nodeElement).find(".rule-engine-port-input"); let nodeOutputObj = $(nodeElement).find(".rule-engine-port-output"); let nodeInput = nodeInputObj.length > 0 ? true : false; let nodeOutput = nodeOutputObj.length > 0 ? true : false; let nodeIconDiv = $(nodeElement).find(".rule-engine-icon"); let nodeIcon = nodeIconDiv[0].classList.length > 1 ? nodeIconDiv[0].classList[1] : ""; this.$nodeModel.description = $(nodeElement).find('.rule-engine-label').text();// 節點展示名稱 this.$nodeModel.nodeTitle = $(nodeElement).attr('title');// 節點提示描述 this.$nodeModel.nodeClass = $(nodeElement).attr('nodeClass');// 節點實現類 this.$nodeModel.nodeIcon = nodeIcon;// 節點圖示(assets/img/rule-engine,assets/css/rule-engine.css) this.$nodeModel.nodeInput = nodeInput;// 節點輸入端點標識 this.$nodeModel.nodeOutput = nodeOutput;// 節點輸出端點標識 this.$nodeModel.nodeStyle = { "background-color": $(nodeElement).css("background-color"), "position": "absolute", "left": $(nodeElement).css("left"), "top": $(nodeElement).css("top") };// 節點樣式屬性 this.$nodeModel.components.options = {}; // 從儲存的$jsPlumbJson獲取options資料 if(this.$jsPlumbJson.nodes){ let length = this.$jsPlumbJson.nodes.length; for(let index = 0; index < length; index++){ if(this.$jsPlumbJson.nodes[index].id == this.$nodeModel.id && this.$jsPlumbJson.nodes[index].nodeType == this.$nodeModel.nodeType){ this.$nodeModel.components.options = new JsPlumbNodeModel(this.$jsPlumbJson.nodes[index]).components.options; break; } } } } // (資料處理)根據$nodeModel操作節點儲存或更新資料到$jsPlumbJson的nodes物件 public _saveOrUpdateRuleNode(){ if(!this.$nodeModel){ return; } if(!this.$jsPlumbJson.nodes){// 節點儲存 this.$jsPlumbJson.nodes = []; this.$jsPlumbJson.nodes.push(new JsPlumbNodeModel(this.$nodeModel)); }else{ let length = this.$jsPlumbJson.nodes.length; if(length == 0){ this.$jsPlumbJson.nodes.push(new JsPlumbNodeModel(this.$nodeModel)); }else{ let isUpdate = false; for(let index = 0; index < length; index++){ if(this.$jsPlumbJson.nodes[index].id == this.$nodeModel.id && this.$jsPlumbJson.nodes[index].nodeType == this.$nodeModel.nodeType){ this.$jsPlumbJson.nodes[index] = new JsPlumbNodeModel(this.$nodeModel); isUpdate = true; break; } } if(!isUpdate){ this.$jsPlumbJson.nodes.push(new JsPlumbNodeModel(this.$nodeModel)); } } } } // (資料處理)根據Dom操作節點刪除對應的$jsPlumbJson的nodes物件 private _deleteRuleNode(nodeElement){ if(!nodeElement){ return; } let id = $(nodeElement).attr('id');// 節點唯一標識 let nodeType = $(nodeElement).attr('nodeType');// 節點型別 // 從儲存的$jsPlumbJson刪除資料 if(this.$jsPlumbJson.nodes){ let length = this.$jsPlumbJson.nodes.length; for(let index = 0; index < length; index++){ if(this.$jsPlumbJson.nodes[index].id == id && this.$jsPlumbJson.nodes[index].nodeType == nodeType){ this.$jsPlumbJson.nodes.splice(index,1); this.$nodeModel = new JsPlumbNodeModel(); break; } } } } // 獲取所有設計的規則引擎節點及連結內容 public _getJsPlumbConnections(){ let oper = this; oper.$jsPlumbJson.connections = []; $.each(oper.$jsPlumbInstance.getAllConnections(), function (index, connection) { const label = connection.getOverlay("label").getLabel(); oper.$jsPlumbJson.connections.push({ uuids: connection.getUuids(), label: label, anchors: $.map(connection.endpoints, function(endpoint) { return [[endpoint.anchor.x, endpoint.anchor.y, endpoint.anchor.orientation[0], endpoint.anchor.orientation[1], endpoint.anchor.offsets[0], endpoint.anchor.offsets[1]]]; }) }); }); return oper.$jsPlumbJson; }
3.核心部分處理已經貼出來了,後續就是對元件的動態載入及屬性自定義實現內容
(it開發交流QQ群:101951157)