更新记录

1.0.1(2025-01-21) 下载此版本

  • readme.md

1.0.0(2025-01-20) 下载此版本

1.0.0(2024-03-20)

  • 首次发布
  • 基础功能:
    • 下拉刷新
    • 上拉加载更多
    • 空状态展示
    • 加载状态展示
    • 自定义高度
    • 自定义提示文本
    • 自定义空状态图片
  • 动画效果:
    • 加载动画
    • 空状态淡入动画
    • 空状态图片漂浮动画
    • 加载更多文字弹入动画
  • 性能优化:
    • 滚动预加载
    • 加载状态优化
    • 内容区域高度自适应
  • 其他特性:
    • Vue3 组合式API
    • TypeScript 支持
    • 完整的类型定义
    • 支持按需引入
    • 支持自定义样式

平台兼容性

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

名称 说明
default 列表内容插槽

注意事项

  1. 高度设置
  • 需要设置合适的高度,建议使用 calc 计算
  • 注意减去导航栏、tabbar 等固定元素的高度
  • 默认 100vh,全屏展示
  1. 性能优化
  • 列表项必须设置 key 值
  • 避免在列表项中使用大量复杂组件
  • loading 状态下会自动禁用刷新和加载
  1. 状态处理
  • 建议处理接口异常情况
  • 注意处理空数据状态
  • hasMore 控制加载更多的显示
  1. 图片资源
  • 空状态图片需要自行提供
  • 建议将图片放在 static 目录下

示例项目

完整示例项目请参考

更新日志

查看更新日志

问题反馈

如果您在使用过程中遇到问题,欢迎在评论区反馈。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

使用中有什么不明白的地方,就向插件作者提问吧~ 我要提问