splumb 流程图,常用功能配置记录(上)

简介: splumb 流程图,常用功能配置记录
前言:

jsplumb 有2个版本一个Toolkit Edition(付费版),另外一个就是Community Edition(社区版本)。Toolkit Edition版本功能集成的比较丰富,社区版本的就差好多,很多功能都没有,需要我们自己去添加,当然了自己添加多多少少有些麻烦,而且也不完善。但是我们还是用Community Edition(社区版本),毕竟不收费,没办法,下边所说的版本默认都是以社区版。

最近公司项目有用到这个流程图,所以也就看到了这个框架,这个框架是英文版本的,地址:https://jsplumbtoolkit.com/community/doc/home.html(可以用浏览器翻译了看)。他的缺陷就是文档不全,api感觉也有点乱,实例交代的也不清楚,github地址是:https://github.com/jsplumb/jsplumb (里面有demo,自己可以下载运行,多动手试试)。如果只是简单的画个图,这个框架是没有什么问题的,demo里也有,但是如果要实现高级的动能呢鞥,还是得多多尝试。此文也是记录一下我自己用到的一些功能,很多我还没用到,用到了在慢慢补充。

image.png

jsplumb.png


上图也就是我这次用到的jsplumb实现的功能,连接线能够拖拽生成,也可以删除,编辑label。

1、数据结构

{
  "nodes": [{  //节点集合
    "icon": "el-icon-loading",
    "id": "start",
    "nodeStyle": {
      "top": 100,
      "left": 200
    },
    "text": "开始",
    "type": "circle"
  }, {
    "icon": "el-icon-upload",
    "id": "end",
    "nodeStyle": {
      "top": 300,
      "left": 400
    },
    "text": "结束",
    "type": "circle"
  }] ,
  "connections": [{  //连接线集合
      "sourceId": "start",
      "targetId": "end",
      "label":"编辑"
    }]
}

jsplumb实例里面的数据结构就是这样的,这里我们沿用他的数据结构,你也可以自己定义自己想的数据结构,但是对比起来这个结构是最清晰也最方便的。

2、初始化

jsplumb在DOM渲染完毕之后才会执行,所以需要为jsplumb的执行代码绑定一个ready事件:

jsPlumb.ready(function() { 
    // your jsPlumb related init code goes here
});

jsplumb默认是注册在浏览器窗口的,将整个页面提供给我们作为一个实例,但我们也可以使用getInstance方法建立页面中一个独立的实例:

var _instance = jsPlumb.getInstance();

3、功能实现(允许哪些元素拖拽,允许拆卸连接)

let instance = jsPlumb.getInstance({
                PaintStyle:{ 
                    strokeWidth:2, 
                    stroke:"#567567", 
                }
            })
        //拖拽功能
        var els = document.querySelectorAll(".box");//.box是允许拖拽的元素class类名
        instance.draggable(els,{
           containment:true,
           filter: ".ep",//除去不能拖拽的,这里是个class类名
        });
        //不允许拆卸连接,不设置的话默认是可以的
        instance.importDefaults({ 
          ConnectionsDetachable:false
        });

4、连线监听事件(拖动connection 事件)

// 监听拖动connection 事件,判断是否有重复链接
        instance.bind("beforeDrop", function(info) {
            // info.connection.getOverlay("label").setLabel(info.connection.id);
            // 判断是否已有该连接
            let isSame = true;
            //下边的forEach循环就是处理数据结构里的connections不能自己跟自己连线。当然你也可以处理其他
            _this.chartData.connections.forEach(item => {
              if ((item.targetId === info.targetId && item.sourceId === info.sourceId) ||  (item.targetId === info.sourceId && item.sourceId === info.targetId)) {
                isSame = false;
              }
            });
            if (isSame) {
                //允许连线后处理的情况
            } else {
              alert("不允许重复连接!");
            }
            return isSame;//这里返回了ture就会自定进行连线。
        });

5、上图实现的完整代码

下边代码就是实现上图的,需要指出的是运用了vue,但是里面掺杂了jquery,和jquery-ui,其实不想用这2个的,但是项目紧,之前项目也用到了,所以就延续了。还有就是上面代码是我自己的测试代码,写的可能有些杂乱,就是测试一个一个功能而写,写的有点乱。

