游乐游手机版
首页/前端开发/文章详情

Vue3+UniApp经典Hook方式实现加载更多列表组件

时间:2026-06-13 06:47
基于Vue3组合式API与uni-app,封装useMoreList函数处理分页参数、触底加载与状态流转,结合MoreList组件与slot插槽构建通用列表架构,支持缺省页、加载中、上拉加载三种状态,显著提升开发效率与复用性。

vue3组合式编程列表组件封装

最终效果示例:

列表类型不重要——文本框、图片流、卡片瀑布,任你选择。通过 slot 插槽注入自己的模板,组件只负责架构,内容交给你。

前言

UniApp 开发中,列表场景是绕不开的标配——初始加载、下拉刷新、分页、上拉加载更多,外加缺省页提示,每次都要写一遍,重复劳动不说,还容易漏掉边界。这次抽离出一个基于 Vue3 组合式 API 的通用列表封装,把那些琐碎的循环逻辑统一收敛到 hook 里,组件只做壳,业务代码只关心数据渲染。实测下来,开发效率提升不少。

组件实现

创建一个 MoreList 组件,核心骨架如下——主体通过 slot 容纳你的自定义列表,当数据为空时展示预设的缺省图,有数据则自动显示 uni-load-more 组件,支持“上拉加载更多”、“加载中”、“已经到底”三种状态。

<script setup>
  import { ref } from "vue";  const props = defineProps({
    moreStatus: {
      type: String,
      default: "more",
    },
    hasData: {
      type: Boolean,
      default: false,
    },
  });
  const emit = defineEmits(["loadMore"]);  const contentText = ref({
    contentdown: "上拉加载更多",
    contentrefresh: "数据加载中……",
    contentnomore: "已经到底啦~",
  });  const handleLoadMore = () => {
    emit("loadMore");
  };
script><template>
  <view class="more-list">
    <view class="list-content">
      <view v-if="hasData">
        <slot>slot>
      view>
      
      <view v-else class="empty-data">
        <image src="/static/images/empty-data.png" class="empty-icon">image>
        <view class="empty-text">数据为空view>
      view>
    view>
    <uni-load-more
      v-if="hasData"
      :status="moreStatus"
      :contentText="contentText"
      @click="handleLoadMore"
    >uni-load-more>
  view>
template><style lang="scss" scoped>
  .more-list {
    height: 100%;
  }
  .empty-data {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 24rpx;
  }
  .empty-icon {
    width: 320rpx;
    height: 320rpx;
  }
  .empty-text {
    font-size: 32rpx;
    color: #666;
  }
style>

hook实现

接下来是核心逻辑层——useMoreList 组合式函数。它接收一个配置对象,自动完成分页参数维护、触底加载、状态流转,并暴露 listDatamoreStatushasData 等响应式变量供组件消费。代码里还预留了 validateConfig 对配置项做校验(这里简化处理,实际可补充类型检查)。

import { ref, computed, isReactive, reactive } from "vue";
import { onReachBottom } from "@dcloudio/uni-app";/**
 * 对配置项进行校验处理
 * @param {*} config 配置项
 * @param {*} fields 需要校验的字段数组
 */
const validateConfig = (config, fields) => {
  // to do ...
};/**
 * 无限滚动列表
 * @param {*} config 配置项
 * @param {*} config.api 接口函数
 * @param {*} config.isAutoLoad 初始是否自动加载列表 默认true
 * @param {*} config.pageSize 每页加载数量
 * @param {*} config.params 其他参数
 * @param {*} config.isRefreshBottom 是否触底刷新默认true
 * @returns
 */
