从零开始Vue3+Element Plus后台管理系统(23)—无限滚动v-infinite-scroll指令实现下拉加载列表

980 阅读4分钟

我在开发 PC 端时,极少使用下拉加载列表,绝大部分都是使用分页,经典好用。

移动端下拉加载列表很频繁,很久以前尝试过 better-scroll,感觉挺顺手的。不过大部分时间都是使用mescroll,对我来说,它真的很好用,99% 满足开发需求。

最近刚好有一个PC 端消息列表的需求,用到了下拉加载列表。用第三方插件也不是不可以,但刚好看到 element plus 有这个 Infinite Scroll 无限滚动的东东,我没有用过,所以想试试。

v-infinite-scroll

v-infinite-scroll文档地址:element-plus.org/zh-CN/compo…

Infinite Scroll 的文档和说明都很简单,总共就下面这些配置。

属性说明类型默认
v-infinite-scroll滚动到底部时,加载更多数据Function
infinite-scroll-disabled是否禁用booleanfalse
infinite-scroll-delay节流时延,单位为msnumber200
infinite-scroll-distance触发加载的距离阈值,单位为pxnumber0
infinite-scroll-immediate是否立即执行加载方法,以防初始状态下内容无法撑满容器。booleantrue
<template>
  <ul v-infinite-scroll="load" class="infinite-list" style="overflow: auto">
    <li v-for="i in count" :key="i" class="infinite-list-item">{{ i }}</li>
  </ul>
</template>

折腾了一会,解决了一些关键性的问题,总算是完成了。

几个关键点

重新加载数据

可以通过设置搜索条件,重新渲染列表数据,所以每次需要重新加载数据。如果用新数据直接替换列表数据的值,貌似行不通——load方法不会重新调用,导致后面的下拉加载不生效。

EP没有提供重新渲染的方法,所以我给外层容器加上v-if="infiniteScrollVisible",通过设置infiniteScrollVisible=true or false来控制组件的重新渲染。

终止加载数据

如果不处理,加载方法会一直执行,控制台的网络请求会不停的请求接口😳, 所以需要在适当的时候把infinite-scroll-disabled赋值为 true,一般是在数据返回为空的时候设为true。

删除单条数据,不会自动执行load方法

infinite-scroll-immediate,为 true时可以立即执行加载方法,在容器没有填充满的时候再次执行加载方法。

但是当我删除多条数据,列表下方出现空白时,它并没有自动执行加载方法,这个时候下拉加载也没有用。

看文档也没看出什么解决办法,所以还是用自己的办法来解决,删除后计算空白的高度,当空白高度达到一定条件,就手动执行加载方法。

实现代码

template

<template>
  <div class="relative">
    <div class="w-full p-4">
      <el-select v-model="queryForm.type" @change="handleChangeType" clearable>
        <el-option
          v-for="item in [1, 2, 3]"
          :key="item"
          :label="`类型${item}`"
          :value="item"
        ></el-option>
      </el-select>
    </div>

    <el-scrollbar style="height: calc(100vh - 160px)" v-if="infiniteVisible" class="px-4">
      <ul
        v-infinite-scroll="load"
        :infinite-scroll-disabled="disabled"
        :infinite-scroll-distance="100"
        id="todoList"
        class="todo-list"
      >
        <li
          v-for="item in todoList"
          :key="item.id"
          class="cursor-pointer todo-item"
          @click="toDetail(item)"
        >
          <div class="text-sm massage-item-hd">
            <el-icon class="mr-1"><Warning /></el-icon>
            <span>{{ item.title }}</span>

            <el-icon size="18" class="icon-delete" @click.stop="handleDelete(item)"
              ><Close
            /></el-icon>
          </div>
          <div class="text-df">{{ item.description }}</div>
          <div class="mt-2 text-xs text-gray-500">{{ item.createTime }}</div>
        </li>
      </ul>

      <!-- loading 动画 start-->
      <div v-if="loading">
        <div class="p-4 ball-pulse">
          <div></div>
          <div></div>
          <div></div>
        </div>
      </div>
      <!-- loading 动画 end -->

      <el-empty v-if="queryForm.pagenum === 1 && todoList.length === 0 && !loading"></el-empty>

      <p v-else-if="noMore" class="p-4 text-sm text-center text-gray-400">没有更多了</p>
    </el-scrollbar>
  </div>
