Vue + Element UI 分页器封装:比直接用 el-pagination 更省心的通用方案

在后台管理系统里,分页几乎是列表页的标配。

如果每个页面都直接写 el-pagination,能用是能用,但很快就会遇到这些问题:

  • 每个页面都要重复写分页状态和事件
  • 切换页码、切换每页条数的逻辑不统一
  • 改分页样式要改很多处
  • 翻页后滚动行为不一致
  • 某些页面要显示"总数、页码、每页条数",某些页面不要,写法越来越散

所以更推荐的做法是:封装一个通用分页器组件,把列表页里最常见的分页逻辑统一收口。

下面我会先讲它的适用场景,再讲它相比直接使用 Element UI 的优势,最后给你一份可以直接拿去用的完整组件代码。


一、分页器封装适合哪些场景

这个组件特别适合下面这些场景:

1. 后台列表页

例如用户管理、角色管理、订单列表、日志列表、内容管理等。

这类页面通常都是:

  • 顶部筛选条件
  • 中间表格
  • 底部分页器

分页逻辑很标准,适合统一封装。

2. 服务端分页接口

如果接口返回的是这样的结构:

json 复制代码
{
  "rows": [],
  "total": 128
}

那就非常适合分页器组件。

页面只需要根据 pageNumpageSize 去请求接口,组件负责页码切换。

3. 多个页面共用同一种分页体验

比如你希望:

  • 默认布局一致
  • pageSize 选项一致
  • 切页后自动滚动到顶部
  • pageSize 改变时自动修正页码

这些都可以统一放进组件里。

4. 需要减少重复代码的项目

当列表页一多,直接写 el-pagination 的重复成本会很高。

封装后,页面代码会明显更干净。


二、直接用 el-pagination 的问题

Element UI 的 el-pagination 本身很好,但它更像一个"基础 UI 控件",而不是完整的列表分页方案。

例如你每个页面都可能要写这些东西:

vue 复制代码
<el-pagination
  :current-page="pageNum"
  :page-size="pageSize"
  :total="total"
  layout="total, sizes, prev, pager, next, jumper"
  :page-sizes="[10, 20, 30, 50]"
  @size-change="handleSizeChange"
  @current-change="handleCurrentChange"
/>

然后在方法里继续写:

js 复制代码
handleSizeChange(val) {
  this.pageSize = val
  this.pageNum = 1
  this.getList()
}

handleCurrentChange(val) {
  this.pageNum = val
  this.getList()
}

看起来不复杂,但问题在于:

  • 每个页面都要写一遍
  • 翻页和切 pageSize 的逻辑容易不统一
  • 如果要加"滚动到顶部",还得每页单独补
  • 如果要统一改样式或 pageSizes,得改很多文件

所以更好的做法是:把这些高频逻辑封装成一个组件


三、封装分页器的核心价值

封装之后,分页器不再只是"页面底部的一排按钮",而变成了一个可复用的列表基础能力。

它的价值主要体现在这几个方面:

1. 降低重复代码

页面只关心"什么时候重新拉数据",不用每次都重复写分页交互。

2. 统一交互体验

比如:

  • 切页后自动滚动到顶部
  • 切换每页条数后自动修正页码
  • 默认分页布局一致

这些都由组件统一处理。

3. 更容易维护

以后你想改:

  • 分页布局
  • pageSize 选项
  • 是否需要滚动到顶部
  • 是否显示背景色

只需要改一个组件。

4. 更适合团队协作

别人接手页面时,只需要记住一套用法。

分页逻辑统一,页面代码也更容易读。


四、完整分页器组件代码

下面给你一份基于 Vue 2 + Element UI 的完整通用分页器组件代码。

这份代码可以直接保存为 Pagination.vue 使用。

vue 复制代码
<template>
  <div v-show="!hidden" class="pagination-container">
    <el-pagination
      :background="background"
      :current-page.sync="currentPage"
      :page-size.sync="pageSize"
      :layout="layout"
      :page-sizes="computedPageSizes"
      :pager-count="pagerCount"
      :total="total"
      v-bind="$attrs"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>

<script>
export default {
  name: 'Pagination',
  inheritAttrs: false,
  props: {
    total: {
      type: Number,
      required: true
    },
    page: {
      type: Number,
      default: 1
    },
    limit: {
      type: Number,
      default: 10
    },
    pageSizes: {
      type: Array,
      default: () => [10, 20, 30, 50]
    },
    pagerCount: {
      type: Number,
      default: 7
    },
    layout: {
      type: String,
      default: 'total, sizes, prev, pager, next, jumper'
    },
    background: {
      type: Boolean,
      default: true
    },
    autoScroll: {
      type: Boolean,
      default: true
    },
    hidden: {
      type: Boolean,
      default: false
    },
    scrollTop: {
      type: Number,
      default: 0
    },
    scrollBehavior: {
      type: String,
      default: 'smooth'
    }
  },
  computed: {
    currentPage: {
      get() {
        return this.page
      },
      set(val) {
        this.$emit('update:page', val)
      }
    },
    pageSize: {
      get() {
        return this.limit
      },
      set(val) {
        this.$emit('update:limit', val)
      }
    },
    computedPageSizes() {
      return this.pageSizes
    }
  },
  methods: {
    handleSizeChange(val) {
      const maxPage = Math.ceil(this.total / val) || 1
      const targetPage = this.currentPage > maxPage ? maxPage : this.currentPage

      this.currentPage = targetPage
      this.$emit('pagination', {
        page: this.currentPage,
        limit: val
      })

      if (this.autoScroll) {
        this.scrollToTop()
      }
    },
    handleCurrentChange(val) {
      this.$emit('pagination', {
        page: val,
        limit: this.pageSize
      })

      if (this.autoScroll) {
        this.scrollToTop()
      }
    },
    scrollToTop() {
      if (typeof window === 'undefined') return

      window.scrollTo({
        top: this.scrollTop,
        behavior: this.scrollBehavior
      })
    }
  }
}
</script>

