前言

U 是一种表示服务器外部尺寸的单位,是 unit 的缩略语,详细的尺寸由作为业界团体的美国电子工业协会(EIA)所决定。之所以要规定服务器的尺寸,是为了使服务器保持适当的尺寸以便放在铁质或铝质的机架上。机架上有固定服务器的螺孔,以便它能与服务器的螺孔对上号,再用螺丝加以固定好,以方便安装每一部服务器所需要的空间。规定的尺寸是服务器的宽(48.26cm=19 英寸)与高(4.445cm 的倍数)。由于宽为19英寸,所以有时也将满足这一规定的机架称为“19 英寸机架”。厚度以 4.445cm 为基本单位。1U 就是 4.445cm,2U 则是 1U 的 2 倍为 8.89cm。所谓“1U 的 PC 服务器”,就是外形满足 EIA 规格、厚度为 4.445cm 的产品。设计为能放置到 19 英寸机柜的产品一般被称为机架服务器。

工控上运用到机柜 U 位的非常普遍,但是经常在创建 2D/3D 模型的时候,我们向内添加设备,每个设备占的 U 位不同,如果只是单纯地向机柜内部添加节点,在节点还未添加的时候我们没法直观地看到具体的效果,所以我就想能不能在添加的过程中就让大家直接看到机房设备的 U 位占位以及效果,这个 Demo 因此而生。

demo 地址: https://hightopo.com/demo/rack-builder/index.html

代码生成

场景搭建

整个 Demo 由最左侧的树,中间部分的列表以及右边的电信机柜拓扑图整体构成,为了让整个布局干净一点,这里结合 splitView 和 borderPane 两种布局方式来进行。首先将场景分为左右两个部分,左边为树,右边是列表和电信机柜拓扑图的组合:

treeView = this.treeView = new ht.widget.TreeView(), // 树组件 (http://www.hightopo.com/guide/guide/core/treeview/ht-treeview-guide.html)
splitView = this.splitView = new ht.widget.SplitView(treeView, null, 'h', 280); // 分割组件,将场景分为左右两个部分,左边为树组件,右边为空,左边的宽度为280,右边的组件先设置为空到时候根据具体情况分配 (http://www.hightopo.com/guide/guide/core/splitview/ht-splitview-guide.html)
this.splitView.addToDOM();


布局结束记得将最外层组件的最底层 div 添加到 body 中,HT 的组件一般都会嵌入 BorderPane、SplitView 和 TabView 等容器中使用,而最外层的HT组件则需要用户手工将 getView() 返回的底层 div 元素添加到页面的 DOM 元素中,这里需要注意的是,当父容器大小变化时,如果父容器是 BorderPane 和 SplitView 等这些HT预定义的容器组件,则HT的容器会自动递归调用孩子组件 invalidate 函数通知更新。但如果父容器是原生的 html 元素, 则 HT 组件无法获知需要更新,因此最外层的 HT 组件一般需要监听 window 的窗口大小变化事件,调用最外层组件 invalidate 函数进行更新。

为了最外层组件加载填充满窗口的方便性,HT 的所有组件都有 addToDOM 函数,其实现逻辑如下,其中 iv 是 invalidate 的简写:

addToDOM = function(){var self = this,view = self.getView(), // 获取组件的底层 divstyle = view.style;document.body.appendChild(view); // 将组件底层div添加进body中style.left = '0'; // ht 默认将所有的组件的 position 都设置为 absolute 绝对定位style.right = '0';style.top = '0';style.bottom = '0';window.addEventListener('resize', function () { self.iv(); }, false); // 窗口大小改变事件,调用刷新函数
}

右边的拓扑图部分是在监听选中变化事件的时候更新的,当然,初始化设置的选中树上的第一个节点就触发了选中变化事件:

cms.treeView.sm().ss(cms.treeView.dm().getDatas().get(0)); // 设置选中树上的第一个节点
treeView.sm().ms(function(){ // 监听选中变化事件var ld = treeView.sm().ld(); // 获取最后选中的节点if (ld) self.updateForm(ld.a('type'));
});
CMS.prototype.updateForm = function(type){var self = this,ld = this.treeView.sm().ld(); // 获取树上选中的最后一个节点if (type === self.TYPE_RACK_SPACE) { // 如果是在树上选中了节点,那么点击“添加机柜”就直接在树上选中的节点下生成if (!this.rackBuild) {this.rackBuild = new RackBuild(this); // 此类中定义了场景的中间列表部分,右边拓扑图部分以及对应的逻辑}this.rackBuild.setData(ld); // 在树上添加一个新的节点this.splitView.setRightView(this.rackBuild.getHTView()); // 设置分割组件右边的内容为整个场景的中间“列表”内容+右边的拓扑内容}
}

