如果用overflow:auto或scroll属性,这样的页面就会出现两个滚动条,用户体验会有折扣,所以给介绍一款插件better-scroll,附上github资源链接。这款插件是基于iscroll插件做的重新封装,改善了一些bug,增加了一些拓展功能(插件作者并不是本人,望周知),有兴趣的小伙伴们可以自行下载体验体验。那么今天就用bs插件配合vue框架对这个页面重构一下,主要实现以下几点功能。- 1、左右侧页面滑动,并且不显示滚动条。 - 2、根据左侧的商品类别对应到右侧相应的选择区间,并且高亮标题。 - 3、根据右侧用户滑动的区间,能够对应到左侧的商品类别,并且高亮选择标题。 ps:除了用到bs插件的一些基础功能以外,还有一些vue的基础知识。前期准备
- 我引入了reset.css对页面的样式做一个算是初始化吧,然后引入js脚本有vue.js,vue-resource.js,bscroll.js,可以进入vue官网下载。
页面布局
页面布局和样式就不浪费时间了,直接上代码了
样式代码
[v-cloak] { display: none; } .goods { position: absolute; width: 100%; top: 174px; bottom: 46px; display: flex; overflow: hidden; } .goods .menu-wrapper { flex: 0 0 80px; width: 80px; background: #f3f5f7; } .goods .menu-wrapper .current { position: relative; z-index: 10; margin-top: -1px; background: #FFFFFF; font-weight: 700; font-size: 14px; } .goods .menu-wrapper .menu-item { display: table; height: 54px; width: 80px; line-height: 14px; padding: 0 12px; border-bottom: 1px solid rgba(7, 17, 27, .1); box-sizing: border-box; } .goods .menu-wrapper .menu-item .icon { display: inline-block; width: 12px; height: 12px; margin-right: 2px; -webkit-background-size: 12px 12px; background-size: 12px 12px; background-repeat: no-repeat; vertical-align: top; } .goods .menu-wrapper .menu-item .text { display: table-cell; width: 56px; vertical-align: middle; font-size: 12px; text-align: center; } .goods .menu-wrapper .menu-item .decrease { background-image: url(img/decrease_2@2x.png); } .goods .menu-wrapper .menu-item .discount { background-image: url(img/decrease_2@2x.png); } .goods .menu-wrapper .menu-item .guarantee { background-image: url(img/decrease_2@2x.png); } .goods .menu-wrapper .menu-item .invoice { background-image: url(img/decrease_2@2x.png); } .goods .menu-wrapper .menu-item .special { background-image: url(img/decrease_2@2x.png); } .goods .foods-wrapper { flex: 1; } .goods .foods-wrapper .title { padding-left: 14px; height: 26px; line-height: 26px; border-left: 2px solid #d9dde1; font-size: 12px; color: rgb(147, 153, 159); background: #F3F5F7; } .goods .foods-wrapper .current { color: #42B983; font-size: 14px; transition: all .5s; line-height: 27px; } .goods .foods-wrapper .food-item { display: flex; margin: 18px 0 18px 0; border-bottom: 1px solid rgba(7, 17, 27, .1); padding-bottom: 18px; } .goods .foods-wrapper .food-item:last-child { border-bottom: 0px solid rgba(7, 17, 27, .1); margin-bottom: 0; } .goods .foods-wrapper .food-item .icon { flex: 0 0 57px; margin-right: 10px; margin-left: 10px; } .goods .foods-wrapper .food-item .content { position: relative; flex: 1; } .goods .foods-wrapper .food-item .content .name { margin: 2px 0 8px 0; height: 14px; line-height: 14px; font-size: 14px; color: rgb(7, 17, 27); } .goods .foods-wrapper .food-item .content .desc { margin-bottom: 8px; line-height: 10px; font-size: 10px; color: rgb(147, 153, 159); } .goods .foods-wrapper .food-item .content .extra { font-size: 10px; color: rgb(147, 153, 159); line-height: 10px; } .goods .foods-wrapper .food-item .content .extra .count { margin-right: 12px; } .goods .foods-wrapper .food-item .content .price { font-weight: 700; line-height: 24px; } .goods .foods-wrapper .food-item .content .price .now { margin-right: 8px; font-size: 14px; color: rgb(240, 20, 20); } .goods .foods-wrapper .food-item .content .price .old { text-decoration: line-through; font-size: 10px; color: rgb(147, 153, 159); } .goods .foods-wrapper .food-item .content .cartcontrol-wrapper { position: absolute; right: 6px; bottom: 12px; }
- 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
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
页面布局代码
<div class="goods" v-cloak> <div class="menu-wrapper" ref="menuwrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex === index}" @click="selectMenu(index,$event)" v-cloak> <span class="text"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]" v-cloak></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodwrapper"> <ul> <li v-for="(item,index) in goods" class="food-list food-list-hook" v-cloak> <h2 class="title" :class="{'current':currentIndex === index}">{{item.name}}</h2> <ul> <li @click="selectfood(food,$event)" v-for="food in item.foods" class="food-item"> <div class="icon"> <img :src="food.icon" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span v-show="food.oldPrice" class="old">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div>
- 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
js代码
goods:[], listHeight:[], scrollY:0,
this.$http.get('./data.json').then((res) => { if(res.status === ERR_OK) { res = res.body.goods; this.goods = res; } })
- 首先我们已经引入了bs插件,我们先让左右两侧的被隐藏的部分滚动起来,在methods方法里面定义一个_initScroll的函数,主要用来对左右两侧dom结构进行初始化。用better-scroll的方法初始化需要滚动的dom结构,vue为我们提供了一个方法可以便利的获取到dom结构,我们在需要获取dom结构的父容器内添加
ref="foodwrapper"
,然后在函数内用this.$refs.menuwrapper
获取到dom。 - 然后在ajax内执行_initScroll() 函数,这个时候需要注意两点,第一使用bs插件的时候子容器的高度一定要大于父容器的高度,才会产生滚动效果。第二,我们要等dom结构完全加载结束在调用_initScroll()方法才会生效,vue的作者也为我们提供了方法,来判断dom结构是否完全加载
this.$nextTick(() => {})
,click: true
属性用来设置可以进行点击事件。
_initScroll() { this.meunScroll = new BScroll(this.$refs.menuwrapper, { click: true }); this.foodScroll = new BScroll(this.$refs.foodwrapper, { click: true }); }
created() { this.classMap = ['decrease', 'discount', 'guarantee', 'invoice', 'special']; this.$http.get('./data.json').then((res) => { if(res.status === ERR_OK) { res = res.body.goods; this.goods = res; //dom结构加载结束 this.$nextTick(() => { this._initScroll(); }) } }); },
- 下面实现左右联动并且实现文本的高亮,左右联动的基本原理其实我们计算出右侧实时变化的y值,落到哪一个区间,我们就显示那一个区间。首先我们要计算整体区间的一个高度,然后分别计算第一个区间的高度,第二个区间的高度,以此类推。然后将区间数存入一个定义好的数组。当我们在滚动的时候实时拿到y轴的高度,然后对比在哪一个区间,这样我们就会得到一个区间的索引值去对应左侧的菜品类别,最后我们用一个vue的class去绑定高亮文本。
- 定义一个方法在_initScroll下面,作为计算高度的方法叫做_calculateHeight () ,在定义一个listHeight:[]数组,存放获取的高度。我们在定义一个food-list-hook类,用来被js选择。不要忘记在created内调用函数。
_calculateHeight () { let foodList = this.$refs.foodwrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for(let i=0; i<foodList.length; i++){ let item = foodList[i] height += item.clientHeight this.listHeight.push(height); } },
- 我们获取到区间高度数组后,我们要实时获取到右侧的y值,和左侧的索引值做一个对比,定义一个scrollY变量用来存放实时获取的y值。bs插件为我们提供了一个实时获取y值的方法,我们在初始化
this.foodScroll
的时候加一个·属性probeType: 3
,其作用就是实时获取y值,相当于探针的作用。 - 我们在添加一个方法
this.foodScroll.on('scroll',(pos) => {})
,作用是实时滚动的时候把获取到的位置给暴露出来。代码如下。
methods: { _initScroll() { this.meunScroll = new BScroll(this.$refs.menuwrapper, { click: true }); this.foodScroll = new BScroll(this.$refs.foodwrapper, { click: true, //探针作用,实时监测滚动位置 probeType: 3 }); //设置监听滚动位置 this.foodScroll.on('scroll', (pos) => { //scrollY接收变量 this.scrollY = Math.abs(Math.round(pos.y)); }) }, _calculateHeight() { let foodList = this.$refs.foodwrapper.getElementsByClassName('food-list-hook'); let height = 0; //把第一个高度送入数组 this.listHeight.push(height); //通过循环foodList下的dom结构,将每一个li的高度依次送入数组 for(let i = 0; i < foodList.length; i++) { let item = foodList[i] height += item.clientHeight this.listHeight.push(height); } }, }
- 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
- 定义一个计算属性computed,用来计算左侧对应的i值,从而定位到左侧边栏的位置
computed:{ currentIndex () { for(let i=0; i<this.listHeight.length; i++){ let height1 = this.listHeight[i]; let height2 = this.listHeight[i+1]; if(!height2 || (this.scrollY >= height1 && this.scrollY < height2)){ return i; } } return 0; },
- 获取到i后,在menu-item绑定一个class
:class="{'current':currentIndex === index}"
,当currentIndex和menu-item对应的index相等时,设置current的样式。这样就可以左右联动了。 - 最后实现左侧点击的功能。在左侧的li下绑定一个selectMenu的点击事件,并传入索引值,这样我们就可以知道点击的是哪一个li
selectMenu (index,event) { if (!event._constructed){ return; } let foodList = this.$refs.foodwrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodScroll.scrollToElement(el, 300); },
- 至此,我们就用bs插件完成了这个左右页面联动的效果,代码会上传到github,有兴趣的可以下载下来看一看,大神务喷!
- 完整的js代码
<script type="text/javascript"> var ERR_OK = 200; new Vue({ el: '.goods', data() { return { msg: 'goods', goods: [], listHeight: [], scrollY: 0, } }, created() { this.classMap = ['decrease', 'discount', 'guarantee', 'invoice', 'special']; this.$http.get('./data.json').then((res) => { if(res.status === ERR_OK) { res = res.body.goods; this.goods = res; //dom结构加载结束 this.$nextTick(() => { this._initScroll(); //计算高度 this._calculateHeight(); }) } }); }, computed: { currentIndex() { for(let i = 0; i < this.listHeight.length; i++) { //判断当currentIndex在height1和height2之间的时候显示 let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; // console.log('height1:'+height1+','+'height2:'+height2) //最后一个区间没有height2 if(!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; } } return 0; } }, methods: { selectMenu(index, event) { // 自己默认派发事件时候(BScroll),_constructed被置为true,但是浏览器原生并没有这个属性 if(!event._constructed) { return; } //运用BScroll接口,滚动到相应位置 let foodList = this.$refs.foodwrapper.getElementsByClassName('food-list-hook'); //获取对应元素的列表 let el = foodList[index]; this.foodScroll.scrollToElement(el, 300); }, _initScroll() { this.meunScroll = new BScroll(this.$refs.menuwrapper, { click: true }); this.foodScroll = new BScroll(this.$refs.foodwrapper, { click: true, //探针作用,实时监测滚动位置 probeType: 3 }); //设置监听滚动位置 this.foodScroll.on('scroll', (pos) => { //scrollY接收变量 this.scrollY = Math.abs(Math.round(pos.y)); }) }, _calculateHeight() { let foodList = this.$refs.foodwrapper.getElementsByClassName('food-list-hook'); let height = 0; //把第一个高度送入数组 this.listHeight.push(height); //通过循环foodList下的dom结构,将每一个li的高度依次送入数组 for(let i = 0; i < foodList.length; i++) { let item = foodList[i] height += item.clientHeight this.listHeight.push(height); } }, } }) </script>
- 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
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
完整项目效果演示