大家好,我是鱼樱!!!
关注公众号【鱼樱AI实验室】
持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~
一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享~~~
下面是一个基于 Vue3、TypeScript 和 better-scroll 封装的万能上拉下拉组件,使用 <script setup>
语法糖实现。
组件代码 (PullRefresh.vue)
html
<template>
<div class="pull-refresh-container" ref="wrapperRef">
<div class="pull-refresh-content">
<!-- 下拉刷新提示区域 -->
<div class="pull-down-tip" v-if="props.enablePullDown">
<template v-if="pullDownStatus === 'pulling'">
<slot name="pulling">
<div class="tip-text">下拉刷新</div>
</slot>
</template>
<template v-else-if="pullDownStatus === 'loosing'">
<slot name="loosing">
<div class="tip-text">释放刷新</div>
</slot>
</template>
<template v-else-if="pullDownStatus === 'loading'">
<slot name="loading">
<div class="tip-text">
<span class="loading-icon"></span>
刷新中...
</div>
</slot>
</template>
</div>
<!-- 主内容区域 -->
<slot></slot>
<!-- 上拉加载提示区域 -->
<div class="pull-up-tip" v-if="props.enablePullUp">
<template v-if="!isFinished">
<slot name="loadingMore" v-if="pullUpStatus === 'loading'">
<div class="tip-text">
<span class="loading-icon"></span>
加载中...
</div>
</slot>
</template>
<template v-else>
<slot name="finished">
<div class="tip-text">没有更多数据了</div>
</slot>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import BScroll from '@better-scroll/core'
import Pulldown from '@better-scroll/pull-down'
import Pullup from '@better-scroll/pull-up'
// 注册插件
BScroll.use(Pulldown)
BScroll.use(Pullup)
// 下拉状态类型
type PullDownStatus = 'idle' | 'pulling' | 'loosing' | 'loading'
// 上拉状态类型
type PullUpStatus = 'idle' | 'loading'
// 组件属性
interface Props {
/**
* 是否开启下拉刷新功能
* @default true
*/
enablePullDown?: boolean
/**
* 是否开启上拉加载功能
* @default true
*/
enablePullUp?: boolean
/**
* 下拉刷新阈值(触发刷新的下拉距离)
* @default 60
*/
pullDownRefreshThreshold?: number
/**
* 回弹动画时长(ms)
* @default 300
*/
bounceTime?: number
/**
* 下拉刷新停留时间(ms),-1 表示不自动回弹
* @default 600
*/
pullDownStayTime?: number
/**
* 上拉加载阈值
* @default 0
*/
pullUpLoadThreshold?: number
/**
* 加载数据是否完成
* @default false
*/
finished?: boolean
/**
* 是否立即触发下拉刷新
* @default false
*/
immediateRefresh?: boolean
/**
* Better-Scroll 配置项
*/
bsOptions?: Record<string, any>
}
// 事件
interface Emits {
/**
* 下拉刷新时触发
*/
(e: 'refresh'): void
/**
* 上拉加载更多时触发
*/
(e: 'loadMore'): void
}
// 默认值处理
const props = withDefaults(defineProps<Props>(), {
enablePullDown: true,
enablePullUp: true,
pullDownRefreshThreshold: 60,
bounceTime: 300,
pullDownStayTime: 600,
pullUpLoadThreshold: 0,
finished: false,
immediateRefresh: false,
bsOptions: () => ({})
})
const emit = defineEmits<Emits>()
// 组件内部状态
const wrapperRef = ref<HTMLElement | null>(null)
const bs = ref<BScroll | null>(null)
const pullDownStatus = ref<PullDownStatus>('idle')
const pullUpStatus = ref<PullUpStatus>('idle')
const isFinished = ref(props.finished)
// 监听 finished 属性
watch(() => props.finished, (newVal) => {
isFinished.value = newVal
})
// 初始化 BetterScroll
onMounted(() => {
if (!wrapperRef.value) return
// 合并基础配置和用户配置
const options = {
click: true,
probeType: 3,
scrollY: true,
...props.bsOptions
}
// 添加下拉刷新配置
if (props.enablePullDown) {
Object.assign(options, {
pullDownRefresh: {
threshold: props.pullDownRefreshThreshold,
stop: props.pullDownRefreshThreshold,
}
})
}
// 添加上拉加载配置
if (props.enablePullUp) {
Object.assign(options, {
pullUpLoad: {
threshold: props.pullUpLoadThreshold
}
})
}
// 创建 BetterScroll 实例
bs.value = new BScroll(wrapperRef.value, options)
// 下拉刷新相关事件处理
if (props.enablePullDown) {
bs.value.on('pullingDown', handlePullDown)
bs.value.on('scroll', handleScroll)
}
// 上拉加载相关事件处理
if (props.enablePullUp) {
bs.value.on('pullingUp', handlePullUp)
}
// 是否立即触发刷新
if (props.immediateRefresh) {
setTimeout(() => {
refresh()
}, 10)
}
})
// 销毁实例
onBeforeUnmount(() => {
bs.value?.destroy()
})
// 下拉过程中的状态处理
const handleScroll = (pos: { x: number, y: number }) => {
if (!props.enablePullDown || pullDownStatus.value === 'loading') return
if (pos.y > 0 && pos.y < props.pullDownRefreshThreshold) {
pullDownStatus.value = 'pulling'
} else if (pos.y >= props.pullDownRefreshThreshold) {
pullDownStatus.value = 'loosing'
} else {
pullDownStatus.value = 'idle'
}
}
// 处理下拉刷新
const handlePullDown = async () => {
if (pullDownStatus.value === 'loading') return
pullDownStatus.value = 'loading'
emit('refresh')
// 延迟结束刷新状态
if (props.pullDownStayTime > 0) {
await new Promise(resolve => setTimeout(resolve, props.pullDownStayTime))
finishRefresh()
}
}
// 处理上拉加载
const handlePullUp = async () => {
if (isFinished.value || pullUpStatus.value === 'loading') {
bs.value?.finishPullUp()
return
}
pullUpStatus.value = 'loading'
emit('loadMore')
}
// 完成下拉刷新
const finishRefresh = () => {
pullDownStatus.value = 'idle'
bs.value?.finishPullDown()
bs.value?.refresh()
}
// 完成上拉加载
const finishLoadMore = () => {
pullUpStatus.value = 'idle'
bs.value?.finishPullUp()
bs.value?.refresh()
}
// 手动触发下拉刷新
const refresh = () => {
if (!bs.value || pullDownStatus.value === 'loading') return
pullDownStatus.value = 'loading'
bs.value.scrollTo(0, props.pullDownRefreshThreshold + 1, props.bounceTime)
handlePullDown()
}
// 暴露给父组件的方法
defineExpose({
/**
* 完成下拉刷新
*/
finishRefresh,
/**
* 完成上拉加载
*/
finishLoadMore,
/**
* 手动触发下拉刷新
*/
refresh,
/**
* 获取BScroll实例
*/
getBScroll: () => bs.value
})
</script>
<style scoped>
.pull-refresh-container {
height: 100%;
width: 100%;
overflow: hidden;
position: relative;
}
.pull-refresh-content {
position: relative;
}
.pull-down-tip,
.pull-up-tip {
height: 50px;
line-height: 50px;
text-align: center;
color: #969799;
}
.tip-text {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.loading-icon {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 5px;
border: 2px solid #ccc;
border-top-color: #666;
border-radius: 50%;
animation: loading-rotate 0.8s linear infinite;
}
@keyframes loading-rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
使用示例
html
<template>
<div class="list-container">
<h1>下拉刷新/上拉加载示例</h1>
<PullRefresh
ref="pullRefreshRef"
@refresh="onRefresh"
@loadMore="onLoadMore"
:finished="finished"
:enablePullDown="true"
:enablePullUp="true"
>
<div class="list">
<div v-for="(item, index) in list" :key="index" class="list-item">
{{ item }}
</div>
</div>
<!-- 自定义下拉提示 -->
<template #pulling>
<div>👇 下拉刷新</div>
</template>
<template #loosing>
<div>👆 释放立即刷新</div>
</template>
<template #loading>
<div>🔄 刷新中...</div>
</template>
<!-- 自定义上拉提示 -->
<template #loadingMore>
<div>🔄 加载更多中...</div>
</template>
<template #finished>
<div>🎉 已加载全部数据</div>
</template>
</PullRefresh>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import PullRefresh from './components/PullRefresh.vue'
const pullRefreshRef = ref<InstanceType<typeof PullRefresh> | null>(null)
const list = ref<string[]>([])
const finished = ref(false)
const page = ref(1)
const pageSize = 10
// 模拟获取数据
const fetchData = (p: number): Promise<string[]> => {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟数据总量为 50 条
if (p > 5) {
resolve([])
} else {
const result = Array.from({ length: pageSize }, (_, i) =>
`Item ${(p - 1) * pageSize + i + 1}`
)
resolve(result)
}
}, 1500) // 模拟网络请求延迟
})
}
// 初始化数据
const initData = async () => {
page.value = 1
const data = await fetchData(page.value)
list.value = data
finished.value = data.length < pageSize
}
// 刷新事件处理
const onRefresh = async () => {
try {
await initData()
} finally {
// 不管成功失败都要结束刷新状态
pullRefreshRef.value?.finishRefresh()
}
}
// 加载更多事件处理
const onLoadMore = async () => {
try {
page.value++
const data = await fetchData(page.value)
if (data.length > 0) {
list.value.push(...data)
}
// 判断是否已加载全部数据
finished.value = data.length < pageSize
} finally {
// 不管成功失败都要结束加载状态
pullRefreshRef.value?.finishLoadMore()
}
}
// 初始化
initData()
</script>
<style scoped>
.list-container {
height: 100vh;
width: 100%;
}
.list {
padding: 10px;
}
.list-item {
margin-bottom: 10px;
height: 60px;
line-height: 60px;
text-align: center;
background-color: #f5f5f5;
border-radius: 8px;
}
</style>
核心功能说明
这个上拉下拉组件具有以下主要特性:
- 高度可配置:通过props提供多种配置选项,包括是否启用上拉/下拉、阈值设置等
- 完全类型化:使用TypeScript提供完整的类型定义,提高代码健壮性
- 自定义插槽:支持自定义各种状态下的提示内容
- 事件驱动 :提供
refresh
和loadMore
事件处理上拉下拉操作 - 手动控制API :暴露
finishRefresh
和finishLoadMore
等方法供外部调用
使用注意事项
-
安装依赖:需要安装以下依赖:
bashnpm install @better-scroll/core @better-scroll/pull-down @better-scroll/pull-up
-
容器高度:使用组件的父容器必须设置固定高度,否则滚动将无法正常工作
-
结束状态 :在数据加载完成后,必须调用
finishRefresh
或finishLoadMore
方法来结束加载状态 -
性能优化:对于大列表,建议配合虚拟列表插件使用,以提升渲染性能
这个组件通过合理的抽象和接口设计,可以适用于大多数列表加载场景,满足各种自定义需求。