试着开发一个Pagination组件

news2024/11/27 16:31:50

1 组件需求和模块设计

我们要实现的分页组件大致效果如下:

在这里插入图片描述

组件需求

  1. 点击左右分页按钮可以跳转到上一页/下一页;
  2. 点击中间的页码按钮可以跳转到相应的页码;
  3. 首页尾页需要始终显示出来(如果只有1页则不显示尾页);
  4. 除首尾页之外,当前页码左右最多只显示2页(共5页);
  5. 页码太多时显示更多页码按钮,点击更多页码按钮跳转5页。

模块设计

从效果图可以看出,Pagination组件主要由2个模块组成:

  1. Button - 左右分页按钮
  2. Pager - 中间的分页器

在这里插入图片描述

目录结构

先设计好pagination组件的目录结构:

├── pagination
|  ├── tests                  // 单元测试
|  |  └── pagination.test.ts
|  ├── index.ts                   // 入口文件
|  └── src                        // 源码
|     ├── components              // 子组件
|     |  ├── icon-arrow-left.tsx  // 左箭头图标组件
|     |  ├── icon-arrow-right.tsx // 右箭头图标组件
|     |  ├── pager.scss           // 分页器组件样式
|     |  ├── pager.tsx            // 分页器组件
|     |  └── pager-type.ts        // 分页器组件类型
|     ├── composables             // Composables
|     |  └── use-page.ts
|     ├── pagination.scss         // 组件样式
|     ├── pagination.tsx          // 分页组件
|     └── pagination-type.ts      // 组件类型

2 实现交互逻辑 - usePage

对于分页组件来说,最核心的交互就是分页:

  • 当我们点击左右分页按钮时,我们在分页
  • 当我们点击中间的页码时,我们也在分页
  • 当我们点击快速跳转按钮时,依然在分页

而且这个分页的动作是一种交互,是UI无关的,是这个组件最核心的部分,所以第一步应该是实现这部分逻辑。

实现基础usePage

Vue3中有Composable的概念,我们将分页逻辑做成一个叫usePage的Composable。

创建一个use-page.ts文件,写入以下代码:

export default function usePage() {}

一开始usePage没有任何内容。

分页的逻辑里面有一个状态叫页码,分页的动作就是改变页码:

  • 当我们点击上一页,页码减1
  • 点击下一页,页码加1
  • 点击中间的页码,则跳转到相应的页码
  • 点击快速往前跳转按钮,页码减5
  • 点击快速往后跳转按钮,页码加5

所以我们应该在usePage中维护一个页码的变量pageIndex

import { ref } from 'vue'

export default function usePage(defaultPageIndex = 1) {
  const pageIndex = ref(defaultPageIndex)
  
  return { pageIndex }
}

因为这个页码是动态的,所以我们把它做成响应式的,用ref包裹起来,并且设置默认值defaultPageIndex,这个默认值是usePage的唯一参数,最后将pageIndex导出,这时我们可以在Pagination组件中使用。

pagination.ts中写入以下代码:

import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'

export default defineComponent({
  name: 'SPagination',
  setup() {
    const { pageIndex } = usePage()

    return () => (
      <div class="s-pagination">
      { pageIndex.value }
      </div>
    )
  }
})

我们只是显示usePage中导出来的页码pageIndex,这是一个静态的页码,传入什么显示什么,比如传入2,则页面显示2。

动态改变页码

我们需要一个方法可以改变页码,比如就叫setPageIndex:

import { ref } from 'vue'

export default function usePage(defaultPageIndex = 1) {
  const pageIndex = ref(defaultPageIndex)

  const setPageIndex = (current: number) => {
    pageIndex.value = current
  }
  
  return { pageIndex, setPageIndex }
}

setPageIndex有一个参数,用来设置pageIndex为第几页。

我们在pagination组件中使用下:

import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'

export default defineComponent({
  name: 'SPagination',
  setup() {
    const { pageIndex, setPageIndex } = usePage()

    return () => (
      <div class="s-pagination">
        <button onClick={() => setPageIndex(2)}>{ pageIndex.value }</button>
      </div>
    )
  }
})

我们点击按钮之后,页码就会从第1页变成第2页。

这是跳转到指定页码,如果要跳转到上一页怎么办呢?很简单:

<button onClick={() => setPageIndex(pageIndex.value - 1)}>{ pageIndex.value }</button>

同理快速往前跳转5页呢?

<button onClick={() => setPageIndex(pageIndex.value - 5)}>{ pageIndex.value }</button>

基础usePage的效果

这时最简单的usePage就已经完成了,我们来试试效果吧。

import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'

