🍀上班摸鱼,手搓了个分页器组件

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>
			&nbsp;
      <Input
        v-model="jumpPageNum"
        :disabled="disabled"
        class="vh-pagination__jump-input"
        @keydown.enter="handleJump"
      />
			&nbsp;
      <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;
}
相关推荐
电蚊拍3 小时前
ADB 实现手机访问电脑上运行的网站,真机调试H5网站
前端
朕的剑还未配妥3 小时前
vue2项目中使用markdown-it插件教程,同时解决代码块和较长单词不换行问题
前端
清羽_ls3 小时前
前端代码CR小知识点汇总
前端·cr
WestWong3 小时前
基于 Web 技术栈的跨端开发模版
前端
饮水机战神3 小时前
小程序被下架后,我连夜加了个 "安全接口"
前端·javascript
小old弟3 小时前
小程序开发:原生 vs 跨平台框架全解析
前端
閞杺哋笨小孩3 小时前
Vue3 点击指令(防抖 / 节流)
前端·vue.js
加油吧zkf3 小时前
Python入门:从零开始的完整学习指南
开发语言·前端·python
柯南二号3 小时前
【大前端】 TypeScript vs JavaScript:全面对比与实践指南
前端·javascript·typescript