模仿了一个饿了吗页面先上图,看一看better-scroll的效果
- 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;
}
页面布局代码
<div class="goods" v-cloak>
<div class="menu-wrapper" ref="menuwrapper">
<ul>
<!--当currentIndex与index相等的时候,设置高亮-->
<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>
<!--food-list-hook用于dom操作,获取整体容器的高度-->
<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>
js代码
- 首先在data内定义几个变量
goods:[],
listHeight:[],
scrollY:0,
- 在created中请求准备好的json数据
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应改为
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);
//通过循环foodList下的dom结构,将每一个li的高度依次送入数组
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);
}
},
}
- 定义一个计算属性computed,用来计算左侧对应的i值,从而定位到左侧边栏的位置
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];
//最后一个区间没有height2
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) {
// 自己默认派发事件时候(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);
},
- 至此,我们就用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>