还有一个想说的就是之前想实现,缩放,引入了panzoom.js,流程图也实现了滚动鼠标放大放小,但是有个问题就是滚动鼠标放大放小后如果拖动单个元素或者连线,你就会发现鼠标点对不齐了,这点还没有解决,如果有好的方案,可以告知我下。Toolkit Edition(付费版)的这些功能都有,就不会出现这样的问题。

<template>
  <div id="test6" style="height:100%;position:relative">
      <section id="focal"  style="position:relative;overflow:hidden;width:610px;height:610px;background:#fff;border:1px solid red">
        <div class="parent" id="parent" style="height:100%;">
          <div class="panzoom" id="panzoom" style="border:1px solid blue;width:6000px;height:6000px; transform:translate(-50%, -50%);position:absolute;">
            <div class="box" :id="item.id" :style="{'top':item.nodeStyle.top+'px','left':item.nodeStyle.left+'px'}" v-for="item in chartData.nodes" :key="item.id">
                <i :class="item.icon" class="oldIcon" :title="item.text"></i>
                <i class="el-icon-circle-close" style="display:none" :title="item.text" :id="item.id"></i>
                <div class="ep"></div>
            </div>
          </div>
      </div>
    </section>
    <div class="source">
        <ul>
            <li v-for="(item,index) in list" :id="item.id" :key="index" class="sourceLi" :disabled="true" :data-icon="item.icon" :data-text="item.text" :data-type="item.type">{{item.text}}</li>
        </ul>
    </div>
        <el-dialog
              title="修改label名称"
              :visible.sync="dialogVisible"
              width="30%"
              :before-close="handleClose">
              <el-input v-model="labelName" placeholder="请输入"></el-input>
              <span slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="changeNote">确 定</el-button>
              </span>
            </el-dialog>
  </div>
