在后台管理系统里,分页几乎是列表页的标配。
如果每个页面都直接写 el-pagination,能用是能用,但很快就会遇到这些问题:
- 每个页面都要重复写分页状态和事件
- 切换页码、切换每页条数的逻辑不统一
- 改分页样式要改很多处
- 翻页后滚动行为不一致
- 某些页面要显示"总数、页码、每页条数",某些页面不要,写法越来越散
所以更推荐的做法是:封装一个通用分页器组件,把列表页里最常见的分页逻辑统一收口。
下面我会先讲它的适用场景,再讲它相比直接使用 Element UI 的优势,最后给你一份可以直接拿去用的完整组件代码。
一、分页器封装适合哪些场景
这个组件特别适合下面这些场景:
1. 后台列表页
例如用户管理、角色管理、订单列表、日志列表、内容管理等。
这类页面通常都是:
- 顶部筛选条件
- 中间表格
- 底部分页器
分页逻辑很标准,适合统一封装。
2. 服务端分页接口
如果接口返回的是这样的结构:
json
{
"rows": [],
"total": 128
}
那就非常适合分页器组件。
页面只需要根据 pageNum、pageSize 去请求接口,组件负责页码切换。
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 做页码和每页条数双向绑定
它把外部传进来的 page 和 limit 包装成计算属性:
currentPagepageSize
这样组件内部一改,外部状态也会同步更新。
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 相比,封装组件的优势主要是:
- 更少重复代码
- 更一致的体验
- 更好维护
- 更适合后台管理系统
- 更方便团队协作
如果你的项目里有大量列表页,这种封装几乎是必做的。