1. 程式人生 > >angular6基於jsplumb的規則引擎流程設計實現

angular6基於jsplumb的規則引擎流程設計實現

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)