上面代码中 splitView.setRightView 函数意为设置右侧组件,有了这个函数,我就可以动态地改变 spliteView 组件中的右侧组件了。

初始化树

既然布局布好了,就该向具体的位置添加内容了。先来看看如何向树上添加电信机柜节点。首先我定义了一个初始化的树上的值 treeData,通过遍历这个数组创建树上的节点以及节点上的父子关系:

var treeData = [{name: 'Racks',type: 8,children: [{name: 'rack1',type: 18,usize: 32}, {name: 'rack2',type: 18}]
}];
CMS.prototype.loadTreeData = function(){ // 加载树上的节点var self = this;setTimeout(function(){var data = treeData;data.forEach(function(d) { // 遍历 treeData 数组的值self.createData(d, null); // 第一个节点父亲为空});self.treeView.expandAll(); // 展开树}, 10);
}

通过 createData 函数创建节点,并给节点设置父子关系:

CMS.prototype.createData = function(data, parent){ // 在树上创建一个节点var self = this,htData = new ht.Data(), // 新建 Data 类型节点dm = this.treeView.dm(); // 获取树的数据容器htData.a(data); // 设置节点业务属性 datahtData.setName(data.name) // 设置节点的 name 属性if (parent) {htData.setParent(parent); // 设置父亲节点}dm.add(htData); // 将节点添加到数据容器中if (data.children) { // 如果节点中有 children 对象data.children.forEach(function(d){ // 遍历 children 对象self.createData(d, htData); // 再创建 children 对象中的节点作为孩子节点});}return htData;
}

创建场景右边部分

眼尖的同学在前面的代码中可能注意到了一个未声明的 RackBuild 类,在此类的声明中我们将场景的右半部分主要分为左右两个部分,左边又分为上下两个部分,右边也分为上下两个部分。

这里先将整个右边的部分进行布局,下面代码中的变量 listBorder 为上图的左半部分,变量 borderPane 为上图的右半部分,至于鹰眼组件部分,是添加到在 borderPane 的上层:

listView = this.listView = new ht.widget.ListView(), // 列表组件(http://www.hightopo.com/guide/guide/core/listview/ht-listview-guide.html)
listForm = this.listForm = new ht.widget.FormPane(), // 表单组件(http://www.hightopo.com/guide/guide/plugin/form/ht-form-guide.html)
listBorder = this.listBorder = new ht.widget.BorderPane(), // 场景中间边框面板组件(http://www.hightopo.com/guide/guide/core/borderpane/ht-borderpane-guide.html)
gv = this.gv = new ht.graph.GraphView(), // 拓扑组件(http://www.hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html#ref_graphview)
borderPane = this.borderPane = new ht.widget.BorderPane(),
toolbar = this.toolbar = new ht.widget.Toolbar(), // 工具条组件(http://www.hightopo.com/guide/guide/core/toolbar/ht-toolbar-guide.html)
splitView = this.splitView = new ht.widget.SplitView(listBorder, borderPane, 'h', 220), // 分割组件
overview = this.overview = new ht.graph.Overview(gv), // 鹰眼组件(http://www.hightopo.com/guide/guide/plugin/overview/ht-overview-guide.html)
overviewDiv = overview.getView(); // 获取鹰眼组件底层 divoverviewDiv.style.height = '120px'; // HT 的组件默认都是绝对定位的
overviewDiv.style.width = '120px';
overviewDiv.style.left = '0';
overviewDiv.style.bottom = '0';
overviewDiv.style.zIndex = 10;
borderPane.getView().appendChild(overview.getView()); // 将鹰眼组件底层 div 添加到面板组件的底层 div 中listBorder.setTopView(listForm); // 设置顶部组件
listBorder.setCenterView(listView); // 设置中间组件
listBorder.setTopHeight(32); // 设置顶部组件高度
listForm.setVPadding(2); // 设置表单顶部和顶部与组件内容的间距
listForm.setHPadding(4); // 设置表单左边和右边与组件内容的间距
listForm.addRow([ // 添加一行组件{comboBox: { // 组合框类labels: ['All', 'Pathch Panel', 'Switch', 'Server', 'Backbone Switch/Router'], // 设置下拉可选值对应文本values: [-1, 5, 9, 10, 11], // 设置下拉可选值value: -1, // 设置当前值,可为任意类型onValueChanged: function(e) { // 值变化触发函数var val = this.getValue(); // 获取当前的值self.listTypeFilter = val;self.listView.ivm(); // 最彻底的刷新方式}}}
], [0.1], 28); // 参数二为行内元素的宽度,参数三为该行高度borderPane.setCenterView(gv); // 设置中间组件
borderPane.setTopView(toolbar); // 设置顶部组件
borderPane.setTopHeight(32); // 设置中间组件高度

