封装一个滚动列表组件

2,017 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

TIP 👉 着意栽花花不发,等闲插柳柳成阴。元·关汉卿《包待制智斩鲁斋郎》

前言

在我们日常项目开发中,我们在会涉及到滚动列表的功能,所以封装了这个滚动列表组件。

滚动列表组件

属性

pullDownRefresh - 下拉刷新
  • 值类型:Boolean | Object
  • 默认值:
{
threshold: flexible.rem2px(100 / 75), // 触发刷新的下拉距离
stop: flexible.rem2px(90 / 75) // 回弹悬停的距离
}
pullUpLoad - 上拉加载
  • 值类型:Boolean | Object
  • 默认值:true
  • Object类型值示例:
{
    threshold: 0 // 触发上拉事件的阈值
}
scrollConfig - 滚动条组件配置
  • 值类型:Object
  • 默认值:
{
wrapperBgColor: '#F5F5F5', // 滚动条包裹器的背景色
bounce: true, // 显示回弹动画
bounceTime: 800, // 回弹动画的动画时长(单位:毫秒)
useTransition: false, // 使用 requestAnimationFrame 做动画
observeDOM: true // 开启对DOM改变的探测
}

具体配置项参考scroll组件

事件

1. refresh - 刷新数据(组件创建成功后或者列表顶部下拉后触发此事件)

参数:

  • callback:刷新数据成功后的回调方法
    • 方法参数: status 数据状态,success(成功)、no-data(没有数据)、no-more(没有更多数据)、fail(失败)

【注意】:

  • 如果有分页此事件的监听应该查询第一页的的数据,查询到的数据应当替换当前数据数组而不是追加到数据数组后
  • 组件创建成功后会自动触发一次 refresh 事件
2. load - 加载数据(滚动到列表底部上拉时触发此事件)

参数:

  • callback:刷新数据成功后的回调方法
    • 方法参数: status 数据状态,success(成功)、no-more(没有更多数据)、fail(失败)

【注意】:

  • 如果有分页此事件的监听应该查询下一页的的数据,查询到的数据追加到数据数组后

示例

<template>
  <ScrollList @refresh="handleRefresh" @load="handleLoad">
    <ul class="data-list" v-if="dataList.length > 0">
      <li class="item" v-for="item in dataList" :key="item.id">
        <div class="title">{{item.title}}</div>
        <div class="date">{{item.articleDate}}</div>
      </li>
    </ul>
  </ScrollList>
</template>
<script>
import ScrollList from '@/components/m/scrollList'

export default {
  components: {
    ScrollList
  },
  data () {
    return {
      pageSize: 10, // 每页大小
      pageNo: 0, // 页码
      count: 0, // 数据总数
      dataList: [], // 列表数据
      isLoading: false // 是否正在加载中
    }
  },
  methods: {
    // 获取列表数据
    getList (pageNo) {
      if (this.isLoading) {
        return
      }
      this.isLoading = true
      pageNo = pageNo || this.pageNo + 1
      return this.$api.post({
        url: '/api/article/list',
        data: { pageSize: this.pageSize, pageNo }
      }, this).then(data => {
        this.pageNo = data.page.pageNo
        this.count = data.page.count
        if (this.pageNo > 1) {
          this.dataList = [...this.dataList, ...data.page.list]
        } else {
          this.dataList = data.page.list
        }
        this.isLoading = false

        let status = 'success' // 查询成功
        if (data.page.count === 0) {
          status = 'no-data' // 暂无数据
        } else if (data.page.count <= data.page.pageNo * this.pageSize) {
          status = 'no-more' // 没有更过数据
        }
        return status
      }, e => {
        this.isLoading = false
        return 'fail' // 查询失败
      })
    },
    // 刷新操作(页面初始化时会自动触发一次刷新事件)
    handleRefresh (callback) {
      this.getList(1).then((status) => {
        callback(status)
      })
    },
    // 加载操作
    handleLoad (callback) {
      this.getList().then((status) => {
        callback(status)
      })
    }
  }
}
</script>

Scroll.vue

