Vue中将better-scroll封装成一个组件,并实现完美吸顶效果

先来看看实现的效果:

在这里插入图片描述
上面的效果就是利用了better-scroll实现的滑动,可见better-scroll实现的弹性效果还是很不错的,但是better-scroll还是有很多bug的,因此对初次使用better-scroll的人不够友好。以往我们做吸顶效果都是直接使用 position:sticky 实现粘性布局,但该属性在该库就失效了,因此吸顶也成了一件难题,还好有人想到了好办法解决了该问题,后面再说。

直接看封装好的better-scroll:

<template>
  <div ref="wrapper" >
    <slot></slot>
  </div>
</template>

<script>
import BScroll from "better-scroll"
export default {
    
    
  props: {
    
    
    data: {
    
    
      type: Array,
      default: () => {
    
    
        return []
      }
    }
  },
  data () {
    
    
    return {
    
    
      scroll: {
    
    }
    }
  },
  mounted () {
    
    
    this.$nextTick(() => {
    
    
      this.initiate()
    })
  },
  methods: {
    
    
    initiate: function () {
    
    
      if (!this.$refs.wrapper) return
      this.scroll = new BScroll(this.$refs.wrapper, {
    
    
        probeType: 3,
        click: true,
        pullUpload: true,
        bounce: true
      })
      this.scroll.on("scroll", position => {
    
    
        this.$emit('currentPosition', position)
      })

    },
    refresh () {
    
    
      this.scroll && this.scroll.refresh && this.scroll.refresh();
    }
  },
  watch: {
    
    
    data () {
    
    
      // 这里监听的是props传入的data,而不是钩子中的data
      // 在该组件并没有使用传进来的data,但很有必要传进来,因为当外部组件通过异步获取数据前,		better-scroll就已经初始化好了,但此时初始化的可滚动的高度是还没有拿到服务器数据就初始化好的
      //那么当数据加载好后,就需要让better-scroll调用refresh()函数刷新一下可滚动的高度,这一步很重要,否则无法滚动。
      setTimeout(this.refresh, 20)
    }
  }
}
</script>

<style scoped>
</style>

主组件代码:不必看,下面分开展示重要代码

<!--  -->
<template>
  <div id="takeOut">
    <position></position>
    <search-box class="boxAttach"
                v-show="showBox"></search-box>
    <better-scroll class='wrapper'
                   @currentPosition="currentPosition"
                   :data="shopList">
      <div class="content">
        <search-box></search-box>
        <food-sort :sorts="foodSorts"></food-sort>
        <swiper>
          <swiper-item v-for="(banner,index) in banners"
                       :key="index">
            <img class="swiperImg"
                 :src="banner.imgUrl">
          </swiper-item>
        </swiper>
        <div class="detailFood">
          <div class="nearRecommend">
            <h3>附近推荐</h3>
            <ul>
              <li v-for="(item ,index) in nearRecommend"
                  :key="index"
                  @click="choseType(index)"
                  :class="chosedType.indexOf(index)!==-1 ? 'hasChose':''"> {
    
    {
    
    item}}
              </li>
            </ul>
            <detail-hotel v-for="(shop,index) in shopList"
                          :key="index"
                          :index="index"
                          :shop="shop"
                          v-show="isNotLike.indexOf(index)===-1 ? true:false"
                          @click.native="skipToDetail(index)"
                          @addNotLike="addNotLike">
            </detail-hotel>
          </div>
        </div>
      </div>
    </better-scroll>
  </div>
</template>

<script>
import Position from "components/Position";
import SearchBox from "components/SearchBox";
import BetterScroll from "components/BetterScroll"
import FoodSort from "components/FoodSort"
import {
    
     Swiper, SwiperItem } from 'components/swiper'
import DetailHotel from "components/detailHotel"
import {
    
     getFoodSort, getSwiper, getShops } from 'network/takeout'
import {
    
     mapMutations } from 'vuex'
