目前公司在基于Vue.js做开发,昨天在帮同事看一个提现的功能,页面结构是一个有序的提现方式列表,如下图所示:
Alt text

列表的展示逻辑是:

  • 每种提现方式分为已绑定和未绑定两种展示状态,如果已绑定,则展示右侧的勾选radio;否则展示向右箭头,以引导用户去绑定相对应的提现方式;
  • 提现方式按照支付宝、微信钱包、银行卡优先级由高到低的顺序进行展示;
  • 页面初始化时,已绑定的提现方式排在前面,其次按照提现优先级进行排列;
  • 页面初始化时,如果已绑定支付宝,则排在第一(置顶),并且要求默认勾选中;
  • 用户只能选择一种提现方式,且勾选可以被取消;
  • 用户点击未绑定的提现方式右侧箭头去绑定完成,并返回到该页面后,需要将刚刚绑定好的提现方式置顶,并勾选。

简单介绍一下Vue.js

什么是Vue.js

Vue.js官网描述:Vue.js(读音 /vjuː/, 类似于 view)是一个构建数据驱动的 web 界面的库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

响应的数据绑定

Vue.js 的核心是一个响应的数据绑定系统,它让数据与 DOM 保持同步非常简单。在使用 jQuery 手工操作 DOM 时,我们的代码常常是命令式的、重复的与易错的。Vue.js 拥抱数据驱动的视图概念。通俗地讲,它意味着我们在普通 HTML 模板中使用特殊的语法将 DOM “绑定”到底层数据。一旦创建了绑定,DOM 将与数据保持同步。每当修改了数据,DOM 便相应地更新。这样我们应用中的逻辑就几乎都是直接修改数据了,不必与 DOM 更新搅在一起。这让我们的代码更容易撰写、理解与维护。如图:
Alt text

OK!通过以上内容,我们可以对Vue.js有个整体的了解了,使用Vue.js我们可以通过操作Data数据来驱动DOM,避免了繁琐复杂的操作DOM文档去实现页面的渲染,但在这里我们暂且先不去研究它是如果实现数据的双向绑定的,我们在这里主要来看看如何操作Data数据来更好的完成DOM的渲染,来不及解释!赶紧上车~

开始行动

先来看静态页面

仅仅一个列表而已:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<ul class="mod-label-listwrap mod-com-list addwd-list">
<li class="checked">
<label class="mod-label"><span class="wd-icons-wx"></span></label>
<div class="mod-cont">
<h3 class="addwd-tit">微信钱包 <i class="wd-tag-forthwith">即时</i></h3>
<h4 class="addwd-txt">Hulk Lv</h4>
<span class="optr checkbox-wd checkType"></span>
</div>
</li>
<li>
<label class="mod-label"><span class="wd-icons-zhifubao"></span></label>
<div class="mod-cont">
<h3 class="addwd-tit">支付宝</h3>
<h4 class="addwd-txt">12492389743</h4>
<span class="optr checkbox-wd checkType"></span>
</div>
</li>
<li>
<label class="mod-label"><span class="wd-icons-yinhang"></span></label>
<div class="mod-cont">
<h3 class="addwd-tit">中国工商银行</h3>
<h4 class="addwd-txt">尾号1234</h4>
<span class="optr checkbox-wd checkType"></span>
</div>
</li>
</ul>

即已绑定的视图如下图:
Alt text

未绑定的样子长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<ul class="mod-label-listwrap mod-com-list addwd-list">
<li class="mod-li-arr">
<label class="mod-label"><span class="wd-icons-zhifubao"></span></label>
<div class="mod-cont">
<h3 class="addwd-tit">支付宝</h3>
<h4 class="addwd-txt">未绑定</h4>
<i class="mod-arr"></i>
</div>
</li>
<li class="mod-li-arr">
<label class="mod-label"><span class="wd-icons-wx"></span></label>
<div class="mod-cont">
<h3 class="addwd-tit">微信钱包</h3>
<h4 class="addwd-txt">未绑定</h4>
<i class="mod-arr"></i>
</div>
</li>
<li class="mod-li-arr">
<label class="mod-label"><span class="wd-icons-yinhang"></span></label>
<div class="mod-cont">
<h3 class="addwd-tit">银行卡</h3>
<h4 class="addwd-txt">未绑定</h4>
<i class="mod-arr"></i>
</div>
</li>
</ul>

即未绑定的视图如下图:
Alt text

谋划一个实现思路

第一步,如何用Vue.js精简的渲染列表

