每天坐地铁,经常看地铁图,有一天突然想到,地铁图不也是一种拓扑结果吗?TWaver到底能与地铁图擦出怎样的火花呢?

 
想到就干,先到网上找幅参考图。各种风格的地铁图还挺多,甚至有大学生自主设计制作,受到地铁相关人士的认可和赞扬。不过看到他花了3周时间,我就比较同情他了,如果学会了TWaver,我保他连3天都不用就可以完成,而且还是纯矢量、可交互、有动态效果、无失真缩放的拓扑图。
 

我们就以上面这幅地铁图为模版来进行制作。

一、数据整理

 
俗话说兵马未动粮草先行,没有数据再好的创意也白搭。
 
数据格式,自然首选JavaScript原生支持的json文件,直观方便。
 

1. 数据结构

 
数据结构是整理数据的重中之重,一个好的结构设计会让后面的编程轻松方便。一种很容易想到的结构是以线路为基础,每条线路依次为各个站点,但是这里面有许多站点存在多线路共用的情况,如何复用就很麻烦。另一种是以站点为基础,再为每个站点添加线路属性,但这样线路的站点次序不够清晰,在程序中很难对线路进行遍历和循环操作。
 
那么比较好的办法,就是将线路和站点分开,这样将来无论是对站点还是对线路进行操作,都会比较方便。
 
  1. {
  2. "stations":{
  3. "l01s01":{ },
  4. …………
  5. }
  6. "lines":{
  7. "l01":{……},
  8. …………
  9. }
  10. "sundrys":{
  11. "railwaystationshanghai":{……},
  12. …………
  13. }
  14. }
其中第3部分“sundrys”,是需要在图中标识的火车站、飞机场等相关元素。
 
当然,大家看到网上例子,有的会把label也单独出来,这样虽然可以灵活定义label 的位置,但却使得站点和label两张皮,而且也增加了数据采集的工作量。TWaver有对label丰富的自定义功能,所以完全没有必要将label单拎出去,只需给其一个位置属性就可以了。
 

2. 站点数据

 
每个站点,首先要有个属性名。属性名是由6位字符组成的,是由最先经过此站的线路名与站点在此线路上的序号组合而成。例如“l01s01”,表示1号线第1个站点。站点的“id”,与站点属性名完全一致。站点的“label”属性,是站点显示名字相对站点的位置。
 
  1. "l01s01":{
  2. "id":"l01s01",
  3. "name":"莘庄",
  4. "loc":{"x":419,"y":1330},
  5. "label":"bottomright.bottomright",
  6. },
  7. …………

3. 线路数据

 
线路属性名是3位字符组成。首字符为线路类型:普通线路以“l”开头,支线以“b”开头,延伸线以“e”开头,磁悬浮以“m”开头。后两位数字为线路的序号。线路的“id”,与线路属性名完全一致。线路的“stations”属性,包含了此线路上的所有站点,不过不要以为各站点的属性名和属性值都是一样的,各站点的属性名是严格按照线路中的顺序命名的,但属性值却是站点的id。比如人民广场站,其id为“l01s13”,但其在不同线路中的属性名可能分别是“l01s13”、“l02s11”、“l08s16”。这样既确保了对线路操作的方便性,又实现了对换乘站点的复用。
 
  1. "l01":{
  2. "id":"l01",
  3. "name":"1号线",
  4. "color":"#e52035",
  5. "stations":{
  6. "l01s01":"l01s01",
  7. "l01s02":"l01s02",
  8. ……
  9. }
  10. },
  11. ……

4. 杂项数据

 
除了站点和线路以外的其他需要展示的元素都可以放到杂项数据中。杂项属性名尽量完整表达此项目的名称。杂项的“sign”属性,是显示图标的注册名称。杂项的“station”属性,是其临靠的地铁站id。杂项的“offset”属性,是其显示图标相对地铁站的方位。
 
  1. "airporthongqiao":{
  2. "sign":"airport",
  3. "station":"l02s20",
  4. "name":"虹桥国际机场",
  5. "offset":{"x":0, "y":-1}
  6. },
  7. ……

二、站点创建

 
地铁线路就是一个拓扑网络,那么站点也就是网络的节点,创建站点也就是新建Node的过程。
 