export default defineComponent({
  name: 'SPagination',
  setup() {
    const { pageIndex, setPageIndex } = usePage()

    return () => (
      <div class="s-pagination">
        <button onClick={() => setPageIndex(pageIndex.value - 1)}>上一页</button>
        <button onClick={() => setPageIndex(1)}>1</button>
        <button onClick={() => setPageIndex(pageIndex.value - 5)}>...</button>
        { pageIndex.value }
        <button onClick={() => setPageIndex(pageIndex.value + 5)}>...</button>
        <button onClick={() => setPageIndex(10)}>10</button>
        <button onClick={() => setPageIndex(pageIndex.value - 1)}>下一页</button>
      </div>
    )
  }
})

在这里插入图片描述
现在我们的分页组件已经具备基本的分页能力:

  • 可以跳转到上一页、下一页
  • 可以跳转到指定页码
  • 可以快速往前、往后跳转N页

做一次小优化吧

我们的usePage现在只有一个setPageIndex方法,所有的分页动作,比如上一页、下一页、快速跳转页码等,都是用的该方法,这会使代码比较难理解,我们来做一个优化吧。

先给usePage增加快速往前或往后跳转N页的功能。

import { ref } from 'vue'

export default function usePage(defaultPageIndex = 1) {
  const pageIndex = ref(defaultPageIndex)

  const setPageIndex = (current: number) => {
    pageIndex.value = current
  }

  // 新增
  const jumpPage = (page: number) => {
    pageIndex.value += page
  }

  return { pageIndex, setPageIndex, jumpPage }
}

这样快速往前、往后5页就可以改成:

<button onClick={() => jumpPage(-5)}>...</button>
{ pageIndex.value }
<button onClick={() => jumpPage(5)}>...</button>

并且我们可以基于jumpPage快速实现上一页、下一页:

import { ref } from 'vue'

export default function usePage(defaultPageIndex = 1) {
  const pageIndex = ref(defaultPageIndex)

  const setPageIndex = (current: number) => {
    pageIndex.value = current
  }

  const jumpPage = (page: number) => {
    pageIndex.value += page
  }

  // 新增
  const prevPage = () => jumpPage(-1)

  const nextPage = () => jumpPage(1)

  return { pageIndex, setPageIndex, jumpPage, prevPage, nextPage }
}

这样我们的例子就可以简化成:

import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'

export default defineComponent({
  name: 'SPagination',
  setup() {
    const { pageIndex, setPageIndex, jumpPage, prevPage, nextPage } = usePage()

    return () => (
      <div class="s-pagination">
        <button onClick={prevPage}>上一页</button>
        <button onClick={() => setPageIndex(1)}>1</button>
        <button onClick={() => jumpPage(-5)}>...</button>
        { pageIndex.value }
        <button onClick={() => jumpPage(5)}>...</button>
        <button onClick={() => setPageIndex(10)}>10</button>
        <button onClick={nextPage}>下一页</button>
      </div>
    )
  }
})

3 基础版Pagination

有了usePage这个内核,我们的基础Pagination组件就很容易实现啦,先实现一个基础版本吧。

实现基础版分页组件

开发组件的第一步时定义它的输入输出,在pagination-type.ts文件中定义Pagination组件的输入。

import { ExtractPropTypes } from 'vue'

export const paginationProps = {
  total: {
    type: Number,
    default: 0,
  },
  pageSize: {
    type: Number,
    default: 10,
  },
} as const

export type PaginationProps = ExtractPropTypes<typeof paginationProps>

我们定义了两个props:

  • total:数据总数,默认为0
  • pageSize:每页展示的数据条数,默认为10

pagination.tsx中添加props:

import { defineComponent, toRefs } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination-type'

export default defineComponent({
  name: 'SPagination',
  props: paginationProps,
  setup(props: PaginationProps) {
    // 获取总条数和单页条数
    const { total, pageSize } = toRefs(props)
    // 相除之后向上取整就是总页数
    const totalPage = Math.ceil(total.value / pageSize.value)
    const { pageIndex, prevPage, nextPage, setPageIndex } = usePage()

    return () => (
      <div class="s-pagination">
        <button onClick={prevPage}>上一页</button>
        <button onClick={() => setPageIndex(1)}>1</button>
        {pageIndex.value}
        {/* 现在可以设置总页数了 */}
        <button onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</button>
        <button onClick={nextPage}>下一页</button>
      </div>
    )
  }
})

这里有一个计算逻辑,就是根据数据总数total和每页数据条数pageSize计算总页码数totalPage

const totalPage = Math.ceil(total.value / pageSize.value)

这里有一个计算逻辑,就是根据数据总数total和每页数据条数p

使用基础版分页组件

可以使用看下效果:

