「敲黑板」Uniapp 实现商城类app左右联动分类菜单

8,692 阅读5分钟

前言

相信所有人对移动端商城app里面的分类页左右两栏联动的菜单都不陌生,几乎所有的商城分类页都是这样,这两天接触了一下uniapp,在做一个商城项目,刚好有这个需求,接下来我们用uniapp实现一下。

先看一张最终效果图:

需求:

  1. 左右两个菜单要能独立滚动;
  2. 点击左边菜单,右边滑动到相应区块;
  3. 滑动右边菜单,左边相应菜单项actived;
  4. 当右边滚动到左边相应分类超出视野范围后,左边菜单也要跟着上/下滚动。

接下来就根据需求逐步实现。

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),向上滚动也是同样的道理,文字很绕?结合图和公式理解一下:

接下来我们要获取以上所述需要的节点信息:

  1. 首先我们通过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();
   
  },
  1. 接着获取左边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 页面)

💕看完三件事:

  1. 点赞 | 你可以点击——>收藏——>退出一气呵成,但别忘了点赞🤭
  2. 关注 | 点个关注,下次不迷路😘
  3. 也可以到GitHub拿我所有文章源文件🤗