赞
踩
接到了数据血缘的需求,前端要求效果类似sqlflow。通过大佬的类似demo发现了jsplumb这个连线库。然后看文档和github一些demo捣鼓出来了。基本效果如下:
连线样式为贝塞尔曲线的表现:
连线样式为状态机的表现:
https://github.com/mizuhokaga/jsplumb-dataLineage
(后端示例json项目附带,后端项目待开源。可参考格式,需注意github中提到的json的node对象的属性id不能带特殊符号和数字!)
目前已实现效果:
设计思想参考无临时表的sqlflow。在没有临时表的情况下,数据血缘只有两种表,起源表和目标(结果)表。起源表在画布左边,仅需要右边的锚点(锚点是jsPlumb的概念,参考jsplumb中文文档)目标表在画布右边仅需要左边的锚点。设计目标类似下图,注意我关闭了show intermediate recordset
,即不显示临时表。
所以我先根据后端json数据依靠模板渲染出不同类型的节点(节点就是起源表和目标表)设置好锚点,再利用jsplumb连线、绑定事件。
<!-- 起源表--> <script id="tpl-Origin" type="text/html"> <div class="pa" id='{{id}}' style='top:{{top}}px;left:{{left}}px'> <div class="panel panel-node panel-node-origin" id='{{id}}-inner'> <div id='{{id}}-heading' data-id="{{id}}" class="table-header">{{name}}</div> <ul id='{{id}}-cols' class="col-group"> </ul> </div> </div> </script> <!-- 目标表--> <script id="tpl-RS" type="text/html"> <div class="pa" id='{{id}}' style='top:{{top}}px;left:{{left}}px'> <div class="panel panel-node panel-node-rs" d='{{id}}-inner'> <div id='{{id}}-heading' data-id="{{id}}" class="table-header" style="background-color: #d26b58;color: white"> {{name}} </div> <ul id='{{id}}-cols' class="col-group"> </ul> </div> </div> </script>
function main() {
jsPlumb.setContainer('bg');
// 请求接口血缘json
$.get(requestURL, function (res, status) {
if (status === "success") {
jsonData = res;
DataDraw.draw(jsonData)
}
}, 'json');
// 或使用本地数据
// DataDraw.draw(json);
}
var DataDraw = { // 核心方法 draw: function (json) { var $container = $(areaId) var that = this //遍历渲染所有节点 json.nodes.forEach(function (item, key) { var data = { id: item.id, name: item.id, top: item.top, left: item.left, }; //根据不同类型的表获取各自的模板并填充数据 var template = that.getTemplate(item); $container.append(Mustache.render(template, data)); //根据json数据添加表的每个列 //将类数组对象转换为真正数组避免前端报错 XX.forEach is not a function item.columns = Array.from(item.columns); //将该表的所有列 item.columns.forEach(col => { var ul = $('#' + item.id + '-cols'); //这里li标签的id应该和 addEndpointOfXXX方法里的保持一致 col-group-item var li = $("<li id='id-col' class='panel-node-list' >col_replace</li>"); //修改每个列名所在li标签的id使其独一无二 li[0].id = item.name + '.' + col.name //填充列名 li[0].innerText = col.name; ul.append(li); }); //根据节点类型找到不同模板各自的 添加端点 方法 if (that['addEndpointOf' + item.type]) { that['addEndpointOf' + item.type](item) } }); //最后连线 this.finalConnect(json.nodes, json.relations) },
根据不同类型的模板添加节点的方法:
addEndpointOfOrigin: function (node) { //节点设置可拖拽 addDraggable(node.id); node.columns = Array.from(node.columns); node.columns.forEach(function (col) { //这里的id应该和draw方法里设置的id保持一致 setOriginPoint(node.id + '.' + col.name, 'Right') }) }, addEndpointOfRS: function (node) { addDraggable(node.id) node.columns = Array.from(node.columns); node.columns.forEach(function (col) { setRSPoint(node.id + '.' + col.name, 'Left') }) },
连线的方法,注释地很详细:
//根据节点类型找到对应的渲染方法 finalConnect: function (nodes, relations) { var that = this; nodes.forEach(function (node) { //RS表要排除, if (node.id != 'RS' && node.type != 'RS') { //遍历每个表的每个列 node.columns.forEach(col => { relations.forEach(relation => { var relName = relation.source.parentName + '.' + relation.source.column; var nodeName = node.name + '.' + col.name; //如果关系中的起始关系等于当前表节点的列,就构建连接 if (relName === nodeName) { //这里sourceUUID、targetUUID应该和addEndpoint里设置的uuid一致 var sourceUUID = nodeName + "-OriginTable"; var targetUUID = relation.target.parentName + '.' + relation.target.column + '-RSTable'; that.connectEndpoint(sourceUUID, targetUUID); //鼠标移动到连接线上后,两边的列高亮 jsPlumb.bind("mouseover", function (conn, originalEvent) { var src_name = conn.sourceId.split("."); var tar_name = conn.targetId.split("."); //注意 . 的转义,参考 https://blog.csdn.net/qq_44831907/article/details/120899676 $("#" + src_name[0] + "-cols").find("#" + src_name[0] + "\\." + src_name[1]).css("background-color", "#faebd7"); $("#" + tar_name[0] + "-cols").find("#" + tar_name[0] + "\\." + tar_name[1]).css("background-color", "#faebd7"); }); jsPlumb.bind("mouseout", function (conn, originalEvent) { var src_name = conn.sourceId.split("."); var tar_name = conn.targetId.split("."); $("#" + src_name[0] + "-cols").find("#" + src_name[0] + "\\." + src_name[1]).css("background-color", "#fff"); $("#" + tar_name[0] + "-cols").find("#" + tar_name[0] + "\\." + tar_name[1]).css("background-color", "#fff"); }); } }); }); } }) }, //真正调用的方法还是jsplumb的连接方法 connectEndpoint: function (from, to) { // 通过编码连接endPoint需要用到uuid jsPlumb.connect({uuids: [from, to]}) },
获取模板的方法:
getTemplate: function (node) {
return $('#tpl-' + node.type).html();
},
几个通用方法:
// 获取基本配置 function getBaseNodeConfig() { return Object.assign({}, visoConfig.baseStyle) }; // 让元素可拖动 function addDraggable(id) { jsPlumb.draggable(id, { containment: '#bg' }) }; // 设置起源表每一列的端点 function setOriginPoint(id, position) { var config = getBaseNodeConfig() config.isSource = true //一个起源表的字段可能是多个RS字段的来源 这里-1不限制连线数 config.maxConnections = -1 jsPlumb.addEndpoint(id, { anchors: [position || 'Right',], uuid: id + '-OriginTable' }, config) }; // 设置RS端点 function setRSPoint(id, position) { var config = getBaseNodeConfig() config.isTarget = true //RS表一个字段可能是来自多个起源表字段 这里-1不限制连线数 config.maxConnections = -1; jsPlumb.addEndpoint(id, { anchors: position || 'Left', uuid: id + '-RSTable' }, config) };
1.流程图下载为png图片
利用html2canvas这个js,由于jsplumb的线是svg无法被html2canvas识别,所以需要额外处理一下,参考这篇文章
function download_png() { if (typeof html2canvas !== 'undefined') { var nodesToRecover = []; var nodesToRemove = []; var svgElem = $("#bg").find('svg');//注意修改选取的dom元素 //将边(svg)转化了canvas的形式 svgElem.each(function (index, node) { var parentNode = node.parentNode; var svg = node.outerHTML.trim(); //canvas 容器 var canvas = document.createElement('canvas'); canvg(canvas, svg); if (node.style.position) { canvas.style.position += node.style.position; canvas.style.left += node.style.left; canvas.style.top += node.style.top; } nodesToRecover.push({ parent: parentNode, child: node }); parentNode.removeChild(node); nodesToRemove.push({ parent: parentNode, child: canvas }); parentNode.appendChild(canvas); }) } //scala属性解决生成的canvas模糊问题 html2canvas($("#bg"), {taintTest: false, scale: 2}).then(canvas => { var a = document.createElement('a'); //转换图片格式方法来自 https://blog.csdn.net/yzding1225/article/details/119215395 var blob = this.dataURLToBlob(canvas.toDataURL('image/png')); //这块是保存图片操作 可以设置保存的图片的信息 a.setAttribute('href', URL.createObjectURL(blob)); //图片名称是当前 时间戳+uuid a.setAttribute('download', new Date().getTime() + this.getUUID() + '.png'); a.click(); URL.revokeObjectURL(blob); a.remove(); //由于生成图片将svg转换了canvas导致边的hover事件失效,需要重新填入数据 or 刷新页面 //TODO:目前直接刷新整个页面 location.reload() }); };
2.流程图下载为json
这里偷懒,直接把后端传过来的json下载了
function download_json() {
//如果血缘信息json是直接从后端请求过来的,直接下载接口数据
var datastr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData));
var a = document.createElement('a');
a.setAttribute("href", datastr);
a.setAttribute("download", new Date().getTime() + this.getUUID() + '.json');
a.click();
a.remove();
};
3.流程图缩放
没什么好方法,暂时用的css的scala属性实现的
//原始尺寸 var baseZoom = 1; //重置缩放 function reset() { if (this.baseZoom !== 1) { this.baseZoom = 1; const zoom = this.baseZoom; this.zoom(zoom); jsPlumb.setZoom(baseZoom); } } //缩放是整个画布及其内容一起缩放 //参考 https://blog.csdn.net/KentKun/article/details/105230475 function zoom(scale) { $("#bg").css({ "-webkit-transform": `scale(${scale})`, "-moz-transform": `scale(${scale})`, "-ms-transform": `scale(${scale})`, "-o-transform": `scale(${scale})`, "transform": `scale(${scale})`, "transform-origin": "0% 0%" }) }; //放大 function zoomin() { this.baseZoom += 0.1; const zoom = this.baseZoom; this.zoom(zoom); jsPlumb.setZoom(zoom); }; //缩小 function zoomout() { this.baseZoom -= 0.1; const zoom = this.baseZoom; this.zoom(zoom); jsPlumb.setZoom(zoom); }
4.流程图拖动
本来想实现画布拖动,最后实现是把流程图中所有节点全部移动造成的假象,参考这里
X = 0; Y = 0; bgX = $("#bg").width(); bgY = $("#bg").height(); //拖动功能不够完善又缺陷。参考 https://blog.csdn.net/join_null/article/details/80266993 //松开鼠标右键 function mouseup(event) { if (event.button == 2) { $("#bg").css("cursor", "Auto") this.flag = false; } // console.log(this.X+"|"+this.Y) } //按下鼠标右键 function mousedown(event) { if (event.button == 2) { this.flag = true; $("#bg").css("cursor", "Grabbing"); var bx = event.offsetX; var by = event.offsetY; this.X = bx; this.Y = by; // console.log(this.X + "|" + this.Y) } } //按住右键拖动,血缘关系图会在框架内移动 function move(event) { if (flag && baseZoom===1) { //获取相对父元素的坐标 var ax = event.offsetX; var ay = event.offsetY; var tmp_x = (ax - this.X), tmp_y = (ay - this.Y); // console.log(tmp_x + "t|" + tmp_y) if (this.flag) { $("#bg .pa").each(function (index, node) { var a = tmp_x + $(node).position().left; var b = tmp_y + $(node).position().top; if (a >= bgX || a <= 0) a = bgX - $(node).width(); else if (b >= bgY || b <= 0) b = bgY - $(node).height(); else { $(node).css('left', $(node).position().left+tmp_x/25); $(node).css('top', $(node).position().top+tmp_y/25); } }); jsPlumb.repaintEverything(); } } };
5.选择连线后线两端节点高亮
利用jsplumb的连线事件实现的
//连线 that.connectEndpoint(sourceUUID, targetUUID); //鼠标移动到连接线上后,两边的列高亮 jsPlumb.bind("mouseover", function (conn, originalEvent) { var src_name = conn.sourceId.split("."); var tar_name = conn.targetId.split("."); //注意 . 的转义,参考 https://blog.csdn.net/qq_44831907/article/details/120899676 $("#" + src_name[0] + "-cols").find("#" + src_name[0] + "\\." + src_name[1]).css("background-color", "#faebd7"); $("#" + tar_name[0] + "-cols").find("#" + tar_name[0] + "\\." + tar_name[1]).css("background-color", "#faebd7"); }); jsPlumb.bind("mouseout", function (conn, originalEvent) { var src_name = conn.sourceId.split("."); var tar_name = conn.targetId.split("."); $("#" + src_name[0] + "-cols").find("#" + src_name[0] + "\\." + src_name[1]).css("background-color", "#fff"); $("#" + tar_name[0] + "-cols").find("#" + tar_name[0] + "\\." + tar_name[1]).css("background-color", "#fff"); });
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。