## 基础功能
设置总条数`total`即可
:::demo 设置total
```vue
<template>
  <SPagination :total="50"></SPagination>
</template>

效果如下:

在这里插入图片描述

50条数据一共5页,所以尾页显示5,翻页功能也是正常的。

4 实现分页器功能

分页器Pager指的是分页组件中间那部分,就是下图中蓝框的部分:
在这里插入图片描述

需求梳理和结构设计

这块比较复杂,涉及到比较多逻辑判断,我们先梳理下:

  1. 首页和尾页是常驻的,如果只有1页,则不显示
  2. 页码按钮有一个最大数量pagerCount,上图是7,也就是说最多显示7个页码按钮
  3. 如果总页数totalPage大于pagerCount,则会出现显示不下的情况,这时显示不下的部分用...表示,并且这个...是可以快速往前、往后跳转N页的
  4. 中间页码应该显示的页码按钮数量在0到pagerCount-2之间
  5. 只有2页的情况下,中间页码按钮数量为0
  6. 大于等于pagerCount的情况下,中间按钮数量等于pagerCount-2
  7. 当中间页码左边的页数大于2时,应该出现左边的 ...
  8. 当中间页码右边的页数小于totalPage-3时,应该出现右边的 ...

我们先设计好Pager的大致结构:

import { defineComponent, toRefs } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination.type'

export default defineComponent({
  name: 'SPagination',
  props: paginationProps,
  setup(props: PaginationProps) {
    const { total, pageSize } = toRefs(props)
    const totalPage = Math.ceil(total.value / pageSize.value)
    const { pageIndex, prevPage, nextPage, setPageIndex } = usePage()

    return () => (
      <div class="s-pagination">
        <button onClick={prevPage}>上一页</button>
        <!-- Pager 部分 start -->
        <ul class="s-pager">
          <li>1</li>
          <li class="more left">...</li>
          
          <li>中间页码</li>
          
          <li class="more right">...</li>
          <li>{totalPage.value}</li>
        </ul>
        <!-- Pager 部分 end -->
        <button onClick={nextPage}>下一页</button>
      </div>
    )
  }
})

我们分两步走:

  1. 先解决首页、尾页、左更多、右更多的渲染条件问题
  2. 再解决中间页码的渲染问题

先解决首尾问题

<ul class="s-pager">
  <!-- 首页是常驻的,不管 -->
  <li>1</li>

  <!-- 左更多按钮显示的条件有2个: -->
  <!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount -->
  <!-- 2. 当前页码大于 Math.ceil(pagerCount / 2) -->
  {totalPage.value > pagerCount.value &&
    pageIndex.value > Math.ceil(pagerCount.value / 2) && (
      <li class="more left">...</li>
    )}
    
  <li>中间页码</li>

  <!-- 右更多按钮显示的条件也有2个: -->
  <!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount(和左更多按钮相同) -->
  <!-- 2. 当前页码小于 totalPage.value - Math.ceil(pagerCount.value / 2) + 1 -->
  {totalPage.value > pagerCount.value &&
    pageIndex.value <
      totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && (
      <li class="more right">...</li>
    )}
  <!-- 尾页显示的条件是:总页码大于1 -->
  { totalPage.value > 1 && <li>{totalPage.value}</li>}
</ul>

我们来测试下这个显示逻辑是否符合预期,主要测试以下用例(默认pageSize为10,pagerCount为7):

用例1:total=1时应该只显示首页,即1

在这里插入图片描述

用例2:total=11时,应该显示首页和尾页,即1和2

在这里插入图片描述

用例3:total=80 && pageIndex=4,应该显示右更多按钮

在这里插入图片描述

用例4:total=80 && pageIndex=5,应该显示左更多按钮

在这里插入图片描述

用例5:total=90 && pageIndex=5,应该显示左、右更多按钮

在这里插入图片描述

用例6:total=90 && pageIndex=4,应该只显示右更多按钮
在这里插入图片描述

用例7:total=90 && pageIndex=6,应该只显示左更多按钮

在这里插入图片描述

经过测试,我们前面写的首页、尾页、左更多、右更多的渲染条件是没问题的。

渲染中间页码按钮

接下来解决中间页码按钮的渲染问题。

中间页码由3个变量共同决定:

  1. 总页码totalPage
  2. 当前页码pageIndex
  3. 最大显示页码数pagerCount

所以我们可以编写一个函数,用来获取中间页码数组。

const getCenterPages = (totalPage, pageIndex, pagerCount) => {  
  const totalPageArr = Array.from(Array(totalPage).keys())

  if (totalPage <= pagerCount) {
    // totalPageArr: [0,1,2,3,4]
    // 总页码较小时,全部显示出来
    // [2,3,4]
    return totalPageArr.slice(2, totalPage)
  } else {
    // 总页码较大时,只显示部分页码
    const middle = Math.ceil(pagerCount / 2)
    // totalPageArr: [0,1,2,3,4,5,6,7,8]
    if (pageIndex <= middle) {
      // pageIndex=3
      // [2,3,4,5,6]
      // 左边全显示
      return totalPageArr.slice(2, pagerCount)
    } else if (pageIndex >= totalPage - middle + 1) {
      // pageIndex=6
      // [4,5,6,7,8]
      // 右边全显示
      return totalPageArr.slice(totalPage - pagerCount + 2, totalPage)
    } else {
      // pageIndex=4
      // [2,3,4,5,6]
      // 中间显示
      return totalPageArr.slice(pageIndex - middle + 2, pageIndex + middle - 1)
    }
  }
}

有了中间页码数组,我们就可以用它来渲染中间页码啦。

import { defineComponent, toRefs, computed } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination.type'

export default defineComponent({
  name: 'SPagination',
  props: paginationProps,
  setup(props: PaginationProps) {
    const pagerCount = ref(7)
    const { total, pageSize } = toRefs(props)
    const totalPage = Math.ceil(total.value / pageSize.value)
    const { pageIndex, prevPage, nextPage, setPageIndex } = usePage()
    // +++
    const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))

    return () => (
      <div class="s-pagination">
        <button onClick={prevPage}>上一页</button>
        <!-- Pager 部分 start -->
        <ul class="s-pager">
          <!-- ... -->
          <!-- +++中间页码+++ -->
          { centerPages.value.map(page => <li>{page}</li>) }        
          <!-- ... -->
        </ul>
        <!-- Pager 部分 end -->
        <button onClick={nextPage}>下一页</button>
      </div>
    )
  }
})

加个样式

.s-pagination {
  display: flex;
  /* your component style */
  .s-pager {
    list-style: none;
    display: flex;
    padding-left: 0;
    li {
      margin-top: 0;
      padding: 0 4px;
    }
    li.current {
      color: rgb(96, 96, 255);
    }
  }
}

使用下看看效果:

<template>
  <SPagination :total="100"></SPagination>
</template>

只有右更多按钮的效果:

在这里插入图片描述

左右更多按钮都有的效果:

在这里插入图片描述

只有左更多按钮的效果:

在这里插入图片描述

到目前为止,分页器复杂的渲染部分就完成了。
接下来实现相对简单的逻辑部分,直接给各种分页按钮加事件即可。

const { pageIndex, setPageIndex, jumpPage } = usePage()

<ul class="s-pager">
  <!-- 首页是常驻的,不管 -->
  <li onClick={() => setPageIndex(1)}>1</li>

  <!-- 左更多按钮显示的条件有2个: -->
  <!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount -->
  <!-- 2. 当前页码大于 Math.ceil(pagerCount / 2) -->
  { totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) && <li class="more left" onClick={() => jumpPage(-5)}>...</li> }

  <!-- 中间页码 -->
  { centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }

  <!-- 右更多按钮显示的条件也有2个: -->
  <!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount(和左更多按钮相同) -->
  <!-- 2. 当前页码小于 totalPage.value - Math.ceil(pagerCount.value / 2) + 1 -->
  { totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && <li class="more right" onClick={() => jumpPage(5)}>...</li> }

  <!-- 尾页显示的条件是:总页码大于1 -->
  { totalPage.value > 1 && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>}
</ul>

我们的基础分页组件基本完成,主要实现了以下功能:

  1. 上一页、下一页
  2. 分页器,包含:
    1. 首尾页
    2. 左右更多按钮(可快速往前、往后跳转N页)
    3. 中间分页按钮

Pagination组件代码如下:

import { defineComponent, toRefs, computed } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination.type'
import { getCenterPages } from './utils'

export default defineComponent({
  name: 'SPagination',
  props: paginationProps,
  setup(props: PaginationProps) {
    const pagerCount = ref(7)
    const { total, pageSize } = toRefs(props)
    const totalPage = Math.ceil(total.value / pageSize.value)
    const { pageIndex, prevPage, nextPage, setPageIndex, jumpPage } = usePage()
    const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))

    return () => (
      <div class="s-pagination">
        <button onClick={prevPage}>上一页</button>
        <!-- Pager 部分 start -->
        <ul class="s-pager">
          <!-- 首页是常驻的,不管 -->
          <li onClick={() => setPageIndex(1)}>1</li>
        
          <!-- 左更多按钮显示的条件有2个: -->
          <!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount -->
          <!-- 2. 当前页码大于 Math.ceil(pagerCount / 2) -->
          { totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) && <li class="more left" onClick={() => jumpPage(-5)}>...</li> }
        
          <!-- 中间页码 -->
          { centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
        
          <!-- 右更多按钮显示的条件也有2个: -->
          <!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount(和左更多按钮相同) -->
          <!-- 2. 当前页码小于 totalPage.value - Math.ceil(pagerCount.value / 2) + 1 -->
          { totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && <li class="more right" onClick={() => jumpPage(5)}>...</li> }
        
          <!-- 尾页显示的条件是:总页码大于1 -->
          { totalPage.value > 1 && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>}
        </ul>
        <!-- Pager 部分 end -->
        <button onClick={nextPage}>下一页</button>
      </div>
    )
  }
})

效果如下:

在这里插入图片描述

5 抽取Pager组件

这时我们发现Pagination组件已经有120行左右代码,代码量看起来还是略显得有些多,特别是随着后续功能的增加,setup的代码会越来越多,与其等代码变成“屎山”,不如在它有恶化的苗头时就开始重构,将“战场”及时打扫干净。

最复杂的部分就是中间的Pager部分:

<ul class="s-pager">
  <li onClick={() => setPageIndex(1)}>1</li>
  { totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) && <li class="more left" onClick={() => jumpPage(-5)}>...</li> }
  { centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
  { totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && <li class="more right" onClick={() => jumpPage(5)}>...</li> }
  { totalPage.value > 1 && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>}
</ul>

这是Pagination组件的主体部分,它和上一页、下一页是独立的,应该抽取出来。

抽取的方式分成三步:

  1. 先把代码拷贝一份到pager.tsx中
  2. 完善pager.tsx,补齐缺失的变量和方法,使之能独立使用
  3. 将pagination.tsx中pager部分的代码移除,使用Pager组件替代
  4. 第一步:拷贝代码

在components子组件目录中创建一个pager.tsx的文件,并将Pager部分的代码拷贝过去。

import { defineComponent } from 'vue'

export default defineComponent({
  name: 'SPager',
  setup() {
    return () => {
      return (
        <ul class="s-pager">
          <li onClick={() => setPageIndex(1)}>1</li>
          {
            totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) 
            && <li class="more left" onClick={() => jumpPage(-5)}>...</li>
          }
          { centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
          {
            totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1
            && <li class="more right" onClick={() => jumpPage(5)}>...</li>
          }
          {
            totalPage.value > 1
            && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>
          }
        </ul>
      )
    }
  }
})   

第二步:缺啥补啥

我们发现缺了很多变量和方法,依然是从Pagination那边拷贝过去,缺啥补啥。

import { defineComponent, toRefs, computed } from 'vue'
import usePage from './composables/use-page'
import { PagerProps, pagerProps } from './pager.type'
import { getCenterPages } from './utils'

export default defineComponent({
  name: 'SPager',
  props: pagerProps,
  setup(props: PagerProps) {
    const pagerCount = ref(7)
    const { total, pageSize } = toRefs(props)
    const totalPage = Math.ceil(total.value / pageSize.value)
    const { pageIndex, setPageIndex, jumpPage } = usePage()
    const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))
    
    return () => {
      return (
        <ul class="s-pager">
          <li onClick={() => setPageIndex(1)}>1</li>
          {
            totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) 
            && <li class="more left" onClick={() => jumpPage(-5)}>...</li>
          }
          { centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
          {
            totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1
            && <li class="more right" onClick={() => jumpPage(5)}>...</li>
          }
          {
            totalPage.value > 1
            && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>
          }
        </ul>
      )
    }
  }
})

需要注意的是,PagerPagination组件的props和相应的类型是一样的,直接从pagination-types.ts中取即可,以下是pager-types.ts的代码:

import { ExtractPropTypes } from 'vue'
import { paginationProps } from '../pagination-type'

export const pagerProps = paginationProps
export type PagerProps = ExtractPropTypes<typeof pagerProps>

第三步:替换

有了Pager组件,我们先单独使用看看效果。

## 分页器 pager
通过`s-pager`直接使用分页器组件

:::demo
```vue
<template>
   When you have few pages
  <SPager :total="50"></SPager>

  When you have more than 7 pages
  <SPager :total="1000"></SPager>
