实现一个分页组件

已更新,pagination 组件已添加到 FanUI 组件库

FanUI-pagination

背景

还是那个重构项目的任务,有分页场景,但是不能用组件库,因为样式都是定制的,所以我自己实现一个

实现步骤

1. 基本的分页功能

我们先定义三个最基本的 props 属性,total 总数据量, pageSize 每页数据量, modelValueupdate:modelValue 是用于双向绑定当前页码 currentPage。 因为后面我们肯定会改变当前页码的,子组件不能改父组件传递的数据,所以我们要把 modelValue 赋值给 currentPage,然后我们监听 currentPage 的变化,调用 update:modelValue,这样我们就实现了 currentPage 的双向绑定。

然后动态计算 pageCount 总页数,用于渲染页码

下面是实现一个最基本的分页组件

vue
<script setup lang="ts">
import { computed, ref, watch } from 'vue'

const props = defineProps({
  modelValue: {
    type: Number,
    default: 1
  },
  total: {
    type: Number,
    required: true
  },
  pageSize: {
    type: Number,
    default: 10
  }
})

const currentPage = ref(props.modelValue)

// currentPage变化时更新数据 实现双向绑定
watch(
  () => currentPage.value,
  newVal => {
    currentPage.value = newVal
    emit('update:modelValue', newVal)
  }
)

const pageCount = computed(() => Math.ceil(props.total / props.pageSize))

const emit = defineEmits(['update:modelValue', 'change'])

const handleChange = (item: number) => {
  currentPage.value = item
  emit('change', item)
}

const handlePrevClick = () => {
  if (currentPage.value <= 1) return
  currentPage.value--
  emit('change', currentPage.value)
}

const handleNextClick = () => {
  if (currentPage.value >= pageCount.value) return
  currentPage.value++
  emit('change', currentPage.value)
}
</script>

<template>
  <div v-if="pageCount !== 1" class="page-container">
    <button
      type="button"
      :class="['page__button__prev', { disabled: currentPage <= 1 }]"
      @click="handlePrevClick"
    >
      prev
    </button>
    <ul class="page-list">
      <li
        v-for="item in pageCount"
        :class="['page-list__item', { active: currentPage === item }]"
        @click="handleChange(item)"
      >
        {{ item }}
      </li>
    </ul>
    <button
      type="button"
      :class="['page__button__next', { disabled: currentPage >= pageCount }]"
      @click="handleNextClick"
    >
      next
    </button>
  </div>
</template>

<style scoped lang="scss">
li {
  list-style: none;
}
button {
  border: none;
  border-radius: 4px;
  background-color: inherit;
  padding: 4px;
  color: #1677ff;
  cursor: pointer;
  &:hover {
    color: rgba(22, 119, 255, 0.8);
  }
}
.page-container {
  display: flex;
  align-items: center;
  gap: 4px;
  .disabled {
    color: #c0c4cc;
    cursor: not-allowed;
  }
  .page-list {
    padding: 0 8px;
    display: flex;
    gap: 8px;
    .page-list__item {
      width: 18px;
      height: 18px;
      line-height: 18px;
      text-align: center;
      padding: 4px;
      border-radius: 4px;
      cursor: pointer;
      &:hover {
        background: #efefef;
      }
    }
    .active {
      background: #1677ff;
      color: #fff;
      &:hover {
        background: #1677ff;
      }
    }
  }
}
</style>

现在写的这个样式是我重新写的,为了展示一个好看的效果。

组件使用:

vue
<script setup lang="ts">
import { ref, watch } from 'vue'
import PageNation from './PageNation.vue'

const currentPage = ref(1)

watch(
  () => currentPage.value,
  value => {
    // 监听currentPage变化,是否实现了双向绑定
    console.log(value)
  }
)
</script>

<template>
  <page-nation v-model="currentPage" :total="50"></page-nation>
</template>

2. 省略页码

如果数据量太多,比如 1000条数据,每页大小10个,那就有100个页码了,全都展示会导致分页组件非常长,我们再优化一下。页码超过七个就适当省略部分页码。

第一页和最后一页的页码是固定的,那问题是如何生成中间页码。

首先我们规定最多只展示7个点击页码,首尾各一个,那就要动态生成中间的5个页码

