You need to sign in or sign up before continuing.
LoadMoreList-迁移指南.md 10.3 KB

LoadMoreList 组件迁移指南

📊 组件对比

迁移前(week-hot-material)

代码行数: ~283 行

核心逻辑:

  • 分页状态管理(散落在各处)
  • useReachBottom + 防抖处理
  • 加载状态(loadingloadingMore
  • 列表渲染、空状态、加载提示
  • 列表项动画效果

问题:

  • ❌ 代码重复(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行代码)

核心改进

  1. 代码复用: 分页逻辑集中在 LoadMoreList 组件
  2. 易于维护: 修改动效只需改一个文件
  3. 一致性: 所有页面的动效保持一致
  4. 可测试性: 组件可以独立测试
  5. 可扩展性: 新增 slot 或 props 即可扩展功能

🚀 下一步

建议迁移顺序

  1. week-hot-material - 最简单,已完成
  2. message - 简单,支持下拉刷新
  3. product-center - 中等,带搜索和Tabs
  4. material-list - 复杂,带搜索、Tabs、分类缓存
  5. search - 复杂,带搜索、双Tab、自动选择

可选优化

  1. 提取 composable: usePageList - 统一分页状态管理
  2. 添加虚拟滚动: 对于超长列表(>1000项)
  3. 添加骨架屏: 提升首次加载体验
  4. 添加错误重试: 加载失败时自动重试

💡 最佳实践

✅ 推荐做法

  1. 使用 key-field prop: 确保列表更新正确
  2. 区分 loadingloadingMore: 提升用户体验
  3. 使用 JSDoc 注释: 提升代码可读性
  4. 使用 slot 自定义: 保持组件灵活性

❌ 避免做法

  1. 在 slot 中处理分页: 应该在父组件处理
  2. 忽略 key-field: 可能导致列表更新异常
  3. 直接修改 props: 应该通过事件通知父组件
  4. 过度自定义 slot: 能用默认的就用默认的

🔗 相关文档


创建时间: 2026-02-08 维护者: Claude Code 版本: 1.0.0