</template>

由于和Pagination的props一样,所以使用方式也是一样的。

效果如下:

在这里插入图片描述

功能和Pagination组件一样,就是少了上一页、下一页的按钮。

确认子组件Pager的功能没问题之后,我们就可以将其用在Pagination中了。

import { defineComponent, toRefs, computed } from 'vue'
import { PaginationProps, paginationProps } from './pagination.type'
import SPager from './components/pager'

export default defineComponent({
  name: 'SPagination',
  props: paginationProps,
  setup(props: PaginationProps) {
    const { total, pageSize } = toRefs(props)

    return () => (
      <div class="s-pagination">
        <button onClick={prevPage}>上一页</button>
        <SPager total={total.value} pageSize={pageSize.value}></SPager>
        <button onClick={nextPage}>下一页</button>
      </div>
    )
  }
})

再次确认功能依然是正常的,这时我们就完成了Pager的抽离。

回顾下这次重构,我们

  1. 先是将pagination.tsx中Pager部分的HTML代码都移到了pager.tsx文件中。
  2. 然后又用缺啥补啥的原则,将相关的变量和方法也一并移过去。
  3. 最后用新创建的Pager子组件替换掉Pagination中的部分,并完成重构。

重构的好处

Pager子组件从Pagination中抽离出来,有诸多好处,最显而易见的好处就是