ts
// 最多展示除开头页和尾页的5个中间页码
const MAX_MEDDLE_PAGES_COUNT = 5
// 最多展示7个页码
const MAX_VISIBLE_PAGES = MAX_MEDDLE_PAGES_COUNT + 2
// 当前页码距离首尾页码多少距离才显示省略号
const MIDDLE_PAGE_POSITION = Math.floor(MAX_MEDDLE_PAGES_COUNT / 2) + 1

const isShowPrevMore = computed(() => {
  return (
    pageCount.value > MAX_VISIBLE_PAGES &&
    currentPage.value > MIDDLE_PAGE_POSITION + 1
  )
})

const isShowNextMore = computed(() => {
  return (
    pageCount.value > MAX_VISIBLE_PAGES &&
    currentPage.value < pageCount.value - MIDDLE_PAGE_POSITION
  )
})

/**
 * 生成中间页码
 */
const generateMiddlePageCount = () => {
  // 工具函数:生成连续页码数组
  const generatePageRange = (
    start: number,
    count: number = MAX_MEDDLE_PAGES_COUNT
  ) => Array.from({ length: count }, (_, i) => start + i)

  // 边界情况处理
  if (pageCount.value <= MAX_VISIBLE_PAGES) {
    return generatePageRange(2, pageCount.value - 2)
  }

  // 根据当前页码位置生成对应页码
  const pageRanges = {
    // 一种是当前页码靠近首页的情况
    nearStart: () => generatePageRange(2),
    // 一种是当前页码靠近尾页的情况
    nearEnd: () => generatePageRange(pageCount.value - MAX_MEDDLE_PAGES_COUNT),
    // 一种是当前页码在中间的情况
    inMiddle: () => generatePageRange(currentPage.value - 2)
  }

  if (currentPage.value <= MIDDLE_PAGE_POSITION + 1)
    return pageRanges.nearStart()
  if (currentPage.value >= pageCount.value - MIDDLE_PAGE_POSITION)
    return pageRanges.nearEnd()

  return pageRanges.inMiddle()
}

const middlePages = computed(() => generateMiddlePageCount())

把生成的 middlePages 渲染上去

html
<div v-if="pageCount !== 1" class="page-container">
    <button
      type="button"
      :class="['page__button__prev', { disabled: currentPage <= 1 }]"
      @click="handlePrevClick"
    >
      prev
    </button>
    <ul class="page-list">
      <li
        :class="['page-list__item', { active: currentPage === 1 }]"
        @click="handleChange(1)"
      >
        1
      </li>
      <li v-if="isShowPrevMore" class="more">...</li>
      <li
        v-for="item in middlePages"
        :key="item"
        :class="['page-list__item', { active: item === currentPage }]"
        @click="handleChange(item)"
      >
        {{ item }}
      </li>
      <li v-if="isShowNextMore" class="more">...</li>
      <li
        :class="['page-list__item', { active: currentPage === pageCount }]"
        @click="handleChange(pageCount)"
      >
        {{ pageCount }}
      </li>
    </ul>
    <button
      type="button"
      :class="['page__button__next', { disabled: currentPage >= pageCount }]"
      @click="handleNextClick"
    >
      next
    </button>
 </div>

样式部分

scss
li {
  list-style: none;
}
button {
  border: none;
  border-radius: 4px;
  background-color: inherit;
  padding: 4px;
  color: #1677ff;
  cursor: pointer;
  &:hover {
    color: rgba(22, 119, 255, 0.8);
  }
}
.page-container {
  display: flex;
  align-items: center;
  gap: 4px;
  .disabled {
    color: #c0c4cc;
    cursor: not-allowed;
  }
  .page-list {
    padding: 0 8px;
    display: flex;
    gap: 8px;
    .more {
      cursor: pointer;
    }
    .page-list__item {
      line-height: 18px;
      text-align: center;
      padding: 4px 8px;
      border-radius: 4px;
      cursor: pointer;
      &:hover {
        background: #efefef;
      }
    }
    .active {
      background: #1677ff;
      color: #fff;
      &:hover {
        background: #1677ff;
      }
    }
  }
}

总结

还有什么指定跳转页码功能,改变每页数据大小等,这些功能实现起来都挺容易的,就不展开讲了。不过这个组件我的 FanUI 组件库里没收录,有空我会在这个项目里加入这个组件,顺便实现其他功能。

实现一个水印组件,含防篡改功能
树形控件