用Vue.js来实现的话,就是要合理将data利用起来啦,尽量少操作或者干脆不要手动去操作DOM,否则都算不上一个正儿八经儿的Vue.js应用。既然视图是一列表,那么我们从服务端拿到的数据自然是一个对象数组,就像这样儿:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
applyCashList: [
{
type: 'yihang', //提现方式
checked: false, //是否选中
account: '234234234234' //账户
},
{
type: 'weixin',
checked: false,
account: 'Hulk Lv'
},
{
type: 'zhifubao',
checked: false,
account: ''
}
]

以上这个对象数组是我们能够用得上的资源之一。那么我们开始分析DOM结构,将可以作为数据驱动的部分抽离出来,比如:

  • 什么时候选中<li class="checked">
  • 每项提现方式左侧的图标是哪个<span class="wd-icons-wx">或者<span class="wd-icons-zhifubao">或者<span class="wd-icons-yinhang">
  • 提现方式的名称是什么<h3 class="addwd-tit">支付宝/微信钱包/银行卡</h3>
  • 账户是什么<h4 class="addwd-txt">未绑定/账户</h4>
  • 每项提现方式右侧显示什么<span class="optr checkbox-wd checkType"></span>(勾选按钮)或者<i class="mod-arr"></i>(向右箭头)

经过上面的分析,可以想到,单单拿着服务端返回的对象数组,不能满足我们完全使用数据驱动DOM的目的,我们还需要设计一个数据对象来控制class,像这样儿:

1
2
3
4
5
classObj: {
zhifubao: {span_class: 'zhifubao', caption: '支付宝'},
weixin: {span_class: 'wx', caption: '微信钱包'},
yinhang: {span_class: 'yinhang', caption: '银行卡'}
}

其实,这样做等于将服务端返回的对象数组与classObj建立起一种映射关系,方便我们自由的控制DOM,然后,我们可以把DOM精简成这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<ul class="mod-label-listwrap mod-com-list addwd-list">
<template v-for="item in applyCashList">
<li :class="{checked: item.checked}">
<label class="mod-label"><span :class="'wd-icons-' + classObj[item.type].span_class"></span></label>
<div class="mod-cont">
<h3 class="addwd-tit">{{classObj[item.type].caption}}<i v-if="item.type == 'weixin'" class="wd-tag-forthwith">即时</i></h3>
<h4 class="addwd-txt">{{item.account == ''? '未绑定' : item.account}}</h4>
<span v-if="item.account != ''" class="optr checkbox-wd checkType" @click="checkFn($index)"></span>
<i v-else class="mod-arr"></i>
</div>
</li>
</template>
</ul>
</template>

有没有一种减肥成功的感觉!关于这里的v-for、v-if、v-else以及:class指令,我们在这里就不多说了,到目前为止,提现方式列表视图完全由数据驱动了,不过这只是第一步,根据需求,我们还需要正确的渲染不同情况下的列表排序和勾选状态。

第二步,如何实现列表的展示逻辑

如果忘了逻辑是啥,请奋力的捣到最顶部再瞅瞅~
首先,这三种提现方式是有优先级的;其次,要分两种情况对待了,一种是页面初始化的时候:看谁绑了,绑了的优先,另外,没绑的也不能乱排,是有优先级的呦~,还有就是如果支付宝被绑了,除了要置顶,还要默认被勾选!;另一种是用户点击未绑定的提现方式右侧箭头去绑定完成,并返回到该页面后,需要将刚刚绑定好的提现方式置顶,并勾选(这时候,谁刚刚被绑,谁老大了!)。
既然,这仨有优先级,不妨给我们的可用资源加上优先级属性,像这样儿:

1
2
3
4
5
classObj: {
zhifubao: {span_class: 'zhifubao', caption: '支付宝', level: 1},
weixin: {span_class: 'wx', caption: '微信钱包', level: 2},
yinhang: {span_class: 'yinhang', caption: '银行卡', level: 3}
}

