HTML5之原生拖拽
随着互联网的发展,人们对前端体验的要求不断提高,过去纯点击式的网页操作难免让人感到厌烦。为了使用户操作更简便,HTML5中新增了一项功能 - 拖拽,它允许用户以鼠标拖拽的方式来操作网页,这更加符合人们的操作习惯。实际上该功能更多的是依赖JavaScript API的支持。除了支持在浏览器内部拖拽元素外,该接口还支持从浏览器外部向浏览器内拖拽文件,它借助的是操作系统的支持以及HTML5新增的另外一个特性 - File。下面我们就一起来看HTML5的拖拽如何使用。
拖拽的基本原理
在HTML5中,拖拽是由一系列与拖拽相关的事件组成的,如下:
事件名 | 产生事件的元素 | 事件说明 |
---|---|---|
dragstart | 被拖拽的元素 | 拖拽开始 |
drag | 被拖拽的元素 | 拖拽过程中 |
dragover | 拖拽时鼠标经过的元素 | 被拖拽元素在当前元素上方移动 |
dragenter | 拖拽时鼠标经过的元素 | 被拖拽元素进入当前元素区域 |
dragleave | 拖拽时鼠标经过的元素 | 被拖拽元素离开当前元素区域 |
drop | 拖放的目标元素 | 有元素被拖放到了当前元素中 |
dragend | 拖放的对象元素 | 拖拽结束 |
上述事件的触发流程为:
- 当鼠标点击一个元素并移动,这会在该元素上触发dragstart和drag事件(如果元素不是链接或图片,则需要设置draggable=“true”,否则不允许拖动)。
- 当被拖拽元素进入某个元素的区域时,会触发该区域元素的dragenter事件。
- 当被拖拽元素在某个元素上方移动时,会触发该区域元素的dragover事件。
- 当被拖拽元素离开某个元素时,会触发该区域元素的dragleave事件。
- 释放鼠标时,会触发目标元素的drop事件,同时会触发被拖拽元素的dragend事件。
这些事件构成了一次拖拽完整的生命周期,拖拽过程中的所有行为都应该在这些事件中定义。拖拽最为核心的流程就是:在拖拽开始时,将我们需要传递的数据写入一个拖拽事件对象内;当释放鼠标时,由目标元素得到这个事件对象,并取出写入的数据执行操作。这个过程中,鼠标所经过的元素都具备获取该事件对象的能力。
现在我们以一个最基本的原生拖拽为例,来讲解拖拽的大致流程。该例子来自W3School(这里是截图,如需体验效果,请移步网页示例原生拖拽):
这里是两个div,其中左边的div里含有一张图片,现在我们希望实现将图片自由在两个div内拖拽。下面是页面的HTML结构(这里省略了css样式代码):
<div id="div1"><img id="drag1" src="/i/eg_dragdrop_w3school.gif"/>
</div><div id="div2"></div>
上面暂时省略了拖拽相关的事件,我们将通过一步步为元素添加事件,来讲解这些事件具体的含义和用法。
首先第一步,我们需要为图片(img元素)设置draggable=“true”(实际上img和a元素默认就是可拖拽的,不需要设置该参数。这里为了讲解原理,我们暂且把图片当做一个普通元素看待)。于是img标签就变成了下面的样子:
<img id="drag1" src="/i/eg_dragdrop_w3school.gif" draggable="true"/>
现在img就成了一个可拖拽的元素。当你用鼠标在该元素上点击并移动时,鼠标的下方就会出现一张浅色的图片跟随鼠标移动,这是浏览器的默认行为,它表示当前你正在拖拽该图片。
虽然浏览器为鼠标下方添加了一张图片,让用户在视觉上认为图片已经跟着鼠标移动了,但事实并不是这样。如果只是添加这一个属性,当你把图片拖拽到右侧容器并释放鼠标后,你会发现什么都没有发生 – 图片仍然在原来的位置。这说明拖拽并不是只开启元素的拖拽功能就可以。想要实现拖拽功能,最重要的是依赖一个事件对象,这个事件对象可以看做一个数据载体,而上面讲到的7个拖拽事件就是允许我们在不同阶段操作这个事件对象。
我们来看这个事件对象在拖拽的过程中的具体行为。
上面我们说到,dragstart在被拖拽元素上触发,于是我们可以为被拖拽元素(也就是img图片)注册一个回调函数,它负责在拖拽开始时向事件对象写入数据:
<img id="drag1" src="/i/eg_dragdrop_w3school.gif" draggable="true" ondragstart="drag(event)"/><script>function drag(event){event.dataTransfer.setData("Text",event.target.id);}</script>
现在img上注册了一个dragstart回调函数。一旦我们对该图片执行了拖拽,浏览器就会封装一个拖拽对象(我们记为event),并触发该元素的dragstart事件,并将该对象传入我们的回调函数。得到这个对象后,我们在回调函数内只定义了一行代码,就是将被拖拽元素的id保存在该对象的dataTransfer属性内。由于拖拽事件是在图片元素上触发的,所以event.target就是这个图片元素,此时dataTransfer内保存的就是img的id:“drag1”。setData的第一个参数“Text”表示当前存储的数据类型是文本(或者说是普通的字符串)。如果你想看一下这个事件对象是什么样的,它大概长这样:
OK,现在让我们跳过这个令人眼花的对象。我们只需要知道这个对象里存储了与本次拖拽相关的所有参数,而我们想要传递的数据也已经写在了它的dataTransfer属性里。
在默认情况下,所有的DOM元素都不接受释放行为。从视觉效果上来看,当你拖拽该图片到某块空白区域时,你可能会发现鼠标变成了禁用图标(一般为一个圆圈带一个斜线),这表示当前区域不允许释放拖拽元素。我们可以通过事件对象的preventDefault()方法来禁止这种默认行为。比如我们现在想让上面例子中右侧的那个div允许释放被拖拽元素,我们就可以给它注册一个ondragover(鼠标拖拽时在当前元素上方移动会触发该事件)回调函数,在该函数内取消浏览器的默认行为。代码如下:
<div id="div2" ondragover="allowDrop(event)"></div><script>function allowDrop(event){event.preventDefault();}
</script>
取消了浏览器的默认行为后,当鼠标拖拽图片移动到右侧的div时,鼠标就会变成可释放的图标(具体图标因浏览器而异),它表示当前区域接受释放。对于上面的代码,如果你尝试拖拽,你会发现虽然从视觉上已经可拖拽了,但是一旦鼠标释放,仍然什么都没有发生,图片并没有按我们所想被放置到右侧的容器中。
这是为什么呢?
因为浏览器没有为我们释放鼠标的事件定义任何默认的行为,我们想做什么事必须自己手动定义。那么浏览器为什么不为我们提供自动移动DOM元素的默认行为呢?原因也很简单:为了避免歧义。我们知道,HTML是一种嵌套的结构,假如有下面两个嵌套的div:
当你在内部的div内释放鼠标时,这个事件不光会被内部的div元素捕获,还会被外部的div捕获(包括document元素也会捕获到这个事件),那么浏览器把被拖拽元素添加到哪个元素上呢?浏览器无法做出选择,因此无法为开发者提供默认的行为(当然可能还有其他原因,但仅这一个原因就已经很充分了)。
但是这个问题对开发者来说就不存在任何歧义,因为开发者可以选择为哪个元素绑定回调来处理这个事件(当然也可以都绑定,它们都会得到执行)。回到上面的例子,现在我们希望把图片拖放到右侧div时移动图片,于是我们给右侧的div绑定一个ondrop事件来监听鼠标的释放事件。代码如下:
<div id="div2" ondrop="drop(event)" ondragover="allowDrop(event)"></div><script>function drop(event){event.preventDefault();var data=event.dataTransfer.getData("Text");event.target.appendChild(document.getElementById(data));}
</script>
我们给右侧div绑定了ondrop事件,当鼠标拖拽图片在该区域释放时就会执行该回调函数,浏览器会把拖拽开始时生成的事件对象作为参数传递进来(还记得吗?我们在这个对象的dataTransfer属性里记录了被拖拽图片元素的id,现在要派上用场了)。
首先第一步,仍然是用preventDefault()禁止浏览器的默认行为(我们在dragover里只是保证鼠标在移动时不出现丑陋的禁用图标,但是鼠标释放时仍然会出现,虽然只有一瞬间,但这非常影响用户体验)。
第二步,取出我们在datatTransfer里写入的图片元素的id。
第三步,用原生选择器从DOM树中找到这个元素,使用appendChild方法添加到当前元素(此时的event.target指的是右侧的容器,因为该事件是在右侧容器上触发的)上。
所以我们真正执行的操作无非就是把图片元素查出来,用原生的DOM方法添加到右侧容器中。但是被拖拽的图片和目标容器本身是相互独立的,只有借助一个事件对象,目标容器才知道到底是哪个元素需要被添加进来。而这个事件的dataTransfer属性就是数据传递的载体。
经过上面的修改,这个代码就可以实现把图片从左侧div拖拽到右侧div了。为了能够实现两个容器的相互拖拽,我们需要为左侧容器也写上同样的监听事件,这样两个div就都具备了放置图片元素的能力,也就是W3School示例中的效果。一个最基本的拖拽也就实现了。
换个角度看拖拽
(原创声明:如需引用该部分内容,请注明出处)
从更高的角度来说,拖拽是浏览器为开发者封装的一个消息通道。这个通道的起点是被拖拽的元素,终点是目标元素。浏览器用一个事件对象用来描述与拖拽相关的参数,它产生于起点,被传递到目标元素,而鼠标经过的元素也可以从这个通道中获取事件对象。
用户将鼠标放到一个元素上,按下并拖动的行为将在该元素上开启这个消息通道。浏览器会生成一个用于描述该拖拽行为的事件对象,我们可以通过该对象写入自己的数据,也可以设置与本次拖拽相关的参数(比如移动时鼠标的样式、允许的拖拽类型等),然后该对象将在消息通道内传递。
浏览器向我们提供了若干个原生事件来从通道中获取这个事件对象,并执行需要执行的操作(比如当鼠标进入某个元素时,该元素可以通过ondragenter事件接口从消息通道中获取事件对象,假如你希望鼠标从当前元素上方掠过时变为可放置的样式,就可以通过event.preventDefault()来实现,就像我们上面做的那样)。让我们从新的角度重新来看浏览器为开发者提供的7个原生事件:
事件名 | 产生事件的元素 | 角色 | 事件说明 |
---|---|---|---|
dragstart | 被拖拽的元素 | 通道起点 | 开启消息通道,生成事件对象,并写入数据或设置参数等 |
drag | 被拖拽的元素 | 通道起点 | 允许在整个拖拽过程中从消息通道里获取事件对象 |
dragend | 拖放的对象元素 | 通道起点 | 释放鼠标时,浏览器通过该事件通知起点元素 |
dragenter | 拖拽时鼠标经过的元素 | 通道的路径 | 允许当鼠标进入某个元素时,从消息通道中获取事件对象 |
dragover | 拖拽时鼠标经过的元素 | 通道的路径 | 允许当鼠标在某个元素上移动时,从消息通道中获取事件对象 |
dragleave | 拖拽时鼠标经过的元素 | 通道的路径 | 允许当鼠标离开某个元素时,从消息通道中获取事件对象 |
drop | 拖放的目标元素 | 通道的终点 | 释放鼠标时,目标元素从通道中得到事件对象 |
从表格中可以看到,被拖拽的元素(通道的起点)可以在拖拽的开始和结束,以及拖拽的整个过程(通常每隔350毫秒触发一次)中监听该事件。鼠标经过的元素只能在鼠标从该元素上方经过时监听到拖拽事件(这个过程又细分为进入、移动和离开)。而目标元素只能在鼠标释放时才能监听到该事件(因为在用户释放鼠标之前,我们无法知道目标元素是谁)。
现在是不是对拖拽又有了新的认识?
既然拖拽只是建立一个消息通道,那么我们可以传递的消息又何止元素的id呢?实际上该对象支持写入四种数据类型:
- “text/plain”:或简写为"text"。纯文本,也就是字符串。
- “text/html”:HTML格式的数据。
- “text/xml”:xml格式的数据。
- “text/url-list”:或简写为“url”。url列表。
实际上第一种数据类型就可以满足大多数情况下的需求。对于非字符串类型的数据,只需要压缩成字符串,最后再解析为原数据结构即可(如json数据可以用JSON.stringify压缩成字符串,再用JSON.parse解析为对象)。
上面我们只是传递了被拖拽元素的id,实际上与该元素相关的任何参数,甚至与该元素无关的数据(只要我们认为它对本次拖拽有用),都可以写入通道。而且释放鼠标时也不一定要执行appendChild来添加元素,我们可以在释放鼠标时做任何我们想做的事(如弹出一个提示框,或者根据传过来的参数生成任意的DOM结构,甚至把被拖拽的元素添加到页面的任何地方(只要你认为需要这样做,浏览器都是允许的,哪怕用户觉得很奇怪))。
下面我将自己写一个示例,来说明拖拽的灵活性(代码在后面可以找到,可以直接保存为一个HTML文件双击运行)。
该例子中,我们把上面的一个可拖拽按钮的textContent(即:“可拖拽元素”这几个字)写入事件对象。然后为第一个div定义的拖拽行为是添加到内部的一个ul中,为第二个定义的行为是使用alert弹出提示框,为第三个定义的行为是将其显示在第一个div内,同时在字符串前面拼上“来自第三个div的”这几个字。
现在当我们向第一个div内拖拽时,列表就会多出一项,文字内容为“可拖拽元素”。而向第二个div内拖拽时,就会出现如图所示的网页提示信息。向第三个div内拖拽时,我们看到在第一个div内列表的最后面多了一项“来自第三个div的可拖拽元素”。js代码如下:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>HTML5原生拖拽</title><style>.des{width:300px;height:200px;float: left;border: 1px solid #e6e6e6;}p{color: #a6a6a6;font-size: 12px;}</style>
</head>
<body>
<button id="src" draggable="true" class="nav">可拖拽元素</button>
<br/><br/><div id="des1" class="des"><p>放进该区域会显示为列表<p/><ul id="container"></ul>
</div><div id="des2" class="des"><p>放进该区域会得到一条提示<p/>
</div><div id="des3" class="des"><p>放进该区域会输出在第一个div内<p/>
</div></body>
<script>var src = document.getElementById("src");var des1 = document.getElementById("des1");var des2 = document.getElementById("des2");var des3 = document.getElementById("des3");src.addEventListener("dragstart", function(e){var dt = e.dataTransfer;dt.effectAllowed = 'all';dt.setData("text/plain", e.target.textContent);});des1.addEventListener("drop", function(e){var dt = e.dataTransfer;var text = dt.getData("text/plain");var container = document.getElementById("container");var li = document.createElement("li");li.textContent = text;container.appendChild(li);e.preventDefault();e.stopPropagation();}, false);des2.addEventListener("drop", function(e){var dt = e.dataTransfer;var text = dt.getData("text/plain");alert(text);e.preventDefault();e.stopPropagation();}, false);des3.addEventListener("drop", function(e){var dt = e.dataTransfer;var text = dt.getData("text/plain");text = "来自第三个div的" + text;var container = document.getElementById("container");var li = document.createElement("li");li.textContent = text;container.appendChild(li);e.preventDefault();e.stopPropagation();}, false);des1.ondragover = function(e){e.preventDefault();}des1.ondrop = function(e){e.preventDefault();}des2.ondragover = function(e){e.preventDefault();}des2.ondrop = function(e){e.preventDefault();}des3.ondragover = function(e){e.preventDefault();}des3.ondrop = function(e){e.preventDefault();}
</script>
</html>
虽然是很简单的示例,但是我想已经可以证明拖拽的灵活性。另外这里只是在释放时有不同的行为,我们还可以对不同的元素在拖拽开始时向事件对象写入任意的内容,这样组合起来拖拽就会变得相当灵活。
下面我们介绍一个将element-ui的el-tree上的节点拖拽到外部的例子,它更能说明原生拖拽的强大之处。
el-tree中节点拖拽的扩展
el-tree是Vue的element-ui中的树组件,该组件提供了较为强大的拖拽功能,一个最基础的可拖拽树只需要写成下面这样即可(来自element-ui官网):
<el-tree:data="data"node-key="id"default-expand-all@node-drag-start="handleDragStart"@node-drag-enter="handleDragEnter"@node-drag-leave="handleDragLeave"@node-drag-over="handleDragOver"@node-drag-end="handleDragEnd"@node-drop="handleDrop"draggable:allow-drop="allowDrop":allow-drag="allowDrag">
</el-tree>
这里的事件监听器就对应我们上面讲到的原生事件,它们对每个节点都是生效的。draggable属性表示开启树的拖拽功能,这样树的每个节点都会被添加draggable属性。
默认情况下,树上的节点只支持在树的内部拖拽。也就是说,你无法把树上的节点拖拽到树的外面。但是这又是一个非常常见的需求(github的issue中说el-tree具备这个能力,但是并没有找到相关示例)。作为前端开发者,如果框架有一定的局限性,原生技术将是我们最强大的武器。下面我将简单介绍我是如何将树上的节点拖拽到树的外部的,希望对感兴趣的同学有所启发。
假设我们现在有一个容器,我们希望可以把树上的某个节点拖拽到这个容器里形成一个列表,这个列表暂时只保留原树节点的文本内容和id(因具体需求而异)。我们继续使用上面的树作为拖拽源,并给出下面一个div作为容器:
<div class="menu-list"@drop="handleTargetDrop"@dragover="handleTargetDragOver"><ul><li v-for="item in menus" :key="item.id><span>{{item.name}}</span></li></ul>
</div>
由于这是在Vue中,我们不需要直接操作DOM,只需要修改该ul对应的数据即可。在拖拽开始之前,menus值为空,所以该容器内不会显示任何内容。
现在我们先来处理el-tree。由于我们不需要改变树结构,因此需要屏蔽树自身的drop行为,这可以很容易通过设置绑定的allow-drop来实现,同时需要设置allow-drag使节点可拖拽:
allowDrop(draggingNode, dropNode, type) {return false;
},
allowDrag(draggingNode) {return true;
},
好的,现在树上的节点都无法在树上移动了,并且都是可拖拽的。接下来要处理向外部拖动的行为了。我们需要定义节点的node-drag-start事件,它与原生事件的dragstart对应,是框架向我们提供的接口。我们可以在该事件的回调函数内将我们需要传递的数据封装进去,为了简单,我们直接传递整个节点的data即可。如下:
handleDragStart(node, ev) {let dt = ev.dataTransfer;ev.dataTransfer.effectAllowed = 'copy';dt.setData("text/plain", JSON.stringify(node.data));
},
现在我们把树上被拖拽的那个节点的data压缩成json字符串写进了事件对象的dataTransfer里。至此,树节点已经可以向外提供节点数据了。
下一步就是要处理我们的容器了。首先,我们要取消浏览器阻止拖拽的默认行为,为了用户体验,我们在dragover和drop中同时阻止该行为(drop的我们后面可以看到)。
handleTargetDragOver(e){e.preventDefault();
},
下面就是要处理drop事件,我们需要在鼠标释放时修改容器的列表所对应的数据menus(从这里就可以看出MVVM的设计理念,我们的视线永远放在如何操作数据上,而不会想着如何操作DOM,因为框架会在数据变化时自动操作DOM)。实际上这相当简单:
handleTargetDrop(e){let data = e.dataTransfer;let content = JSON.parse(data.getData("text/plain"));this.menus.push({id: content.id, name: content.name});e.preventDefault();//通常不需要阻止冒泡,但是当出现容器嵌套时最好这么做//它可以防止节点被添加到数组中两次e.stopPropagation();
}
我们看到,只需要非常简单的代码,就可以将树上的节点拖拽到外部容器了,这再一次证明了原生拖拽的灵活性和强大。
总结
本文并不是一篇详细介绍原生拖拽细节的文章。实际上拖拽事件中有很多的参数都可以设置,比如你可以设置当前拖拽只能复制,或者修改鼠标移动时跟随鼠标移动的图片,你还可以设置当元素进入某个区域时,底部的元素产生一定的动态效果,这样会带来相当高级的用户体验。
此外,在事件对象的dataTransfer中还包含一个有用的属性files,它存储了从浏览器外部拖拽进来的文件,如果我们在某个元素的drop事件中读取这个文件列表,就可以获取用户拖拽进来的文件,HTML5的file API允许我们直接在浏览器显示该文件,或者选择上传到服务器等(如果你使用的是Chrome,并且征得了用户同意,甚至可以修改这些文件,这依赖fileWriter接口,但由于安全问题,该接口的支持性不是很好)。除此之外,单凭拖拽甚至可以写出一些有趣的HTML5网页游戏,而这完全取决于你的创造能力。
本文最重要的目的不是展示该技术可以被使用得多么神奇,而是希望探究它的基本原理,为以后的使用打下良好的基础。希望对不了解原生拖拽的同学有所帮助。
HTML5之原生拖拽相关推荐
- html5 上传超大文件,HTML5教程 如何拖拽上传大文件
本篇教程探讨了HTML5教程 如何拖拽上传大文件,希望阅读本篇文章以后大家有所收获,帮助大家HTML5+CSS3从入门到精通 . < 前言: 大文件传输一直是技术上的一大难点.文件过大时,一些性 ...
- html5拖动鼠标直线,html5的鼠标拖拽
鼠标拖拽 Title .one {width:200px;height:200px;border:1px solid blue;margin:10px;} .two {width:50px;heigh ...
- java drag drop_原生拖拽,拖放事件(drag and drop)
原生拖拽,拖放事件(drag and drop) 拖拽,拖放事件可以通过拖拽实现数据传递,达到良好的交互效果,如:从操作系统拖拽文件实现文件选择,拖拽实现元素布局的修改. drag and drop事 ...
- HTML5原生拖拽/拖放 Drag Drop 详解
转载自:juejin.im/post/5a169d- 前言 拖放(drap && drop)在我们平时的工作中,经常遇到.它表示:抓取对象以后拖放到另一个位置.目前,它是HTML5标准 ...
- HTML5原生拖拽/拖放(drag drop)详解
前言 拖放(drap && drop)在我们平时的工作中,经常遇到.它表示:抓取对象以后拖放到另一个位置.目前,它是HTML5标准的一部分.我从几个方面学习并实践这个功能. 拖放的流程 ...
- html5 原生拖拽,原生JS实现拖拽效果
这篇文章主要为大家详细介绍了原生JS实现拖拽效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 本文实例为大家分享了JS实现拖拽效果的具体代码,供大家参考,具体内容如下 ...
- 原生拖拽太拉跨了,纯JS自己手写一个拖拽效果,纵享丝滑
前言 提到元素拖拽,通常都会先想到用 HTML5 的拖拽放置 (Drag 和 Drop) 来实现,它提供了一套完整的事件机制,看起来似乎是首选的解决方案,但实际却不是那么美好,主要是它的样式太过简陋, ...
- HTML5 drag drop 拖拽与拖放简介
by zhangxinxu from http://www.zhangxinxu.com 本文地址:http://www.zhangxinxu.com/wordpress/?p=1419 一.前面的话 ...
- span标签的鼠标滑入提示_彻底搞懂拖拽——基于鼠标事件的拖拽以及基于HTML5 API的拖拽...
一.基于鼠标事件的拖拽 原理--onmousedown.onmousemove.onmouseup onmousedown 该事件会在鼠标按键被按下时触发 支持该事件的HTML标签: html < ...
- html5限制拖拽区域怎么实现,html5怎么实现拖拽
html5实现拖拽的方法:首先新建一个空的HTML5结构:然后在body元素中放置一个div:最后通过allowDrop,drag和drop三个函数实现拖拽功能即可. 本文操作环境:Windows7系 ...
最新文章
- Standby Redo Log 的设定原则、创建、删除、查看、归档位置
- 从零开始学习PYTHON3讲义(一)认识Python
- fastjson判空_fastjson JSON 对象为空保留null
- pycharm ssh mysql_PyCharm使用之配置SSH Interpreter的方法步骤
- 产品报价单模板_一文说透报价单,这么做才是专业!附模板及注意事项
- C语言字符串函数大全
- 腾讯2016春招之算法编程解析
- mongodb java id 查询数据_java 用 _id 查找 MongoDB 下的数据
- C#Convert.ToInt32(char)方法-将char值转换为int
- 解决uploadify在Firefox下丢失session的问题
- linux hook 任意内核函数,【求助】Kernel 4.8下编译编写的Netfilter Hook函数失败
- 各种存储分配算法java代码实现_Java实现操作系统中四种动态内存分配算法:BF+NF+WF+FF...
- tf.one_hot函数用法
- 2018年了,Windows2000还能用吗?
- 前端实现一个登录验证的滑块
- eclipse下载哪个版本开发java_官网上有很多版本的eclipse,下载哪个版本比较合适?...
- 规划高速公路上完全可再生动力充电站:数据驱动的鲁棒优化方法 ,用于在公路网络上采用和大化独立电动电动机充电站
- matlab 图像 放大缩小,图像的放大与缩小(MATLAB 代码)
- 【转】欧几里德结构数据(Euclidean Structure Data) 以及非欧几里德结构数据(Non-Euclidean Structure Data)
- mysql小王 保密_街机斗地主小王搓牌