1. 文件导入

 
所有的数据都存放在json文件中,首先要能够读取进来。
 
  1. function loadJSON(path,callback){
  2. var xhr = new XMLHttpRequest();
  3. xhr.onreadystatechange = function(){
  4. if (xhr.readyState === 4) {
  5. if (xhr.status === 200) {
  6. dataJson = JSON.parse(xhr.responseText);
  7. callback && callback();
  8. }
  9. }
  10. };
  11. xhr.open("GET", path, true);
  12. xhr.send();
  13. }

因为读取文件是一个异步的过程,所以要程序的展开都要放在文件读取函数的内部。

  1. function init(){
  2. loadJSON("shanghaiMetro.json", function(){
  3. initNetwork(dataJson);
  4. initNode(dataJson);
  5. });
  6. }

2. 站点初创

 
开始我们先不管站点的类型,对所有站点进行一次遍历,将站点的基本信息添加到站点Node中。有心人会发现这里没有直接设定Node的位置,而只是将位置信息存到了“location”自定义属性中,这是因为以后统一定位可以避免由于image大小不同等原因造成的位置偏移。
 
  1. for(staId in json.stations){
  2. var station = json.stations[staId];
  3. staNode = new twaver.Node({
  4. id: staId,
  5. name: station.name,
  6. image:'station',
  7. });
  8. staNode.s('label.color','rgba(99,99,99,1)');
  9. staNode.s('label.font','12px 微软雅黑');
  10. staNode.s('label.position',station.label);
  11. staNode.setClient('location',station.loc);
  12. box.add(staNode);
  13. }

3. 站点分类

 
站点主要有3种不同的类型:普通站点、换乘站点、支线共用站点。换乘站和共用站的区别,是换乘站在不同的线路中,一般并不是同一个空间,车跑的也完全不是同一个线路;而共用站却完全是同一个地点,车也在同一条线路上跑,但不同时间跑的车可能是不同支线的车。不过对于始发或终到的共用站点,一般也都作为换乘站处理。
 
对于不同站点的判断,无需在原始数据中指定,完全可以通过逻辑判断来设定:只在某一条线路中出现的就是普通站点;仅在支线中重复出现的就是支线共用站点;在非支线的不同线路中重复出现的就是换乘站点。
 
最后,对不同的类型,用不同图标显示出来,让用户一目了然分辨站点。
 

4. 显示站名

 
由于地铁线路交错复杂,站名的显示位置就变得非常重要,如果不进行判断和设置,很有可能会造成遮挡重叠,画面会非常难看。这也是有些程序员甚至将其独立于站点之外,作为单独的网元重新统计创建的原因。在TWaver中可以很方便地定义label的显示位置,甚至可以调整其显示的角度和距离。当然可以通过程序,对站点周围的空间进行判断,智能调整显示位置。但是由于有些地方过于密集和复杂,逻辑判断的难度会非常大,不如直接在数据中手动添加位置信息来的方便。
 

5. 站点图标

 
按照站点的分类,设计了三种不同的站点图标,与参考地铁图相比,增加了支线共用站图标。换乘站图标没有选择参考地铁图的长方形,而是采用了更为灵活简便的圆形图标,省却了方向和旋转,方便了程序设计。
 

三、线路设计

 
地铁线路由TWaver的Link实现,具有丰富的定制功能,完全可以满足不同情况下线路显示的需求。
 

1. 连接站点

 
对数据文件中的各条线路进行遍历,再对每条线路中的各个站点进行遍历,在站点间依次创建Link,基本的地铁图就呈现出来了。
 
  1. for(lineId in json.lines) {
  2. ……
  3. for(staSn in line.stations) {
  4. ……
  5. var link = new twaver.Link(linkId,prevSta,staNode);
  6. link.s('link.color', line.color);
  7. link.s('link.width', linkWidth);
  8. link.setToolTip(line.name);
  9. box.add(link);
  10. }
  11. }

可能有的地铁图也就到此为止了,基本的示意功能已经具备了嘛。但追求完美的TWaver怎么可能忍受,起码线路走向要规整一些,不能两个站点间直线一连就完事了。
 