<template>
  <Scroll v-if="config" ref="scroll" v-bind="config" @created="init">
    <div v-if="inited" class="pulldown-wrapper">
      <div v-show="beforePullDown" class="pulldown-tips">
        <span>松开即可刷新</span>
      </div>
      <div v-show="!beforePullDown">
        <div v-show="isPullingDown" class="loading">
          <BaseSpinner spinner="bubbles" size="s"></BaseSpinner>
          <span class="loading-txt">加载中...</span>
        </div>
        <div v-show="!isPullingDown" class="refresh-result">
          <span v-if="isFail">刷新失败</span>
          <span v-else>刷新成功</span>
        </div>
      </div>
    </div>
    <div v-else-if="isIniting" class="init-tips">
      <div class="loading">
        <BaseSpinner spinner="bubbles" size="s"></BaseSpinner>
        <span class="loading-txt">加载中...</span>
      </div>
    </div>
    <div v-else-if="isFail" class="error-tips">
      <div v-if="!isPullingDown" class="error-content" @click="emitRefresh">
        <Icon name="cry" class="error-icon"></Icon>
        <span>查询失败,点击重试</span>
      </div>
    </div>

    <slot ref="list"></slot>

    <template v-if="inited">
      <div v-if="status === 'no-data'" class="no-data-tips">
        <div v-if="!isPullingDown" class="no-data-content">
          <Icon name="no-data" class="no-data-icon"></Icon>
          <span>暂无数据</span>
        </div>
      </div>
      <div v-else class="pullup-tips">
        <div v-if="status === 'no-more'" class="no-more-tips">
          <span>没有更多数据了</span>
        </div>
        <div v-else-if="!isPullUpLoad">
          <span v-if="isFail">加载失败,请上拉重试</span>
          <span v-else>上拉加载更多</span>
        </div>
        <div v-else class="loading">
          <BaseSpinner spinner="bubbles" size="s"></BaseSpinner>
          <span class="loading-txt">加载中...</span>
        </div>
      </div>
    </template>
  </Scroll>
</template>
<script>
import Scroll from '@/components/base/scroll'
import BaseSpinner from '@/components/base/spinner'

const defaultConfig = {
  wrapperBgColor: '#F5F5F5', // 滚动条包裹器的背景色
  bounce: true, // 显示回弹动画
  bounceTime: 800, // 回弹动画的动画时长(单位:毫秒)
  useTransition: false, // 使用 requestAnimationFrame 做动画
  observeDOM: true // 开启对DOM改变的探测
}