<style scoped>
.pagination-container {
  background: #fff;
  padding: 24px 16px;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}

.pagination-container.is-hidden {
  display: none;
}
</style>

五、这个组件是怎么工作的

这份组件主要做了几件事:

1. 用 .sync 做页码和每页条数双向绑定

它把外部传进来的 pagelimit 包装成计算属性:

  • currentPage
  • pageSize

这样组件内部一改,外部状态也会同步更新。

2. 统一向父组件抛出 pagination 事件

无论是切页还是切 pageSize,父组件都只要监听一个事件:

vue 复制代码
@pagination="getList"

这样页面逻辑会非常统一。

3. 切换 pageSize 时自动修正页码

如果当前页已经超过最大页,组件会自动把页码修正到合法范围,避免出现"第 8 页不存在"的问题。

4. 默认滚动到顶部

翻页以后自动回到页面顶部,用户体验更好,特别适合长列表页。

5. 支持属性透传

通过 v-bind="$attrs",你仍然可以继续传 Element UI 支持的其他参数,不会把分页器能力封死。


六、组件使用方式

封装完之后,父组件里就非常简单了。

1. 定义分页状态

js 复制代码
data() {
  return {
    loading: false,
    tableData: [],
    page: {
      pageNum: 1,
      pageSize: 10,
      total: 0
    },
    queryParams: {
      keyword: ''
    }
  }
}

2. 在模板中使用分页器

vue 复制代码
<pagination
  v-show="page.total > 0"
  :total="page.total"
  :page.sync="page.pageNum"
  :limit.sync="page.pageSize"
  :page-sizes="[10, 20, 50, 100]"
  @pagination="getList"
/>

3. 请求列表数据

js 复制代码
methods: {
  getList() {
    this.loading = true
    apiList({
      ...this.queryParams,
      pageNum: this.page.pageNum,
      pageSize: this.page.pageSize
    }).then(res => {
      if (res.code === 200) {
        this.tableData = res.rows || []
        this.page.total = res.total || 0
      }
    }).finally(() => {
      this.loading = false
    })
  }
}

4. 查询和重置时重置页码

js 复制代码
handleSearch() {
  this.page.pageNum = 1
  this.getList()
}

handleReset() {
  this.queryParams = {
    keyword: ''
  }
  this.page.pageNum = 1
  this.getList()
}

七、推荐的列表页写法

一个比较标准的列表页结构可以参考下面这种:

vue 复制代码
<template>
  <div class="page">
    <el-form inline :model="queryParams">
      <el-form-item label="关键词">
        <el-input v-model="queryParams.keyword" placeholder="请输入关键词" clearable />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleSearch">查询</el-button>
        <el-button @click="handleReset">重置</el-button>
      </el-form-item>
    </el-form>

    <el-table :data="tableData" v-loading="loading">
      <el-table-column prop="name" label="名称" />
      <el-table-column prop="createTime" label="创建时间" />
    </el-table>

    <pagination
      :total="page.total"
      :page.sync="page.pageNum"
      :limit.sync="page.pageSize"
      :page-sizes="[10, 20, 50, 100]"
      @pagination="getList"
    />
  </div>
</template>

这种结构清晰,后期维护也方便。


八、什么时候不建议封装太重

虽然封装分页器很实用,但也不是越重越好。

如果你的项目只是:

  • 一个很小的演示页
  • 一个简单的小工具
  • 只有一两个短列表

那直接写 el-pagination 也完全可以。

封装组件的前提是:你真的有复用需求

如果没有太多列表页,封装太多反而会显得复杂。


九、总结

分页器封装的本质,不是"把 Element UI 再包一层",而是:

  • 把列表页最常用的分页逻辑收口
  • 把重复代码从页面里拿掉
  • 把交互规范统一起来
  • 让页面只专注数据请求和展示

和直接使用 el-pagination 相比,封装组件的优势主要是:

  • 更少重复代码
  • 更一致的体验
  • 更好维护
  • 更适合后台管理系统
  • 更方便团队协作

如果你的项目里有大量列表页,这种封装几乎是必做的。

相关推荐
亲亲小宝宝鸭2 小时前
Vue3中那些冷门但实用的方法
前端·vue.js
M ? A2 小时前
Vue 转 React | VuReact 实时监听开发指南
前端·vue.js·后端·react.js·面试·开源·vureact
Lkstar2 小时前
ES6+ 必备特性复习:解构、展开运算符、Symbol、Proxy
javascript·面试
半兽先生2 小时前
vue高性能下拉组件 支持上万数据不卡顿
前端·javascript·vue.js
invicinble2 小时前
前端框架使用vue-cli( 第二层:工程配置层--路由页面配置)
javascript·vue.js·前端框架
懂懂tty3 小时前
Vue3 架构
前端·vue.js
四岁爱上了她3 小时前
自定义标签切换动画
javascript·css·css3
invicinble3 小时前
前端框架使用vue-cli( 第二层:工程配置层--总览)
前端·vue.js·前端框架
坤盾科技3 小时前
Docker 离线地图服务器搭建实战:Node.js + OpenLayers + MBTiles
linux·javascript·arcgis·docker·node.js