vue3+uniapp经典hook方式实现一个更多加载的列表组件

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

最终效果示例:

列表不重要,什么列表都行,通过slot实现自己的列表

前言

为了快速的在uniapp中实现列表功能上的开发,不必繁琐的每次都要去写列表实现方式,统一的在hook完成初始加载,搜索,分页,加载更多,缺省页提示等等......

组件实现

创建MoreList组件

html 复制代码
<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钩子函数

js 复制代码
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,
  };
};

使用

js 复制代码
<template>
  <view class="container" :style="{ backgroundColor: postList.length > 0 ? '#f5f7fa' : '#ffffff' }">
    <MoreList :moreStatus="moreStatus" :hasData="true" @loadMore="handleLoadMore">
      <view class="post-list">
        <view class="post-card" v-for="post in postList" :key="post.id">
          <view class="post-title">{{ post.title }}</view>
          <view class="post-desc">{{ post.desc }}</view>
          <view class="post-images" :class="{ single: post.images.length === 1 }">
            <image v-for="(img, index) in post.images" :key="index" :src="img" class="post-img" mode="aspectFill" />
          </view>
          <view class="post-actions">
            <view class="delete-btn" @click="handleDelete(post.id)">删除帖子</view>
          </view>
        </view>
      </view>
    </MoreList>
  </view>
</template>

<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>

结尾

总的来说可以满足基础使用,亲测有效,不过时间长促,尚未完善的很好,可以根据自己的需求来定制,代码总的来说很好理解,如vue2的混入概念类似。如果需要特别的定制,也可以在评论区告诉我,我尽量完善其代码。

相关推荐
HjhIron1 小时前
从手机号校验到模板引擎:正则表达式的实战之旅
javascript
浩风祭月1 小时前
前端错误监控方案对比:Sentry SaaS vs 自部署 vs 纯开源组合
前端·openai·ai编程
用户938515635071 小时前
前端必会:从 Fetch 到 DeepSeek,一篇搞懂 HTTP 请求的方方面面
javascript·架构
ze_juejin1 小时前
promise和try catch的比较
前端
用户573240037231 小时前
AgentForge-WX v0.3.0:12项更新 + 框架重新定位,把微信小程序AI对话的坑全填了
前端
米丘1 小时前
HTTP 传输层 TCP 三次握手 / 四次挥手
前端·网络协议·http
小lan猫1 小时前
多域 RAG 知识库:从 Vue 前端到 NestJS + PGVector 的全栈实践
前端·人工智能·typescript
半个烧饼不加肉1 小时前
JS 底层探究--执行上下文
开发语言·前端·javascript
极光技术熊1 小时前
从零构建在线Excel:一个Java全栈工程师的实战记录
前端·后端