Pagination组件的代码少了一半(40多行降到不到20行)

代码少了,可读性就提升了,一眼就能看懂代码的意图,后续维护起来也方便,20行代码bug几乎无处藏身。

除了这些好处,还有一个比较隐藏的好处,就是组件的灵活性和可扩展性提升了。

假如有一个用户要用我们的分页组件,但是不需要上一页、下一个的按钮,这时我们可能会在props中加一个:showPrevNextBtn

export const paginationProps = {
  total: {
    type: Number,
    default: 0,
  },
  pageSize: {
    type: Number,
    default: 10,
  },
  showPrevNextBtn: {
    type: Boolean,
    default: true,
  }
}

然后在代码里判断这个api是否设置成true,如果设置成true则展示上一个、下一页按钮,否则就隐藏。


<div class="s-pagination">
  { showPrevNextBtn && <button onClick={prevPage}>上一页</button> }
  <!-- Pager 部分 -->
  { showPrevNextBtn && <button onClick={nextPage}>下一页</button> }
</div>

该用户使用的时候,则加上这个参数

<template>
  <SPagination :total="50" :showPrevNextBtn="false"></SPagination>
</template>

有了Pager子组件,用户不需要上一页、下一页,很简单,直接用Pager就行呀

<template>
  <SPager :total="50"></SPager>
