我在开发 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 | 是否禁用 | boolean | false |
infinite-scroll-delay | 节流时延,单位为ms | number | 200 |
infinite-scroll-distance | 触发加载的距离阈值,单位为px | number | 0 |
infinite-scroll-immediate | 是否立即执行加载方法,以防初始状态下内容无法撑满容器。 | boolean | true |
<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 ✨ 点个赞