从上面的代码可以看出,splitView 为最外层组件,通过 getHTView 函数返回这个组件,在前面动态设置整个场景的右半部分的组件的时候我们就是通过设置 this.splitView.setRightView(this.rackBuild.getHTView()) 设置场景的右半部分为 rackBuild 的底层 div:

getHTView: function(){ // 获取最外层组件return this.splitView;
}

添加工具条内容

toolbar 工具条中总共的元素就三个:添加机柜,编辑机柜和删除机柜。这三个元素只需要通过 setItems 的方式添加到 toolbar 工具条组件上即可,元素的具体定义如下:

var toolbarItems = [ // 工具条上三个的元素{icon: self.getToolbarIcon('toolbar.add.rack'), // 用的是我们前面声明过的图片toolTip: 'Add a rack', // 文字提示显示内容action: function(){ // 点击按钮后触发的函数self._editingRack = null;self.addRackForm.reset();self.addRackDialog.show(); // 弹出对话框,添加一个新的机架,并填写该机架的信息}},{icon: self.getToolbarIcon('toolbar.edit.rack', function(){ // 判断右侧拓扑图上最后选中的节点 来决定这个图标的显示颜色(如果没有选中机柜,那么此图标显示颜色为灰色)return self.gv.sm().ld() instanceof Rack;}),toolTip: 'Edit rack info',action: function(){var ld = self.gv.sm().ld(); // 获取 gv 中最后选中的节点if (!ld) return;self._editingRack = ld;self.addRackForm.v('name', ld.a('name')); // 弹出框中的 name 赋值为 ld 的业务属性 name 的值self.addRackForm.v('usize', ld.a('usize')); // 弹出框中的 usize 赋值为 ld 的业务属性 usize 的值self.addRackDialog.show(); // 点击此按钮会出现弹出框}},{icon: self.getToolbarIcon('toolbar.delete', function(){return self.gv.sm().ld() instanceof Rack; // 判断右侧拓扑图上最后选中的节点的类型}),toolTip: 'Delete a rack',action: function(){self.handleRemoveRack(); // 在拓扑图上删除机柜,并删除树上此机柜对应的节点}},
]

接下来只要把这个 item 添加到 toolbar 中并设置一下排布的方式即可:

toolbar.setItems(toolbarItems); // 设置工具条元素数组
toolbar.setStickToRight(true); // 设置工具条是否向右对齐排布
toolbar.enableToolTip(true); // 工具条允许文字提示