2. 连线分型

 
观察参考地铁图,是对线路进行了美化,基本只保留了横平竖直和正斜的走向。这就需要对一些站点间的连线,加上必要的拐点,使得连线始终按照横、竖和正斜的方向来走。
 
参考地铁图中,拐点的添加有时比较随意。在一段路径上,有的只添加一个拐点,有的又会添加两个拐点,规律性不是很强,用程序很难模仿。

 

为了方便程序实现,这里最多只在相邻两个站点间添加一个拐点,以达到使线路方向只有直或正斜的效果。这样的话,所有的连线就只有无拐点和有拐点两种了。其中,无拐点连线,包括横向、纵向、正斜向(与x轴夹角45°或-45°)三种;有拐点连线又可分为先直后斜和先斜后直两种。
 

3.智能拐点

 
首先我们要找到需要添加拐点的连线,这个很简单,只需要把斜率不是1的斜线找出来就可以了。下一步才是关键,就是判断拐点类型,是先直后斜还是先斜后直。
 
添加拐点的一个原则,就是拐点前后,要尽量保持平直;如果必须产生夹角,也要选夹角更大的,这样整理后的路线,才比较美观,不会有过多不合理的转角。
 
  1. var setTrunType = function(json){
  2. box.forEach(function (ele) {
  3. var id = ele.getId();
  4. if(ele instanceof twaver.Link){
  5. var link = ele;
  6. var f = link.getFromNode().getCenterLocation();
  7. var t = link.getToNode().getCenterLocation();
  8. if(needAddPoint(f, t)){
  9. var so=0, os=0;
  10. if(link.getClient('prevLink')){
  11. so += byPrevPoint(f,t,link).so;
  12. os += byPrevPoint(f,t,link).os;
  13. }
  14. if(link.getClient('nextLink')){
  15. os += byNextPoint(f,t,link).os;
  16. so += byNextPoint(f,t,link).so;
  17. }
  18. p = os>so ? obliqueStraight(f, t) : straightOblique(f, t);
  19. link.setClient('point', p);
  20. link.setClient('truntype', os>so?'os':'so');
  21. }
  22. }
  23. });
  24. }

4. 人工拐点

 
上面考虑的智能拐点的添加,但其也有局限性,不够灵活,碰到比较复杂的情况就招架不住了。比如磁悬浮线,只有始发和终到站,而且线路比较长,只添加一个拐点无法反映真实情况,这时就必须可以人工添加多个拐点了。
 
人工拐点需要在数据中添加拐点的位置信息,然后在连线上添加拐点。人工拐点可以用setLinkPathFunction方法,但在与智能拐点混用的情况下,智能拐点判断就比较麻烦。还有一种思路,就是将人工拐点设成一个隐形的节点,实现起来就非常容易了。
 
  1. var createTurnSta = function(line, staSn){
  2. staTurn = new twaver.Node(staSn);
  3. staTurn.setImage();
  4. staTurn.setClient('lineColor',line.color);
  5. staTurn.setClient('lines',[line.id]);
  6. var loc = line.stations[staSn];
  7. staTurn.setClient('location',loc);
  8. box.add(staTurn);
  9. return staTurn;
  10. }

5.接点偏移

 
地铁图中,有些路段是两条线路并行的。在某些线路交叉的地方,有时甚至会在局部出现多条线段并行的情况。如果不进行设计和处理,要么多条线会重合在一起,只能显示出其中的一条;要么两条线会随意分合,线路在站点处出现不美观的弯曲。
 
当然有多种思路来解决这个问题,本例中是采取了虚拟站点的办法。就是在站点的旁边,添加一个Follower(但并不显示出来),让并行的不同线路连接到不同的Follower上。通过调整Follower的位置,就可以完美显示线路的并行效果了。
 
  1. var createFollowSta = function(json, line, staNode, staId){
  2. staFollow = new twaver.Follower(staId);
  3. staFollow.setImage();
  4. staFollow.setClient('lineColor',line.color);
  5. staFollow.setClient('lines',[line.id]);
  6. staFollow.setHost(staNode);
  7. var az = azimuth[staId.substr(6,2)];
  8. var loc0 = json.stations[staId.substr(0,6)].loc;
  9. var loc = {x:loc0.x+az.x, y:loc0.y+az.y};
  10. staFollow.setClient('location',loc);
  11. box.add(staFollow);
  12. return staFollow;
  13. }

