一.自己实现
1.实现步骤
微信小程序的页面级下拉刷新依赖:
json
{
"enablePullDownRefresh": true
}
然后在页面写:
scss
onPullDownRefresh(() => {
...
})
onReachBottom(() => {
...
})
2.需要解决的问题
- 上拉时文字 ''加载中...", ''没有更多了''的切换
- 触底时loading的加载
- 触底判断当前list的长度和后端返回的total长度比较,去判断当前是''加载中''还是''没有更多了''
- 数据的拼接处理,downloadClassList.value = [...downloadClassList.value, ...res.rows];
- 下拉加载
onPullDownRefresh时需要处理请求页数的问题
3.不足
- onPullDownRefresh手机呈现的效果是**"整个页面一起拉下来"**,也就是页面级的。当你页面顶部有搜索框、tabs 等内容时, 微信小程序原生的
enablePullDownRefresh会一起被下拉,体验很差。 - 相对复杂,需要自己去实现判断后端的总数据total和数据list长度;来维护hasMore从而控制是否还需要请求,是否需要loading等
4.具体代码示例
xml
<template>
<view class="downloadHistory">
<view class="topSearch">
<van-dropdown-menu active-color="#29a1f7">
<van-dropdown-item :value="resourceClass" :options="option1" @change="selectClass" />
</van-dropdown-menu>
</view>
<view class="downloadCollection" v-if="downloadClassList.length !== 0">
<view class="downloadCard" v-for="(item, key) in downloadClassList" :key="key" @click="toDownloadList(item)">
<van-image width="100" height="75" :src="item.coverUrl" />
<view class="picInfo">
<view class="picTitle">{{ item.name }}</view>
<view class="picNum">
<span class="mr-20">{{ item.videoNum }}视频</span><span>{{ item.pictureNum }}图片</span>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-status">
<van-loading v-if="loading" type="spinner" size="32rpx"> 加载中... </van-loading>
<text v-else-if="!hasMore && downloadClassList.length > 0" class="no-more"> - 没有更多了 - </text>
</view>
</view>
<view class="null-page" v-else>
<van-empty description="暂无数据" />
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { queryDownload } from '@/api/downloadHistory.js';
import { onShow } from '@dcloudio/uni-app';
import { onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
const resourceClass = ref('');
const option1 = ref([
{ text: '全部记录', value: '' },
{ text: '文旅景区', value: 'scenic' },
{ text: '体育赛事', value: 'event' },
]);
const page = ref(1); // 表示下一次要请求的页码,初始请求为 1
const pageSize = 10;
const loading = ref(false);
const hasMore = ref(true);
const total = ref(0);
const downloadClassList = ref([]);
function selectClass(event) {
downloadClassList.value = [];
resourceClass.value = event.detail;
loadMore();
}
// 前往下载列表
function toDownloadList(item) {
uni.navigateTo({
url: '/pages/downloadList/index',
success: res => {
res.eventChannel.emit('recordFolder', item);
},
});
}
// loadMore:refresh 为 true 时表示下拉刷新/重新加载第一页
async function loadMore(refresh = false) {
// 并发保护(防止重复请求)
if (loading.value) return;
loading.value = true;
const token = uni.getStorageSync('token');
if (!token) {
loading.value = false;
uni.navigateTo({ url: '/pages/loginPage/index' });
return;
}
try {
if (refresh) {
// 请求第一页
const res = await queryDownload(
{
originType: resourceClass.value,
pageNum: 1,
pageSize,
},
token
);
// 覆盖数据
downloadClassList.value = res.rows || [];
total.value = res.total || (res.rows ? res.rows.length : 0);
// 如果返回的行数小于 pageSize,说明没有更多
hasMore.value = downloadClassList.value.length < total.value;
// 重要:refresh 后把 page 设为下一页(2)
page.value = 2;
} else {
// 非刷新场景,请求 page(page 表示下一次要请求的页码)
if (!hasMore.value) {
loading.value = false;
return;
}
const res = await queryDownload(
{
originType: resourceClass.value,
pageNum: page.value,
pageSize,
},
token
);
const rows = res.rows || [];
// 追加数据
downloadClassList.value = [...downloadClassList.value, ...rows];
total.value = res.total || total.value;
// 成功追加后,page 自增为下一次要请求的页码
page.value = page.value + 1;
// 如果本次返回的数量 < pageSize 或 当前长度 >= total,则没有更多
if (rows.length < pageSize || downloadClassList.value.length >= total.value) {
hasMore.value = false;
} else {
hasMore.value = true;
}
}
} catch (err) {
console.error('loadMore error', err);
// 请求失败时不改变 page(避免乱跳),并可视需要设置 hasMore / 显示错误
} finally {
loading.value = false;
}
}
// 下拉刷新:调用 loadMore(true),并等待完成再停止刷新动画
onPullDownRefresh(async () => {
if (loading.value) return; // 避免同时下拉和触底并发
console.log('pull down refresh');
await loadMore(true);
// 结束下拉动画
uni.stopPullDownRefresh();
});
// 触底加载:await loadMore(),并依赖 hasMore 控制
onReachBottom(async () => {
// 防止重复触发
if (loading.value || !hasMore.value) return;
await loadMore(false);
});
// dom挂载加载一次
onMounted(() => {
// getDownload();
});
// 每次进入页面加载一次
onShow(() => {
downloadClassList.value = [];
page.value = 1;
loadMore();
});
</script>
<style lang="scss" scoped>
.downloadHistory {
min-height: 100vh;
background-color: #f5f5f5;
}
.topSearch {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 99;
}
.downloadCollection {
padding: 30rpx;
padding-top: 130rpx;
}
.downloadCard {
background-color: #fff;
border-radius: 8rpx;
padding: 30rpx;
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 30rpx;
}
.downloadCard:last-child {
margin-bottom: 0;
}
.picInfo {
margin-left: 20rpx;
}
.picTitle {
font-size: 30rpx;
color: #333;
margin-bottom: 50rpx;
}
.picNum {
font-size: 28rpx;
color: #666;
}
.mr-20 {
margin-right: 20rpx;
}
.loading-status {
text-align: center;
padding: 40rpx 0;
}
.no-more {
font-size: 24rpx;
color: #999;
}
</style>
二.利用插件z-paging
1.前提
- 需要"enablePullDownRefresh": false
json
{
"path": "pages/downloadHistory/index",
"style": {
"navigationBarTitleText": "下载记录",
"enablePullDownRefresh": false
}
},
z-paging` 不使用小程序原生下拉刷新,它自己封装了一整套刷新与分页逻辑
- 下拉刷新
- 上拉加载更多
- 滚动监听
- 加载状态管理
- 空数据提示
- 自动触底加载
所以不需要再用原生的下拉刷新,否则可能会冲突。
z-paging 自己监听 scroll-view,不依赖页面能力
z-paging 通过内部的:
scroll-viewcustom-refresher- 自定义"下拉刷新动画"
来实现刷新效果,而不是依赖微信提供的页面级 enablePullDownRefresh。
所以用 z-paging 时,pages.json 中完全不需要开启此项。
2.优点
- 使用简单,采用vue组件的方式,通过
propseventslot来快速构建 z-paging不使用页面的下拉刷新能力(enablePullDownRefresh)
3.实现原理
z-paging 使用的是 组件内部 scroll-view 的下拉刷新能力, 内部类似这样:
xml
<scroll-view
scroll-y
:refresher-enabled="useRefresher"
:lower-threshold="50"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
>
<!-- 顶部 slot -->
<slot name="top"></slot>
<!-- 列表内容 -->
<slot></slot>
<!-- loading 动画 -->
<loading-view v-if="loading" />
<!-- 没有更多 -->
<no-more v-if="!hasMore" />
<!-- ...更多插槽 -->
</scroll-view>
4.具体代码实现
xml
<template>
<view class="downloadHistory">
<view class="downloadCollection">
<z-paging ref="paging" v-model="dataList" @query="queryList" :default-page-size="10">
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中,如果需要跟着滚动,则不要设置slot="top" -->
<!-- 注意!此处的z-tabs为独立的组件,可替换为第三方的tabs,若需要使用z-tabs,请在插件市场搜索z-tabs并引入,否则会报插件找不到的错误 -->
<template #top>
<view>
<van-dropdown-menu active-color="#29a1f7">
<van-dropdown-item :value="resourceClass" :options="option1" @change="selectClass" />
</van-dropdown-menu>
</view>
</template>
<!-- 设置自己的empty组件,非必须。空数据时会自动展示空数据组件,不需要自己处理 -->
<template #empty>
<van-empty description="暂无数据" />
</template>
<!-- 自定义的没有更多数据view -->
<template #loadingMoreNoMore>
<view class="no-more">- 没有更多了 -</view>
</template>
<view class="downloadCard" v-for="(item, key) in dataList" :key="key" @click="toDownloadList(item)">
<van-image width="100" height="75" :src="item.coverUrl" />
<view class="picInfo">
<view class="picTitle">{{ item.name }}</view>
<view class="picNum">
<span class="mr-20">{{ item.videoNum }}视频</span><span>{{ item.pictureNum }}图片</span>
</view>
</view>
</view>
</z-paging>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { queryDownload } from '@/api/downloadHistory.js';
const paging = ref(null);
const dataList = ref([]);
const resourceClass = ref('');
const option1 = ref([
{ text: '全部记录', value: '' },
{ text: '文旅景区', value: 'scenic' },
{ text: '体育赛事', value: 'event' },
]);
// @query所绑定的方法不要自己调用!!需要刷新列表数据时,只需要调用paging.value.reload()即可
const queryList = (pageNo, pageSize) => {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好,直接传给服务器即可
const params = {
originType: resourceClass.value,
pageNum: pageNo,
pageSize,
};
const token = uni.getStorageSync('token');
if (token) {
queryDownload(params, token)
.then(res => {
// 将请求的结果数组传递给z-paging
paging.value.complete(res.rows);
})
.catch(res => {
// 如果请求失败写paging.value.complete(false);
// 注意,每次都需要在catch中写这句话很麻烦,z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时,写uni.$emit('z-paging-error-emit');即可
paging.value.complete(false);
});
}
};
function selectClass(event) {
resourceClass.value = event.detail;
paging.value.reload(); // 刷新分页
}
// 前往下载列表
function toDownloadList(item) {
uni.navigateTo({
url: '/pages/downloadList/index',
success: res => {
res.eventChannel.emit('recordFolder', item);
},
});
}
</script>
<style lang="scss" scoped>
.downloadHistory {
min-height: 100vh;
background-color: #f5f5f5;
}
.downloadCollection {
padding: 30rpx;
padding-top: 130rpx;
}
.downloadCard {
background-color: #fff;
border-radius: 8rpx;
padding: 30rpx;
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 30rpx;
&:nth-child(1) {
margin-top: 30rpx;
}
}
.downloadCard:last-child {
margin-bottom: 0;
}
.picInfo {
margin-left: 20rpx;
}
.picTitle {
font-size: 30rpx;
color: #333;
margin-bottom: 50rpx;
}
.picNum {
font-size: 28rpx;
color: #666;
}
.mr-20 {
margin-right: 20rpx;
}
.loading-status {
text-align: center;
padding: 40rpx 0;
}
.no-more {
font-size: 24rpx;
color: #999;
text-align: center;
margin-bottom: 40rpx;
}
</style>
参考文档