更新记录
1.0.1(2025-01-21)
下载此版本
1.0.0(2025-01-20)
下载此版本
1.0.0(2024-03-20)
- 首次发布
- 基础功能:
- 下拉刷新
- 上拉加载更多
- 空状态展示
- 加载状态展示
- 自定义高度
- 自定义提示文本
- 自定义空状态图片
- 动画效果:
- 加载动画
- 空状态淡入动画
- 空状态图片漂浮动画
- 加载更多文字弹入动画
- 性能优化:
- 其他特性:
- Vue3 组合式API
- TypeScript 支持
- 完整的类型定义
- 支持按需引入
- 支持自定义样式
平台兼容性
App |
快应用 |
微信小程序 |
支付宝小程序 |
百度小程序 |
字节小程序 |
QQ小程序 |
HBuilderX 3.1.0 app-vue app-nvue |
√ |
√ |
√ |
√ |
√ |
√ |
钉钉小程序 |
快手小程序 |
飞书小程序 |
京东小程序 |
鸿蒙元服务 |
× |
× |
× |
× |
× |
H5-Safari |
Android Browser |
微信浏览器(Android) |
QQ浏览器(Android) |
Chrome |
IE |
Edge |
Firefox |
PC-Safari |
√ |
√ |
√ |
√ |
√ |
√ |
√ |
√ |
√ |
x-list 通用列表组件
一个支持下拉刷新和上拉加载更多的通用列表组件,适用于各种列表场景。
特性
- 支持下拉刷新
- 支持上拉加载更多
- 支持空状态展示
- 支持加载状态展示
- 支持自定义高度
- 支持自定义提示文本
- 支持自定义空状态图片
- 优雅的动画效果
- Vue3 组合式 API
- TypeScript 支持
安装
在插件市场中搜索并导入 x-list
基础用法
<template>
<view class="container">
<x-list
:list="dataList"
:loading="loading"
:has-more="hasMore"
:empty-text="'暂无测试数据'"
:height="'calc(100vh - 44px)'"
@refresh="onRefresh"
@loadmore="onLoadMore"
>
<view class="list-content">
<!-- 列表项 -->
<view
class="list-item slide-in"
v-for="(item, index) in dataList"
:key="item.id"
>
<view class="item-header">
<text class="title">测试项 #{{item.id}}</text>
<text class="time">{{item.time}}</text>
</view>
<view class="item-content">
<image class="thumb" :src="item.image" mode="aspectFill"></image>
<view class="info">
<text class="desc">{{item.description}}</text>
<view class="tags">
<text class="tag" v-for="(tag, tagIndex) in item.tags" :key="tagIndex">
{{tag}}
</text>
</view>
</view>
</view>
<view class="item-footer">
<text class="count">浏览 {{item.viewCount}}</text>
<button class="action-btn" @click="handleAction(item)">查看详情</button>
</view>
</view>
</view>
</x-list>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 状态定义
const dataList = ref([])
const loading = ref(false)
const hasMore = ref(true)
const page = ref(1)
const pageSize = 5
// 生成测试数据
const generateMockData = (startIndex) => {
return Array(pageSize).fill().map((_, index) => ({
id: startIndex + index + 1,
time: formatDate(new Date(Date.now() - Math.random() * 10000000000)),
image: `https://picsum.photos/200/200?random=${startIndex + index}`,
description: `这是一段测试描述文本,用来演示列表项的展示效果。这是第 ${startIndex + index + 1} 条数据。`,
tags: ['标签1', '标签2', '标签3'].slice(0, Math.floor(Math.random() * 3) + 1),
viewCount: Math.floor(Math.random() * 1000)
}))
}
// 格式化日期
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
// 加载数据
const loadData = async (isRefresh = false) => {
if (loading.value || (!hasMore.value && !isRefresh)) return
loading.value = true
let newData
try {
// 模拟接口延迟
await new Promise(resolve => setTimeout(resolve, 1000))
// 生成模拟数据
const startIndex = isRefresh ? 0 : dataList.value.length
newData = generateMockData(startIndex)
// 模拟最多加载5页数据
hasMore.value = page.value < 5
} catch (e) {
console.error(e)
uni.showToast({
title: '加载失败',
icon: 'none'
})
return
} finally {
loading.value = false
}
// 数据更新放在loading之后,避免loading状态影响渲染
if (isRefresh) {
dataList.value = newData
} else {
dataList.value = [...dataList.value, ...newData]
}
}
// 刷新处理
const onRefresh = async () => {
console.log('onRefresh');
page.value = 1
await loadData(true)
}
// 加载更多处理
const onLoadMore = async () => {
if (hasMore.value && !loading.value) {
page.value++
await loadData()
}
}
// 点击操作
const handleAction = (item) => {
uni.showToast({
title: `点击了项目 #${item.id}`,
icon: 'none'
})
}
// 页面加载时初始化数据
onMounted(() => {
loadData(true)
})
</script>
<style>
.container {
background-color: #f8f8f8;
box-sizing: border-box;
height: 100%;
}
.list-content {
padding: 20rpx;
}
.list-item {
margin: 0 20rpx 20rpx;
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.time {
font-size: 24rpx;
color: #999;
}
.item-content {
display: flex;
margin-bottom: 20rpx;
}
.thumb {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
margin-right: 20rpx;
transition: opacity 0.3s ease;
}
.thumb[lazy-load] {
opacity: 0;
}
.thumb.loaded {
opacity: 1;
}
.info {
flex: 1;
}
.desc {
font-size: 28rpx;
color: #666;
line-height: 1.5;
margin-bottom: 20rpx;
}
.tags {
display: flex;
flex-wrap: wrap;
}
.tag {
font-size: 24rpx;
color: #007AFF;
background-color: rgba(0, 122, 255, 0.1);
padding: 4rpx 16rpx;
border-radius: 20rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
transition: all 0.3s ease;
}
.tag:active {
transform: scale(0.9);
opacity: 0.8;
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 1rpx solid #eee;
}
.count {
font-size: 24rpx;
color: #999;
}
.action-btn {
font-size: 24rpx;
color: #007AFF;
background-color: rgba(0, 122, 255, 0.1);
padding: 10rpx 30rpx;
border-radius: 30rpx;
transition: all 0.3s ease;
}
.action-btn:active {
transform: scale(0.95);
background-color: rgba(0, 122, 255, 0.2);
}
/* 添加滑入动画 */
.slide-in {
animation: slideIn 0.3s ease-out both;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(30rpx);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 点击效果 */
.list-item:active {
transform: scale(0.98);
box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
}
/* 标签动画 */
.tag {
transition: all 0.3s ease;
}
/* 按钮动画 */
.action-btn {
transition: all 0.3s ease;
}
/* 图片加载动画 */
.thumb {
transition: opacity 0.3s ease;
}
.thumb[lazy-load] {
opacity: 0;
}
.thumb.loaded {
opacity: 1;
}
</style>
API
Props
属性名 |
说明 |
类型 |
默认值 |
list |
列表数据 |
Array |
[] |
loading |
是否加载中 |
Boolean |
false |
hasMore |
是否有更多数据 |
Boolean |
true |
height |
列表高度 |
String |
'100vh' |
enablePullRefresh |
启用下拉刷新 |
Boolean |
true |
enableLoadMore |
启用上拉加载 |
Boolean |
true |
emptyText |
空状态文本 |
String |
'暂无数据' |
emptyImage |
空状态图片 |
String |
'/static/empty.png' |
loadingText |
加载中文本 |
String |
'正在加载...' |
noMoreText |
无更多数据文本 |
String |
'没有更多数据了' |
refreshText |
下拉刷新文本 |
String |
'下拉可以刷新' |
Events
事件名 |
说明 |
回调参数 |
refresh |
下拉刷新触发 |
- |
loadmore |
上拉加载触发 |
- |
Methods
方法名 |
说明 |
参数 |
refresh |
手动触发刷新 |
- |
Slots
注意事项
- 高度设置
- 需要设置合适的高度,建议使用 calc 计算
- 注意减去导航栏、tabbar 等固定元素的高度
- 默认 100vh,全屏展示
- 性能优化
- 列表项必须设置 key 值
- 避免在列表项中使用大量复杂组件
- loading 状态下会自动禁用刷新和加载
- 状态处理
- 建议处理接口异常情况
- 注意处理空数据状态
- hasMore 控制加载更多的显示
- 图片资源
- 空状态图片需要自行提供
- 建议将图片放在 static 目录下
示例项目
完整示例项目请参考
更新日志
查看更新日志
问题反馈
如果您在使用过程中遇到问题,欢迎在评论区反馈。