上面出现的点击 toolbar 工具条按钮触发的事件中有一个“弹出对话框”的操作,通过 this.addRackDialog.show() 来实现,addRackDialog 对象定义在 initDialog 函数中,作用为创建一个 dialog 对话框(http://www.hightopo.com/guide/guide/plugin/dialog/ht-dialog-guide.html),我们设置此对话框中的内容为一个 form 表单进行显示,同时还设计了两个按钮,“OK”按钮作为执行创建/更改机柜的属性,“Cancel”按钮不执行其他操作,只是将对话框隐藏:

initDialog: function(){ // 初始化点击“增改”出现的对话框var self = this,addRackDialog = this.addRackDialog = new ht.widget.Dialog(),addRackForm = this.addRackForm = new FormPane(), // 此类继承于 ht.widget.FormPanelabelWidth = 72;addRackForm.addRow([ // 添加行'Name',{id: 'name',textField: {}}], [labelWidth, 0.1]);addRackForm.addRow(['Height(U)',{id: 'usize',textField: {type: 'number'}}], [labelWidth, 0.1]);addRackDialog.setConfig({ // 配置对话框的标题,尺寸,内容等title: "New Rack", // 对话框的标题content: addRackForm, // 指定对话框的内容width: 320, // 指定对话框的宽度height: 220, // 指定对话框的高度draggable: true, // 指定对话框是否可拖拽调整位置closable: true, // 可选值为true/false,表示是否显示关闭按钮resizeMode: "none", // 鼠标移动到对话框右下角可改变对话框的大小 none 表示不可调整宽高buttons: [ // 指定对话框按钮组内容{label: "Ok", // 按钮显示文本action: function(button, e) { // action为回调函数,当此按钮被当点击时,回调函数会执行var formData = addRackForm.getValueObject(), rack;if (!formData.usize) { // 如果没有填写 Height 的值,则默认高度为18formData.usize = 18;}if (self._editingRack) { // 如果是“编辑rack信息”的弹框rack = self._editingRack;rack.a(formData);rack.a('treeNode').a(rack.getAttrObject()); // }else { // “增加”新的机柜rack = self.createRack(formData); // 创建一个新的 rack 模型self.gv.dm().add(rack); // 在拓扑图上添加这个rack// update treeformData.type = self.cms.TYPE_RACK;var treeNode = self.cms.createData(formData, cms.treeView.sm().ld());rack.a('treeNode', treeNode);}self.gv.fitContent(1); // 添加元素之后,让所有的图元显示在界面上addRackDialog.hide(); // 隐藏对话框}}, {label: 'Cancel',action: function(){addRackDialog.hide(); // 隐藏对话框}}],buttonsAlign: "right"});
}

上面代码出现的 FormPane 类,继承于 ht.widget.FormPane 类,在 htwidget.FormPane 的基础上修改也增加了一些函数,主要的内容还是 ht.widget.FormPane 的实现,文章篇幅有限,这里就不贴代码了,有兴趣的可以参考 FormPane.js 文件。

实现了添加和编辑机房机柜的两个功能,删除机房机柜的功能实现上非常容易,只要将节点从拓扑图和树上移除即可:

handleRemoveRack: function(){ // 在拓扑图上删除机柜,并删除树上此机柜对应的节点var ld = this.gv.sm().ld(); // 获取 gv 上选中的最后一个节点if (ld && ld instanceof Rack) { // 机柜是 Rack 类型this.cms.treeView.dm().remove(ld.a('treeNode')); // 移出树上的有 treeNode 属性的节点this.gv.dm().remove(ld); // 删除 gv 中的节点}
}

列表中元素拖拽


所有的内容都创建完毕,接下来要考虑的就是交互的内容了。列表组件中有 handleDragAndDrop 函数实现拖拽的功能:

listView.handleDragAndDrop = this.handleListDND.bind(this); // 列表上拖拽事件监听(http://www.hightopo.com/guide/guide/core/listview/ht-listview-guide.html)
handleListDND: function(e, state){ // 拖拽listView列表组件中的事件监听var self = this,listView = self.listView,gv = self.gv,dm = gv.dm(),dnd = self.dnd;// handleDragAndDrop 函数有 prepare-begin-between-end 四种状态if (state ==='prepare') {var data = listView.getDataAt(e); // 传入逻辑坐标点或者交互event事件参数,返回当前点下的数据元素listView.sm().ss(data); // 在拖拽的过程中设置列表组件中的被拖拽的元素被选中if (dnd && dnd.parentNode) {document.body.removeChild(dnd);}dnd = self.dnd = ht.Default.createDiv(); // 创建一个 divdnd.style.zIndex = 10;dnd.innerText = data.getName();}else if (state === 'begin') {if (dnd) {var pagePoint = ht.Default.getPagePoint(e); // 返回页面坐标dnd.style.left = pagePoint.x - dnd.offsetWidth * 0.5 + 'px'; dnd.style.top = pagePoint.y - dnd.offsetHeight * 0.5 + 'px';document.body.appendChild(dnd)}}else if (state === 'between') {if (dnd) {var pagePoint = ht.Default.getPagePoint(e);dnd.style.left = pagePoint.x - dnd.offsetWidth * 0.5 + 'px';dnd.style.top = pagePoint.y - dnd.offsetHeight * 0.5 + 'px';self.showDragHelper(e);}}else { // 拖拽“放开”鼠标后的操作if (ht.Default.containedInView(e, self.gv)) { // 判断交互事件所处位置是否在View组件之上if (dm.contains(self.dragHelper)) { // 判断容器是否包含该data对象var rect = self.dragHelper.getRect(), // 获取图元的矩形区域(包括旋转)target = self.showDragHelper(e), // node,ld = self.listView.sm().ld(),uindex = target.getCellIndex(rect.y);node = self.createPane(rect, ld.getAttrObject(), target, uindex);// 创建设备dm.add(node);// update tree datavar treeNode = self.cms.createData(ld.getAttrObject(), target.a('treeNode')); // 在树上创建节点,并设置父亲节点treeNode.a('uindex', uindex);node.a('treeNode', treeNode);dm.remove(self.dragHelper);}}document.body.removeChild(dnd);self.dnd = null;}
}

设备拖动

既然有了从列表组件上拖拽下来的交互动作,接下来应该是做设备在机柜上的拖拽改变位置的功能了,我们通过监听拓扑组件 gv 的交互事件来对节点移动进行事件处理:

gv.mi(this.handleInteractor.bind(this)); // 监听交互
handleInteractor: function(e){ // 移动机柜中的设备 的事件监听if (e.kind.indexOf('Move') < 0) return; // 如果非 move 事件则直接返回不做处理var self = this,listView = self.listView,gv = self.gv,dm = gv.dm(), // 获取数据容器target = gv.sm().ld(), // 获取最后选中的节点uHeight = target.a('uHeight') || 1; // target.a('uHeight')获取最后选中的节点的高度if (e.kind === 'prepareMove') { // 准备移动self._oldPosition = target.p(); // 获取节点当前的位置}else if (e.kind === 'betweenMove') { // 正在移动self.showDragHelper(e.event, uHeight);dm.sendToTop(target); // 将 data 在拓扑上置顶,显示在最顶层 不会被别的节点遮盖}else if (e.kind === 'endMove') { // 结束移动var rack = self.showDragHelper(e.event, uHeight);if (dm.contains(self.dragHelper)) { // 判断容器是否包含该 data 对象target.p(self.dragHelper.p()); // 设置节点的坐标target.a('uindex', rack.getCellIndex(target.p().y)); // 设置节点的业务属性 uindexdm.remove(self.dragHelper); // 移除self._savable = true;self.toolbar.iv();target.setHost(rack); // 设置宿主节点target.setParent(rack); // 设置父亲节点// update treevar treeNode = target.a('treeNode'); // 获取拓扑图上对应的树上的节点treeNode.setParent(rack.a('treeNode'));}else {target.p(self._oldPosition);}}
}

代码中的 showDragHelper 就是在设备拖动的过程中,显示在机柜上,设备下的作为占位的绿色的矩形,为了方便看到当前移动的位置在机柜上显示的位置。有兴趣的可以自己了解一下,篇幅有限,这里就不提了。

列表组件过滤

会不会有同学对列表栏顶部的 form 表单做过滤有些好奇?这块代码非常简单,只需要对选中的类型进行过滤即可:

listView.setVisibleFunc(function(data){ // 设置可见过滤器if (!self.listTypeFilter || self.listTypeFilter === -1)return true;return data.a('type') === self.listTypeFilter; // 根据节点的自定义属性 type 来判断节点属于哪个类型 返回与当前 form 表单中选中的名称相同的所有节点进行显示
});

主要的代码就解释到这里,其他部分的内容有兴趣的同学可以自己去抠代码了解 https://hightopo.com/demo/rack-builder/index.html 还有不懂的可以上官网了解 https://hightopo.com/

基于 Canvas 的 HTML5 工控机柜 U 位动态管理相关推荐

  1. 基于 HTML5 Canvas 的工控机柜 U 位动态管理

    前言 U 是一种表示服务器外部尺寸的单位,是 unit 的缩略语,详细的尺寸由作为业界团体的美国电子工业协会(EIA)所决定.之所以要规定服务器的尺寸,是为了使服务器保持适当的尺寸以便放在铁质或铝质的 ...

  2. 7个华丽的基于Canvas的HTML5动画

    说起HTML5,可能让你印象更深的是其基于Canvas的动画特效,虽然Canvas在HTML5中的应用并不全都是动画制作,但其动画效果确实让人震惊.本文收集了7个最让人难忘的HTML5 Canvas动 ...

  3. 基于 HTML5 Canvas 的电信机柜 U 位动态管理

    前言 U 是一种表示服务器外部尺寸的单位,是 unit 的缩略语,详细的尺寸由作为业界团体的美国电子工业协会(EIA)所决定.之所以要规定服务器的尺寸,是为了使服务器保持适当的尺寸以便放在铁质或铝质的 ...

  4. 基于 Canvas 的 HTML5 交互式地铁线路图

    前言 前两天在 echarts 上寻找灵感的时候,看到了很多有关地图类似的例子,地图定位等等,但是好像就是没有地铁线路图,就自己花了一些时间捣鼓出来了这个交互式地铁线路图的 Demo,地铁线路上的点是 ...

  5. 飞机大战HTML5游戏源码,基于Canvas制作的网页版飞机大战游戏+飞机大战手机端

    简介: 飞机大战HTML5游戏源码是一款基于Canvas制作的网页版飞机大战游戏,画质精美的飞机大战手机端游戏源码 网盘下载地址: http://kekewangLuo.net/W1S2LQcqAT2 ...

  6. Html5基于Canvas画一个动态时钟

    文章目录 前言 一.前期准备 二.绘制刻度 1.流程 2.效果图 三.绘制文字 1.流程 2.效果图 四.绘制指针 1.取得当前时间 2.绘制秒针 3.绘制分针 4.绘制时针 5.效果图 五.绘制圆心 ...

  7. 图片怎么转为html5,将图片转化为矢量并canvas化的容易工具(基于Node.js + HTML5 canvas)...

    将图片转化为矢量并canvas化的简单工具(基于Node.js + HTML5 canvas) 一.前言 最近需要做一个图标的矢量化,但是没有数据,因此采用了node.js作为数据处理工具,canva ...

  8. html5可视化图形编辑器(基于canvas)

    我以前特别喜欢flash,不过flash水平一般,那是的我并不是程序员,充其量也就是个爱好者,在这个html5的时代中,我依旧对那个有时间轴的flash编辑界面念念不忘.于是便有了这篇文章.我的目标是 ...

  9. HTML5基于canvas的网页绘画系统

    绘画是一种在平面上以手工方式临摹自然或非自然,以其达到二维(平面或三维)效果的艺术,在中世纪的欧洲,常把绘画称作"猴子的艺术",因为如同猴子喜欢模仿人类活动一样,绘画也是模仿场景. ...

最新文章

  1. Python工具 | 9个用来爬取网络站点的 Python 库
  2. SQL中的关联更新和关联删除
  3. JFlash ARM对stm32程序的读取和烧录
  4. 在装好的xp系统里面如何添加新的硬件设备
  5. 为什么不能同时用const和static修饰成员函数?
  6. Spring Boot基础学习笔记18:Spring Boot整合Redis缓存实现
  7. 简单DNS服务器架设
  8. 转:Java NIO系列教程(二) Channel
  9. win8 附件数据库失败解决方案《1》
  10. c#异步文件传输功能
  11. kali foremost 分离文件_软件架构之分离关注点
  12. SIGIR 2020最佳论文公布,清华大学揽多个奖项,大三学生摘得最佳短论文奖
  13. Revit二次开发——轴网
  14. matlab 马氏距离 实例,MATLAB求马氏距离(Mahalanobis distance)
  15. 论文心得:BatchNorm及其变体
  16. 69、消防电源及其配电的设置要求
  17. 【BZOJ2037】Sue的小球(动态规划)
  18. 科目二难点——倒车入库
  19. 解决Couldn‘t determine repo type for URL
  20. 【bzoj3698】XWW的难题 有源汇上下界网络流最大流

热门文章

  1. Adobe Illustrator AI 选择颜色的时候,使用吸管工具,如何快捷操作?
  2. javax.mail.FolderClosedException: * BYE JavaMail Exception: java.io.IOException: Connection dropped
  3. Windows 10 安装安卓子系统 WSA(Magisk/KernelSU)使用 WSA 工具箱安装 APK
  4. switch连接wifi服务器没有响应,无线使用时,Joy-Con 没有响应
  5. 【前端面试题】04—33道基础CSS3面试题(附答案)
  6. scip指令集_追踪产品中的关注化学物质:SCIP数据库投入使用
  7. 线性分类(五)-- 朴素贝叶斯法
  8. OpenCV图像处理基本操作 Open_CV系列(一)
  9. java实现批量插入数据
  10. Linux下ffmpeg安装教程(亲测有效)