</template>
<script>
import ChartNode from "@/components/ChartNode";
export default {
  name: "test6",
  data() {
    return {
        dialogVisible:false,
        labelName:"",
        curSourceId:'',
        curTargetId:'',
        addLabelText:'',//拖拽后生成的连线label文字
        jsp:null,
        myscale:1,
        curScreen:[],//当前屏幕宽高
        chartData: {
            nodes: [],
            connections: [],//{ "targetId": "box2", "sourceId": "box1" }
            props: {},
            screen:[610,610]//提交屏幕宽高
        },
        list: [
          {
            icon: "el-icon-goods",
            text: "伴随车牌",
            type: "circle",
            id:'li1'
          },
          {
            icon: "el-icon-bell",
            text: "常住人口筛选",
            type: "diamond",
            id:"li2"
          },
          {
            icon: "el-icon-date",
            text: "伴随imsi",
            type: "circle",
            id:"li3"
          }
        ]
    };
  },
  mounted() {
    let _this = this
    jsPlumb.ready(function() {
        var $section = $('#focal');
        var $panzoom = $section.find('.panzoom').panzoom({ 
             minScale: 0.3,
             maxScale:2,
             eventNamespace: ".panzoom",
             $zoomRange: $(".jtk-endpoint"),
             $set: $section.find('.jtk-overlay'),
             eventsListenerElement: document.querySelector('.box')
        });
        $(document).on('mouseover','.box,.jtk-draggable,.jtk-overlay,.ep',function(){
          $('.panzoom').panzoom("disable");
        })
        $(document).on('mouseleave','.box,.jtk-draggable,.jtk-overlay,.ep',function(){
          $('.panzoom').panzoom("enable");
        })
        let instance = jsPlumb.getInstance({
                PaintStyle:{ 
                    strokeWidth:2, 
                    stroke:"#567567", 
                },
                // Connector: ["Straight", { stub: [0,0], gap:[-30,-30] }],
                Connector:[ "Straight", { curviness: 0 } ],
                Endpoint: ["Blank",{ cssClass: "chart-dot", hoverClass: "chart-dot-hover", radius: 5 }],
                EndpointStyle : { fill: "blue"  },
                HoverPaintStyle:{
                    stroke:"red", 
                },
                DragOptions: { cursor: "pointer", zIndex: 2000 },
                ConnectionOverlays: [
                  [
                    "Arrow",
                    {
                      location: 1,
                      visible: true,
                      width: 11,
                      length: 11,
                      id: "ARROW",
                      events: {
                        click: function() {
                          alert("you clicked on the arrow overlay");
                        }
                      }
                    }
                  ],
                  ["Label", { label: "", id: "label", cssClass: "aLabel" }]
                ],
                Container: "panzoom"
            })
        _this.jsp = instance;
        //拖拽功能
        var els = document.querySelectorAll(".box");
        instance.draggable(els,{
           containment:true,
           filter: ".ep",//除去不能拖拽的
           grid:[50,50]
        });
        //不允许拆卸连接,不设置的话默认是可以的
        instance.importDefaults({ 
          ConnectionsDetachable:false
        });
        // 监听拖动connection 事件,判断是否有重复链接
        instance.bind("beforeDrop", function(info) {
            // info.connection.getOverlay("label").setLabel(info.connection.id);
            console.log(info);
            // 判断是否已有该连接
            let isSame = true;
            _this.chartData.connections.forEach(item => {
              if ((item.targetId === info.targetId && item.sourceId === info.sourceId) ||  (item.targetId === info.sourceId && item.sourceId === info.targetId)) {
                isSame = false;
              }
            });
            if (isSame) {
                _this.addLabelText = "新label"
                _this.chartData.connections.push({
                    sourceId: info.sourceId,
                    targetId: info.targetId,
                    label:_this.addLabelText
                });
            } else {
              alert("不允许重复连接!");
            }
            return isSame;
        });
            var initNode = function(el) {
                     instance.draggable(el, {
                      // containment: true,
                      start(params) {
                        // 拖动开始
                        // console.log(params);
                      },
                      drag(params) {
                        // 拖动中
                        // console.log(params);
                      },
                      stop(params) {
                        // 拖动结束
                        console.log(params);
                        let id = params.el.id;
                        _this.chartData.nodes.forEach(item => {
                          if (item.id === id) {
                            item.nodeStyle.left = params.pos[0];
                            item.nodeStyle.top = params.pos[1] ;
                          }
                        });
                      }
                    });
                    instance.makeSource(el, {
                        filter: ".ep",
                        anchor: ["Perimeter", { shape: "Rectangle" }],
                        // anchor: ["Perimeter", { shape: "Dot" }],
                        connectorStyle: {
                            stroke: "#5c96bc",
                            strokeWidth: 2,
                            outlineStroke: "transparent",
                            outlineWidth: 4
                        },
                        extract: {
                            action: "the-action"
                        },
                        maxConnections: -1,
                        onMaxConnections: function(info, e) {
                            alert("Maximum connections (" + info.maxConnections + ") reached");
                        }
                    });
                    instance.makeTarget(el, {
                      dropOptions: { hoverClass: "dragHover" },
                      anchor: ["Perimeter", { shape: "Rectangle" }],
                      allowLoopback: false
                    });
                    // instance.fire("jsPlumbDemoNodeAdded", el);
                };
            //初始化遮罩层   
            var init = function(connection) {
                if(_this.addLabelText){
                    connection.getOverlay("label").setLabel(_this.addLabelText);
                }else{
                     connection.getOverlay("label").setLabel('编辑');
                }
                $(connection.getOverlay("label").canvas).attr('mySourceId',connection.sourceId)
                $(connection.getOverlay("label").canvas).attr('myTargetId',connection.targetId)
            };
  // 将模块拖入画板中
      $(".sourceLi").draggable({
        scope: "plant",
        helper: "clone",
        opacity: 0.7,
        containment: $("#test1")
      });
      $("#panzoom").droppable({
        scope: "plant",
        drop: function(ev, ui) {
          console.log(ev, ui);
          let helper = ui.helper;
          let id = jsPlumbUtil.uuid();
          let item = {
            id,
            icon: helper.attr("data-icon"),
            type: helper.attr("data-type"),
            text: helper.attr("data-text"),
            nodeStyle: {
              top: ui.offset.top - $("#panzoom").offset().top ,
              left: ui.offset.left - $("#panzoom").offset().left 
            }
          };
          console.log(ui.position)
          _this.chartData.nodes.push(item);
          _this.$nextTick(() => {
            initNode(id);
          });
        }
      });
         instance.batch(() => {
                jsPlumb.getSelector(".box").forEach(item => {
                    console.log(item)
                    initNode(item);
                });
                instance.bind("connection", function(connInfo, originalEvent) {
                    init(connInfo.connection);
                    //显示删除按钮
                    $(connInfo.connection.getOverlay("label").canvas).hover(function() {
                        $(this).append('<div class="x" style="position: absolute;">X</div>');
                    }, function() {
                        $(this).find(".x").stop().remove();
                    })
                    //删除连接
                    $(connInfo.connection.getOverlay("label").canvas).on('click','.x',function(){
                        console.log("shanchu")
                        let _connections = _this.chartData.connections;
                        _connections.forEach((val,index)=>{
                            if(val.targetId == connInfo.connection.targetId && val.sourceId == connInfo.connection.sourceId){
                                _connections.splice(index,1)
                            }
                        })
                        instance.deleteConnection(connInfo.connection);
                        $('.panzoom').panzoom("enable");//这个是为了杜绝删除前的禁止拖拽事件
                    })
                    //label双击事件
                    $(connInfo.connection.getOverlay("label").canvas).on("dblclick",function(conn, connInfo){
                        let _allConnections = _this.jsp.getAllConnections();
                        _this.dialogVisible = true
                        _this.curSourceId = $(conn.target).attr('mySourceId')
                        _this.curTargetId = $(conn.target).attr('myTargetId')
                        _allConnections.forEach((val,index)=>{
                            if(val.targetId == $(conn.target).attr('myTargetId') && val.sourceId == $(conn.target).attr('mySourceId')){
                                _this.labelName =  val.getOverlay('label').label
                            }
                        })
                    })               
                });
        });
        instance.fire("jsPlumbDemoLoaded", instance);
        $(document).on("dblclick",".box",function(){
            $(this).find(".oldIcon").css('display','none')
            $(this).find('.el-icon-circle-close').css('display','inline-block')
        })
        $(document).on("click",".el-icon-circle-close",function(){
            let _note = _this.chartData.nodes
            let _id = $(this).attr("id")
            let _connections = _this.chartData.connections;
            let _allConnections = instance.getAllConnections();
            _this.chartData.connections = _connections.filter((val)=>{
                return (val.targetId != _id && val.sourceId != _id)
            })
            _note.forEach((val,index)=>{
                if(val.id == _id){
                    _note.splice(index,1)
                }
            })
              _allConnections.forEach((val,index)=>{
                if(val.targetId == _id || val.sourceId == _id){
                    instance.deleteConnectionsForElement(_id)
                }
            })
        })
            _this.handleClickTemp(1)
});
  },
  methods:{
        myclick(){
            alert("myclickmyclickmyclickmyclick")
        },
        // 初始化node节点
        initNode(el) {
          // initialise draggable elements.
          // 元素拖动,基于 katavorio.js 插件
          let _self = this;
          this.jsp.draggable(el, {
            // containment: true,
            start(params) {
              // 拖动开始
              // console.log(params);
            },
            drag(params) {
              // 拖动中
              // console.log(params);
            },
            stop(params) {
              // 拖动结束
              console.log(params);
              let id = params.el.id;
              _self.chartData.nodes.forEach(item => {
                if (item.id === id) {
                  item.nodeStyle.left = params.pos[0]
                  item.nodeStyle.top = params.pos[1]
                }
              });
            }
          });
          this.jsp.makeSource(el, {
            filter: ".ep",
            // anchor: "Continuous",
            anchor: ["Perimeter", { shape: "Rectangle" }],
            connectorStyle: {
              stroke: "#5c96bc",
              strokeWidth: 2,
              outlineStroke: "transparent",
              outlineWidth: 4
            },
            extract: {
              action: "the-action"
            },
            maxConnections: -1,
            onMaxConnections: function(info, e) {
              alert("Maximum connections (" + info.maxConnections + ") reached");
            }
          });
          this.jsp.makeTarget(el, {
            dropOptions: { hoverClass: "dragHover" },
            anchor: ["Perimeter", { shape: "Rectangle" }],
            allowLoopback: false
          });
          // this is not part of the core demo functionality; it is a means for the Toolkit edition's wrapped
          // version of this demo to find out about new nodes being added.
          //
          this.jsp.fire("jsPlumbDemoNodeAdded", el);
        },
        handleClickTemp(key) {
          this.chartData = {
            nodes: [],
            connections: [],
            props: {}
          };
          this.jsp.empty("panzoom");
          if (key) {
            let url = "/static/json/" + 1 + ".json";
            this.$axios
              .get(url)
              .then(resp => {
                console.log(resp);
                let _data = resp.data
                let _reloatScreen = _data.screen
                let _scale = $("#focal").width() / _data.screen[0]
                let _focalWidth = $("#focal").width()
                let _focalHeight = $("#focal").height()
                let _panzoomWidth = $("#panzoom").width()
                debugger
                _data.nodes.forEach((val,index)=>{
                    val.nodeStyle.left = parseInt(val.nodeStyle.left) * _scale - (_panzoomWidth*_scale-_panzoomWidth)/2 
                    val.nodeStyle.top  = parseInt(val.nodeStyle.top) * _scale - (_panzoomWidth*_scale-_panzoomWidth)/2 
                })
                // $("#panzoom").css({'width':_panzoomWidth*_scale+'px','height':_panzoomWidth*_scale+'px'})
                this.chartData = _data;
                this.$nextTick(() => {
                  this.chartData.nodes.forEach(item => {
                    this.initNode(item.id);
                  });
                  this.chartData.connections.forEach(item => {
                    let _connects = this.jsp.connect({
                      source: item.sourceId,
                      target: item.targetId
                    });
                     _connects.getOverlay("label").setLabel(item.label)
                     $(_connects.getOverlay("label").canvas).attr('mySourceId',item.sourceId)
                     $(_connects.getOverlay("label").canvas).attr('myTargetId',item.targetId)
                  });
                });
              })
              .catch(err => {
                console.log(err);
              });
          } else {
            this.$nextTick(() => {
              this.chartData.nodes.push({
                id: "start",
                icon: "el-icon-loading",
                type: "circle",
                text: "开始",
                nodeStyle: {
                  top: "100px",
                  left: "300px"
                }
              });
              this.$nextTick(() => {
                this.jsp.batch(() => {
                  this.initNode(jsPlumb.getSelector("#start"));
                });
              });
            });
          }
        },
        changeNote(){//修改label
            if(!this.labelName){
                alert("名称没有填写")
                return false
            }
            let _allConnections = this.jsp.getAllConnections();
            _allConnections.forEach((val,index)=>{
                if(val.sourceId == this.curSourceId && val.targetId == this.curTargetId ){
                    val.getOverlay("label").setLabel(this.labelName)
                }
            })
            this.chartData.connections.forEach(val => {
                if(val.sourceId == this.curSourceId && val.targetId == this.curTargetId ){
                    val.label = this.labelName
                }
           });
           this.dialogVisible = false
        },
        handleClose(){
            this.dialogVisible = false
        }
  },
  components: {
    ChartNode
  }
};
</script>
<style lang="scss" scoped>
#test1{
  position:relative;
  width:90%;
  height:90%;
  border:1px solid #ddd;
  background:#fff;
}
.box{
    border-radius:50%;
    text-align: center;
    cursor: pointer;
  background-color: white;
  border: 1px solid #346789;
  text-align: center;
  z-index: 24;
  cursor: pointer;
  box-shadow: 2px 2px 19px #aaa;
  -o-box-shadow: 2px 2px 19px #aaa;
  -webkit-box-shadow: 2px 2px 19px #aaa;
  -moz-box-shadow: 2px 2px 19px #aaa;
  position: absolute;
  color: black;
  padding: 0.5em;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-transition: -webkit-box-shadow 0.15s ease-in;
  -moz-transition: -moz-box-shadow 0.15s ease-in;
  -o-transition: -o-box-shadow 0.15s ease-in;
  transition: box-shadow 0.15s ease-in;
    .ep {
        opacity: 0;
        position: absolute;
        right: -10px;
        top: 0;
        width: 10px;
        height: 10px;
        background: #409eff;
        border-radius: 5px;
      }
      &:hover {
        .ep {
          opacity: 1;
        }
      }
      &.dragHover {
        .ep {
          opacity: 0;
        }
      }
  }
