总结 vue 实现分页器的基本思路

简介

本文介绍基于 vue2 框架实现基本的分页器,包括页码前进/后退、页码点击跳转、显示 ... 、显示总页数、显示总数据条数 等功能。

效果预览

快速跳转

视图部分样式部分数据部分逻辑处理部分

实现思路

视图部分

视图方面比较简单,将各功能部分依次搭建即可,包括前进键(>>)、后退键(<<)、中间以及首尾(1和页码上限)的页码、...、以及页码总计和数据总计。

代码示例

<template>
  <div class="pagination">
    <div class="pagination_wrap">
      <div class="pagination_total-data pagination_info">
        <span>共{{ total }}条数据</span>
      </div>
      <div class="pagination_action-group">
        <div
          class="prev pagination_btn"
          :class="{ disabled: isPrevDisabled }"
          :disabled="isPrevDisabled"
          @click="handlePrev"
        >
          <a>«上一页</a>
        </div>
        <div class="pagination_btn-group" @click="handlePageSelect">
          <template v-if="isShowPrevDot">
            <li class="pagination_btn">
              <a data-page="1">1</a>
            </li>
            <li class="pagination_dotted">
              <span>...</span>
            </li>
          </template>
          <li
            class="pagination_btn"
            :class="{ active: currentPage === i }"
            v-for="i in pageList"
            :key="i"
          >
            <a :data-page="i">{{ i }}</a>
          </li>
          <template v-if="isShowNextDot">
            <li class="pagination_dotted">
              <span>...</span>
            </li>
            <li class="pagination_btn">
              <a :data-page="totalPages">{{ totalPages }}</a>
            </li>
          </template>
        </div>
        <div
          class="next pagination_btn"
          :class="{ disabled: isNextDisabled }"
          :disabled="isNextDisabled"
          @click="handleNext"
        >
          <a>下一页»</a>
        </div>
      </div>
      <div class="pagination_total-pages pagination_info">
        <span>共{{ totalPages }}页</span>
      </div>
    </div>
  </div>
</template>

样式部分

样式部分(scss),通过 Flex 布局使元素水平排列,并赋予基本样式

// 全局 css 变量
:root {
  --gap-small: 5px;
  --gap-primary: 15px;
}

// 覆盖浏览器默认样式
body {
  li {
    box-sizing: border-box;
  }
  // 注意添加下面这条样式,否则部分浏览器在 ... 旁边会显示一个额外的 ·
  [class*='dotted']::marker {
    content: '';
  }
}

.pagination {
  width: 100%;
  overflow: hidden;

  .pagination_wrap {
    margin: 18px 0;
    display: flex;
    justify-content: center;

    .pagination_action-group {
      margin-left: 0;
      margin-bottom: 0;
      vertical-align: middle;
      display: flex;

      .pagination_btn {
        line-height: 18px;
        display: inline-block;

        a {
          position: relative;
          line-height: 18px;
          text-decoration: none;
          background-color: #fff;
          border: 1px solid #e0e9ee;
          font-size: 14px;
          padding: 9px 18px;
          color: #333;
        }

        &.active {
          a {
            background-color: #fff;
            color: #e1251b;
            border-color: #fff;
            cursor: default;
          }
        }

        &.prev,
        &.next {
          a {
            background-color: #fafafa;
            cursor: pointer;
          }
        }

        &.disabled {
          a {
            color: #999;
            cursor: default;
          }
        }
      }

      .pagination_dotted {
        span {
          position: relative;
          line-height: 18px;
          text-decoration: none;
          background-color: #fff;
          font-size: 14px;
          border: 0;
          padding: 9px 18px;
          color: #333;
        }
      }

      .pagination_btn-group {
        display: flex;
      }
    }

    .pagination_info {
      color: #333;
      font-size: 14px;
    }

    .pagination_total-data {
      margin-right: var(--gap-primary);
    }

    .pagination_total-pages {
      margin-left: var(--gap-primary);
    }
  }
}

数据和逻辑

接下里是关键的逻辑实现,包括前进、后退、页码点选跳转、显示 ... 等功能。

数据部分

props 中定义的 4 个变量与数据显示相关,通常从服务端获取,且不支持内部修改,因此由父组件传递进来;data 和 computed 中定义的 8 个变量与视图构建或更新相关,属于组件内私有,仅支持内部修改。

