大列表

Vue3+ElementPlus大数据列表实现(虚拟滚动/分页/懒加载)

#tech / dev / frame #type / howto #status / growing

Vue3 + Element Plus 大数据列表实现指南

概述

在处理大量数据时,前端需要采用合适的策略来优化用户体验和性能。本指南介绍三种主要的数据展示策略:

  1. 传统翻页 (Pagination) - 适合结构化数据浏览
  2. 虚拟滚动 (Virtual Scrolling) - 适合大量数据快速浏览
  3. 懒加载 (Lazy Loading) - 适合无限滚动场景

1. 传统翻页实现

1.1 后端接口设计

// API 请求参数
interface PostListReqVO {
  compId: string | number
  pageNo: number      // 页码,从1开始
  pageSize: number    // 每页条数,建议10-50
  name?: string       // 可选搜索条件
  code?: string
  // ... 其他搜索条件
}

// API 响应数据
interface ApiResponse<T> {
  code: number
  data: {
    list: T[]         // 当前页数据
    total: number     // 总记录数
  }
  msg: string
}

1.2 前端组件实现

<script setup lang="ts">
// 1. 数据状态定义
const loading = ref(false)
const list = ref<PostRespVO[]>([])
const total = ref(0)

// 2. 查询参数(响应式)
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  compId: undefined,
  name: undefined,
  // ... 其他参数
})

// 3. 获取列表数据
const getList = async () => {
  loading.value = true
  try {
    const data = await JobDescApi.getPostList(queryParams)
    list.value = data.list || []
    total.value = data.total || 0
  } finally {
    loading.value = false
  }
}

// 4. 搜索功能
const handleQuery = () => {
  queryParams.pageNo = 1  // 重置到第一页
  getList()
}

// 5. 重置功能
const resetQuery = () => {
  queryParams.pageNo = 1
  queryParams.name = undefined
  // 重置其他查询条件
  getList()
}
</script>

<template>
  <!-- 搜索表单 -->
  <ContentWrap>
    <el-form :model="queryParams" :inline="true">
      <el-form-item label="岗位名称" prop="name">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入岗位名称"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery">搜索</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
  </ContentWrap>

  <!-- 数据表格 -->
  <ContentWrap>
    <el-table v-loading="loading" :data="list">
      <el-table-column label="岗位名称" prop="name" />
      <el-table-column label="岗位编码" prop="code" />
      <el-table-column label="操作" width="120">
        <template #default="scope">
          <el-button @click="handleEdit(scope.row)">编辑</el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- 分页组件 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
  </ContentWrap>
</template>

1.3 关键实现点

  1. 响应式查询参数: 使用 reactive 包装查询条件
  2. 页码重置: 搜索时重置 pageNo = 1
  3. 加载状态: 使用 loading 提供用户反馈
  4. 分页组件: 使用双向绑定 v-model:pagev-model:limit

2. 虚拟滚动实现

2.1 适用场景

  • 数据量巨大 (>1000条)
  • 不需要复杂搜索和筛选
  • 用户需要快速浏览数据

2.2 核心原理

// 虚拟滚动核心逻辑
interface VirtualScrollConfig {
  itemHeight: number      // 每行高度
  containerHeight: number // 容器高度
  buffer: number         // 缓冲区行数
}

const useVirtualScroll = (data: Ref<any[]>, config: VirtualScrollConfig) => {
  const scrollTop = ref(0)
  
  // 计算可见范围
  const visibleRange = computed(() => {
    const { itemHeight, containerHeight, buffer } = config
    const visibleCount = Math.ceil(containerHeight / itemHeight)
    const start = Math.max(0, Math.floor(scrollTop.value / itemHeight) - buffer)
    const end = Math.min(data.value.length, start + visibleCount + buffer * 2)
    return { start, end }
  })
  
  // 可见数据
  const visibleData = computed(() => {
    const { start, end } = visibleRange.value
    return data.value.slice(start, end)
  })
  
  // 滚动处理
  const handleScroll = (event: Event) => {
    scrollTop.value = (event.target as HTMLElement).scrollTop
  }
  
  return {
    visibleData,
    visibleRange,
    handleScroll,
    totalHeight: computed(() => data.value.length * config.itemHeight)
  }
}

2.3 实现示例

<script setup lang="ts">
const allData = ref<PostRespVO[]>([])
const { visibleData, handleScroll, totalHeight } = useVirtualScroll(
  allData,
  {
    itemHeight: 50,
    containerHeight: 400,
    buffer: 5
  }
)
</script>

<template>
  <div class="virtual-list" @scroll="handleScroll">
    <div :style="{ height: totalHeight + 'px' }" class="virtual-content">
      <div
        v-for="item in visibleData"
        :key="item.id"
        class="virtual-item"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<style>
