其实当前Web库实现Canvas绘制树状结构的组件很多,而且功能也很强大,但是难免有些场景无法实现需要自己开发,本文主要是提供一种思路

先附一个不错的拓扑图开发地址:https://www.zhihu.com/question/41026400

一、开发思路

开发最大的难点是如何计算每个节点所在的位置坐标,保证所有节点的居中对称性,如果有了坐标绘制起来就方便很多,具体可见下图

1. 将每个分支看作是一个组,比如节点1看错是一个Group,下面三个分支分别又是Group1、Group2、Group3,而Group1中又有三个Group(比如Group4 等等...)。

2. 对节点数据采用递归循环的方式找到最大的层数,列中为4层。

3. 再次递归循环源数据,判断如果当前层数据非最大层时,则自动补充一个虚拟节点到数据中,一直递归直到最大一层停止。

4. 到第3步可以说所有节点已经补充齐,然后再次递归第3步获取的数据,到最后一层节点时即向上报数,父节点收到消息后计算+1,同时给自己父节点上报消息,如此循环反复,即可准确获得每个节点所包含的最下面一层对应的节点个数,通过节点个数*节点宽度即可获得每个Group所需要的宽度,同时也就能计算各个节点自己的中心点坐标了,简易流程如图二

二、上代码

1. 首先创建三个文件element.js、arrow.js、group.js分别代表元素、箭头和组 的类,可以通过对类的继承实现不同的元素效果,本文只展示简单的要素,就不演示继承的用法了