props: {
  total: Number, // 数据总数
  currentPage: Number, // 当前页码数
  pagerCount: Number, // 允许同时显示的页码数
  totalPages: Number, // 页码总数
},
data() {
  return {
    pageList: [], // 中间区域显示的页码范围
    start: 0, // 中间区域显示的开始页码指针
    end: 0, // 中间区域显示的结束页码指针
    isShowPrevDot: false,
    isShowNextDot: false,
  };
},
computed: {
  resourceData() {
    return { total: this.total, currentPage: this.currentPage };
  },
  isPrevDisabled() {
    return this.currentPage === 1;
  },
  isNextDisabled() {
    return this.currentPage === this.totalPages;
  },
  // 总页数超过当前允许的同时显示页数,才有可能需要显示 ...
  isDotNeeded() {
    return this.totalPages > this.pagerCount;
  },
},

逻辑处理部分

逻辑处理部分包括:

  • 两个内部变量更新方法:setupPageList(start, end) 和 setupPagination(currentPage, totalPages, pagerCount)
  • 一个 emit 通知方法:updateCurrentPage(newPage)
  • 三个事件处理方法:handlePrev() 、handleNext() 、handlePageSelect(e)
  • 一个侦听器:侦听 currentPage 并初始化
methods: {
  // 根据开始和结束指针,重新生成中间区域的页码
  setupPageList(start, end) {
    let pageList = [];
    for (let i = start; i <= end; i++) {
      pageList.push(i);
    }
    return pageList;
  },
  // 根据操作后的当前页码,重新判定是否显示两侧的 ... ,以及更新中间区域的页码范围
  setupPagination(currentPage, totalPages, pagerCount) {
    // 初始化 start 和 end
    let start = 1, end = totalPages;
    if (this.isDotNeeded) {
      // 从当前页码开始,向左右两侧延伸 pagerCount 一半的距离
      let halfOffset= Math.floor(pagerCount / 2, 0);
      start = Math.max(1, currentPage - halfOffset);
      end = Math.min(totalPages, currentPage + halfOffset);
      // 若中间页码数量仍小于 pagerCount,先拉长到 pagerCount
      if (end - start + 1 < pagerCount) {
        if (end - pagerCount > -1) {
          // 左侧仍有剩余页码
          start = end - pagerCount + 1;
        } else {
          end = start + pagerCount - 1;
        }
      }
      // 根据延伸后的左右指针,判定是否需要显示左右两侧的 ...
      this.isShowPrevDot = start > 1;
      this.isShowNextDot = totalPages - end > 0;
    } else {
      this.isShowPrevDot = false;
      this.isShowNextDot = false;
    }
    // 根据 ... 的判定结果,赋值两侧指针的最终值
    this.start = Math.max(1, start + (this.isShowPrevDot ? 1 : 0));
    this.end = Math.min(totalPages, end - (this.isShowNextDot ? 1 : 0));
    // 根据最新的指针,生成中间区域的页码
    this.pageList = [].concat(this.setupPageList(this.start, this.end));
  },
  handlePrev() {
    const oldPage = this.currentPage;
    const newPage = Math.max(1, oldPage - 1);
    if (oldPage !== newPage) {
      this.updateCurrentPage(newPage);
      // 由于侦听器的存在,不需要手动触发分页器更新;next 与 select 同理
      // this.setupPagination(newPage);
    }
  },
  handleNext() {
    const oldPage = this.currentPage;
    const newPage = Math.min(this.totalPages, oldPage + 1);
    if (oldPage !== newPage) {
      this.updateCurrentPage(newPage);
    }
  },
  handlePageSelect(e) {
    const newPage = +e.target.dataset.page;
    this.updateCurrentPage(newPage);
  },
  // 通知父组件修改当前页
  updateCurrentPage(newPage) {
    this.$emit('updateCurrentPage', newPage);
  },
},
// 由于数据从服务端获取,需要在当前组件或作为父组件的业务组件中监听初始数据的变化,并触发分页器的初始化
// 同时,用户改变当前页码时,也会触发分页器更新
watch: {
  resourceData: {
  // 若只侦听当前页码变化,则数据源变化但当前页未变时无法自动更新
  // 因此,改为侦听一个计算属性,该计算属性包含了需要侦听的多个属性: total 和 currentPage
  // currentPage: { 
    immediate: true,
    handler() {
      this.$nextTick(() => {
        this.setupPagination(
          this.currentPage,
          this.totalPages,
          this.pagerCount
        );
      });
    },
  },
},
posted @ 2022-10-19 11:29  CJc_3103  阅读(266)  评论(0编辑  收藏  举报