export default {
    
    
  name: "TakeOut",
  created () {
    
    
    请求分类数据
    this.getFoodSortData()
    请求轮播图数据
    this.getSwiperData()
    请求商家列表
    this.getShopList()
  },
  data () {
    
    
    return {
    
    
      position: 0,
      banners: [],
      foodSorts: [],
      shopList: [],
      shopListTemp: this.$store.state.shopList,
      nearRecommend: ['津贴优惠', '满减优惠', '下单返红包', '进店领红包'],
      isNotLike: [],
      chosedType: [],
      showBox: false
    }
  },
  computed: {
    
    

  },
  components: {
    
    
    Position,
    SearchBox,
    BetterScroll,
    Swiper,
    SwiperItem,
    FoodSort,
    DetailHotel
  },
  methods: {
    
    
    ...mapMutations(['saveShopList']),
    addNotLike (index) {
    
    
      this.isNotLike.push(index)
    },
    //监听betterScroll滚动传过来的position
    currentPosition (position) {
    
    
      this.position = position;
      this.showBox = this.position.y < -40
    },
    getFoodSortData () {
    
    
      getFoodSort().then(result => {
    
    
        this.foodSorts = result.data.sortData
      }).catch(_ => {
    
    
        this.showMessage({
    
     type: 'error', message: _ + '食品分类获取失败' })
      })
    },
    getSwiperData () {
    
    
      getSwiper().then(result => {
    
    
        this.banners = result.data.banners
      }).catch(_ => {
    
    
        this.showMessage({
    
     type: 'error', message: _ + '轮播图获取失败' })
      })
    },
    getShopList () {
    
    
      getShops().then(res => {
    
    
        this.shopList = res.data.shopList
        this.saveShopList(this.shopList)//将数据通过mutations提交给vuex管理,因为我们排序时会改变this.shopList,因此每次排序需要从vuex获取原数据
      }).catch(_ => {
    
    
        this.showMessage({
    
     type: 'error', message: _ + '商家列表获取失败' })
      })
    },

    //选择附近推荐的类型
    choseType (index) {
    
    
      let isExit = this.chosedType.indexOf(index);//如果有则返回它所在的下标,没有则返回-1
      if (isExit !== -1) {
    
    
        //当存在数组时,说明上一次点击过,已经高亮,那么这次点击就是取消高亮,从里面删除
        this.chosedType.splice(isExit, 1);//将该下标元素删除
      } else {
    
    
        this.chosedType.push(index)
      }
      //对商家进行过滤,将满足条件的商家展示出来
      //当chosedType为空时,说明用户没有进行点击筛选,则不用进入循环,浪费性能
      if (this.chosedType.length !== 0) {
    
    
        this.shopList = []//清空,再往里面添加满足条件的商家
        for (let shop of this.shopListTemp) {
    
    
          let isSatisfy = this.chosedType.every((type) => {
    
    
            return shop.shopType.indexOf(type) !== -1
          })
          isSatisfy ? this.shopList.push(shop) : ''
        }
      } else {
    
    
        //为什么需要这一步?当用户点击了筛选条件,然后又移除筛选条件为空时,如果没有这一步,那么用户此时看到的还是筛选的数据,因为没有回复原数据
        this.shopList = this.shopListTemp
      }
    },
    //点击商家进入详情,根据
    skipToDetail (id) {
    
    
      this.$router.push({
    
    
        path: '/detail',
        query: {
    
    
          id: id + 1
        }
      })

    },
    showMessage (option) {
    
    
      let {
    
     type, message } = option
      this.$message({
    
    
        type,
        message,
        duration: 1500,
        center: true,
        offset: 50
      })
    }
  }
}

</script>

