uniapp实现上拉刷新和下拉刷新的两种方式

一.自己实现

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-view
  • custom-refresher
  • 自定义"下拉刷新动画"

来实现刷新效果,而不是依赖微信提供的页面级 enablePullDownRefresh

所以用 z-paging 时,pages.json 中完全不需要开启此项。

2.优点

  • 使用简单,采用vue组件的方式,通过props event slot来快速构建
  • 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>

参考文档

uniapp.dcloud.net.cn/component/s...

z-paging.zxlee.cn/

相关推荐
FinelyYang3 小时前
uniapp+unipush2.0+WebRTC实现h5一对一视频通话
uni-app·音视频·webrtc
天蓝色的鱼鱼7 小时前
mescroll老用户亲测z-paging:这些功能让我果断切换!
前端·uni-app
anyup8 小时前
🔥100+ 天,已全面支持鸿蒙!uView Pro 近期更新盘点及未来计划
前端·uni-app·harmonyos
半兽先生18 小时前
uniapp高性能ui框架uni-ui
ui·uni-app
qq_3168377520 小时前
uniapp 观察列表每个元素的曝光时间
前端·javascript·uni-app
iOS阿玮1 天前
打个广告,帮忙招一个iOS开发的扛把子~
uni-app·app·apple
Cerrda1 天前
🌟让你的uniapp应用拥有更现代的交互体验,一个支持滚动渐变透明的导航栏组件🌟
uni-app
2501_916007471 天前
iOS 应用性能测试的工程化流程,构建从指标采集到问题归因的多工具协同测试体系
android·ios·小程序·https·uni-app·iphone·webview
00后程序员张1 天前
iOS 抓不到包怎么办?从 HTTPS 解密、QUIC 排查到 TCP 数据流分析的完整解决方案
android·tcp/ip·ios·小程序·https·uni-app·iphone