/**
* 元素,包含元素的样式属性以及绘制范围等信息
* 每个要素所有的子元素都被包含在Group要素中
*/
import util from "./util.js";
import Group from './group.js'; export default class Element {
constructor(options) {
// 横向文字距边框宽度
this.rowPadding = 10;
// 纵向文字距边框宽度
this.coloumnPadding = 5;
// 元素外边距
this.margin = util.CON.MARGIN; this.fontSize = 20; this.width = util.CON.WIDTH; this.height = util.CON.HEIGHT; // 名称,可随机
this.name = options.name; // ID值 可随机
this.id = options.id || util.guid('element'); // 显示文本内容
this.text = options.text; // 所有子和孙辈数据个数
this.chiCount = 0; // 标识该要素展开还是收缩的
this.openFlag = false; // 元素的中心位置
this.center = this.getCenter(options.xRange, options.yRange); this.coordinates = this.getCoordinate(); this.group = this.creat(options.children || [], options.needBorder); // 收缩框数据集
this.shrink = options.shrink;
} /**
* 获取要素的中心点
* @param {*} xRange
* @param {*} yRange
*/
getCenter(xRange, yRange) {
return [(xRange[1] - xRange[0]) / 2 + xRange[0], (yRange[1] - yRange[0]) / 2 + yRange[0]];
} /**
* 根据圆心以及边框宽高获取圆心
* 规则为 [左上, 右上,右下,左下,左上]
*/
getCoordinate() {
return [
[-this.width / 2, -this.height / 2],
[this.width / 2, -this.height / 2],
[this.width / 2, this.height / 2],
[-this.width / 2, this.height / 2]
]
} /**
* 绘制矩形边框,包含圆角
*/
draw() {
const radius = 4;
const coor = this.coordinates;
const ctx = util.getContext(); ctx.save();
ctx.translate(this.center[0], this.center[1]);
ctx.lineWidth = 2;
ctx.strokeStyle = '#558dbd';
ctx.fillStyle = '#f6fafd';
ctx.shadowColor = '#a9d4f5';
ctx.shadowBlur = 5; ctx.beginPath();
ctx.arc(coor[0][0] + radius, coor[0][1] + radius, radius, Math.PI, Math.PI * 3 / 2);
ctx.lineTo(coor[1][0] -radius, coor[1][1]);
ctx.arc(coor[1][0] - radius, coor[0][1] + radius, radius, -Math.PI / 2, 0);
ctx.lineTo(coor[2][0], coor[2][1] - radius);
ctx.arc(coor[2][0] - radius, coor[2][1] - radius, radius, 0, Math.PI / 2);
ctx.lineTo(coor[3][0] + radius, coor[3][1]);
ctx.arc(coor[3][0] + radius, coor[3][1] - radius, radius, Math.PI / 2, Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fill();
ctx.restore(); if (this.group) {
this.group.draw();
} this.drawText();
this.drawCircle();
} /**
* 绘制文本内容
*/
drawText() {
const ctx = util.getContext();
ctx.save();
ctx.translate(this.center[0], this.center[1]);
ctx.font = this.fontSize + 'px serif';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText(this.text, 0, 0 + this.fontSize / 3, this.width - this.rowPadding * 2);
ctx.restore();
} /**
* 绘制圆圈
*/
drawCircle() {
const circleRadius = 6;
const ctx = util.getContext();
ctx.save();
ctx.strokeStyle = '#16ade7';
ctx.lineWidth = 3;
ctx.fillStyle = '#fff';
ctx.translate(this.center[0], this.center[1]); if (this.group.children.length || this.openFlag) {
ctx.beginPath();
ctx.arc(0, this.height / 2, circleRadius, 0, Math.PI * 2);
ctx.closePath();
// ctx.strokeStyle = '#f18585';
if (this.openFlag) {
ctx.lineWidth = 1;
ctx.moveTo(0, -circleRadius + 2 + this.height / 2);
ctx.lineTo(0, circleRadius - 2 + this.height / 2);
ctx.moveTo(-circleRadius + 2, this.height / 2);
ctx.lineTo(circleRadius - 2, this.height / 2);
} else {
ctx.lineWidth = 1;
ctx.moveTo(-circleRadius + 2, this.height / 2);
ctx.lineTo(circleRadius - 2, this.height / 2);
}
ctx.fill();
ctx.stroke();
} ctx.restore();
} /**
* 根据子元素生成对应的类
*/
creat(children, needBorder) {
return new Group({ children, needBorder, parent: this });
} /**
* 计算元素的宽和高,用于后续计算元素的位置
*/
calWidthHeight() {
const style = util.getContext().measureText(this.text);
this.width = style.width + this.rowPadding * 2;
this.height = this.fontSize + this.coloumnPadding * 2;
}
}

element.js

import util from "./util.js";

/**
* 箭头类
* 主要通过箭头的起始要素和终点要素,以此来计算起点位置箭头的角度偏移量和方向
*/
export default class Arrow {
constructor(options, fromEle, toEle) {
this.id = options.id || util.guid('arrow');
// 箭头起始要素
this.from = fromEle;
// 箭头结束要素
this.to = toEle;
// 箭头坐标信息
this.coordinates = this.getCoordinate();
} /**
* 获取坐标信息
* @returns
*/
getCoordinate() {
const fromC = this.from.center;
const toC = this.to.center;
return [
[fromC[0], fromC[1] + this.from.height / 2],
[toC[0], toC[1] - (toC[1] - fromC[1]) / 2],
[toC[0], toC[1] - this.to.height / 2]
]
} /**
* 主要绘制边框等信息内容
*/
draw() {
const coor = this.coordinates;
const ctx = util.getContext();
ctx.save();
ctx.lineWidth = 1;
ctx.strokeStyle = '#558dbd'; ctx.setLineDash([5, 2]);
ctx.beginPath();
ctx.moveTo(coor[0][0], coor[0][1]);
ctx.lineTo(coor[1][0], coor[1][1]);
ctx.lineTo(coor[2][0], coor[2][1]);
ctx.stroke();
ctx.restore();
this.drawArrow(ctx, coor[2]);
} /**
* 绘制箭头
* @param {*} ctx
* @param {*} points
*/
drawArrow(ctx, points) {
const angle = Math.PI * 20 / 180;
const height = 12; ctx.save(); ctx.lineWidth = 1;
ctx.fillStyle = '#558dbd';
ctx.translate(points[0], points[1]);
ctx.beginPath();
ctx.lineTo(0, 0);
ctx.lineTo(Math.tan(angle) * height, -height);
ctx.arc(0, -height * 1.5, height * 0.6, Math.PI / 2 - Math.PI / 4.8, Math.PI / 2 + Math.PI / 4.8);
ctx.lineTo(-Math.tan(angle) * height, -height);
ctx.closePath();
ctx.fill(); ctx.restore();
}
}

arrow.js

import Element  from "./element.js";
import Arrow from "./arrow.js";
import util from "./util.js"; /**
* 组类
* 用于存储子节点和子节点的箭头对象
*/
export default class Group {
constructor(options) {
this.id = options.id || util.guid('group'); // 是否需要绘制组边框
this.needBorder = options.needBorder || false; // 当前组所有子和孙节点的个数
this.chiCount = options.chiCount; // 组下的子元素以及箭头
this.createObject(options.children, options.parent); // 计算中心点
this.center = this.getCenter(); // 计算坐标点信息
this.coordinates = this.getCoordinate();
} /**
* 根据子元素获取子元素最大最小中心坐标用于计算Group的中心坐标和边框
* @returns
*/
getMaxMin() {
const coordX = [];
const coordY = [];
this.children.forEach(e => {
coordX.push(e.center[0]);
coordY.push(e.center[1]);
}) const maxX = Math.max(...coordX);
const minX = Math.min(...coordX);
const maxY = Math.max(...coordY);
const minY = Math.min(...coordY);
return { maxX, minX, maxY, minY };
} /**
* 获取要素的中心点
*/
getCenter() {
const maxMin = this.getMaxMin();
return [(maxMin.maxX + maxMin.minX) / 2, (maxMin.maxY + maxMin.minY) / 2];
} /**
* 获取坐标信息
* @returns
*/
getCoordinate() {
const maxMin = this.getMaxMin(); const maxX = maxMin.maxX;
const minX = maxMin.minX;
const maxY = maxMin.maxY;
const minY = maxMin.minY; const width = util.CON.WIDTH / 2;
const height = util.CON.HEIGHT / 2;
const margin = util.CON.MARGIN / 3;
const betaLong = (maxX - minX + width * 2) / 2;
const betaHeight = (maxY - minY + height * 2) / 2; return [
[-betaLong - margin, -betaHeight - margin],
[betaLong + margin, -betaHeight - margin],
[betaLong + margin, betaHeight + margin],
[-betaLong - margin, betaHeight + margin],
]
} /**
* 创建子对象,包括Element和Arrow对象
* @param {*} children
* @param {*} parent
*/
createObject(children, parent) {
const arrows = [];
const child = [];
children.forEach(e => {
if (!e.buildSelf) {
const ele = new Element(e);
child.push(ele);
arrows.push(new Arrow(e, parent, ele));
}
})
this.arrows = arrows;
this.children = child;
} /**
* 主要绘制边框等信息内容
*/
draw() {
this.children.forEach(e => e.draw());
this.arrows.forEach(e => e.draw()); this.drawBorder();
} /**
* 绘制边框
* @returns
*/
drawBorder() {
if (this.children.length === 0 || !this.needBorder) {
return;
}
const radius = 10;
const coor = this.coordinates;
const ctx = util.getContext(); ctx.save();
ctx.translate(this.center[0], this.center[1]);
ctx.lineWidth = 2;
ctx.strokeStyle = '#99745e'; ctx.beginPath();
ctx.arc(coor[0][0] + radius, coor[0][1] + radius, radius, Math.PI, Math.PI * 3 / 2);
ctx.lineTo(coor[1][0] -radius, coor[1][1]);
ctx.arc(coor[1][0] - radius, coor[0][1] + radius, radius, -Math.PI / 2, 0);
ctx.lineTo(coor[2][0], coor[2][1] - radius);
ctx.arc(coor[2][0] - radius, coor[2][1] - radius, radius, 0, Math.PI / 2);
ctx.lineTo(coor[3][0] + radius, coor[3][1]);
ctx.arc(coor[3][0] + radius, coor[3][1] - radius, radius, Math.PI / 2, Math.PI);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}

group.js

2. 再创建一个util.js文件,主要是对源数据做虚拟节点的增加和计算最低一层的子节点个数

const CANVASINFO = {};
import Element from './element.js'; const CON = {
WIDTH: 120, // Element元素的宽度
HEIGHT: 40, // Element元素的高度
MARGIN: 30, // 两个Element元素的高度
OUTERHEIGHT: 200 // 每行Element的高度
} /**
* 设置canvas对象,便于其他组件使用
*/
function setSanvas(canvas) {
CANVASINFO.obj = canvas;
CANVASINFO.context = canvas.getContext('2d');
CANVASINFO.width = canvas.offsetWidth;
CANVASINFO.height = canvas.offsetHeight;
} /**
* 获取canvas对象
*/
function getCanvas() {
return CANVASINFO.obj;
} function getContext() {
return CANVASINFO.context;
} /**
* 获取UUID
* @returns
*/
function guid(prefix) {
return prefix + '_xxxx-xxxx-yxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
} /**
* 获取最大的层级值,同时标记每层要素的层级值,方便后续计算虚拟节点使用
* @param {*} src
* @returns
*/
function getMaxLevel(src) {
const INITLEVEL = 0;
let maxLevel = INITLEVEL; function cal(src, level) {
if (level > maxLevel) {
maxLevel = level;
}
src.forEach(e => {
e.topo_level = level;
if (e.children && e.children.length) {
cal(e.children, level + 1);
}
})
}
if (Array.isArray(src)) {
cal(src, INITLEVEL);
} else {
cal([src], INITLEVEL);
}
return maxLevel;
} /**
* 计算每个节点包含的所有最大层级的子和孙子节点的个数
* 原理是循环到最下层的Element对象,每个Element元素向上汇报计算+1
* @param {*} data
*/
function countNum(data) {
const INITLEVEL = 0;
let maxLevel = getMaxLevel(data); function count(src, level, parents = []) {
src.forEach(e => {
if (Number.isNaN(e.chiCount) || e.chiCount === undefined) {
e.chiCount = 0;
}
if (e.children && e.children.length) {
// 此处添加parents时不可以使用push,否则将会导致部分parent重复
count(e.children, level + 1, parents.concat([e]));
} else if (level < maxLevel) {
// 通过buildSelf标识自插入属性,该对象不创建Ele对象
e.children.push({ children: [], level: level + 1, buildSelf: true });
count(e.children, level + 1, parents.concat([e]));
} else if (level === maxLevel) {
for (let i = parents.length - 1; i >= 0; i--) {
parents[i].chiCount++;
}
}
})
}
if (Array.isArray(data)) {
count(data, INITLEVEL);
} else {
count([data], INITLEVEL);
} return data;
} /**
* 将每个分支看作一组,计算每组的横纵坐标范围
* @param {*} data
*/
function calRange(data) {
let eleWidth = CON.WIDTH + CON.MARGIN;
const startX = 0;
const startY = 0; function range(src, start) {
src.forEach((e, i) => {
e.yRange = [e.topo_level + startY, (e.topo_level + 1) * CON.OUTERHEIGHT + startY];
if (e.children) {
if (i === 0) {
e.xRange = [start, start + eleWidth * (e.chiCount === 0 ? 1 : e.chiCount)];
range(e.children, start);
} else {
e.xRange = [src[i - 1].xRange[1], src[i - 1].xRange[1] + eleWidth * (e.chiCount === 0 ? 1 :e.chiCount)];
range(e.children, src[i - 1].xRange[1]);
}
}
});
}
if (Array.isArray(data)) {
range(data, startX);
} else {
range([data], startX);
}
return data;
} /**
* 获取无子节点的Element
*/
function flatElement(data) {
const arr = []; function flat(src) {
if (!src.group || src.group.children.length === 0) {
arr.push(src);
} else {
src.group.children.forEach(e => flat(e));
}
}
flat(data);
return arr;
} /**
* 克隆数据
* @param {*} data
* @returns
*/
function clone(data) {
return JSON.parse(JSON.stringify(data));
} export default {
CON,
setSanvas,
getCanvas,
getContext,
guid,
clone,
countNum,
calRange,
flatElement,
};

util.js

3. 创建一个main.js文件,主要是和用来创建元素对象以及绘制页面等功能

main.js

import util from "./util.js";
import Element from "./element.js"; let data = [];
let arrows = []; function init(options) {
util.setSanvas(document.getElementById(options.id));
addEvent(); // 克隆数据,避免数据污染
const cloneData = util.clone(options.data); // 计算每个父节点包含的所有子和孙节点数据个数
const numData = util.countNum(cloneData); // 计算每个节点的横纵坐标范围
const rangeData = util.calRange(numData);
// 创建要素集
data = new Element(rangeData); redraw();
} /**
* 重新绘制
*/
function redraw() {
const ctx = util.getContext();
ctx.clearRect(0, 0, 1800, 800); arrows.forEach(e => e.draw());
data.draw();
} function addEvent() {
let initWidth = 1800;
let initHeight = 800;
let init = 1;
const beta = 0.1;
const dom = util.getCanvas();
const ctx = util.getContext(); let startPos = [0, 0];
let down = false;
let lastPos = [0, 0]; /**
* 点击事件,计算合并或者展开
* @param {*} event
*/
dom.onclick = (event) => {
const clickX = event.offsetX;
const clickY = event.offsetY;
let selectEle = null; function getEle(e) {
const upX = e.center[0];
const upY = e.center[1] + e.height / 2;
if (Math.pow(clickX - upX, 2) + Math.pow(clickY - upY, 2) < 10) {
selectEle = e;
}
if (!selectEle && e.group && e.group.children.length) {
e.group.children.forEach(e => {
getEle(e);
})
}
}
getEle(data);
if (selectEle) {
if (selectEle.openFlag) {
selectEle.openFlag = false;
selectEle.group.children = selectEle.srcChildren;
selectEle.group.arrows = selectEle.srcArrows; delete selectEle.srcArrows;
delete selectEle.srcChildren;
} else {
selectEle.openFlag = true;
selectEle.srcChildren = selectEle.group.children;
selectEle.srcArrows = selectEle.group.arrows; selectEle.group.children = [];
selectEle.group.arrows = [];
} redraw();
}
} } export default { init }

4. 新增一个页面index.html,用来渲染要素点

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas绘制树状结构</title>
<script src="./arrow.js" type="module"></script>
<script src="./element.js" type="module"></script>
<script src="./group.js" type="module"></script>
<script src="./main.js" type="module"></script>
<script src="./util.js" type="module"></script>
</head>
<body>
<canvas id="canvas" width="1800" height="800" style="width: 1800px;height: 800px;border: 1px solid gray;margin: auto;display: flex;"></canvas>
</body>
</html>
<script type="module">
import main from './main.js';
// 数据格式如下,主要是children字段
var s = {
level: -1,
children: [
{
level: 0,
children: [
{
level: 1,
needBorder: true,
children: [
{
level: 2,
children: []
},
{
level: 2,
children: []
}
]
}
]
},
{
level: 0,
children: [
{
level: 1,
children: [
{
level: 2,
children: [
],
},
]
}
]
},
{
level: 0,
children: []
}
]
}; function setAttr(data, params, index) {
data.text = params.text + '_' + index;
if (data.children) {
data.children.forEach((e, i) => {
setAttr(e, { text: data.text }, i);
})
}
}
window.onload = (() => {
setAttr(s, {text: '' }, 0);
main.init({
data: s,
id: 'canvas'
});
})
</script>

index.html

三、效果图展示,节点可以点击进行收缩(图二)

Canvas原生绘制树状结构拓扑图的更多相关文章

  1. 原生JS实现树状结构列表

    树状结构列表,这个技术点之前有写过了,是基于vue讲解,但似乎都没有解决痛点,最基础的原生JS该怎么实现呢? 这篇文章会全面详细的介绍树状结构列表的实现,从数据处理成树状结构,到动态生成dom节点渲染 ...

  2. 树状结构Java模型、层级关系Java模型、上下级关系Java模型与html页面展示

    树状结构Java模型.层级关系Java模型.上下级关系Java模型与html页面展示 一.业务原型:公司的组织结构.传销关系网 二.数据库模型 很简单,创建 id 与 pid 关系即可.(pid:pa ...

  3. 分享使用NPOI导出Excel树状结构的数据,如部门用户菜单权限

    大家都知道使用NPOI导出Excel格式数据 很简单,网上一搜,到处都有示例代码. 因为工作的关系,经常会有处理各种数据库数据的场景,其中处理Excel 数据导出,以备客户人员确认数据,场景很常见. ...

  4. 由简入繁实现Jquery树状结构

    在项目中,我们经常会需要一些树状结构的样式来显示层级结构等,比如下图的样式,之前在学.net的时候可以直接拖个服务端控件过来直接使用非常方便.但是利用Jquery的一些插件,也是可以实现这些效果的,比 ...

  5. php实现树状结构无级分类

    php实现树状结构无级分类   ).",'树2-1-1-2')";mysql_query($sql);?>

  6. Android无限级树状结构

    通过对ListView简单的扩展.再封装,即可实现无限层级的树控件TreeView. package cn.asiontang.nleveltreelistview; import android.a ...

  7. 使用Map辅助拼装树状结构,消除递归调用

    目前菜单或其他树状结构在数据库中的存储,多数是以一个parentid作为关联字段,以一维形式存储.使用时全部查询出来,然后在内存中拼装成树状结构.现在主要涉及的是拼装方法的问题. 一般可以进行 递归调 ...

  8. lua 怎样输出树状结构的table?

    为了让游戏前端数据输出更加条理,做了一个简单树状结构来打印数据. ccmlog.lua local function __tostring(value, indent, vmap) local str ...

  9. js List<Map> 将偏平化的数组转为树状结构并排序

    数据格式: [ { "id":"d3e8a9d6-e4c6-4dd8-a94f-07733d3c1b59", "parentId":&quo ...

  10. 浅谈oracle树状结构层级查询之start with ....connect by prior、level及order by

    浅谈oracle树状结构层级查询 oracle树状结构查询即层次递归查询,是sql语句经常用到的,在实际开发中组织结构实现及其层次化实现功能也是经常遇到的,虽然我是一个java程序开发者,我一直觉得只 ...

随机推荐

  1. MySQL InnoDB加锁规则分析

    1.  基础知识回顾 1.索引的有序性,索引本身就是有序的 2.InnoDB中间隙锁的唯一目的是防止其他事务插入间隙.间隙锁可以共存.一个事务取得的间隙锁并不会阻止另一个事务取得同一间隙上的间隙锁.共 ...

  2. Critical error detected c0000374

    我发现出现上述错误是 free 两次内存 float* dd=new float[2]; delete[] dd; delete[] dd;

  3. 华企盾DSC无法从网页下载客户端(无法访问web端)

    解决方法1:服务器安装目录需要安装在英文目录,否则DSCApache.exe会启动不了,导致无法访问5580网页. 解决方法2:5580端口占用也会导致DSCApache.exe启动不了,可打开服务器 ...

  4. 华企盾DSC防泄密软件:svn、git更新后有感叹号常见处理方法

    1.查看客户端日志检查TSVNcache.exe进程是否是legal:1 2.TSVNcache.exe进程是否允许访问未配置加密进程的后缀 3.svn服务器不是加密进程也未装网络驱动,或者加密类型未 ...

  5. idea2020.1.3汉化包报错问题

    已解决:idea2020.1.3汉化包报错问题 问题描述:插件市场提供的版本不对.不兼容,所以需要手动下载安装 这里附上文件 https://wwsi.lanzouq.com/b03czdtwf 密码 ...

  6. MySQL篇:bug2_ Navicate无法添加或更新子行-外键约束失败

    问题产生原因 Mysql中如果表和表之间建立的外键约束,则无法删除表及修改表结构. 解决办法 解决方法是在Mysql中取消外键约束: SET FOREIGN_KEY_CHECKS=0; 再添加值, 然 ...

  7. 业务并发度不够,数仓的CN可以来帮忙

    摘要: CN全称协调节点(Coordinator Node),是和用户关系最密切也是DWS内部非常重要的一个组件,它负责提供外部应用接口.优化全局执行计划.向Datanode分发执行计划,以及汇总.处 ...

  8. 利用Appuploader上架IPA步骤

      Appuploader可以辅助在Windows.linux或mac系统直接申请iOS证书p12,及上传ipa到App Store.方便在没有苹果电脑情况下上架IPA操作. 一.下载安装iOS上架辅 ...

  9. jQuery模糊匹配checkbox全选 value实现checkbox部分或全部全选

    本文章总结jQuery实现checkbox三种情况的全选功能 第一种:等值全选,也称name的等值全选,通过checkbox的名称name实现. 第二种:模糊全选,也称id模糊全选,通过checkbo ...

  10. 火山引擎 DataLeap 通过中国信通院测评,数据管理能力获官方认可!

      近日,火山引擎大数据研发治理套件 DataLeap 通过中国信通院第十五批"可信大数据"测评,在数据管理平台基础能力上获得认证.   "可信大数据"产品能力 ...