<style scoped>
#takeOut {
    
    
  position:relative;
  height: 100vh;
}
.wrapper {
    
    
  position: absolute;
  top: 40px;
  bottom: 49px;
  left: 0px;
  right: 0px;
  background-color: white;
}
.boxAttach {
    
    
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
}
.swiperImg {
    
    
  height: 80px;
}
.detailFood {
    
    
  background-color: white;
  border-top-left-radius: 15px;
  border-top-right-radius: 15px;
  margin-top: -5px;
}
.nearRecommend h3 {
    
    
  font-weight: 500;
  font-size: 16px;
  padding: 15px 0 10px 10px;
}
.nearRecommend ul {
    
    
  display: flex;
  height: 30px;
  line-height: 30px;
  font-size: 14px;
  justify-content: space-evenly;
}
.nearRecommend ul li {
    
    
  background-color: rgb(233, 232, 232);
  padding: 0 10px;
  border-radius: 5px;
}
.nearRecommend ul .hasChose {
    
    
  background-color: rgb(217, 217, 243);
}
</style>

上面展示代码是文章开头展示效果图的主组件代码,这里引用了很多common组件和Vuex,是不能直接复制使用的,我只是用来辅助说明封装的better-scroll如何使用。关键看<better-scroll>标签的元素:

<better-scroll class='wrapper'
               @currentPosition="currentPosition"
               :data="shopList">
      <div class="content">
     	........//很多内容撑起content的高度
      </div>
</better-scroll>

注意到类名:wrapper和content。这两个类是实现滚动的关键,看官网的一张图:

在这里插入图片描述
官网清清楚楚的说明了这两个容器的用法:

  1. wrapper作为父元素,它必须有固定的高度,即我们希望看到页面可滚动展示内容的高度。看看我这个例子向展示的高度:
    在这里插入图片描述
    上面圈框框的位置就是用来滑动展示内容的高度,我在类中使用的是绝对定位给父元素固定的高度
.wrapper {
    
    
  position: absolute;
  top: 40px;
  bottom: 49px;
  left: 0px;
  right: 0px;
  background-color: white;
}

即除了上面40px和底部49px,其余都是wraper的高。

  1. content作为wrapper的第一个子元素,这很重要,说明要滚动展示的内容标签都必须包含在content里面。要想有滚动效果,还必须content的内容高度必须大于wrapper设置的固定高度,也就是溢出了wrapper才能滚动。

下面说说如何实现吸顶效果,我们已经看到了:
在这里插入图片描述

在这里插入图片描述
当将这个搜索框滑动到顶部时,它就吸在了顶部,不再跟随其它内容往上面滑。上面我们已经说过了,粘性布局已经失效了,我这里实现的是使用“障眼法”,这几乎是解决betterScroll吸顶效果公用的方法。

看代码:

	<search-box class="boxAttach"
                v-show="showBox"></search-box>
    <better-scroll class='wrapper'
                   @currentPosition="currentPosition"
                   :data="shopList">
	      <div class="content">
	        <search-box></search-box>
	        .....
	      </div>
     </better-scroll>

障眼法:本质上,在better-scroll内部的<search-box>并没有吸附在顶部,它还是随着内容被滑出我们不可视区域,那么为什么我们还能看到有一个搜索框吸附在顶部呢?是因为我们在<better-scroll>外部也使用了<search-box>这个组件,我们通过v-show控制着它在某个条件下显示或隐藏,即当betterscroll内部的搜索框滑到了顶部时,让它显示。

看看外部搜索框的样式:

.boxAttach {
    
    
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
}

即让它满足条件时固定在顶部,这就误以为实现了吸顶。

那他满足什么条件显示呢?怎么知道它滑动了多少位置呢?

我们在封装betterscroll时就已经监听了滚动:

 this.scroll.on("scroll", position => {
    
    
        this.$emit('currentPosition', position)
      })

即滚动时,就向外面发射currentPosition事件并传递position,告知它滑动了的位置。

 //监听betterScroll滚动传过来的position
    currentPosition (position) {
    
    
      this.showBox = position.y < -40
    },

为什么是-40呢?因为navBar(鸭绒外卖)高度就是40px,当position.y=-40时,说明搜索框滑到了顶部,即让隐藏的searchBox显示出来。这就是吸顶效果的实现。

猜你喜欢

转载自blog.csdn.net/weixin_43334673/article/details/110130720