前言
相信所有人对移动端商城app里面的分类页左右两栏联动的菜单都不陌生,几乎所有的商城分类页都是这样,这两天接触了一下uniapp,在做一个商城项目,刚好有这个需求,接下来我们用uniapp实现一下。
先看一张最终效果图:
「需求:」
- 「左右两个菜单要能独立滚动;」
- 「点击左边菜单,右边滑动到相应区块;」
- 「滑动右边菜单,左边相应菜单项actived;」
- 「当右边滚动到左边相应分类超出视野范围后,左边菜单也要跟着上/下滚动。」
接下来就根据需求逐步实现。
1、利用uniapp的scroll-view组件实现左右两个可独立滚动的菜单
很简单,就左右两个scroll-view,左边占20%,右边80%,这里要注意一下左右两个scroll-view外层只能包一个父级view,不然他们俩是同步滚动的。
<view class="d-flex h-100" >
<scroll-view class="left-scroll flex-1 border-right" scroll-y>
<view v-for="i in 50" :key="i">{{i}}</view>
</scroll-view>
<scroll-view class="right-scroll flex-3" scroll-y>
<view v-for="i in 50" :key="i">{{i}}</view>
</scroll-view>
</view>
完成后给左边的导航菜单项一个加一个active时的样式,并做相应的过渡优化,不然点击会很生硬。然后模拟一下数据。
<script>
export default {
data(){
return {
activeIndex:0,// 左边当前菜单项的索引
cate: [],//左边导航菜单
list:[]// 右边菜单
}
},
created(){
this.getData()
},
methods:{
getData() {
for (let i = 0; i < 20; i++) {
this.cate.push({
name: '分类' + i
})
}
},
// 点击左边导航栏
changeCate(index) {
this.activeIndex = index
},
}
}
</script>
接下来就是写右边菜单的布局并模拟数据。这里要注意一下右边的数据结构的设计(需要和后端对接),首先有很多个分类,每个分类有一个类别名称和多个商品(数组),每个商品又是一个对象(含有多个属性),所以循环时用双for循环进行遍历,具体看代码。 「html」
<scroll-view scroll-y class="right-scroll flex-3">
<view class="row right-scroll-item" v-for="(item,index) in list" :key="index">
<view class="w-100 text-center text-muted font-md">{{item.name}}</view>
<view class="span24-8 text-center py-2" v-for="(item2 ,index2) in item.list" :key="index2">
<image :src="item2.src" mode="" style="width: 120upx;height: 120upx;"></image>
<text class="d-block text-light-muted font">{{item2.name}}</text>
</view>
</view>
</scroll-view>
「模拟data数据」
getData() {
for (let i = 0; i < 20; i++) {
this.cate.push({
name: '分类' + i
})
this.list.push({
name: `—— 产品分类${i} ——`,
list: []
})
}
for (let i = 0; i < this.list.length; i++) {
for (let j = 0; j < 24; j++) {
this.list[i].list.push({
src: '/static/images/demo/demomi.png',
name: `分类${i}-商品${j}`
})
}
}
},
2、点击左边菜单,右边滑动到相应区块
下文中的top值指的是当前dom距离屏幕顶部的距离,也是在触发事件时该dom向上滚动的距离
这一part的思路是拿到左右两边每个dom距离屏幕顶端的距离(top),放入leftDomsTop[]和rightDomsTop[]中备用,当我们点击左边导航菜单项时,我们改变右边相同索引dom在竖直方向 向上的滚动距离(其滚动距离就是该索引对应dom距离顶部的距离),可能有点绕,多读两遍就好了。uniapp的scroll-view组件有个scroll-top属性可以很方便的改变top值。
那如何获取所有节点的top值呢,uniapp官网给我们提供了节点信息的API,我们将官网实例代码拷贝过来,改造一下:(注意:获取节点信息需要在dom挂在完成后,所以得在mounted
钩子函数中写,uniapp相应的钩子函数是onReady
)
onReady() {
const query = uni.createSelectorQuery().in(this);
query.selectAll('.left-scroll-item').boundingClientRect(data => {
this.leftDomsTop = data.map(v => v.top)
console.log('左边top:',this.leftDomsTop)
}).exec();
query.selectAll('.right-scroll-item').boundingClientRect(data => {
this.rightDomsTop = data.map(v => v.top)
console.log('右边top:',this.rightDomsTop)
}).exec();
},
可以看到通过上面的API我们可以很方便的获取左右两边每个dom节点距顶部的距离(top),拿到top值后存入先前定义的两个数组,接下来点击左边菜单时,我们改变右边对应菜单的top值就可以实现左右联动了。为了良好体验,可以加上滚动动画(在scroll-view上增加scroll-with-animation属性即可)。
// html
<scroll-view scroll-y class="right-scroll flex-3" :scroll-top="rightScrollTop" scroll-with-animation></scroll-view>
// 点击左边导航栏
changeCate(index) {
this.activeIndex = index
// 右边scroll-view滚动到对应区块
this.rightScrollTop = this.rightDomsTop[index]
// console.log(this.rightScrollTop)
},
看一下效果:
这里要注意一下,改变右边菜单top值有可能失败,如果遇到请参考官网给出的两种解决方案 传送门
3、滑动右边菜单,左边相应菜单项actived
这一part我们通过监听右边菜单的滚动事件(@scroll="onRightScroll"),可以拿到右边dom滚动的top值数组,拿到后去匹配左边相同索引的dom,将该索引值赋值给activeIndex即可。
// 监听右边滚动事件
async onRightScroll(e) {
// 匹配当前scrollTop所处的索引
this.rightDomsTop.forEach((v, k) => {
if (v < e.detail.scrollTop + 3) {
this.activeIndex = k
return false
}
})
}
这时我们差目标越来越近了,先看看现在的效果:
4、当右边滚动到左边相应分类超出视野范围后,左边菜单也要跟着上/下滚动
先简单说一下原理,我们可以监听左边菜单项activeIndex变化的时候,当左边状态为active的dom的leftDomsTop[activeIndex] + 该dom本身高度(cateItemHeight)> 左边scroll-view的高度(H)+ 其本身的top值(ST)时,我们让该dom节点向上滚动其本身高度距离(cateItemHeight),向上滚动也是同样的道理,文字很绕?结合图和公式理解一下:
接下来我们要获取以上所述需要的节点信息:
- 首先我们通过uniapp的fields API获取左边每个dom的尺寸及布局信息,这样就可以拿到每个dom的高度(cateItemHeight)和top值(leftScrollTop)。
onReady() {
const query = uni.createSelectorQuery().in(this);
query.selectAll('.left-scroll-item').fields({
size:true,// 尺寸
rect:true // 布局信息
},data => {
this.cateItemHeight = data.map(v => {
this.cateItemHeight = v.height
return v.top
})
}).exec();
},
- 接着获取左边scroll-view的节点信息,并监听activeIndex的变化,然后按照上面所述判断即可
wacth:{
activeIndex(newValue,oldValue){
// 获取左边scroll-view的高度,top值
const query = uni.createSelectorQuery().in(this);
query.selectAll('#leftScroll').fields({
size:true,
scrollOffset:true,// 滚动状态
},data =>{
let H = data.height
let ST = data.scrollTop
// 下边
if ((this.leftDomsTop[newValue] + this.cateItemHeight) > (H + ST)) {
return this.leftScrollTop = this.leftDomsTop[newValue] + this.cateItemHeight - H
}
// 上边
if (ST > this.cateItemHeight) {
this.leftScrollTop = this.leftDomsTop[newValue]
}
})
}
}
到此整个需求就完成了,可以返回去看看篇头的效果图了。接下来我们优化一下代码,因为我们在watch和onReady中都有获取节点信息,写了很多重复的代码,我们可以将获取节点信息封装成一个方法,用到的地方调用即可。
</p>
// 获取节点信息
getElInfo(obj = {}) {
return new Promise((res, rej) => {
let option = {
size: obj.size ? true : false,
rect: obj.rect ? true : false,
scrollOffset: obj.scrollOffset ? true : false,
}
const query = uni.createSelectorQuery().in(this);
let q = obj.all ? query.selectAll(`.${obj.all}-scroll-item`) : query.select('#leftScroll')
q.fields(option, data => {
res(data)
}).exec();
})
},
完整源码:传送门 (位于pages/class.vue 页面)
💕看完三件事:
- 点赞 | 你可以
点击——>收藏——>退出
一气呵成,但别忘了点赞🤭 - 关注 | 点个关注,下次不迷路😘
- 也可以到GitHub拿我所有文章源文件🤗