</template>

也不用加额外的参数。

6 给Pagination加功能

有了核心的分页能力,并且实现了Pager子组件,要实现各种功能就是加props的问题啦。

左右分页按钮的可用状态逻辑

当页码在第一页时,我们的上一页按钮应该禁用,不可点击;

同理当页码在最后一页时,我们的下一页按钮也应该禁用。

我们先加上这个逻辑:

setup(props: PaginationProps) {
  const pager = ref()
  // 按钮禁用状态
  const disablePrev = computed(() => 
    pager.value ? pager.value.pageIndex < 2 : true
  )
  const disableNext = computed(() => 
    pager.value ? pager.value.pageIndex > pager.value.totalPage - 1 : true
  )
  return () => (
    <div class="s-pagination">
      <button disabled={disablePrev.value} onClick={prevPage}>上一页</button>
      <SPager ref={pager}></SPager>
      <button disabled={disableNext.value} onClick={nextPage}>下一页</button>
    </div>
  )
}

动态改变total和pageSize

目前我们的Pagination组件只有两个参数:

  • total 总数据条数,默认值为0
  • pageSize 每页数据条数,默认值为10

并且都不是必填的,当什么都不传时,会显示一个高亮的1,并且上一页、下一页按钮都是禁用状态。

在这里插入图片描述

我们可以只传total,比如后台返回了10000条数据给前端,因为pageSize默认时10,所以会显示1000页。
在这里插入图片描述
假如用户删除了部分数据,这时total是需要动态变化的,目前我们的Pagination组件是会有一个bug的。

还记得我们之前计算totalPage的逻辑吗?之前为了动态控制下一页按钮的可用状态,我们需要比较当前页码和最大页码,所以需要计算totalPage。

const totalPage = Math.ceil(total.value / pageSize.value)
<button disabled={pageIndex.value > totalPage - 1} onClick={nextPage}><IconArrowRight /></button>

由于totalPage是一次性计算的,当total动态变化时,totalPage并不会跟着变化,这会导致下一页按钮不会动态变化,这就出bug啦。

这时我们需要将totalPage的计算逻辑用computed包裹一下,成为一个计算属性:

const totalPage = computed(() => Math.ceil(total.value / pageSize.value))

这时total就能动态变化啦,并且如果我们需要动态改变pageSize的值,比如每页10条数据太少了,想设置每页100条数据,目前也是可以实现的。

设置最大页码按钮数

还记得我们开发Pager时,提到一个pagerCount最大页码按钮数,该变量当时是写死的7,这个变量是用来控制页码按钮数量的,避免显示太多页码按钮,导致页面不美观。

const pagerCount = ref(7)

如果用户觉得这个数字太大或者太小,想调整怎么办呢?

这时我们就需要增加第3页props:pagerCount

修改pagination-type.ts文件:

export const paginationProps = {
  total: {
    type: Number,
    default: 0,
  },
  pageSize: {
    type: Number,
    default: 10,
  },
  pagerCount: {
    type: Number,
    default: 7
  }
};

然后把写死的pagerCount改成从props中获取:

const pagerCount = ref(7)
const { total, pageSize } = toRefs(props)

->

const { total, pageSize, pagerCount } = toRefs(props)

由于用到pagerCount变量的centerPages是computed计算属性,因此pagerCount也是可以动态设置的。

const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))

效果如下:
在这里插入图片描述

pageIndex双向绑定

开发了那么多功能,到头来我们会发现业务根本用不起来:

因为我们的Pagination组件没有输出

业务使用的时候,关键是需要知道当前页码是什么,目前当前页码状态是封闭在Pagination组件内部的,并没有暴露出去。

我们考虑把pageIndex当前页码做成双向绑定,这样方便业务灵活使用。

这时我们需要增加第4个props:modelValue,以及与之配套的emits:update:modelValue