export const useMoreList = (config) => {
  validateConfig(config);
  // 列表
  const listData = ref([]);
  // 加载状态 more:加载更多 / noMore:没有更多了 / loading:加载中
  const moreStatus = ref("more");
  // 是否触底刷新
  const isRefreshBottom = ref(true);
  // 初始是否自动加载列表
  const isAutoLoad = ref(true);
  // 分页参数
  const pagination = ref({
    currentPage: 1,
    pageSize: config?.pageSize || 10,
  });
  // api其他参数
  const params = ref(null);
  // 列表是否有数据
  const hasData = computed(() => listData.value.length > 0);
  // 加载更多
  const handleLoadMore = async () => {
    if (moreStatus.value !== "more") {
      return;
    }
    moreStatus.value = "loading";
    // 校验api函数是否合法
    if (!config.api || typeof config.api !== "function") {
      console.error("useMoreList 中 config.api 必须是一个函数");
      return;
    }
    const res = await config.api({
      ...pagination.value,
      ...params.value,
    });
    if (res.code === 0) {
      listData.value = [...listData.value, ...res.data];
      moreStatus.value = listData.value.length >= res.data.total ? "noMore" : "more";
    } else {
      moreStatus.value = "noMore";
    }
  };  // 初始化配置项
  const initConfig = () => {
    if (config.params) {
      params.value = reactive(config.params);
    }    if (config.isRefreshBottom !== undefined) {
      isRefreshBottom.value = config.isRefreshBottom;
    }    if (config.isAutoLoad !== undefined) {
      isAutoLoad.value = config.isAutoLoad;
    }    // 初始加载列表
    if (isAutoLoad.value) {
      handleLoadMore();
    }    // 是否绑定触底刷新事件
    if (isRefreshBottom.value) {
      onReachBottom(() => {
        pagination.value.currentPage++;
        handleLoadMore();
      });
    }
  };  // 重置列表
  const reset = () => {
    listData.value = [];
    moreStatus.value = "more";
    pagination.value.currentPage = 1;
    pagination.value.pageSize = config?.pageSize || 10;
    handleLoadMore();
  };  // 初始化配置项
  initConfig();  return {
    listData,
    moreStatus,
    hasData,
    pagination,
    params,
    handleLoadMore,
    reset,
  };
};

使用

实际业务中怎么用?很简单——引入 MoreList 组件和 useMoreList hook,把接口回调传入,然后在 slot 里渲染你的列表模板。下面是一份带删除功能的帖子列表示例,卡片用了九宫格图片布局,代码结构很清晰。

<script setup>
  import { ref } from "vue";
  import MoreList from "@/components/MoreList/index.vue";
  import { useMoreList } from "@/hooks/useMoreList.js";  const { params, listData, moreStatus, handleLoadMore, hasData, reset } = useMoreList({
    api: () => {},
  });  const postList = ref([
    {
      id: 1,
      title: "周末露营|风景超美",
      desc: "和朋友一起去山里露营,晚上看星星,空气特别清新~",
      images: ["https://picsum.photos/400/300?random=1"],
    },
    {
      id: 2,
      title: "日常穿搭分享",
      desc: "简约通勤风,舒适百搭,上班族必备~",
      images: [
        "https://picsum.photos/400/300?random=2",
        "https://picsum.photos/400/300?random=3",
        "https://picsum.photos/400/300?random=4",
      ],
    },
    {
      id: 3,
      title: "我的生活碎片|6图",
      desc: "记录生活中的美好瞬间,美食、风景、日常~",
      images: [
        "https://picsum.photos/400/300?random=5",
        "https://picsum.photos/400/300?random=6",
        "https://picsum.photos/400/300?random=7",
        "https://picsum.photos/400/300?random=8",
        "https://picsum.photos/400/300?random=9",
        "https://picsum.photos/400/300?random=10",
      ],
    },
    {
      id: 4,
      title: "九宫格生活照|9图",
      desc: "一次性分享9张生活照片,满满都是回忆~",
      images: [
        "https://picsum.photos/400/300?random=11",
        "https://picsum.photos/400/300?random=12",
        "https://picsum.photos/400/300?random=13",
        "https://picsum.photos/400/300?random=14",
        "https://picsum.photos/400/300?random=15",
        "https://picsum.photos/400/300?random=16",
        "https://picsum.photos/400/300?random=17",
        "https://picsum.photos/400/300?random=18",
        "https://picsum.photos/400/300?random=19",
      ],
    },
    {
      id: 5,
      title: "美食探店|打卡网红餐厅",
      desc: "终于打卡了这家网红餐厅,环境超棒,菜品也很好吃!",
      images: [
        "https://picsum.photos/400/300?random=20",
        "https://picsum.photos/400/300?random=21",
        "https://picsum.photos/400/300?random=22",
      ],
    },
    {
      id: 6,
      title: "读书分享|近期书单",
      desc: "最近读了几本书,分享给大家,每本都很值得一读~",
      images: ["https://picsum.photos/400/300?random=23"],
    },
  ]);  const handleDelete = (id) => {
    uni.showModal({
      title: "提示",
      content: "确定要删除这篇帖子吗?",
      success: (res) => {
        if (res.confirm) {
          const index = postList.value.findIndex((post) => post.id === id);
          if (index > -1) {
            postList.value.splice(index, 1);
            uni.showToast({
              title: "删除成功",
              icon: "success",
            });
          }
        }
      },
    });
  };