.virtual-list {
  height: 400px;
  overflow-y: auto;
}
.virtual-item {
  height: 50px;
  border-bottom: 1px solid #eee;
}
</style>

3. 懒加载 (无限滚动) 实现

3.1 适用场景

  • 类似社交媒体的信息流
  • 数据按时间或相关性排序
  • 用户倾向于连续浏览

3.2 核心实现

// 懒加载 Hook
const useInfiniteScroll = <T>(
  fetchFn: (page: number, pageSize: number) => Promise<{ list: T[], hasMore: boolean }>
) => {
  const list = ref<T[]>([])
  const loading = ref(false)
  const hasMore = ref(true)
  const page = ref(1)
  const pageSize = 20

  // 加载更多数据
  const loadMore = async () => {
    if (loading.value || !hasMore.value) return

    loading.value = true
    try {
      const data = await fetchFn(page.value, pageSize)
      
      if (page.value === 1) {
        list.value = data.list
      } else {
        list.value.push(...data.list)
      }
      
      hasMore.value = data.hasMore
      page.value++
    } finally {
      loading.value = false
    }
  }

  // 重置数据
  const refresh = async () => {
    page.value = 1
    hasMore.value = true
    await loadMore()
  }

  // 滚动到底部检测
  const handleScroll = (event: Event) => {
    const target = event.target as HTMLElement
    const { scrollTop, scrollHeight, clientHeight } = target
    
    // 距离底部50px时开始加载
    if (scrollTop + clientHeight >= scrollHeight - 50) {
      loadMore()
    }
  }

  return {
    list,
    loading,
    hasMore,
    loadMore,
    refresh,
    handleScroll
  }
}

3.3 使用示例

<script setup lang="ts">
const fetchPosts = async (page: number, pageSize: number) => {
  const response = await api.getPosts({ page, pageSize })
  return {
    list: response.data.list,
    hasMore: response.data.list.length === pageSize
  }
}

const { list, loading, hasMore, refresh, handleScroll } = useInfiniteScroll(fetchPosts)

onMounted(() => {
  refresh()
})
</script>

<template>
  <div class="infinite-scroll" @scroll="handleScroll">
    <div v-for="item in list" :key="item.id" class="list-item">
      {{ item.title }}
    </div>
    
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">
      <el-loading />
    </div>
    
    <!-- 没有更多数据 -->
    <div v-else-if="!hasMore" class="no-more">
      没有更多数据了
    </div>
  </div>
</template>

4. 性能优化建议

4.1 数据处理优化

// 1. 使用 Object.freeze 冻结只读数据
const processListData = (rawData: any[]) => {
  return rawData.map(item => Object.freeze({
    ...item,
    // 预处理计算属性
    displayName: `${item.name} (${item.code})`
  }))
}

// 2. 防抖搜索
const debouncedSearch = debounce(() => {
  handleQuery()
}, 300)

// 3. 缓存搜索结果
const searchCache = new Map<string, any>()
const getCachedData = (key: string, fetchFn: () => Promise<any>) => {
  if (searchCache.has(key)) {
    return Promise.resolve(searchCache.get(key))
  }
  return fetchFn().then(data => {
    searchCache.set(key, data)
    return data
  })
}

4.2 用户体验优化

<template>
  <!-- 1. 骨架屏 -->
  <el-skeleton v-if="loading && !list.length" :rows="10" animated />
  
  <!-- 2. 空状态 -->
  <el-empty v-else-if="!loading && !list.length" description="暂无数据" />
  
  <!-- 3. 错误状态 -->
  <el-result
    v-else-if="error"
    icon="error"
    title="加载失败"
    :sub-title="error.message"
  >
    <template #extra>
      <el-button type="primary" @click="retry">重试</el-button>
    </template>
  </el-result>
</template>

5. 最佳实践总结

5.1 选择合适的策略

场景数据量用户行为推荐方案
管理后台<10k查询浏览传统翻页
数据分析>10k快速浏览虚拟滚动
内容平台不限连续浏览懒加载

5.2 关键技术要点

  1. API设计: 统一的分页参数和响应格式
  2. 状态管理: 合理使用响应式数据
  3. 用户反馈: 加载状态、错误处理、空状态
  4. 性能优化: 防抖、缓存、预处理
  5. 用户体验: 骨架屏、平滑过渡、快捷操作

5.3 注意事项

  • 合理设置 pageSize,通常 10-50 条
  • 搜索时要重置页码
  • 处理网络异常和错误状态
  • 考虑 SEO 和路由状态同步
  • 移动端适配和触摸优化

通过以上实现方案,可以有效处理各种规模的数据展示需求,提供良好的用户体验。

创建于 2025/1/1 更新于 2026/5/27