先在pagination-types.ts文件中增加modelValue

export const paginationProps = {
  modelValue: {
    type: Number,
    default: 1
  }
};

再在pagination.tsx中增加update:modelValue

export default defineComponent({
  name: 'SPagination',
  props: paginationProps,
  emits: ['update:modelValue'], // 新增
  setup(props: PaginationProps, { emit }) {
    ...

    // 每当内部的 pageIndex 发生变化,都将最新的 pageIndex 值暴露给外部
    onMounted(() => {
      watch(
        () => pager.value.pageIndex,
        (newVal: number) => {
          emit('update:modelValue', newVal)
        }
      )      
    })
    
    ...
  }
})

效果如下:
在这里插入图片描述
这样就够了吗?

这样做只是打通了从组件内到组件外的数据传递,并没有将组件外部的数据同步给组件内部,比如v-model初始值设置为2,这时咱们的Pagination并没有高亮第2页。
在这里插入图片描述
因此我们还需要加上从外到内的数据同步:

// modelValue 代表外部数据,当 modelValue 发生变化时,应该同步改变内部的 pageIndex
watch(
  () => props.modelValue,
  (newVal: number) => {
    pager.value.pageIndex = newVal
  }
)

到目前为止我们的Pagination组件就完成了,主要包含以下特性:

  1. 动态设置total / pageSize / pagerCount
  2. pageIndex支持双向绑定
  3. 可独立使用的分页器Pager
  4. UI无关的usePage
  5. 左右分页

基于现有的核心功能我们可以很轻易地扩展出以下辅助功能和特定样式的分页组件。

比如带背景色的分页:
在这里插入图片描述
又比如这种小型分页:

在这里插入图片描述

以及各种带附加功能的分页:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88502.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字孪生助力油气管道行业实现资产管理

随着数字孪生技术的发展日臻成熟&#xff0c;各个行业领域都在经历一场翻天覆地的变化。结合国内的油气管网系统建设现状&#xff0c;数字孪生技术对油气管道行业数智化建设必将有重大而深远的意义。 数字孪生助力油气管道行业实现资产管理 北京智汇云舟科技有限公司成立于201…

【发表案例】2/3区计算机视觉类SCI,3个月19天录用

2/3区计算机视觉类SCI 【期刊简介】IF:2.5-3.0&#xff0c;JCR2/3区&#xff0c;中科院4区 【检索情况】SCI 在检&#xff0c;正刊 【征稿领域】面向智能交通应用的物联网驱动计算机视觉技术 录用案例&#xff1a;3个月19天录用 2022.12.05 | Accepted 2022.11.17 | Edit…

全新的 React 组件设计理念 Headless UI

其实&#xff0c;最早接触 Headless UI 是在去年&#xff0c;碰巧看到了一个非常前沿且优秀的组件库 ---- Chakra UI&#xff0c;这个组件库本身就是 Headless UI 的实践者&#xff0c;同时也是 CSS-IN-JS 的集大成者。 我当时看过之后&#xff0c;就对该理念产生了很大的兴趣…

(2022最新)Xray、Rad两款工具的使用与联动

1、Xray的简介 xray 是一款功能强大的安全评估工具&#xff0c;由多名经验丰富的一线安全从业者呕心打造而成&#xff0c;主要特性有: 1、检测速度快。发包速度快; 漏洞检测算法效率高。 2、支持范围广。大至 OWASP Top 10 通用漏洞检测&#xff0c;小至各种 CMS 框架 POC&am…

ClickHouse Senior Course Ⅵ

序言 这里单独说明下分布式表引擎,不用分布式表引擎,感觉ClickHouse就没必要使用了cuiyaonan2000163.com 参考网址: 分布式引擎 | ClickHouse Docs 分布式表引擎的位置: 分布式引擎 分布式引擎本身不存储数据, 但可以在多个服务器上进行分布式查询。 读是自动并行的。读取…

内核动力之源——内存管理

目录 内存管理背后的故事 内存管理概述 常见内存分配策略 LwIP的宏配置及内存管理 见招拆招——动态内存堆 数据结构描述 函数实现 ​以不变应万变——动态内存池 数据结构描述 函数实现 使用C库管理内存策略 无论在哪种系统中&#xff0c;动态内存都是一个非常重要的…

12.5、后渗透测试--内网主机屏幕截图

攻击主机&#xff1a; Kali 192.168.11.106靶机&#xff1a;windows server 2008 r2 192.168.11.134前提&#xff1a;获得 meterpreter shell操作屏幕的几种方式&#xff1a;screenshotscreenshare加载espia模块&#xff0c;使用screengrab一、screenshot # 截图 meterprete…

数据分析案例-往届世界杯数据可视化

目录 1.引言 2.项目简介 2.1数据集介绍 2.2技术工具 3.数据可视化 3.1往届世界杯获奖国家TOP5 3.2往届世界杯比赛数据情况 3.3往届世界杯观众人数情况 3.4往届世界杯主办方情况 3.5往届世界杯冠军队情况 1.引言 足球是世界上非常受欢迎的运动之一&#xff0c;在全球…