这样儿有什么用呢,咱接着看。根据展示逻辑,我们可以写出如下的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
init (){
let self = this;
self.sortByAccount();
},
sortByAccount() {
let self = this;
//1、找出已经绑定的提现方式
let hasAccountArr = self.applyCashList.filter((item) => {
return !!item.account && item.account != '';
});
//2、根据不同情况来做不同的排序处理
if (hasAccountArr.length == 0 || hasAccountArr.length == 3) {
//直接按照优先级排序
self.applyCashList = self.applyCashList.sort((item1, item2) => {
return self.classObj[item1.type].level - self.classObj[item2.type].level;
});
} else if (hasAccountArr.length == 1) {
let hasNoAccountArr = self.applyCashList.filter((item) => {
return !item.account ||item.account == '';
});
//对未绑定的那俩按照优先级进行排序
let ordered = hasNoAccountArr.sort((item1, item2) => {
return self.classObj[item1.type].level - self.classObj[item2.type].level;
});
//合并已绑定和未绑定
self.applyCashList = hasAccountArr.concat(ordered);
}else if (hasAccountArr.length == 2) {
let lastItem = self.applyCashList.filter((item) => {
return !item.account ||item.account == '';
});
//对已绑定的那俩按照优先级进行排序
let ordered = hasAccountArr.sort((item1, item2) => {
return self.classObj[item1.type].level - self.classObj[item2.type].level;
});
//合并已绑定和未绑定
self.applyCashList = ordered.concat(lastItem);
}
//3、如果首项已绑定,且是支付宝,则勾选
if (self.applyCashList[0].account != '' && self.applyCashList[0].type == 'zhifubao') {
self.applyCashList[0].checked = true;
self.applyCashList[1].checked = false;//当然只有一个可以被选中喽!我只是陪衬
self.applyCashList[2].checked = false;//当然只有一个可以被选中喽!我只是陪衬
}
},
//勾选按钮点击事件
checkFn (index) {
let self = this;
if (!self.applyCashList[index].checked) {
let currentCheckedIndex = -1;
let temp = self.applyCashList.filter((item, i, arr) => {
if (item.checked) {
currentCheckedIndex = i;
return true;
}
});
if (currentCheckedIndex > -1) {
self.applyCashList[currentCheckedIndex].checked = false;
}
}
self.applyCashList[index].checked = !self.applyCashList[index].checked;
}

这里需要说明的是,我们将这仨提现方式手起刀落分成两部分,一部分是已绑定的方式,另一部分是未绑定的方式,这样做可以对需要被排序的那部分按照优先级来排序,也就是如果只有它自己,还排个毛线,两个、三个就不一样了,当然要比一比了。注释里面已经写的很清楚了,如果有好的方法,当然希望可以拿来说说。
到这里,我们已经实现了页面初始化时,列表的排列和默认勾选逻辑,那用户刚刚绑定成功返回到该页面时的逻辑呢?这里我们先草率的抛开绑定过程,以及页面跳转回来的过程,只将关注点锁定到当前这个列表页面,也就是说当我们在对列表排序之前,先要判断入口来源,才能确定要请谁当老大(即要哪种提现方式置顶并默认勾选),所以就有了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
init (){
let self = this;
let from = 'init';//入口来源 init表示页面初始化,bind表示刚刚绑定完返回
let type = 'yinhang';//刚刚绑定完的提现方式。我是老大!
if (from == 'bind') {
self.classObj[type].level = -1;//将优先级调到最高
}
self.sortByAccount(from);
},
sortByAccount(from) {
let self = this;
//1、找出已经绑定的提现方式
let hasAccountArr = self.applyCashList.filter((item) => {
return !!item.account && item.account != '';
});
//2、根据不同情况来做不同的排序处理
if (hasAccountArr.length == 0 || hasAccountArr.length == 3) {
//直接按照优先级排序
self.applyCashList = self.applyCashList.sort((item1, item2) => {
return self.classObj[item1.type].level - self.classObj[item2.type].level;
});
} else if (hasAccountArr.length == 1) {
let hasNoAccountArr = self.applyCashList.filter((item) => {
return !item.account ||item.account == '';
});
//对未绑定的那俩按照优先级进行排序
let ordered = hasNoAccountArr.sort((item1, item2) => {
return self.classObj[item1.type].level - self.classObj[item2.type].level;
});
//合并已绑定和未绑定
self.applyCashList = hasAccountArr.concat(ordered);
}else if (hasAccountArr.length == 2) {
let lastItem = self.applyCashList.filter((item) => {
return !item.account ||item.account == '';
});
//对已绑定的那俩按照优先级进行排序
let ordered = hasAccountArr.sort((item1, item2) => {
return self.classObj[item1.type].level - self.classObj[item2.type].level;
});
//合并已绑定和未绑定
self.applyCashList = ordered.concat(lastItem);
}
//3、如果首项已绑定,且是支付宝或者是刚刚绑完,则勾选
if (self.applyCashList[0].account != '' && (self.applyCashList[0].type == 'zhifubao' || from == 'bind')) {
self.applyCashList[0].checked = true;
self.applyCashList[1].checked = false;//当然只有一个可以被选中喽!我只是陪衬
self.applyCashList[2].checked = false;//当然只有一个可以被选中喽!我只是陪衬
}
}

怎么样,是不是很好玩?!
以上简述排序这段,只是关注如何实现列表的展示逻辑,重在回答问题的思路,并没有什么特别高级的方法方式,不过通过这段逻辑,我们可以看到操作数据比操作DOM要简单的多,且代码简洁的多,少了各种DOM属性的定义,少了各种选择器的套用,可以安静的玩玩数据,其他的先不操心了。