当然具体到每条线路在某个站点怎么偏移,很难用程序智能判断和调整(希望有高手可以用简洁的方式实现)。本例是手动修改线路数据,在站点的原id后添加了方位代码。比如原为l01s11的站点,在某条线路中将其改为l01s11tt,就实现了该线路在站点顶部经过效果。具体方位代码定义如下:
 
  1. var azimuth = {
  2. bb: {x: 0, y: linkWidth*zoom/2},
  3. tt: {x: 0, y: -linkWidth*zoom/2},
  4. rr: {x: linkWidth*zoom/2, y: 0},
  5. ll: {x: -linkWidth/2, y: 0},
  6. br: {x: linkWidth*zoom*0.7/2, y: linkWidth*zoom*0.7/2},
  7. bl: {x: -linkWidth*zoom*0.7/2, y: linkWidth*zoom*0.7/2},
  8. tr: {x: linkWidth*zoom*0.7/2, y: -linkWidth*zoom*0.7/2},
  9. tl: {x: -linkWidth*zoom*0.7/2, y: -linkWidth*zoom*0.7/2},
  10. BB: {x: 0, y: linkWidth*zoom},
  11. TT: {x: 0, y: -linkWidth*zoom},
  12. RR: {x: linkWidth*zoom, y: 0},
  13. LL: {x: -linkWidth, y: 0},
  14. BR: {x: linkWidth*zoom*0.7, y: linkWidth*zoom*0.7},
  15. BL: {x: -linkWidth*zoom*0.7, y: linkWidth*zoom*0.7},
  16. TR: {x: linkWidth*zoom*0.7, y: -linkWidth*zoom*0.7},
  17. TL: {x: -linkWidth*zoom*0.7, y: -linkWidth*zoom*0.7}
  18. };

四、动态显示

 
TWaver做出的图,可不是一张死图,而是能呈现许多动态效果的生动的活的图片。
 

1. 文本提示

 
动态鼠标提示,是TWaver的基本功能。每一个网元,不管是节点还是连线,只要设置了name属性,鼠标移入后,默认都会以弹窗的方式将name显示出来。当然,用户也可以定制弹窗显示的内容。比如,我们可以把某个站点首班和末班车的时间显示出来,也可以把换乘信息等显示出来,只需要一个setToolTip就可以了。
 
 

2. 站点显示

 
当鼠标移入站点的时候,我们希望站点能有所变化,以给出动态提示。这是通过在注册站点矢量图形时,加入动态判断实现的。以下代码是普通站点的矢量图形:
 
  1. twaver.Util.registerImage('station',{
  2. w: linkWidth*1.6,
  3. h: linkWidth*1.6,
  4. v: function (data, view) {
  5. var result = [];
  6. if(data.getClient('focus')){
  7. result.push({
  8. shape: 'circle',
  9. r: linkWidth*0.7,
  10. lineColor: data.getClient('lineColor'),
  11. lineWidth: linkWidth*0.2,
  12. fill: 'white',
  13. });
  14. result.push({
  15. shape: 'circle',
  16. r: linkWidth*0.2,
  17. fill: data.getClient('lineColor'),
  18. });
  19. }else{
  20. result.push({
  21. shape: 'circle',
  22. r: linkWidth*0.6,
  23. lineColor: data.getClient('lineColor'),
  24. lineWidth: linkWidth*0.2,
  25. fill: 'white',
  26. });
  27. }
  28. return result;
  29. }
  30. });

3. 站点动画

 
在换乘站图标中,还实现了旋转的动态效果,这对于来说TWaver也很容易,只不过对rotae属性进行了动态改变而已。
 
  1. twaver.Util.registerImage('rotateArrow', {
  2. w: 124,
  3. h: 124,
  4. v: [{
  5. shape: 'vector',
  6. name: 'doubleArrow',
  7. rotate: 360,
  8. animate: [{
  9. attr: 'rotate',
  10. to: 0,
  11. dur: 2000,
  12. reverse: false,
  13. repeat: Number.POSITIVE_INFINITY
  14. }]
  15. }]
  16. });
另外,本例还实现了站点selected和loading的动画效果,方法都是大同小异的。
 
  
 