数据可视化的最佳实践【不容错过】

在当前的市场中&#xff0c;数据可视化已经成为了传播数据信息的标准和载体。从商业智能BI到新闻媒体行业&#xff0c;处处都存在着数据可视化的影子&#xff0c;它帮助了我们更好的理解数据和交流数据中传达出的信息。研究表明&#xff0c;大脑对于可视化呈现出来的信息更加容…

Spring Cloud Alibaba基础教程:nacos安装

我们在学习springCloud的时候用的注册中心是Eureka: springBoot集成springCloud&#xff08;一&#xff09;注册中心 但是由于houlai Eureka2.0后续不维护&#xff0c;国内就需要一个可靠的注册中心。所以现在大部分都是用nacos。下面我们来说下如何安装nacos 一&#xff1a…

PMP证书含金量高在哪里?

关于 PMP 含金量的问题&#xff0c;争议一直挺大的&#xff0c;报考费这么贵、通过率这么高&#xff0c;身边都有这个证了&#xff0c;考了没有用上就没有含金量了。相信很大一部分人都是这么想的&#xff0c;但是每年依然有上万考生参加考试&#xff0c;这是为啥呢&#xff1f…

量子技术相关的精简介绍

量子信息 量子信息通信技术是利用量子特性的新一代信息通信技术利用量子力学状态的量子密码通信&#xff08;量子密钥分配、量子随机数发生器等&#xff09;、量子计算机&#xff08;处理器&#xff09;及量子传感的技术量子信息通信技术不仅会带来现有ICT技术的划时代变化,而…

Mongodb数据库之主从复制配置实战

Mongodb数据库之主从复制配置实战一、本次实践环境规划1.环境规划2.副本集介绍二、检查本地Mongodb状态1.检查主节点Mongodb状态2.查看从节点mongodb状态三、创建mongodb用户1.进入主节点mongodb2.创建admin账号3.创建root账号四、全部节点的统一配置1.在主节点创建key文件2.将…

抢订单,稳增长!道可云元宇宙平台助力企业竞逐海外市场

受新冠肺炎疫情和国际政治经济形势错综复杂的不利影响&#xff0c;我国的外贸企业普遍面临订单下滑、供应链不畅、经营压力大等困难&#xff0c;国际需求大幅萎缩。随着后疫情时代的来临&#xff0c;我国的疫情防控政策不断优化调整&#xff0c;市场对企业出海抢占商机的关注度…

node只能以管理员的身份运行才能正常使用

node只能以管理员的身份运行才能正常使用&#xff0c;解决办法 下载node后&#xff0c;默认下载包安装在c盘中&#xff0c; 修改方式&#xff1a; npm config set prefix “D:\Program Files\nodejs\node_global” npm config set cache “D:\Program Files\nodejs\node_cache…

Java+SSM客户信息管理系统(含源码+论文+答辩PPT等)

项目功能简介: 该项目采用的技术实现如下&#xff1a; 后台框架&#xff1a;Spring、SpringMVC、MyBatis UI界面&#xff1a;JSP、jQuery 、BootStrap 数据库&#xff1a;MySQL 该客户信息管理系统以实际运用为开发背景&#xff0c;Java开发语言&#xff0c;使用JSP设计页面&am…

Geoserver中预览图层时放大到某一层级不显示(样式Styles中未设置对应比例尺的样式)

场景 GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践)&#xff1a; GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践)_霸道流氓气质的博客-CSDN博客_geoserver简介 geoserver在预览图层时&#xff0c;当发大图层时内容不显示。 比如文字text图…

智能门驱动光耦合器与集成的反馈控制器

介绍 新的ACPL-302J是一种改进的智能门驱动光耦合器隔离电源和简化门驱动的设计。 ACPL-302J具有一个集成的反控制器为DC-DC转换器和一个完整一套故障安全的IGBT诊断、保护和故障报告&#xff0c;提供一个完整的成本有效的门驱动解决方案&#xff08;图1&#xff09;。带2.5A…

网络工程毕业设计题目100例

文章目录0 简介1 如何选题2 最新网络工程选题2.1 Java web - SSM 系统2.2 大数据方向2.3 人工智能方向2.4 其他方向4 最后0 简介 丹成学长&#xff0c;搜集分享最新的网络工程专业毕设毕设选题&#xff0c;难度适中&#xff0c;适合作为毕业设计&#xff0c;大家参考。 学长整…

Cuda __CUDA_ARCH__宏

__CUDA_ARCH__属于NVCC的宏 5.7.4. Virtual Architecture Macros 给出说明 The architecture identification macro __CUDA_ARCH__ is assigned a three-digit value string xy0 (ending in a literal 0) during each nvcc compilation stage 1 that compiles for compute_xy…