LoadMoreList 迁移指南.md
10.3 KB
LoadMoreList 组件迁移指南
📊 组件对比
迁移前(week-hot-material)
代码行数: ~283 行
核心逻辑:
- 分页状态管理(散落在各处)
-
useReachBottom+ 防抖处理 - 加载状态(
loading、loadingMore) - 列表渲染、空状态、加载提示
- 列表项动画效果
问题:
- ❌ 代码重复(5个页面都实现了相同逻辑)
- ❌ 维护困难(修改动效需要改5个文件)
- ❌ 新页面集成成本高(需要复制粘贴大量代码)
迁移后(使用 LoadMoreList)
代码行数: ~230 行(减少 ~18%)
核心逻辑:
- ✅ 分页状态管理(集中管理)
- ✅
useReachBottom+ 防抖处理(组件内部) - ✅ 加载状态(通过 props 传入)
- ✅ 列表渲染、空状态、加载提示(组件内部)
- ✅ 列表项动画效果(组件内部)
优势:
- ✅ 逻辑复用(5个页面共享同一份代码)
- ✅ 维护简单(修改组件即可影响所有页面)
- ✅ 新页面集成成本低(只需 10 行代码)
🎯 组件设计
Props 设计
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
list |
Array |
[] |
列表数据源 |
page |
Number |
- | 当前页码(必需) |
pageSize |
Number |
10 |
每页数量 |
hasMore |
Boolean |
true |
是否还有更多数据 |
loading |
Boolean |
false |
首次加载状态 |
loadingMore |
Boolean |
false |
加载更多状态 |
keyField |
String |
'id' |
唯一标识字段名 |
showHeader |
Boolean |
true |
是否显示固定头部 |
enablePullDownRefresh |
Boolean |
false |
是否启用下拉刷新 |
noPadding |
Boolean |
false |
列表容器是否不需要padding |
Events 设计
| Event | 参数 | 说明 |
|---|---|---|
load-more |
page: number |
触底加载更多时触发,传入下一页页码 |
refresh |
- | 下拉刷新时触发 |
Slots 设计
| Slot | 说明 | 作用域参数 |
|---|---|---|
header |
自定义头部区域 | - |
item |
自定义列表项 | { item, index } |
loading |
自定义首次加载状态 | - |
loading-more |
自定义加载更多状态 | - |
empty |
自定义空状态 | - |
no-more |
自定义"没有更多"提示 | - |
📝 使用示例
1. 简单列表(week-hot-material、message)
<template>
<LoadMoreList
:list="currentList"
:page="currentPage"
:page-size="pageSize"
:has-more="hasMore"
:loading="loading"
:loading-more="loadingMore"
key-field="meta_id"
@load-more="handleLoadMore"
>
<template #header>
<NavHeader title="本周热门资料" />
</template>
<template #item="{ item }">
<MaterialCard
:id="item.meta_id"
:title="item.name"
:file-size="item.size"
:collected="item.collected"
:extension="item.extension"
:download-url="item.downloadUrl"
/>
</template>
</LoadMoreList>
</template>
<script setup>
import { ref } from 'vue'
import { useLoad } from '@tarojs/taro'
import LoadMoreList from '@/components/LoadMoreList.vue'
import MaterialCard from '@/components/MaterialCard.vue'
const currentList = ref([])
const currentPage = ref(0)
const pageSize = 20
const hasMore = ref(true)
const loading = ref(false)
const loadingMore = ref(false)
// 加载更多处理
const handleLoadMore = async (page) => {
currentPage.value = page
// 调用API加载数据...
}
</script>
2. 带搜索和Tabs的列表(material-list、product-center、search)
<template>
<LoadMoreList
:list="products"
:page="page"
:page-size="limit"
:has-more="hasMore"
:loading="loading"
:loading-more="loadingMore"
@load-more="handleLoadMore"
>
<template #header>
<NavHeader title="产品中心" />
<view class="px-[24rpx] py-[16rpx] bg-white">
<SearchBar
v-model="searchValue"
placeholder="搜索产品名称..."
@search="onSearch"
/>
</view>
<nut-tabs v-model="activeTabId">
<template #titles>
<view class="filter-tabs-wrapper">
<view
v-for="item in tabsData"
:key="item.id"
:class="[
'filter-tab-item',
activeTabId === item.id ? 'filter-tab-active' : 'filter-tab-inactive'
]"
@tap="onTabClick(item.id)"
>
<text class="filter-tab-text">{{ item.name }}</text>
</view>
</view>
</template>
</nut-tabs>
</template>
<template #item="{ item }">
<ProductCard :product="item" />
</template>
<template #empty>
<nut-empty description="暂无相关产品" image="empty" />
</template>
</LoadMoreList>
</template>
3. 带下拉刷新的列表(message)
<template>
<LoadMoreList
:list="messageList"
:page="page"
:page-size="limit"
:has-more="hasMore"
:loading="loading"
:loading-more="loadingMore"
:enable-pull-down-refresh="true"
@load-more="handleLoadMore"
@refresh="handleRefresh"
>
<template #header>
<NavHeader title="我的消息" />
</template>
<template #item="{ item }">
<view class="message-item" @tap="handleItemClick(item)">
<view class="title">{{ item.title }}</view>
<view class="intro">{{ item.intro }}</view>
</view>
</template>
</LoadMoreList>
</template>
<script setup>
const handleRefresh = () => {
page.value = 1
fetchMessageList(true)
}
</script>
🔄 迁移步骤
第 1 步:导入组件
import LoadMoreList from '@/components/LoadMoreList.vue'
第 2 步:定义状态
// 页面状态
const currentList = ref([])
const currentPage = ref(0) // 或 ref(1),根据API要求
const pageSize = 10
const hasMore = ref(true)
const loading = ref(false)
const loadingMore = ref(false)
第 3 步:定义加载函数
const fetchData = async (params = {}, isLoadMore = false) => {
try {
// 设置加载状态
if (isLoadMore) {
loadingMore.value = true
} else {
loading.value = true
}
// 调用 API
const res = await yourAPI(params)
if (res.code === 1) {
const listData = res.data.list || []
if (isLoadMore) {
// 追加数据
currentList.value = [...currentList.value, ...listData]
} else {
// 替换数据
currentList.value = listData
}
// 更新 hasMore
hasMore.value = listData.length >= pageSize
}
} catch (err) {
console.error('获取数据失败:', err)
} finally {
// 清除加载状态
if (isLoadMore) {
loadingMore.value = false
} else {
loading.value = false
}
}
}
第 4 步:处理加载更多事件
const handleLoadMore = async (page) => {
currentPage.value = page
await fetchData({ page, limit: pageSize }, true)
}
第 5 步:使用组件
<template>
<LoadMoreList
:list="currentList"
:page="currentPage"
:page-size="pageSize"
:has-more="hasMore"
:loading="loading"
:loading-more="loadingMore"
@load-more="handleLoadMore"
>
<template #header>
<!-- 自定义头部 -->
</template>
<template #item="{ item }">
<!-- 自定义列表项 -->
</template>
</LoadMoreList>
</template>
🎨 动效特性
列表项动画
效果: 列表项逐个进入(从下往上滑入)
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.list-item {
animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}
实现:
-
backwards: 动画在第一帧就应用(避免闪烁) -
animationDelay: 每个列表项延迟 50ms,形成波浪效果
加载动画
效果: 旋转的绿色圆圈
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #E5E7EB;
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
📊 迁移前后对比
week-hot-material
| 指标 | 迁移前 | 迁移后 | 改善 |
|---|---|---|---|
| 代码行数 | 283 行 | 230 行 | -18% |
| 模板代码 | 55 行 | 36 行 | -34% |
| 脚本代码 | 103 行 | 135 行 | +31%(JSDoc注释) |
| 样式代码 | 68 行 | 3 行 | -96% |
| 分页逻辑 | 散落在各处 | 集中管理 | ✅ |
| 可维护性 | 低 | 高 | ✅ |
| 新页面集成 | 难(复制粘贴) | 易(10行代码) | ✅ |
核心改进
-
代码复用: 分页逻辑集中在
LoadMoreList组件 - 易于维护: 修改动效只需改一个文件
- 一致性: 所有页面的动效保持一致
- 可测试性: 组件可以独立测试
- 可扩展性: 新增 slot 或 props 即可扩展功能
🚀 下一步
建议迁移顺序
- ✅ week-hot-material - 最简单,已完成
- message - 简单,支持下拉刷新
- product-center - 中等,带搜索和Tabs
- material-list - 复杂,带搜索、Tabs、分类缓存
- search - 复杂,带搜索、双Tab、自动选择
可选优化
-
提取 composable:
usePageList- 统一分页状态管理 - 添加虚拟滚动: 对于超长列表(>1000项)
- 添加骨架屏: 提升首次加载体验
- 添加错误重试: 加载失败时自动重试
💡 最佳实践
✅ 推荐做法
-
使用
key-fieldprop: 确保列表更新正确 -
区分
loading和loadingMore: 提升用户体验 - 使用 JSDoc 注释: 提升代码可读性
- 使用 slot 自定义: 保持组件灵活性
❌ 避免做法
- 在 slot 中处理分页: 应该在父组件处理
-
忽略
key-field: 可能导致列表更新异常 - 直接修改 props: 应该通过事件通知父组件
- 过度自定义 slot: 能用默认的就用默认的
🔗 相关文档
创建时间: 2026-02-08 维护者: Claude Code 版本: 1.0.0