五、交互功能

 
交互功能是TWaver的精髓,如果只是为了图画的漂亮,那完全可以选择其他作图工具了。
 

1. 拖拽回弹

 
为了判断是不是一张死图,大家往往会下意识地去拖拽站点,看看能不能拖动。既然我们做的不是一张死图,当然要让站点能够拖动。但如果站点会被随便拖走,那么很快整个地铁图就会变得乱七八糟了,所以在松开鼠标后站点必须还能回到原来位置。
 
 
要说这个功能有什么用,我也只能呵呵了。但无聊的时候可以随便玩上几十分钟我也是信的。
 

2. 混合缩放

 
既然是矢量图,当然可以实现无失真缩放。TWaver还实现了综合物理缩放和逻辑缩放优势的混合缩放模式:在放大时使用逻辑缩放,更好展现站点逻辑关系;缩小时使用物理缩放,避免图形失真。当然还有缩小后文字自动隐藏等贴心小功能,就不一一列举了。
 
  1. network.setZoomManager(new twaver.vector.MixedZoomManager(network));
  2. network.setMinZoom(0.2);
  3. network.setMaxZoom(3);
  4. network.setZoomVisibilityThresholds({
  5. label : 0.6,
  6. });

3. 经过路线

 
连续单击同一站点(注意不是双击),可以将经过此站点的所有线路突出显示出来。
 
 

4. 路径规划

 
连续单击不同的两个站点,则自动规划两站之间的合理路径。
 
 

5. 电子地图

 
一张地铁图,即使做的再复杂,功能也是有限的,有时候调用其他软件是扩展功能的一个好办法,比如双击站点后显示站点周围的电子地图。
 
  1. network.addInteractionListener(function(e){
  2. if(mapDiv){
  3. mapDiv.style.display = 'none';
  4. mapDiv = null;
  5. dbclickSta = null;
  6. }
  7. if(e.kind == 'doubleClickElement' && e.element && e.element.getClassName() == 'twaver.Node' && e.element.getId().length == 6){
  8. dbclickSta = e.element;
  9. if(dbclickSta.getClient('coord')){
  10. coord = dbclickSta.getClient('coord');
  11. mapDiv = createMap(coord, e.event);
  12. }else{
  13. dbclickSta.setClient('dbclick', true);
  14. var lineName = json.lines[dbclickSta.getId().substr(0,3)].name;
  15. var stationName = dbclickSta.getName();
  16. var addr = "上海市地铁" + lineName + stationName;
  17. var geocoder = new qq.maps.Geocoder();
  18. geocoder.getLocation(addr);
  19. geocoder.setComplete(function(result) {
  20. coord = result.detail.location;
  21. mapDiv = createMap(coord, e.event);
  22. dbclickSta.setClient('dbclick', false);
  23. });
  24. geocoder.setError(function() {
  25. var coord = {"lat":31.188,"lng":121.425};
  26. mapDiv = createMap(coord, e.event);
  27. });
  28. }
  29. }
  30. });
在电子地图中定位站点,可以通过在站点数据中加入站点的经纬度,也可以通过站点关键字在电子地图中直接查询。
 
 
当然,TWaver能实现不仅仅是例子中展示的这一点点,只有你想不到,没有你做不到。你完全可以赋予地铁图更强大的功能,也可以举一反三做出高铁图、交通图等等类似实例。
 
 
(需要源码的可私信索取)

