【缤纷多彩】列表无缝滚动实现方式浅谈

1,908 阅读2分钟

在项目开发中,特别是大屏开发,我们不难遇到需要上下滚动表格的需求。现在我们无非两种:

实现效果

为了方便讲解与实现,现在我们假设表格数据总条数为7条,滚动容器可视区域最多可刚好完整展示4条。(事实上可视区域是可以不刚好完整展示整数条数据的,然而若不完整展示对于无缝滚动实现会稍微复杂点,这个在后续讲解会提到)

1.非无缝衔接:

原始表格数据从第一行数据滚动到最后一行数据然后再回到第一行重新执行相同过程,视觉效果为 头 → 尾 突变 头 → 尾往复执行,视觉上是到尾部突然就回到头部,没有衔接效果。示意图如下:

image.png image.png image.png

2.无缝衔接:

往原始表格数据尾部追加一定条数的数据,追加数据从原始表格数据的头部截取。然后从第一行数据滚动到最后一行数据然后再回到第一行重新执行相同过程,视觉效果为 头 → 尾 → 头 → 尾 → 头...... 执行,相比第一种方式,看起来很流畅,没有突变感,衔接感好。示意图如下:

image.png image.png image.png

实现方法

1.使用css动画实现:

模板代码
<a-spin :spinning="loading">
  <div class="content-container" @scroll="loadTable">
    <div
      ref="scrollWrap"
      class="scroll-wrap"
      :style="{ ...style, animationPlayState: animationPlayState }"
      @mouseover="animationPlayState = 'paused'"
      @mouseout="animationPlayState = 'running'"
    >
      <div v-for="rowItem in tableData" :key="rowItem.id" class="content-body">
        <span
          v-for="colItem in columns"
          :key="colItem.key"
          :style="{ width: `${colItem.width}px` }"
          class="item-span"
        >
          <template>{{ rowItem[colItem.key] || '—' }}</template>
        </span>
      </div>
    </div>
  </div>
</a-spin>
js部分

1.请求数据:

// 获取数据
getData() {
  this.loading = true
  this.animationPlayState = 'paused'
  handleTable(this.paging)
    .then(res => {
      if (res.success && res.code === 200) {
        const result = res.result || {}
        this.tableData = [...this.tableData, ...result.records]
        if (this.tableData.length < result.total) {
          this.paging.current++
        } else {
          this.finish = true
          this.isJoinData = true
        }
      }
      this.loading = false
      this.setTableScroll()
    })
    .catch(() => {
      this.loading = false
      this.setTableScroll()
    })
}

2.设置滚动动画(包含懒加载表格数据部分):

// 根据滚动行数设置滚动动画时间与滚动高度
setTableScroll() {
  // 如果原始表格数据不超出滚动容器则不设置动画
  if (this.tableData.length <= 4) return
  if (this.finish && this.isJoinData) {
    const endData = this.tableData.slice(0, 4)
    this.tableData = [...this.tableData, ...endData]
    // 置反防止重复追加
    this.isJoinData = false
  }
  this.$nextTick(() => {
    const time = this.tableData.length * 3
    this.style = {
      animationDuration: `${time}s`
    }
    // 滚动容器可视区域
    const viewH = this.$refs.scrollWrap.offsetHeight
    // 滚动容器总高度
    const totalH = this.$refs.scrollWrap.scrollHeight
    // 非无缝滚动则直接设置为y = `calc(-${100% - viewHpx})`即可
    const y = `-${totalH - viewH}px`
    document.styleSheets[0].insertRule(
      `@keyframes contentContainer {
      0% {
          transform: translateY(0);
          }
      100% {
          transform: translateY(${y});
          }
      }`,
      0
    )
    this.animationPlayState = 'running'
    /**
     * 滚动5s后进行下一组数据请求,前提是该时间内表格未滚动到底部,否则缩短时间或追加表格数据
     * 或者通过监听滚动事件,触底时触发下一页加载事件,下文使用定时器时采用该方式加载下一页
     */
    this.loadTable(5000)
  })
},

// 懒加载数据
loadTable(time) {
  clearInterval(this.timer)
  this.timer = setTimeout(() => {
    const canLoad = this.animationPlayState === 'running' && !this.finish && !this.loading
    canLoad && this.getData()
    !canLoad && this.loadTable(5000)
  }, time)
}

2.使用定时器实现:

模板代码
<a-spin :spinning="loading">
  <div
    ref="scrollWrap"
    class="scroll-wrap"
    @mouseover="pausedScroll"
    @mouseout="runningScroll"
  >
    <div
      v-for="(rowItem, rowIndex) in tableData"
      :key="rowItem.id"
      class="content-body"
    >
      <span
        v-for="colItem in columns"
        :key="colItem.key"
        :style="{ width: `${colItem.width}px` }"
        class="item-span"
      >
        <template>{{ rowItem[colItem.key] || '—' }}{{ rowIndex }}</template>
      </span>
    </div>
  </div>
</a-spin>
js部分

1.请求数据:

// 懒加载数据
loadTable() {
  this.pausedScroll()
  this.loading = true
  handleTable(this.paging)
    .then(res => {
      if (res.success && res.code === 200) {
        const result = res.result || {}
        this.tableData = [...this.tableData, ...result.records]
        if (this.tableData.length < result.total) {
          this.paging.current++
        } else {
          this.finish = true
          this.isJoinData = true
        }
      }
      this.loading = false
      this.runningScroll()
    })
    .catch(() => {
      this.loading = false
      this.runningScroll()
    })
}

2.设置滚动动画(包含懒加载表格数据部分):

// 表格滚动开始
runningScroll() {
  this.pausedScroll()
  this.$nextTick(() => {
    const tableBody = this.$refs.scrollWrap
    const viewH = tableBody.offsetHeight
    const totalH = tableBody.scrollHeight
    // 表格数据足以撑可视区域则进行滚动
    if (totalH > viewH) {
      this.timer = setInterval(() => {
        tableBody.scrollTop += this.scrollStep
        if (tableBody.scrollTop + viewH >= totalH) {
          if (this.finish) {
            if (this.isJoinData) {
              /*
               * 全部请求完成跟据可视区域高度添加合适的行数以撑满可视区域进行无缝滚动
               * 为方便这里假设可视区域高度为160,行高固定为40,则需添加4行数据
               * 若可视区域可展示行数不为整数,则全部加载完并追加完数据则之后需根据追
               * 加数据高度与可视高度之差得出scrollTop回滚顶部阈值,即:
               * 假设追加部分高度为addH,则阈值为:
               * tableBody.scrollTop + viewH >= totalH - (addH - viewH)
               */
              const endData = this.tableData.slice(0, 4)
              this.tableData = [...this.tableData, ...endData]
              // 置反防止重复追加
              this.isJoinData = false
            } else {
              tableBody.scrollTop = 0
            }
          } else {
            this.loadTable()
          }
        }
      }, this.intervalTime)
    }
  })
},
// 表格滚动暂停
pausedScroll() {
  this.timer && clearInterval(this.timer)
}

以上内容如有纰漏或更优解,望不吝赐教~