前言

AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。

G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。

它是一款国产可视化插件,中文官方文档方便阅读和学习。G6可以实现很多d3才能实现的可视化图表,d3作为一款国外很强大的可视化插件,它的官方文档是非汉语文档,社区虽然很活跃,但几乎是英文文档,阅读和学习起来并不是那么轻松,尤其是英语不太好的同学,阅读和学习d3更吃力。这时候G6就是不错的选择。因为G6包含丰富的图表类型,还可以实现节点,边等自定义。

使用步骤

安装&引入

npm install --save @antv/g6 //安装import G6 from '@antv/g6';   //在需要的js文件引入

使用

创建容器

<div id="container"></div>
<style>
#container{width: 100%;height: 800px;
}
</style>

数据格式

const data = {// 节点nodes: [{id: 'node1',x: 100,y: 200,},{id: 'node2',x: 300,y: 200,},],// 边集edges: [// 表示一条从 node1 节点连接到 node2 节点的边{source: 'node1',target: 'node2',},],
};

节点样式

主要包括nodes(节点)和edges(边),节点是画布上显示的小矩形,边是两个节点的连线,节点样式可以设置为

  • circle:圆;
  • rect:矩形;
  • ellipse:椭圆;
  • polygon:多边形;
  • fan:扇形;
  • image:图片;
  • marker:标记;
  • path:路径;
  • text:文本;
  • dom(svg):DOM(图渲染方式 renderer'svg' 时可用)。

常用方法

  • draw()画布的绘制方法。新增shape或group后,调用此方法将最新的内容渲染到画布上。
  • changeSize(width, height)改变画布的大小
  • getClientByPoint(x, y)将窗口坐标转换为canvas坐标。
  • getPointByClient(x, y)将canvas坐标转换为窗口坐标。
  • on(eventType, callback)绑定事件。
  • off(eventType, callback)事件解绑。
  • addShape(shape, attrs)添加单个图形到画布。
  • addGroup(attrs)添加单个组到画布。
  • attr()设置或获取实例的绘图属性,无参数获取,有参数更新
  • set(name, value)设置实例的属性,如visible, zIndex, id等。
  • get(name)获取实例的属性值
  • show()显示某实例对应的图形。
  • hide()隐藏某实例对应的图形
  • remove()删除实例本身
  • destroy()销毁实例
  • getBBox()获取实例的包围盒

graph method

  • graph.save()
  • graph.read(data) 读数据渲染
  • graph.find(id) 寻找数据模型
  • graph.add(type, model)
  • graph.remove(item)
  • graph.update(item, model) item为id或 项对象
  • graph.getItems();获取图内所有项
  • graph.getNodes()
  • graph.getEdges()
  • graph.getGroups()
  • graph.preventAnimate(callback) 阻止动画
  • getShape(x,y)返回该坐标点最上层的元素。
  • findById(id)根据元素ID返回对应的实例

案例

  • 脑图有个特点,就是传入data可以不需要提供x,y,new了graph之后,自动生成了x,y,然后方向可以在node里进行配置:
let centerX = 0;
graph.node(function (node: NodeConfig) {if (node.id === "root") {centerX = node.x as number;//把第一个节点的x作为center}return {label: node.id,labelCfg: {position://看有没有孩子和孩子数量配置左右node.children && node.children.length > 0? "right": (node.x as number) > centerX? "right": "left",offset: 5,},};
});
  • 节点拖拽移动。主要监听node:drag事件,然后去取鼠标的xy,转换为graph的xy,最后赋给节点。
graph.on("node:dragstart", (e: any) => {const item = e.item;const model = item.getModel();model.style.cursor = "grab";graph.update(item, model);graph.paint();
});graph.on("node:drag", (e: any) => {// 鼠标所在位置 转化为现在目标节点所在位置const { clientX, clientY } = e;// 将视口坐标转换为屏幕/页面坐标。const point = graph.getPointByClient(clientX, clientY);const item = e.item;const model = item.getModel();item.updatePosition(point);graph.update(item, model);graph.paint();
});graph.on("node:dragend", (e: any) => {const item = e.item;const model = item.getModel(); //直接取得model没style。。。model.style.cursor = "default";graph.update(item, model);graph.paint();
});
graph.on("canvas:drag", (e: any) => {//   console.log(e);
});
graph.on("dragstart", (e: any) => {//比node:dragstart先
});
graph.on("mousedown", (e: any) => {//比dragstart先const item = e.item;if (item) {const model = item.getModel();model.style.cursor = "grab";graph.update(item, model);graph.paint();}
});

自定义节点

  • 其实是先注册个节点,注册节点时,可以利用addshape做节点样子,同时可以绑定事件,给节点赋文本。
  • 需要注意是这里用的是图形分组概念,g6里面不知道哪个人搞了那么多名字全是group要么是groups很难区分。
  • 通过一个g6实例的group,可以找到其所属的item。
import React, { useEffect, useRef } from "react";
import G6 from "@antv/g6";
import { NodeConfig } from "@antv/g6/lib/types";
import GGroup from "@antv/g-canvas/lib/group";
import { IShape } from "@antv/g-canvas/lib/interfaces";
const data = {nodes: [{id: "Model",type: "model-node", //这个就是注册的x: 100,y: 100,style: {width: 160,height: 100,fill: "#f1b953",stroke: "#f1b953",},openIcon: {x: 180, // 控制图标在横轴上的位置y: 45, // 控制图标在纵轴上的位置fontSize: 20,style: {fill: "#fc0",},},hideIcon: {x: 180, // 控制图标在横轴上的位置y: 45, // 控制图标在纵轴上的位置fontSize: 20,style: {fill: "#666",},},labels: [{x: 10,y: 20,label: "标题,最长10个字符~~",labelCfg: {fill: "#666",fontSize: 14,maxlength: 10,},},{x: 10,y: 40,label: "描述,最长12个字333符~~~",labelCfg: {fontSize: 12,fill: "#999",maxlength: 12,},},],},{id: "node1", // String,该节点存在则必须,节点的唯一标识x: 100, // Number,可选,节点位置的 x 值y: 200, // Number,可选,节点位置的 y 值},],
};interface modelNodeType extends NodeConfig {openIcon: {x: number;y: number;fontSize: number;style: Object;};hideIcon: {x: number;y: number;fontSize: number;style: Object;};labels: Array<any>;
}// 注册自定义节点
G6.registerNode("model-node",{drawShape(cfg: modelNodeType, group) {const opts = cfg;const openIcon = opts.openIcon;const hideIcon = opts.hideIcon;// 添加节点const shape = group!.addShape("rect", {name: "model-node",draggable: true, // 让自定义节点支持拖拽attrs: cfg.style,});const openSwitch = group!.addShape("circle", {draggable: true,attrs: {r: 10,...openIcon,...openIcon.style,},className: "state-open",});const hideSwitch = group!.addShape("circle", {draggable: true,attrs: {r: 10,...hideIcon,...hideIcon.style,},className: "state-hide",});// 添加多行文本for (let i = 0; i < cfg.labels.length; i++) {const item = cfg.labels[i];const {label,labelCfg: { maxlength },} = item;let text = maxlength ? label.substr(0, maxlength) : label || "";if (label.length > maxlength) {text = `${text}...`;}group!.addShape("text", {attrs: {text,...item,...item.labelCfg,},});}this.bindEvent(group, openSwitch);this.bindEvent(group, hideSwitch);return shape;},bindEvent(group: GGroup, btn: IShape) {//ggroup就是graphics group缩写btn.on("click", () => {const open = group.get("children").find((child: any) => child.cfg.className === "state-open");const close = group.get("children").find((child: any) => child.cfg.className === "state-hide");if (btn.cfg.className === "state-open") {const item = group.get("item"); //在这个图形分组下的itemconst model = item.getModel();open.toBack();close.toFront(); //这个是让2个圆z轴位置变化 item上的方法model.style.height = 100;item.update(model);} else if (btn.cfg.className === "state-hide") {const item = group.get("item");const model = item.getModel();close.toBack(); //Item 上的方法open.toFront(); //item上的方法model.style.height = 50;item.update(model); //item上的方法}});},},"single-node"
); // 继承自内置节点
function App() {const ref = useRef<HTMLDivElement>(null);useEffect(() => {const graph = new G6.Graph({container: ref.current!,width: 800,height: 800,// renderer: 'svg',fitCenter: true,modes: {default: ["drag-canvas", "zoom-canvas", "drag-node"],},});// 传入数据graph.data(data);// 执行渲染graph.render();// graph.fitView();}, []);return <div ref={ref} id="container"></div>;
}export default App;
  • g6的自定义边跟自定义节点是类似操作。
G6.registerEdge("hvh", {draw(cfg, group) {const startPoint = cfg!.startPoint!;const endPoint = cfg!.endPoint!;const startArrow = (cfg!.style && cfg!.style.startArrow) || undefined;const endArrow = (cfg!.style && cfg!.style.endArrow) || undefined;const shape = group!.addShape("path", {attrs: {stroke: "#333",path: [["M", startPoint.x, startPoint.y],["L",endPoint.x / 3 + (1 / 3) * startPoint.x,endPoint.y / 2 + (1 / 3) * startPoint.y,],["L",endPoint.x * 1.1 + (2 / 3) * startPoint.x,endPoint.y / 2 + (2 / 3) * startPoint.y,],["L", endPoint.x, endPoint.y],],startArrow, //初始化配统一的箭头endArrow,},// must be assigned in G6 3.3 and later versions. it can be any value you wantname: "path-shape",});return shape;},setState(name, value, item) { //这个方法在3.3后可以不用,改为直接设置全局node/edge StateStylesconst group = item!.getContainer();const shape = group.get("children")[0]; // 顺序根据 draw 时确定if (name === "active") {if (value) {//shape.attr("stroke", "red");shape.attr("lineWidth", 3);} else {shape.attr("stroke", "#333");shape.attr("lineWidth", 1);}}if (name === "selected") {if (value) {shape.attr("lineWidth", 3);} else {shape.attr("lineWidth", 1);}}},
});
  • 初始化可以统一配个箭头样式:
defaultEdge: {type: "line-arrow",style: {stroke: "#F6BD16",startArrow: {path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",fill: "#F6BD16",},endArrow: {path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",fill: "#F6BD16",},},
},

说明

  • g6里为了传递信息,设置了state,这个状态有全局设置或者单个设置。一般来说,统一用全局设置。这个主要用来判断点击了没有hover了没有之类。

  • 有点诡异的是好像相同行为触发的不同状态之间会冲突,所以最好是1个状态对应多值而不是多个状态对应二值。

  • 然后文档全篇在说怎么设置值,没说怎么获取值。。。。。。。后来发现item里写了个方法getState可以拿到state所有值,我也是服了。。state的概念里不写怎么获取,只写hasState是用来判断二值的。然后这个方法获取的值也很诡异,是个数组字符串,比如设置的值是active , ‘1’ ,那么数组里面值是:active:1的字符串。(这设计谁想出来的。。。不能做成key value?不能按key取值?实在不行按active:1字符串取值也行啊)

  • 比如上面那个例子就有冲突,监听那里改下:

graph.on("edge:click", (ev: IG6GraphEvent) => {const edge = ev.item;const value = edge!.getStates()[0];if (value !== "active:0") {if (value === "active:2") {graph.setItemState(edge!, "active", "0");} else {graph.setItemState(edge!, "active", "2"); // 切换选中}}
});graph.on("edge:mouseenter", (ev: IG6GraphEvent) => {const edge = ev.item;const value = edge!.getStates()[0];if (value !== "active:2") {graph.setItemState(edge!, "active", "1");}
});graph.on("edge:mouseleave", (ev: IG6GraphEvent) => {const edge = ev.item;const value = edge!.getStates()[0];if (value !== "active:2") {graph.setItemState(edge!, "active", "0");}
});

编辑器

对于编辑,我摸索了一番,实际就是要自己做个dom面板来进行编辑,我以右键弹出选框选择编辑label为例(实际应该点击后把input设置成显示,修改完毕搞个按钮然后点击保存。再关闭input)。我直接就input不操作显示与否了:

import React, { useEffect, useRef, useState } from "react";
import G6 from "@antv/g6";
import { NodeConfig, Item, IG6GraphEvent } from "@antv/g6/lib/types";
import GGroup from "@antv/g-canvas/lib/group";
import { IShape } from "@antv/g-canvas/lib/interfaces";
const data = {nodes: [{id: "Model",type: "model-node", //这个就是注册的x: 200,y: 100,style: {width: 160,height: 100,fill: "#f1b953",stroke: "#f1b953",},openIcon: {x: 180, // 控制图标在横轴上的位置y: 45, // 控制图标在纵轴上的位置fontSize: 20,style: {fill: "#fc0",},},hideIcon: {x: 180, // 控制图标在横轴上的位置y: 45, // 控制图标在纵轴上的位置fontSize: 20,style: {fill: "#666",},},labels: [{x: 10,y: 20,label: "标题,最长10个字符~~",labelCfg: {fill: "#666",fontSize: 14,maxlength: 10,},},{x: 10,y: 40,label: "描述,最长12个字333符~~~",labelCfg: {fontSize: 12,fill: "#999",maxlength: 12,},},],anchorPoints: [//这属性用来设定边的连接中心[0, 0.5],[0, 1],],},{id: "node1", // String,该节点存在则必须,节点的唯一标识label: "node1",x: 10, // Number,可选,节点位置的 x 值y: 200, // Number,可选,节点位置的 y 值size: 50,anchorPoints: [//这属性用来设定边的连接中心[1, 0.5],[1, 0.8],],},{id: "node2", // String,该节点存在则必须,节点的唯一标识label: "node2",size: 50,x: 70, // Number,可选,节点位置的 x 值y: 20, // Number,可选,节点位置的 y 值anchorPoints: [//这属性用来设定边的连接中心[1, 0.5],[0, 0.5],[0.5, 1],],},],edges: [{id: "edge1",target: "Model",source: "node1",type: "hvh",// 该边连入 source 点的第 0 个 anchorPoint,sourceAnchor: 1,// 该边连入 target 点的第1个 anchorPoint,targetAnchor: 1,},{id: "edge2",target: "node2",source: "node1",type: "hvh",},{id: "edge3",target: "node2",source: "Model",type: "hvh",targetAnchor: 2,},],
};interface modelNodeType extends NodeConfig {openIcon: {x: number;y: number;fontSize: number;style: Object;};hideIcon: {x: number;y: number;fontSize: number;style: Object;};labels: Array<any>;
}function shapesAddAttr(shape: Array<any>, key: string, value: any) {shape.forEach((v) => v.attr(key, value));
}G6.registerEdge("hvh", {draw(cfg, group) {const startPoint = cfg!.startPoint!;const endPoint = cfg!.endPoint!;const startArrow = (cfg!.style && cfg!.style.startArrow) || undefined;const endArrow = (cfg!.style && cfg!.style.endArrow) || undefined;const shape = group!.addShape("path", {attrs: {stroke: "#333",path: [["M", startPoint.x, startPoint.y],["L",endPoint.x / 3 + (1 / 3) * startPoint.x,endPoint.y / 2 + (1 / 3) * startPoint.y,],["L",endPoint.x * 1.1 + (2 / 3) * startPoint.x,endPoint.y / 2 + (2 / 3) * startPoint.y,],["L", endPoint.x, endPoint.y],],startArrow, //初始化配统一的箭头endArrow,},// must be assigned in G6 3.3 and later versions. it can be any value you wantname: "path-shape",});return shape;},setState(name, value, item) {//这个方法在3.3后可以不用,改为直接设置全局node/edge StateStylesconst group = item!.getContainer();const shape = group.get("children"); // 顺序根据 draw 时确定if (name === "active") {switch (value) {case "0":shapesAddAttr(shape, "stroke", "#333");shapesAddAttr(shape, "lineWidth", 1);break;case "1":shapesAddAttr(shape, "stroke", "red");shapesAddAttr(shape, "lineWidth", 3);break;case "2":shapesAddAttr(shape, "stroke", "blue");shapesAddAttr(shape, "lineWidth", 3);break;default:return;}}},
});// 注册自定义节点
G6.registerNode("model-node",{drawShape(cfg: modelNodeType, group) {const opts = cfg;const openIcon = opts.openIcon;const hideIcon = opts.hideIcon;// 添加节点const shape = group!.addShape("rect", {name: "model-node",draggable: true, // 让自定义节点支持拖拽attrs: cfg.style,});const openSwitch = group!.addShape("circle", {draggable: true,attrs: {r: 10,...openIcon,...openIcon.style,},className: "state-open",});const hideSwitch = group!.addShape("circle", {draggable: true,attrs: {r: 10,...hideIcon,...hideIcon.style,},className: "state-hide",});// 添加多行文本for (let i = 0; i < cfg.labels.length; i++) {const item = cfg.labels[i];const {label,labelCfg: { maxlength },} = item;let text = maxlength ? label.substr(0, maxlength) : label || "";if (label.length > maxlength) {text = `${text}...`;}group!.addShape("text", {attrs: {text,...item,...item.labelCfg,},});}this.bindEvent(group, openSwitch);this.bindEvent(group, hideSwitch);return shape;},bindEvent(group: GGroup, btn: IShape) {//ggroup就是graphics group缩写btn.on("click", () => {const open = group.get("children").find((child: any) => child.cfg.className === "state-open");const close = group.get("children").find((child: any) => child.cfg.className === "state-hide");if (btn.cfg.className === "state-open") {const item = group.get("item"); //在这个图形分组下的itemconst model = item.getModel();open.toBack();close.toFront(); //这个是让2个圆z轴位置变化 item上的方法model.style.height = 100;item.update(model);} else if (btn.cfg.className === "state-hide") {const item = group.get("item");const model = item.getModel();close.toBack(); //Item 上的方法open.toFront(); //item上的方法model.style.height = 50;item.update(model); //item上的方法}});},},"single-node"
); // 继承自内置节点function App() {const ref = useRef<HTMLDivElement>(null);const [state, setState] = useState("");const [changeItem, setChangeItem] = useState<Item>();useEffect(() => {const contextMenu = new G6.Menu({getContent(graph) {console.log("graph", graph);return `<div>编辑lable</div>`;},handleMenuClick: (target, item) => {//target是dom item 是Item//只有click了才知道是哪个节点触发的const model = item.getModel();const value = model.label;if (typeof value === "string") {//将节点的值赋给input并绑上onchange给它setState(value);setChangeItem(item); //要调用更新,最小是item item才有update}},});const graph = new G6.Graph({container: ref.current!,width: 800,height: 800,// renderer: 'svg',fitCenter: true,modes: {default: ["drag-canvas", "zoom-canvas", "drag-node"],//edit: ["click-select", "click-add-node"],},defaultEdge: {type: "line-arrow",style: {stroke: "#F6BD16",startArrow: {path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",fill: "#F6BD16",},endArrow: {path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",fill: "#F6BD16",},},},plugins: [contextMenu],});// graph.on("node:click", (ev: any) => {//     console.log(ev);//  graph.setMode("edit");// });graph.on("edge:click", (ev: IG6GraphEvent) => {const edge = ev.item;const value = edge!.getStates()[0];if (value !== "active:0") {if (value === "active:2") {graph.setItemState(edge!, "active", "0");} else {graph.setItemState(edge!, "active", "2"); // 切换选中}}});graph.on("edge:mouseenter", (ev: IG6GraphEvent) => {const edge = ev.item;const value = edge!.getStates()[0];if (value !== "active:2") {graph.setItemState(edge!, "active", "1");}});graph.on("edge:mouseleave", (ev: IG6GraphEvent) => {const edge = ev.item;const value = edge!.getStates()[0];if (value !== "active:2") {graph.setItemState(edge!, "active", "0");}});// 传入数据graph.data(data);// 执行渲染graph.render();// graph.fitView();}, []);const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {setState(e.target.value);//修改state后改变节点Labelif (changeItem) {//其他样式控制同理这么操作const model = changeItem.getModel();model.label = e.target.value;changeItem.update(model);}};return (<div><div id="editor"><span>修改label :</span><input  value={state} onChange={handleChange}></input></div><div ref={ref} id="container"></div></div>);
}export default App;

最后需要注意下自定义节点的格式,如果像我这么写自定义节点,那么label很可能就不对或者没有,所以事先需要规划好到底哪些属性应该去配置,哪些属性可以配置。

antv/G6使用详细介绍,一篇文章说清antv G6如何使用相关推荐

  1. UFS详细介绍---终章

    UFS详细介绍-终章 UNIVERSAL FLASH STORAGE (UFS),通用闪存存储器.目前最新的标准是UFS4.0:UFS的出现是因为替代eMMC产品的,但是因为价格等,目前没办法做到完全 ...

  2. 一篇文章说清Python数据分析,这个学习路线绝了

    近年来,数据分析师的需求非常大,90%的岗位技能需要掌握Python作为数据分析工具. 2021年史上最全Python数据分析学习路线,从语言基础.数据工具.商业分析.到机器学习,一篇文章帮你搞定,奥 ...

  3. 超详细!一篇文章带你轻松入门神经调控

    关注"心仪脑"查看更多脑科学知识的分享. 关键词:神经调控.脑科学.神经科学.TMS.tDCS 我们已经知道,EEG可以在头皮表面测量神经元的放电活动:fNIRS通过向脑内发射近红 ...

  4. 这是我见过解释java内部类最详细的一篇文章了

    https://www.runoob.com/w3cnote/java-inner-class-intro.html

  5. (详细易懂)一篇文章让你读懂到底什么是Ajax

    文章目录 一.AJAX的功能 二.AJAX的核心 1.XMLHttpRequest对象 同步请求(设置参数为false) 响应返回 异步请求(默认或设置参数为true) 三.实现AJAX基本步骤的简单 ...

  6. 【数据库】MySQL概念知识语法-基础(DDL/DML),真的很详细,一篇文章你就会了

    目录 通用语法及分类 DDL(数据定义语言) 数据库操作 表操作 DML(数据操作语言) 添加数据 更新和删除数据 通用语法及分类 ● DDL: 数据定义语言,用来定义数据库对象(数据库.表.字段) ...

  7. py2neo 创建关系_py2neo详细介绍第一章

    1.1 节点和关系的对象 官网的例子,创建两个节点,并为两个节点创建关系. from py2neo.data import Node, Relationship a = Node("Pers ...

  8. 一篇文章讲清Go的内存布局和分配原理

    Go内存分配 Go 之所以在高并发环境下表现优异,除了咱们都知道的GMP模型,其实Go的内存布局和分配机制也起到了不少作用. 今天邀请到公众号「Go编程时光」的号主大佬明哥,给大家盘一盘 Go 中关于 ...

  9. 一篇文章讲清什么是NVMe

    因为NVMe的出现,硬盘的性能得到了极大的提升.这个极大是多少呢?读带宽从500MB/s提高到了3200MB/s,写带宽从400MB/s提高到了1200MB/s左右.而读IOPS则达到了50万,甚至更 ...

  10. 一篇文章讲清什么是零知识证明

    作用 抽象领域 零知识证明是打通链上数据与链下计算的关键技术,也是实现链上数据隐私保护的重要途径. 零知识证明技术可以解决数据的信任问题,计算的信任问题! 具体领域 数据的隐私保护:在一个数据表格中, ...

最新文章

  1. linux动态链接库的使用,Linux动态库soname的使用
  2. golang中的可见性
  3. 笔记本电脑5年没清灰了_2020年5月轻薄办公笔记本电脑推荐(上半月版)
  4. Servlet 3的异步Servlet功能
  5. 计算机公共基础知识教材,国家计算机二级考试公共基础知识教材
  6. 杭电oj2047-2049、2051-2053、2056、2058
  7. 第五十期:详解语音识别技术的发展
  8. sqlserver空间数据 + c# 实现查询附近的设备
  9. CSS3单词及属性大全
  10. java buffer类_Java ByteBuffer类
  11. Android SharedPreferences的简单使用
  12. mysql5.5编译安装_mysql5.5编译安装及配置
  13. scala和java集合的区别_Scala中Array和List的区别
  14. Mysql语句字符串拼接
  15. 组装一台计算机必需的配件有,哪位可以告诉我自己想组装一台电脑需要那些配件...
  16. Windows下MySQL5.7压缩包安装教程
  17. 带Fn的键盘linux能用吗,实用技巧:如何更有效率的使用Linux键盘
  18. 悟彻菩提真妙理 断魔归本合元神
  19. linux系统网络老掉线,Linux使用ADSL上网时经常掉线
  20. 单片机中数码管的十六进制转换

热门文章

  1. 酒香还怕巷子深?如何打造一个优秀的GitHub开源项目
  2. 酒香不怕巷子深,有心人才找得到的京都茶寮
  3. 【转自Testerhome】iOS 真机如何安装 WebDriverAgent
  4. linux kvm参数,virt-install创建kVM参数
  5. cpan mysql dbd_安装PERL cpan DBD::mysql错误笔记
  6. python中def main是什么意思_python main用法解析
  7. 产品经理必修课(4):深挖需求
  8. 基于java springboot android 安卓记账本源码(毕设)
  9. java开发tv上转盘抽奖_java实现大转盘抽奖的简单思路
  10. 立创eda学习笔记一:pcb板基础知识