TWaver初学实战——基于HTML5的交互式地铁图的更多相关文章

  1. 基于 HTML5 + WebGL 的地铁 3D 可视化系统

    前言 工业互联网,物联网,可视化等名词在我们现在信息化的大背景下已经是耳熟能详,日常生活的交通,出行,吃穿等可能都可以用信息化的方式来为我们表达,在传统的可视化监控领域,一般都是基于 Web SCAD ...

  2. 基于HTML5实现3D热图Heatmap应用

    Heatmap热图通过众多数据点信息,汇聚成直观可视化颜色效果,热图已广泛被应用于气象预报.医疗成像.机房温度监控等行业,甚至应用于竞技体育领域的数据分析. http://www.hightopo.c ...

  3. TWaver初学实战——如何在EasyUI中插入TWaver

    TWaver是一款强大的图形界面开发组件,可以很方便地集成到其他开发工具中.今天就用一个小例子来操练如何结合TWaver和EasyUI进行网页开发. 准备工作 俗话说他山之玉可以直接拿来,EasyUI ...

  4. TWaver初学实战——如何在TWaver属性表中添加日历控件?

    在日期输入框中添加日历控件,是一种非常流行和实用的做法.临渊羡鱼不如退而写代码,今天就看看在TWaver中是如何实现的.   资源准备   TWaver的在线使用文档中,就有TWaver Proper ...

  5. TWaver初学实战——如何在EasyUI中插入TWaver(续)

    上次文章虽然简单易懂,但很有些小伙伴不满意:你这TWaver和EasyUI结合,只不过生硬地把TWaver图形插进去了,数据和人家EasyUI没一毛钱关系.嘿嘿,不就是想发生关系嘛,没问题啊!咱就还用 ...

  6. 基于HTML5 的互联网+地铁行业

    前言 近几年,互联网与交通运输的融合,改变了交易模式,影响着运输组织和经营方式,改变了运输主体的市场结构.模糊了运营与非营运的界限,也更好的实现了交通资源的集约共享,同时使得更多依靠外力和企业推动交通 ...

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

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

  8. 快速开发 HTML5 交互式地铁线路图

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

  9. 基于HTML5的RDP访问实战

    基于HTML5的RDP访问实战 1.安装guacamole   2.下载源码   3.安装服务端 安装报错 错误   参考 http://www.remotespark.com/html5.html ...

随机推荐

  1. Android IOS WebRTC 音视频开发总结(二六)-- webrtc调用堆栈

    本文主要是自己之前研究WebRTC代码结构时的一些资料(包括Android,iOS,PC),文章来自博客园RTC.Blacker,转载请说明出处. 1.WEBRTC模块:音频数据采集.发送.接收.播放 ...

  2. 软件工程 speedsnail 冲刺7

    2015-5-11 完成任务:蜗牛移动的一部分: 遇到问题: 问题1 速度,坐标,角度: 速度分级别设置: 坐标记录功能,方便障碍物检测: 暂定初始45度角: 解决1 未解决上述问题 明日任务: 蜗牛 ...

  3. Windows 2008 R2 X64 安装WebsitePanel(WSP虚拟主机管理面板)

                   Windows 2008 R2 X64  安装WebsitePanel(WSP2.0虚拟主机管理面板) 估计很多同学都还不知道WebsitePanel是什么东东吧,Web ...

  4. windwos异地备份Mysql数据库

    @echo off@title Mysqlbackup echo MySQL数据库备份 echo ***********************echo Today %date%echo Time % ...

  5. 第一部分 CLR基础:第1章 CLR的执行模型

    1.1将源代码编译成托管模块

  6. objective-C运算符和表达式

    运算符可以分为以下几种: 算术运算符:+,-,*,/,%,++,—-. 关系运算符:<,>,<=,>=,==,!= 布尔逻辑运算符:!,&&,|| 位运算符:| ...

  7. source insight用于C语言编程的工具脚本

    简单来说,source insight提供的功能功能还不够傻瓜,用起来还不够方便,所以写了此脚本,提高开发效率. 部分source insight提供的功能也包含了进来,主要是因为我不喜欢使用太多的快 ...

  8. ASP.NET的错误处理机制之一(概念)

    对Web应用程序来说,发生不可预知的错误和异常在所难免,我们必须为Web程序提供错误处理机制.当错误发生时,我们必须做好两件事情:一是将错误信息记录日志,发邮件通知网站维护人员,方便技术人员对错误进行 ...

  9. 【原创】解决鼠标经过子元素触发mouseout,mouseover事件的问题

    关键词:父子元素关系  mouseout  mouseover  事件  事件冒泡 初期代码: <!DOCTYPE html> <html> <head> < ...

  10. ASP.NET MVC5学习笔记之Action参数模型绑定基本过程

    当我们在Controller中定义一个Action,通常会定义一个或多个参数,每个参数称为一个模型,ASP.NET MVC框架提供了一种机制称为模型绑定,会尝试自动从请求的信息中实例化每一个模型并赋值 ...