.box:hover {
  border: 1px solid #123456;
  box-shadow: 2px 2px 19px #444;
  -o-box-shadow: 2px 2px 19px #444;
  -webkit-box-shadow: 2px 2px 19px #444;
  -moz-box-shadow: 2px 2px 19px #fff;
  opacity: 0.9;
}
.box:hover,
.box.jtk-source-hover,
.box.jtk-target-hover {
  border: 1px solid orange;
  color: orange;
}
.box1{
  top:50px;
  left:50px;
}
.box2{
  top:160px;
  left:250px;
}
.box3{
  top:360px;
  left:150px;
}
.box4{
  top:350px;
  left:450px;
}
.chart-dot-hover{
  display: block;
  background: red
}
.source{
    position:absolute;
    top:50px;
    right:50px;
    border:1px solid red;
    width:200px;
    height:300px;
    li{
        line-height:36px;
        border:1px solid #ddd;
        margin-bottom:10px;
        cursor:pointer
    }
}
</style>
<style>
    .aLabel{
       border: 1px solid blue;
       padding: 4px;
}
.x{
    top:-10px;
    right:-10px;
    cursor: pointer;
}
.jtk-overlay{
    padding: 0
}
</style>


相关文章
|
26天前
|
测试技术 开发者
设计文档中的流程图,靠得住吗?
本文讨论了软件开发设计文档中图形化设计图的重要性,如流程图、思维导图等,它们有助于清晰传达设计意图和提高沟通效率。然而,当面临迭代更新、人员变动时,基于截图的图形设计图可能会带来协作难题。作者提倡使用简单文字格式搭配标签和符号作为替代方案,分享了团队内部实践,通过表格来实现类似思维导图和流程图的功能,以增强文档的可维护性和协作性。同时,作者强调这不是反对使用设计图,而是提出在某些场景下的一种有效补充方法。
80 7
|
7月前
Axure快速入门(05) - Master母版减少重复操作
Axure快速入门(05) - Master母版减少重复操作
21 0
|
10月前
|
SQL Java 关系型数据库
从系统报表页面导出20w条数据到本地只用了4秒,我是如何做到的
最近有个学弟找到我,跟我描述了以下场景: 他们公司内部管理系统上有很多报表,报表数据都有分页显示,浏览的时候速度还可以。但是每个报表在导出时间窗口稍微大一点的数据时,就异常缓慢,有时候多人一起导出时还会出现堆溢出。 他知道是因为数据全部加载到jvm内存导致的堆溢出。所以只能对时间窗口做了限制。以避免因导出过数据过大而引起的堆溢出。最终拍脑袋定下个限制为:导出的数据时间窗口不能超过1个月。
|
11月前
|
JavaScript 前端开发 API
使用Jsmind实现前端流程图功能
使用Jsmind实现前端流程图功能
|
数据可视化 Linux 异构计算
如何自动生成设计文件的状态机跳转图(仿真工具使用技巧)【Modesim/Questasim】
如何自动生成设计文件的状态机跳转图(仿真工具使用技巧)【Modesim/Questasim】
如何自动生成设计文件的状态机跳转图(仿真工具使用技巧)【Modesim/Questasim】
|
JavaScript 小程序 计算机视觉
记录一次小程序卡片组件封装的实战
来分析一下我这次所做项目的需求,首先重要的是卡片内部的布局需要卡片组件需要应用在两个场景下,每个场景的部分文本内容有区别,所以需要进行文本控制,而又要考虑到文本长度的问题,所以需要对文本内容进行一些处理,整理一下得出以下三个点
162 0
记录一次小程序卡片组件封装的实战
|
Web App开发 存储 负载均衡
RPA 流程梳理和适用场景以及控制台功能展示(一)| 学习笔记
快速学习 RPA 流程梳理和适用场景以及控制台功能展示。
348 0
RPA 流程梳理和适用场景以及控制台功能展示(一)| 学习笔记
|
人工智能 监控 安全
RPA 流程梳理和适用场景以及控制台功能展示(二)| 学习笔记
快速学习 RPA 流程梳理和适用场景以及控制台功能展示。
369 0
RPA 流程梳理和适用场景以及控制台功能展示(二)| 学习笔记
|
Web App开发 文字识别 负载均衡
RPA 流程梳理和适用场景以及控制台功能展示(一)|学习笔记
快速学习 RPA 流程梳理和适用场景以及控制台功能展示(一)
638 0
RPA 流程梳理和适用场景以及控制台功能展示(一)|学习笔记
|
人工智能 监控 安全
RPA 流程梳理和适用场景以及控制台功能展示(二)|学习笔记
快速学习 RPA 流程梳理和适用场景以及控制台功能展示(二)
152 0
RPA 流程梳理和适用场景以及控制台功能展示(二)|学习笔记