Pagination 分页组件
基于Element Plus的增强分页组件,提供更丰富的功能和更好的用户体验。
📋 基础用法
简单分页
vue
<template>
<div>
<!-- 数据表格 -->
<el-table :data="tableData" border>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="createTime" label="创建时间" />
</el-table>
<!-- 分页组件 -->
<Pagination
v-model:current="queryParams.pageNum"
v-model:size="queryParams.pageSize"
:total="total"
@change="handlePageChange"
/>
</div>
</template>
<script setup lang="ts">
import Pagination from '@/components/Pagination/index.vue'
const queryParams = reactive({
pageNum: 1,
pageSize: 10
})
const total = ref(0)
const tableData = ref([])
const handlePageChange = () => {
console.log('页码变化:', queryParams.pageNum, queryParams.pageSize)
// 重新获取数据
fetchData()
}
const fetchData = async () => {
// 模拟API调用
console.log('获取数据...', queryParams)
}
</script>完整功能分页
vue
<template>
<div>
<el-table :data="tableData" border>
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="status" label="状态" />
</el-table>
<Pagination
v-model:current="pagination.current"
v-model:size="pagination.size"
:total="pagination.total"
:show-size-changer="true"
:show-quick-jumper="true"
:show-total="true"
:page-sizes="[10, 20, 50, 100]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
@change="handleChange"
@size-change="handleSizeChange"
/>
</div>
</template>
<script setup lang="ts">
const pagination = reactive({
current: 1,
size: 10,
total: 0
})
const tableData = ref([])
const handleChange = (page: number) => {
console.log('页码变化:', page)
fetchData()
}
const handleSizeChange = (size: number) => {
console.log('页面大小变化:', size)
pagination.current = 1 // 重置到第一页
fetchData()
}
const fetchData = async () => {
// API调用逻辑
}
</script>🎯 组件实现
Pagination 组件
vue
<!-- components/Pagination/index.vue -->
<template>
<div
v-if="total > 0"
class="pagination-container"
:class="{ 'hidden': hidden }"
>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="pageSizes"
:layout="layout"
:background="background"
:small="small"
:disabled="disabled"
:hide-on-single-page="hideOnSinglePage"
:pager-count="pagerCount"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
<!-- 额外信息 -->
<div v-if="showInfo" class="pagination-info">
<span class="info-text">
共 {{ total }} 条记录,每页显示 {{ pageSize }} 条
</span>
<span v-if="showRange" class="range-text">
显示第 {{ rangeStart }} - {{ rangeEnd }} 条记录
</span>
</div>
<!-- 快速跳转 -->
<div v-if="showQuickJumper && !layout.includes('jumper')" class="quick-jumper">
<span>跳至</span>
<el-input-number
v-model="jumpPage"
:min="1"
:max="totalPages"
:controls="false"
size="small"
style="width: 80px"
@keyup.enter="handleJump"
/>
<span>页</span>
<el-button size="small" @click="handleJump">跳转</el-button>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
// v-model 绑定
current?: number
size?: number
// 基础配置
total: number
pageSizes?: number[]
layout?: string
background?: boolean
small?: boolean
disabled?: boolean
hidden?: boolean
// 显示配置
hideOnSinglePage?: boolean
pagerCount?: number
showInfo?: boolean
showRange?: boolean
showQuickJumper?: boolean
// 自定义配置
showSizeChanger?: boolean
showTotal?: boolean
}
interface Emits {
(e: 'update:current', value: number): void
(e: 'update:size', value: number): void
(e: 'change', current: number, size: number): void
(e: 'current-change', current: number): void
(e: 'size-change', size: number): void
}
const props = withDefaults(defineProps<Props>(), {
current: 1,
size: 10,
pageSizes: () => [10, 20, 50, 100],
layout: 'total, sizes, prev, pager, next, jumper',
background: true,
small: false,
disabled: false,
hidden: false,
hideOnSinglePage: false,
pagerCount: 7,
showInfo: false,
showRange: false,
showQuickJumper: false,
showSizeChanger: true,
showTotal: true
})
const emit = defineEmits<Emits>()
// 双向绑定
const currentPage = computed({
get: () => props.current,
set: (value) => emit('update:current', value)
})
const pageSize = computed({
get: () => props.size,
set: (value) => emit('update:size', value)
})
// 计算属性
const totalPages = computed(() => Math.ceil(props.total / props.size))
const rangeStart = computed(() => {
return (props.current - 1) * props.size + 1
})
const rangeEnd = computed(() => {
return Math.min(props.current * props.size, props.total)
})
// 快速跳转
const jumpPage = ref(props.current)
// 处理页码变化
const handleCurrentChange = (page: number) => {
emit('update:current', page)
emit('current-change', page)
emit('change', page, props.size)
}
// 处理页面大小变化
const handleSizeChange = (size: number) => {
// 计算新的页码,保持当前数据位置尽量不变
const newCurrent = Math.min(
Math.ceil(((props.current - 1) * props.size + 1) / size),
Math.ceil(props.total / size)
)
emit('update:size', size)
emit('update:current', newCurrent)
emit('size-change', size)
emit('change', newCurrent, size)
}
// 快速跳转
const handleJump = () => {
if (jumpPage.value >= 1 && jumpPage.value <= totalPages.value) {
handleCurrentChange(jumpPage.value)
}
}
// 监听当前页变化,同步快速跳转输入框
watch(() => props.current, (newCurrent) => {
jumpPage.value = newCurrent
})
</script>🔧 高级功能
分页状态管理
typescript
// composables/use-pagination-state.ts
export interface PaginationState {
current: number
size: number
total: number
showSizeChanger: boolean
showQuickJumper: boolean
pageSizes: number[]
}
export function usePaginationState(
initialState?: Partial<PaginationState>
) {
const state = reactive<PaginationState>({
current: 1,
size: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
pageSizes: [10, 20, 50, 100],
...initialState
})
// 计算属性
const totalPages = computed(() => Math.ceil(state.total / state.size))
const offset = computed(() => (state.current - 1) * state.size)
const hasNext = computed(() => state.current < totalPages.value)
const hasPrev = computed(() => state.current > 1)
// 方法
const setTotal = (total: number) => {
state.total = total
// 如果当前页超出范围,调整到最后一页
if (state.current > totalPages.value && totalPages.value > 0) {
state.current = totalPages.value
}
}
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
state.current = page
return true
}
return false
}
const nextPage = () => {
if (hasNext.value) {
state.current++
return true
}
return false
}
const prevPage = () => {
if (hasPrev.value) {
state.current--
return true
}
return false
}
const changeSize = (size: number) => {
// 保持当前数据位置尽量不变
const currentOffset = offset.value
state.size = size
state.current = Math.floor(currentOffset / size) + 1
}
const reset = () => {
state.current = 1
state.total = 0
}
const getRange = () => {
const start = offset.value + 1
const end = Math.min(offset.value + state.size, state.total)
return { start, end }
}
return {
state: readonly(state),
totalPages,
offset,
hasNext,
hasPrev,
setTotal,
goToPage,
nextPage,
prevPage,
changeSize,
reset,
getRange
}
}分页数据管理
typescript
// composables/use-paginated-data.ts
export interface PaginatedData<T> {
records: T[]
total: number
current: number
size: number
pages: number
}
export function usePaginatedData<T>(
fetchFn: (params: { current: number; size: number; [key: string]: any }) => Promise<PaginatedData<T>>,
options: {
immediate?: boolean
defaultParams?: Record<string, any>
onSuccess?: (data: PaginatedData<T>) => void
onError?: (error: any) => void
} = {}
) {
const { immediate = true, defaultParams = {}, onSuccess, onError } = options
const { state: pagination, setTotal, reset } = usePaginationState()
const data = ref<T[]>([])
const loading = ref(false)
const error = ref<any>(null)
// 获取数据
const fetchData = async (extraParams: Record<string, any> = {}) => {
loading.value = true
error.value = null
try {
const params = {
current: pagination.current,
size: pagination.size,
...defaultParams,
...extraParams
}
const result = await fetchFn(params)
data.value = result.records
setTotal(result.total)
onSuccess?.(result)
} catch (err) {
error.value = err
onError?.(err)
console.error('获取分页数据失败:', err)
} finally {
loading.value = false
}
}
// 刷新当前页
const refresh = () => {
fetchData()
}
// 重置并重新获取
const reload = () => {
reset()
fetchData()
}
// 页码变化处理
const handlePageChange = (current: number, size: number) => {
pagination.current = current
pagination.size = size
fetchData()
}
// 搜索(重置到第一页)
const search = (searchParams: Record<string, any> = {}) => {
pagination.current = 1
fetchData(searchParams)
}
// 立即执行
if (immediate) {
onMounted(() => {
fetchData()
})
}
return {
// 数据状态
data: readonly(data),
loading: readonly(loading),
error: readonly(error),
pagination,
// 方法
fetchData,
refresh,
reload,
handlePageChange,
search
}
}虚拟分页
typescript
// composables/use-virtual-pagination.ts
export function useVirtualPagination<T>(
allData: Ref<T[]>,
pageSize = 10
) {
const currentPage = ref(1)
// 计算属性
const total = computed(() => allData.value.length)
const totalPages = computed(() => Math.ceil(total.value / pageSize))
const currentData = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return allData.value.slice(start, end)
})
const pagination = computed(() => ({
current: currentPage.value,
size: pageSize,
total: total.value,
pages: totalPages.value
}))
// 方法
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const reset = () => {
currentPage.value = 1
}
return {
currentData,
pagination,
goToPage,
nextPage,
prevPage,
reset
}
}📱 移动端适配
响应式分页组件
vue
<!-- components/ResponsivePagination/index.vue -->
<template>
<div class="responsive-pagination">
<!-- 桌面端分页 -->
<el-pagination
v-if="!isMobile"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="pageSizes"
:layout="layout"
:background="background"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
<!-- 移动端分页 -->
<div v-else class="mobile-pagination">
<!-- 页码信息 -->
<div class="page-info">
<span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
<span>({{ total }} 条记录)</span>
</div>
<!-- 简化控制 -->
<div class="page-controls">
<el-button
:disabled="currentPage <= 1"
@click="goToPrev"
>
上一页
</el-button>
<!-- 页码选择器 -->
<el-select
v-model="currentPage"
size="small"
style="width: 80px"
@change="handleCurrentChange"
>
<el-option
v-for="page in totalPages"
:key="page"
:label="page"
:value="page"
/>
</el-select>
<el-button
:disabled="currentPage >= totalPages"
@click="goToNext"
>
下一页
</el-button>
</div>
<!-- 每页条数选择 -->
<div class="size-selector">
<span>每页</span>
<el-select
v-model="pageSize"
size="small"
style="width: 80px"
@change="handleSizeChange"
>
<el-option
v-for="size in pageSizes"
:key="size"
:label="size"
:value="size"
/>
</el-select>
<span>条</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useBreakpoints } from '@vueuse/core'
const props = defineProps<{
current: number
size: number
total: number
pageSizes?: number[]
layout?: string
background?: boolean
}>()
const emit = defineEmits<{
'update:current': [value: number]
'update:size': [value: number]
'change': [current: number, size: number]
}>()
// 响应式断点
const breakpoints = useBreakpoints({
mobile: 768
})
const isMobile = breakpoints.smaller('mobile')
// 双向绑定
const currentPage = computed({
get: () => props.current,
set: (value) => emit('update:current', value)
})
const pageSize = computed({
get: () => props.size,
set: (value) => emit('update:size', value)
})
// 计算属性
const totalPages = computed(() => Math.ceil(props.total / props.size))
// 方法
const handleCurrentChange = (page: number) => {
emit('update:current', page)
emit('change', page, props.size)
}
const handleSizeChange = (size: number) => {
emit('update:size', size)
emit('change', props.current, size)
}
const goToPrev = () => {
if (currentPage.value > 1) {
handleCurrentChange(currentPage.value - 1)
}
}
const goToNext = () => {
if (currentPage.value < totalPages.value) {
handleCurrentChange(currentPage.value + 1)
}
}
</script>Pagination组件为Vue3应用提供了完整的分页解决方案,支持桌面端和移动端的不同展示方式,并提供了丰富的配置选项和数据管理功能。