export default {
  name: 'ScrollList',
  components: {
    Scroll,
    BaseSpinner
  },
  props: {
    // 下拉刷新
    pullDownRefresh: {
      type: [Boolean, Object],
      default: () => {
        let threshold = 100
        let stop = 90
        if (window.lib && window.lib.flexible) {
          let flexible = window.lib.flexible
          threshold = flexible.rem2px(threshold / 75)
          stop = flexible.rem2px(stop / 75)
        }
        return {
          threshold: threshold, // 触发刷新的下拉距离
          stop: stop // 回弹悬停的距离
        }
      }
    },
    // 上拉加载
    /* 示例对象
      {
        threshold: 0 // 触发上拉事件的阈值
      }
     */
    pullUpLoad: {
      type: [Boolean, Object],
      default: true
    },
    // 滚动条组件配置
    scrollConfig: {
      type: Object,
      default: () => { return {} }
    }
  },
  data () {
    return {
      config: null,
      inited: false, // 是否已初始化
      isIniting: false, // 是否正在初始化
      isFail: false, // 是否失败
      status: '', // 当前状态:success、no-data、no-more
      beforePullDown: true, // 是否下拉松手前
      isPullingDown: false, // 是否正在下拉刷新
      isPullUpLoad: false // 是否正在上拉加载
    }
  },
  watch: {
    scrollConfig (val) {
      this.config = this.getConfig()
    },
    pullDownRefresh (val) {
      this.config = this.getConfig()
    },
    pullUpLoad (val) {
      this.config = this.getConfig()
    }
  },
  created () {
    this.config = this.getConfig()
    // 触发刷新
    this.emitRefresh()
  },
  mounted () {
    // 设置内容区域的最小高度比包裹器大1像素,保证能够滚动
    this.setContentMinHeight()
  },
  methods: {
    // 初始化
    init (bs) {
      bs.on('pullingDown', this.pullingDownHandler)
      bs.on('pullingUp', this.pullingUpHandler)
      // bs.on('scroll', e => { console.log('scroll') })
      // bs.on('scrollEnd', e => { console.log('scrollEnd') })
    },
    getConfig () {
      const config = {
        ...defaultConfig,
        ...this.scrollConfig
      }
      if (this.pullDownRefresh) {
        config.pullDownRefresh = this.pullDownRefresh
      }
      if (this.pullUpLoad) {
        config.pullUpLoad = this.pullUpLoad
      }
      return config
    },
    // 触发刷新
    emitRefresh () {
      if (this.isIniting) {
        return
      }
      this.isIniting = true
      this.$emit('refresh', (status) => {
        this.status = status
        this.isIniting = false
        if (status !== 'fail') {
          this.inited = true
          this.isFail = false
        } else {
          this.isFail = true
        }
      })
    },
    // 下拉操作
    pullingDownHandler () {
      if (!this.inited || this.isPullingDown || this.isPullUpLoad) {
        if (!this.isPullingDown) {
          const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
          bs.finishPullDown()
        }
        return
      }

      this.beforePullDown = false
      this.isPullingDown = true
      this.$emit('refresh', (status) => {
        if (status !== 'fail') {
          this.status = status
          this.isFail = false
        } else {
          this.isFail = true
        }
        this.isPullingDown = false
        const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
        if (bs) {
          bs.finishPullDown()
          setTimeout(() => {
            this.beforePullDown = true
            bs.refresh()
            if (this.pullUpLoad) {
              bs.openPullUp(this.pullUpLoad)
            }
          }, this.config.bounceTime + 100)
        }
      })
    },
    // 上拉操作
    pullingUpHandler () {
      if (!this.inited || this.isPullingDown || this.isPullUpLoad ||
          this.status === 'no-data' || this.status === 'no-more') {
        const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
        if (bs) {
          bs.finishPullUp()
        }
        return
      }
      this.isPullUpLoad = true

      this.$emit('load', (status) => {
        if (status !== 'fail') {
          this.status = status
          this.isFail = false
        } else {
          this.isFail = true
        }
        this.isPullUpLoad = false
        const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
        if (bs) {
          bs.finishPullUp()
          bs.refresh()
        }
      })
    },
    // 设置内容区域的最小高度比包裹器大1像素,保证能够滚动
    setContentMinHeight () {
      const scroll = this.$refs.scroll
      const wrapperHeight = scroll.$refs.wrapper.getBoundingClientRect().height
      scroll.$refs.content.style.minHeight = (wrapperHeight + 1) + 'px'
    }
  }
}
</script>
<style lang="scss" scoped>
.pulldown-wrapper{
  position: absolute;
  width: 100%;
  padding: 20px;
  box-sizing: border-box;
  transform: translateY(-100%) translateZ(0);
  text-align: center;
  color: #999;
}
.pulldown-tips {
  line-height: 110px;
}
.init-tips {
  padding-top: 20px;
}
.loading {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #999;
  .loading-txt {
    margin-left: 12px;
  }
}
.refresh-result {}

.no-data-tips {
  padding-top: 50px;
  text-align: center;
  color: #999;
  .no-data-content {
    display: flex;
    flex-direction: column;
    .no-data-icon {
      font-size: 120px;
      margin-bottom: 20px;
    }
  }
}
.error-tips {
  padding-top: 40px;
  text-align: center;
  color: #999;
  .error-content {
    display: flex;
    flex-direction: column;
    .error-icon {
      font-size: 120px;
      margin-bottom: 10px;
    }
  }
}
.pullup-tips {
  padding: 20px;
  text-align: center;
  color: #999;
  .no-more-tips {
    padding-bottom: 30px;
  }
}
</style>

index.js

/**
 * 滚动列表组件
 * @see https://github.com/ustbhuangyi/better-scroll
 * @see https://better-scroll.github.io/docs/zh-CN/
 */
import ScrollList from './ScrollList.vue'
export default ScrollList

「欢迎在评论区讨论」

希望看完的朋友可以给个赞,鼓励一下