</template>

js

<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
import { Close, Warning } from '@element-plus/icons-vue'
import todoApi from '~/api/todo'

const visible = ref(false)
// 控制infiniteScroll重新加载
const infiniteVisible = ref(false)

interface TodoItem {
  id: string
  title: string
  type: string
  description: string
  createTime: string
  isRead: boolean
}

const todoList = ref<TodoItem[]>([])

const loading = ref(false)
const noMore = ref(false)
const refresh = ref(false)
const disabled = computed(() => refresh.value || loading.value || noMore.value)

const queryForm = reactive<{
  pagesize: number
  pagenum: number
  type: string
  isRead?: boolean
}>({
  pagesize: 10,
  pagenum: 1,
  type: ''
})

async function loadData() {
  loading.value = true

  todoApi
    .getTodoList({ ...queryForm })
    .then((res: any) => {
      console.log(res.list.length)
      if (res.list.length > 0) {
        todoList.value = todoList.value.concat(res.list)
        queryForm.pagenum += 1
      } else {
        noMore.value = true
      }
    })
    .finally(() => {
      loading.value = false
    })
}

const load = async () => {
  // 防止重复加载
  if (refresh.value) {
    return
  }
  await loadData()
}

function refreshInfiniteList() {
  refresh.value = true
  noMore.value = false

  todoList.value.length = 0
  infiniteVisible.value = false

  nextTick(() => {
    infiniteVisible.value = true
    refresh.value = false
    queryForm.pagenum = 1
    load()
  })
}

onMounted(() => {
  refreshInfiniteList()
})

function handleDelete(item: TodoItem) {
  todoList.value = todoList.value.filter((val) => val.id !== item.id)

  // 如果删除元素使得下方出现空白,加载更多
  nextTick(() => {
    const el = document.getElementById('todoList')

    const bottom = window.innerHeight - el!.getBoundingClientRect().bottom
  
    if (bottom > 0) {
      load()
    }
  })
}

function handleChangeType(e: string) {
  console.log(e)
  refreshInfiniteList()
}

async function toDetail(item: TodoItem) {
  visible.value = false
}
</script>

css

项目大部分样式都使用 tailwindcss,少部分自己手写。

<style lang="scss" scoped>
.todo-item {
  padding: 16px;
  border-bottom: 1px solid var(--el-border-color-light);

  .massage-item-hd {
    position: relative;
    margin-bottom: 8px;
    display: flex;
    align-items: center;
    color: var(--el-text-color-regular);
    .icon-delete {
      position: absolute;
      right: 0px;
      display: none !important;
      color: var(--color);
      cursor: pointer;
    }
  }

  &:hover {
    background-color: var(--el-border-color-light);
    .icon-delete {
      display: inline-flex !important;
    }
  }
  &.todo-item-read,
  &.todo-item-read .massage-item-hd {
    color: var(--el-text-color-secondary);
  }
}

.ball-pulse {
  text-align: center;
  > div:nth-child(1) {
    -webkit-animation: scale 0.75s 0.12s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
    animation: scale 0.75s 0.12s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
  }
  > div:nth-child(2) {
    -webkit-animation: scale 0.75s 0.24s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
    animation: scale 0.75s 0.24s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
  }

  > div:nth-child(3) {
    -webkit-animation: scale 0.75s 0.36s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
    animation: scale 0.75s 0.36s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
  }

  > div {
    background-color: var(--el-border-color-light);
    width: 12px;
    height: 12px;
    border-radius: 100%;
    margin: 2px;
    -webkit-animation-fill-mode: both;
    animation-fill-mode: both;
    display: inline-block;
  }
}

@keyframes scale {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(0.5);
  }
  10% {
    transform: scale(1);
  }
}
</style>

虽然v-infinite-scroll并没有像 meScroll 提供那么多配置和功能,毕竟 EP 只暴露了 5 个选项,但是满足PC 端基本的需求应该还是够用的。

项目地址

本项目GIT地址:github.com/lucidity99/…

如果有帮助,给个star ✨ 点个赞