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的混入概念类似。如果需要特别的定制,也可以在评论区告诉我,我尽量完善其代码。