script><style lang="scss" scoped>
  .container {
    min-height: 100vh;
    padding: 24rpx;
    box-sizing: border-box;
  }  /* 列表容器 */
  .post-list {
    display: flex;
    flex-direction: column;
    gap: 32rpx;
  }  /* 帖子卡片 */
  .post-card {
    background: #fff;
    border-radius: 32rpx;
    padding: 32rpx;
    box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
  }  /* 标题 */
  .post-title {
    font-size: 34rpx;
    font-weight: 600;
    color: #1a1a1a;
    margin-bottom: 16rpx;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }  /* 描述 */
  .post-desc {
    font-size: 28rpx;
    color: #666;
    margin-bottom: 24rpx;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }  /* 图片布局:自动支持 1~9 张 */
  .post-images {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12rpx;
    margin-bottom: 28rpx;
    width: 100%;
  }  /* 1张图时占满整行 */
  .post-images.single {
    grid-template-columns: 1fr;
  }  .post-img {
    width: 100%;
    height: auto;
    aspect-ratio: 1/1;
    border-radius: 20rpx;
    background: #f0f2f5;
  }  /* 操作栏 */
  .post-actions {
    display: flex;
    justify-content: flex-end;
    padding-top: 24rpx;
    border-top: 2rpx solid #f0f2f5;
  }  /* 删除按钮 */
  .delete-btn {
    padding: 12rpx 28rpx;
    border-radius: 40rpx;
    background-color: #227cff;
    color: #fff;
    font-size: 26rpx;
  }
style>

结尾

整体来看,这套封装足够覆盖日常开发里 80% 的列表场景——初始加载、自动触底、缺省图、重置刷新,一应俱全。代码没有写得很复杂,核心逻辑就在 hook 里,跟 Vue2 的 mixin 思路有点像,但组合式 API 让数据流更清晰、类型推导更友好。如果以上逻辑还不能完全匹配你的业务,欢迎直接在评论区聊聊需求,后续会持续迭代优化。(实测效果稳定,没有发现什么副作用。)

来源:https://juejin.cn/post/7647437715006291977
上一篇响应式系统对比:链表提升computed更新效率 下一篇纯前端实现海报生成与下载方案
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Layui弹出层监听子页面键盘快捷键实现方法
前端开发 · 2026-07-06

Layui弹出层监听子页面键盘快捷键实现方法

子页面键盘事件监听需在DOM加载完成后绑定,父页无法直接监听子页按键,必须由子页自身监听后通过parent或postMessage通知父页。典型写法为子页调用父页已定义的关闭函数。需注意焦点状态、输入法及layui版本兼容性等陷阱。

Layui表单提交时携带当前页面Meta信息的实现方法
前端开发 · 2026-07-06

Layui表单提交时携带当前页面Meta信息的实现方法

Layui表单提交不会自动携带页面Meta信息,需在form on( submit )回调中手动读取meta内容并拼接到表单数据,注意后端字段映射及特殊字符编码,多meta时按需选取。

HTML5拖拽事件流状态转移监控调试
前端开发 · 2026-07-06

HTML5拖拽事件流状态转移监控调试

HTML5拖拽事件流易因漏监听或未调用preventDefault而中断。需掌握dragstart设置数据、dragover接受放置、drop触发条件等关键点。通过统一日志捕获事件上下文、识别常见状态丢失场景并配合可视化面板,可清晰定位拖拽过程断点。

uni-app实现小红书商品详情图卡片切换
前端开发 · 2026-07-06

uni-app实现小红书商品详情图卡片切换

通过手写touch事件与transform控制五张卡片,动态计算translateX、scale、opacity及z-index模拟层叠滑动效果。滑动距离超过80rpx触发切换,否则复位。图片仅渲染当前及前后两张,有效优化加载性能与渲染效率。

图像旋转倾斜与扭曲的Canvas像素矩阵变换
前端开发 · 2026-07-06

图像旋转倾斜与扭曲的Canvas像素矩阵变换

Canvas图像变形本质是操作坐标系,图像被动跟随。旋转需先平移原点至目标中心再旋转后复位;倾斜通过仿射变换矩阵实现;扭曲无原生API,可用分块模拟或转用WebGL。每次变换前保存状态,完成后恢复,避免坐标系偏移。