Pagination 组件
效果图

使用
vue
<template>
<Pagination :total="100" v-model:pageNum="pageNum" v-model:pageSize="pageSize" @size-change="handleSizeChange" @current-change="handlecurrentChange"/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const pageNum = ref(1);
const pageSize = ref(10);
// 页切换
const handlecurrentChange = (value: number) => {
console.log(value);
}
// 页大小切换
const handleSizeChange = (value: number) => {
console.log(value);
}
</script>
一、组件目录结构
text
|-Pagination
|--Pagination.vue(组件)
|--style.css(样式)
|--types.ts(类型定义)
|--index.ts(入口文件)
二、Pagination.vue
vue
<template>
<div
:class="[
paginationClass,
{
'vh-pagination--small': small,
'vh-pagination--background': background,
'is-disabled': disabled
}
]"
>
<!-- 总条数信息 -->
<div v-if="showTotal" class="vh-pagination__total">
{{ t('total', { total: total }) }}
</div>
<!-- 上一页 -->
<button
v-if="showPrevJump"
class="vh-pagination__btn-prev"
:disabled="disabled || pageNum <= 1"
@click="handlePrev"
@keydown.enter="handlePrev"
>
<Icon :icon="'angle-left'" />
</button>
<!-- 第一页 -->
<button
v-if="showFirstJump"
class="vh-pagination__item"
:class="{ 'vh-pagination__item--active': pageNum === 1 }"
:disabled="disabled || pageNum === 1"
@click="handleCurrentChange(1)"
>
1
</button>
<!-- 左边省略号 -->
<span v-if="leftEllipsis" class="vh-pagination__ellipsis">
{{ ellipsisText }}
</span>
<!-- 中间页码 -->
<template v-for="item in pages" :key="item">
<button
v-if="item !== '...'"
class="vh-pagination__item"
:class="{ 'vh-pagination__item--active': pageNum === item }"
:disabled="disabled || pageNum === item"
@click="handleCurrentChange(item)"
>
{{ item }}
</button>
<span v-else class="vh-pagination__ellipsis">
{{ ellipsisText }}
</span>
</template>
<!-- 右边省略号 -->
<span v-if="rightEllipsis" class="vh-pagination__ellipsis">
{{ ellipsisText }}
</span>
<!-- 最后一页 -->
<button
v-if="showLastJump"
class="vh-pagination__item"
:class="{ 'vh-pagination__item--active': pageNum === lastPage }"
:disabled="disabled || pageNum === lastPage"
@click="handleCurrentChange(lastPage)"
>
{{ lastPage }}
</button>
<!-- 下一页 -->
<button
v-if="showNextJump"
class="vh-pagination__btn-next"
:disabled="disabled || pageNum >= lastPage"
@click="handleNext"
@keydown.enter="handleNext"
>
<Icon :icon="'angle-right'" />
</button>
<!-- 每页显示条数选择器 -->
<div v-if="showSizeChanger" class="vh-pagination__sizes">
<Select
v-model="innerPageSize"
:disabled="disabled"
:options="pageSizesOptions"
style="width: 100px;"
@change="handleSizeChange"
/>
</div>
<!-- 跳转功能 -->
<div v-if="showQuickJumper" class="vh-pagination__jumper">
<span>{{ t('goto') }} </span>
<Input
v-model="jumpPageNum"
:disabled="disabled"
class="vh-pagination__jump-input"
@keydown.enter="handleJump"
/>
<span>{{ t('page') }} </span>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import Icon from '../Icon/Icon.vue'
import Select from '../Select/Select.vue';
import Input from '../Input/Input.vue';
import type { SelectOption } from '../Select/types';
import type { PaginationProps, PaginationEmits } from './types'
// 定义组件名称
defineOptions({ name: 'vhPagination' })
// 定义属性
const props = withDefaults(defineProps<PaginationProps>(), {
pageNum: 1,
pageSize: 10,
pageSizes: () => [10, 20, 50, 100],
layout: 'total, sizes, prev, pager, next, jumper',
background: false,
disabled: false,
small: false,
pagerCount: 7,
prevText: '',
nextText: '',
ellipsisText: '...',
jumper: false,
sizeSelector: false,
totalSelector: false
})
// 定义事件
const emits = defineEmits<PaginationEmits>()
const pageSizesOptions = computed(() => {
return props.pageSizes.map(size => ({
label: `${size}条/页`,
value: size
})) as SelectOption[];
});
// 内部状态
const innerPageNum = ref(props.pageNum)
const innerPageSize = ref(props.pageSize)
const jumpPageNum = ref('')
// 监听外部pageNum变化
watch(
() => props.pageNum,
(newVal) => {
innerPageNum.value = newVal
}
)
// 监听外部pageSize变化
watch(
() => props.pageSize,
(newVal) => {
innerPageSize.value = newVal
}
)
// 计算总页数
const lastPage = computed(() => {
return Math.max(1, Math.ceil(props.total / innerPageSize.value))
})
// 计算当前显示的页码数组
const pages = computed(() => {
const { pagerCount } = props
const current = innerPageNum.value
const totalPage = lastPage.value
const half = Math.floor(pagerCount / 2)
let start = current - half
let end = current + half
// 处理边界情况
if (start < 1) {
start = 1
end = Math.min(pagerCount, totalPage)
}
if (end > totalPage) {
end = totalPage
start = Math.max(1, end - pagerCount + 1)
}
// 生成页码数组
const result = []
for (let i = start; i <= end; i++) {
result.push(i)
}
return result
})
// 是否显示左边省略号
const leftEllipsis = computed(() => {
return pages.value[0] > 2
})
// 是否显示右边省略号
const rightEllipsis = computed(() => {
return pages.value[pages.value.length - 1] < lastPage.value - 1
})
// 根据布局配置显示相应的部分
const showTotal = computed(() => {
return props.layout.includes('total') || props.totalSelector
})
const showSizeChanger = computed(() => {
return props.layout.includes('sizes') || props.sizeSelector
})
const showQuickJumper = computed(() => {
return props.layout.includes('jumper') || props.jumper
})
const showPrevJump = computed(() => {
return props.layout.includes('prev')
})
const showNextJump = computed(() => {
return props.layout.includes('next')
})
const showFirstJump = computed(() => {
return props.layout.includes('pager') && pages.value[0] !== 1
})
const showLastJump = computed(() => {
return props.layout.includes('pager') && pages.value[pages.value.length - 1] !== lastPage.value
})
// 分页器基础样式类名
const paginationClass = computed(() => {
return props.small ? 'vh-pagination vh-pagination--small' : 'vh-pagination'
})
// 国际化文本处理
const t = (key: string, args?: any) => {
const locale = {
total: '共 {total} 条',
sizes: '每页',
goto: '跳至 ',
page: ' 页',
confirm: '确定'
}
let text = locale[key as keyof typeof locale] || ''
if (args) {
Object.keys(args).forEach((k) => {
text = text.replace(new RegExp(`{${k}}`, 'g'), args[k])
})
}
return text
}
// 处理页码变化
const handleCurrentChange = (val: number) => {
if (val === innerPageNum.value || props.disabled) return
if (val < 1) val = 1
if (val > lastPage.value) val = lastPage.value
innerPageNum.value = val
emits('update:pageNum', val)
emits('current-change', val)
}
// 处理每页条数变化
const handleSizeChange = (value: number) => {
if (props.disabled) return;
emits('update:pageSize', value);
emits('size-change', value);
// 重置到第一页
const newPageNum = 1;
innerPageNum.value = newPageNum;
emits('update:pageNum', newPageNum);
emits('current-change', newPageNum);
};
// 处理上一页
const handlePrev = () => {
if (props.disabled || innerPageNum.value <= 1) return
const newPage = innerPageNum.value - 1
innerPageNum.value = newPage
emits('update:pageNum', newPage)
emits('current-change', newPage)
emits('prev-click', newPage)
}
// 处理下一页
const handleNext = () => {
if (props.disabled || innerPageNum.value >= lastPage.value) return
const newPage = innerPageNum.value + 1
innerPageNum.value = newPage
emits('update:pageNum', newPage)
emits('current-change', newPage)
emits('next-click', newPage)
}
// 处理跳转
const handleJump = () => {
let num = Number(jumpPageNum.value)
if (isNaN(num) || props.disabled) {
jumpPageNum.value = ''
return
}
num = Math.max(1, Math.min(num, lastPage.value))
handleCurrentChange(num)
jumpPageNum.value = ''
}
</script>
types.ts
typescript
import type { PropType } from 'vue'
export type PaginationLayout = string
export interface PaginationProps {
// 当前页码
pageNum?: number
// 每页显示条数
pageSize?: number
// 总条数
total: number
// 每页显示条数选择器的选项
pageSizes?: number[]
// 布局配置 (total, sizes, prev, pager, next, jumper)
layout?: PaginationLayout
// 背景是否显示
background?: boolean
// 是否禁用
disabled?: boolean
// 是否显示小型分页
small?: boolean
// 页码按钮的数量,当总页数超过该值时会折叠
pagerCount?: number
// 上一页按钮的文本
prevText?: string
// 下一页按钮的文本
nextText?: string
// 省略时显示的内容
ellipsisText?: string
// 自定义跳转内容
jumper?: boolean
// 自定义大小选择器的内容
sizeSelector?: boolean
// 自定义总条数显示的内容
totalSelector?: boolean
}
// 事件类型定义
export interface PaginationEmits {
(e: 'update:pageNum', value: number): void
(e: 'update:pageSize', value: number): void
(e: 'size-change', size: number): void
(e: 'current-change', current: number): void
(e: 'prev-click', current: number): void
(e: 'next-click', current: number): void
}
index.ts
typescript
import type { App } from 'vue'
import Pagination from './Pagination.vue'
Pagination.install = (app: App) => {
app.component(Pagination.name, Pagination)
}
export default Pagination
export * from './types'
style.css
css
.vh-pagination {
--vh-pagination-font-size: var(--vh-font-size-base);
--vh-pagination-button-size: 32px;
--vh-pagination-item-bg-color: var(--vh-fill-color-blank);
--vh-pagination-item-text-color: var(--vh-text-color-regular);
--vh-pagination-item-hover-bg-color: var(--vh-fill-color-light);
--vh-pagination-item-hover-text-color: var(--vh-color-primary);
--vh-pagination-item-active-bg-color: var(--vh-color-primary);
--vh-pagination-item-active-text-color: var(--vh-color-white);
--vh-pagination-disabled-bg-color: var(--vh-fill-color-blank);
--vh-pagination-disabled-text-color: var(--vh-text-color-placeholder);
--vh-pagination-disabled-border-color: var(--vh-border-color-light);
--vh-pagination-input-width: 50px;
--vh-pagination-margin-left: 20px;
}
.vh-pagination {
display: inline-flex;
align-items: center;
font-size: var(--vh-pagination-font-size);
}
.vh-pagination__total {
margin-right: var(--vh-pagination-margin-left);
color: var(--vh-text-color-regular);
}
.vh-pagination__item,
.vh-pagination__btn-prev,
.vh-pagination__btn-next {
display: inline-flex;
justify-content: center;
align-items: center;
min-width: var(--vh-pagination-button-size);
height: var(--vh-pagination-button-size);
padding: 0 10px;
margin: 0 2px;
box-sizing: border-box;
background-color: var(--vh-pagination-item-bg-color);
border: 1px solid var(--vh-border-color);
border-radius: var(--vh-border-radius-base);
color: var(--vh-pagination-item-text-color);
cursor: pointer;
user-select: none;
transition: all var(--vh-transition-duration);
}
.vh-pagination__item:hover,
.vh-pagination__btn-prev:hover,
.vh-pagination__btn-next:hover {
color: var(--vh-pagination-item-hover-text-color);
border-color: var(--vh-pagination-item-hover-text-color);
background-color: var(--vh-pagination-item-hover-bg-color);
}
.vh-pagination__item--active {
color: var(--vh-pagination-item-active-text-color);
background-color: var(--vh-pagination-item-active-bg-color);
border-color: var(--vh-pagination-item-active-bg-color);
}
.vh-pagination__item--active:hover {
color: var(--vh-pagination-item-active-text-color);
background-color: var(--vh-pagination-item-active-bg-color);
border-color: var(--vh-pagination-item-active-bg-color);
}
.vh-pagination__ellipsis {
margin: 0 8px;
color: var(--vh-text-color-secondary);
}
.vh-pagination__sizes {
display: inline-flex;
align-items: center;
margin-left: var(--vh-pagination-margin-left);
color: var(--vh-text-color-regular);
}
.vh-pagination__sizes-select {
margin: 0 6px;
padding: 4px;
border: 1px solid var(--vh-border-color);
border-radius: var(--vh-border-radius-base);
background-color: var(--vh-pagination-item-bg-color);
color: var(--vh-pagination-item-text-color);
outline: none;
cursor: pointer;
}
.vh-pagination__sizes-select:focus {
border-color: var(--vh-pagination-item-hover-text-color);
}
.vh-pagination__jumper {
display: inline-flex;
align-items: center;
margin-left: var(--vh-pagination-margin-left);
color: var(--vh-text-color-regular);
.vh-input {
width: 60px;
}
}
.vh-pagination__jump-input {
width: var(--vh-pagination-input-width);
margin: 0 6px;
padding: 4px;
border: 1px solid var(--vh-border-color);
border-radius: var(--vh-border-radius-base);
text-align: center;
outline: none;
}
.vh-pagination__jump-input:focus {
border-color: var(--vh-pagination-item-hover-text-color);
}
.vh-pagination__jump-confirm-btn {
margin-left: 6px;
padding: 4px 12px;
background-color: var(--vh-pagination-item-hover-text-color);
color: var(--vh-color-white);
border: none;
border-radius: var(--vh-border-radius-base);
cursor: pointer;
transition: all var(--vh-transition-duration);
}
.vh-pagination__jump-confirm-btn:hover {
background-color: var(--vh-color-primary-light-7);
}
.vh-pagination__jump-confirm-btn:disabled,
.vh-pagination__jump-confirm-btn.is-disabled {
background-color: var(--vh-pagination-disabled-bg-color);
color: var(--vh-pagination-disabled-text-color);
cursor: not-allowed;
}
.vh-pagination--small {
--vh-pagination-font-size: var(--vh-font-size-small);
--vh-pagination-button-size: 24px;
--vh-pagination-input-width: 40px;
}
.vh-pagination--background .vh-pagination__item--active {
background-color: var(--vh-color-primary-light-9);
color: var(--vh-pagination-item-active-text-color);
}
.vh-pagination.is-disabled .vh-pagination__item,
.vh-pagination.is-disabled .vh-pagination__btn-prev,
.vh-pagination.is-disabled .vh-pagination__btn-next {
background-color: var(--vh-pagination-disabled-bg-color);
color: var(--vh-pagination-disabled-text-color);
border-color: var(--vh-pagination-disabled-border-color);
cursor: not-allowed;
}
.vh-pagination.is-disabled .vh-pagination__item:hover,
.vh-pagination.is-disabled .vh-pagination__btn-prev:hover,
.vh-pagination.is-disabled .vh-pagination__btn-next:hover {
background-color: var(--vh-pagination-disabled-bg-color);
color: var(--vh-pagination-disabled-text-color);
border-color: var(--vh-pagination-disabled-border-color);
}
.vh-pagination.is-disabled .vh-pagination__sizes-select,
.vh-pagination.is-disabled .vh-pagination__jump-input {
background-color: var(--vh-pagination-disabled-bg-color);
color: var(--vh-pagination-disabled-text-color);
border-color: var(--vh-pagination-disabled-border-color);
cursor: not-allowed;
}