乐趣区

第九集-从零开始实现一套pc端vue的ui组件库-分页器组件

第九集: 从零开始实现(分页器组件)

本集定位:
分页器这个组件也算是个老朋友了, 还记得刚学 js 的时候, 写个分页器要 300 行代码, 要是能穿越回去, 我得好好教教我自己设计模式????.

随着现在手机地位的提升, 大部分人上网的时间都用在了手机上, pc 端的确是少了很多很多, 而分页器这种类型的组件, 真的并不很适合手机, 已经不符合人类的操作体验了, 人们现在都是划划划或拉拉拉的动作驱动翻页, 分页器其实要配合鼠标才能有很好的感受, 人的手指点击并不精准, 而且点击的时候还会遮盖住视野, 反正本人更推荐 pc 端使用分页器来跳转, 移动端可不要这么玩, 多想点让用户更舒服的操作吧.

本次编写参考了饥人谷的视频, 同时也看了 element 的源码, 但最终还是按我自己的想法构建了这个组件.

  1. 增加了自己的一些理解, 实现方式与想法挺有趣的, 毕竟代码这种东西不是一成不变的, 更多的玩法才能使编程更有生命力,
  2. 去掉了比如说一个输入框, 可以跳转到固定页数, 这个功能我去年做了半年的后台管理系统, 一次也没用上.
  3. 没有去想传统组件一样, 用户传入总条数, 然后我来给分成对应的页数, 而是采用用户自己传入分成了多少页.
  4. 本次激活的页面会以 v -model 的形式与用户进行交互, 也就是说这个变量是双向的, 上面说去掉的两个功能就非常好实现了, 但本次不会做, 毕竟实践经验告诉我, 做了真没啥用

一. 基本结构
这个结构是参考的别人的源码 … 还有挺好的, 虽然 dom 写的有点丑, 但是逻辑清晰, 易维护.

老样子
vue-cc-ui/src/components/Pagination/index.js

import Pagination from './main/pagination.vue';

Pagination.install = function(Vue) {Vue.component(Pagination.name, Pagination);
  };

export default Pagination;

躯壳

  1. 单独写了 第一位与最后一位, 默认就是这两位要一直展示给用户选择
  2. … 这种标志也是单独写了, 毕竟首尾都直接写了, 那他也可以这样操作
  3. 左右两边的两个按钮允许用户插入自己的代码
  4. 这个结构的好处就是把问题具体化了, 不用考虑其他的, 当前核心问题就是如何求出中间 for 循环的数据, 也就是本集的重点了.
<template>
  <div class='cc-pagination'>
    <button class="btn-prev">
      <slot name='previous'> &lt; </slot>
    </button>
    <ul class='cc-pagination__box'>
      <li>1</li>
      <li>··</li>
      <li ....>{{item}}</li>
      <li>··</li>
      <li v-if="总页数!== 1">{{总页数}}</li>
    </ul>
    <button class="btn-prev">
      <slot name="next"> &gt; </slot>
    </button>
  </div>
</template>

先展示一下基本的样子

css 方面


@include b(pagination) {
    cursor: pointer;
    color: #606266; // 这个颜色很柔和的黑
    align-items: center;
    display: inline-flex;
    justify-content: center;
    .btn-prev { // 按钮去掉默认样式
        border: none;
        outline: none;
        background-color: transparent;
        &:hover { // 这个 nomal 是个柔和的蓝色
            color: $--color-nomal
        }
    }
}

二. 功能的定义

  1. pageTotal: 总的页数, 就是说比这波数据分成 700 页显示, 那就传进来 700,
  2. pageSize: 最多显示多少个分页标示, 比如说 传入了 3, pageTotal 传入了 6, 那就是
    1,2 … 6 页面上只显示这三个数. 经过很多次实验, 这个数最小也要传 5, 不然体验会很差, 最大可以传无限, 朋友们有机会可以自己试试.
  3. value: 实现 v -model 的基本元素
  4. validator: 这个函数是子组件接收参数时的校验函数, 这里不能修改参数, 他只负责告诉用户传的对不对就好了, 不要有太多功能, 逻辑分散的话不好维护.
  5. 下面的代码出现了三个重复的函数, 那么 必须要封装一个共用的工具函数了
pageSize: {
      type: Number,
      default: 5,
      validator: function(value){if (value < min || value !== ~~value) {throw new Error(` 最小为 5 的整数 `);
       }
       return true;
      }
    },
    value: {
      // 选中页
      type: Number,
      required: true,
      validator: function(value){if (value < min || value !== ~~value) {throw new Error(` 最小为 1 的整数 `);
       }
       return true;
      }
    },
    pageTotal: {
      // 总数
      type: Number,
      default: 1,
      required: true,
      validator: function(value){if (value < 1 || value !== ~~value) {throw new Error(` 最小为 1 的整数 `);
       }
       return true;
      }
    }

抽离工具函数
vue-ui/my/vue-cc-ui/src/assets/js/utils.js

// inspect 单词就是检测的意思, 暂时业务只需要传入一个最小值;
export function inspect(min) {
// 返回一个函数作为真正的校验函数
  return function(value) {
// 小于这个最小值或不是整数的都要抛错
// ~~ 这个位运算符的写法的意思就是取整, 取整之后与没取整相等, 当然就不是浮点数
// ~ 运算符是对位求反,1 变 0,0 变 1,也就是求二进制的反码
    if (value < min || value !== ~~value) {throw new Error(` 最小为 ${min}的整数 `);
    }
    return true;
  };
}

经过抽离, 我这里就可以化简了, 清爽了很多

    pageSize: {
      type: Number,
      default: 5,
      validator: inspect(5)
    },
    value: {
      type: Number,
      required: true,
      validator: inspect(1)
    },
    pageTotal: {
      type: Number,
      default: 1,
      required: true,
      validator: inspect(1)
    }

三. 完善页码的展示 (重点)
逐一分析:

  1. 前面说了, 首尾页码已经直接写上了, 所以比如用户定义的 pageSize 为 5 那么我就要取出中间的 3 个,
    比如用户当前在 第 6 页, 总页数 12 页, 那么 1,…,5,6,7,…,12 中间的 567 就是我要获取的目标
  2. 本次选择用计算属性来做, 可以监控 v -model 的实时变化. 名为 showPages, 供 li 去循环展示;
  3. 兼容 value 值出现错误的情况
  4. 这种做法肯定是有偏移的, 比如说 用户输入了 pageSize 为 4, 会出现两种情况让你选择
    1,…,5,6,…,12, 1,…,6,7,…,12 在 6 被激活时, 到底是要 5, 还是 7 这个没必要纠结, 随便写一个就好了, 因为我纠结了一下感觉没意义????;
  5. 做法思路, 拿到当前要激活的页码 value, 然后向他左右延伸, 比如拿到 value 是 6, 那么左右就是 5,7, 这样不断的遍历拿值, 最终在规定数量内, 并且不要触及边界条件.
showPages() {
// 习惯性的定义返回的变量
      let result = [],
      // 拿到所需的变量
        value = this.value,
        pageTotal = this.pageTotal,
        // 因为要去掉头尾, 所以 -2
        pageSize = this.pageSize - 2;
      // 防止用户输入错误引起的混乱, 比如用户的缓存, 要返给用户, 让用户去处理, 因为很可能 v -model 出现死循环
      if (value > pageTotal) {
      // 友好的触发一个错误事件
        this.$emit("error", value, pageTotal);
        value = pageTotal;
      }
      // 如果被激活的页面在 1 与 end 之间, 则把 value 放入数组, 不然的话会出现重复值
      if (value > 1 && value < pageTotal) result.push(value);
      // 左右开弓, 求出当前激活的页码左右的数据
      for (let i = 1; i <= pageSize; i++) {
      // 加法, 所以检测小于总数就行
        if (value + i < pageTotal) {result.push(value + i);
          // 随时甄别是否已经符合条件, 取值已够就退出;
          if (result.length >= pageSize) break;
        }
      // 减法, 只要检测大于 1 就行
        if (value - i > 1) {result.unshift(value - i);
          if (result.length >= pageSize) break;
        }
      }
      return result;
    },

上面的 li 标签 放心遍历了

 <li v-for="item in showPages"
          :key='item'
          :class="{'is-active':value === item}">{{item}}</li>

四. 定义事件

说了这么多, 结构已经做好了, 那么就需要事件的驱动了;

  1. 这个事件负责通知父级改变值, 同时会做相应的校验;
  2. 参数为当前想要激活哪一页
  3. 每次事件都通知父级会有重复的激活, 所以这个方法里面会把想要激活的页码与当前激活的页码进行比较, 放在抛出的事件的第二个参数里面, 用户只要判断 isNoChange 的真伪就知道是否要请求新数据了, 用户还可以根据这个提示用户 ” 您已在 xx 页 ”
 handlClick(page) {if (page < 1) page = 1;
      if (page > this.pageTotal) {page = this.pageTotal;}
      let isNoChange = this.value === page;
      this.$emit("input", page);
      // 当前值, 与当前值相比是否有变化
      this.$emit("onChange", page, isNoChange);
    }
  1. 前进后退直接 +1- 1 就行了
  2. … 按钮要做一下处理, 因为涉及到前进与后退的加减,
  3. … 按钮的点击我设计为跳转到一个当前正好看不到的页面, 当前点不到的就行
  4. 分为两个函数来处理
 // 左侧的...
    previous() {
      // 左侧未显示的第一个
      let page = this.showPages[0];
      this.handlClick(page - 1);
    },
    // 右侧的...
    next() {
    // 右侧未显示的第一个
      let len = this.showPages.length,
        page = this.showPages[len - 1] + 1;
      this.handlClick(page + 1);
    },

把时间放到 dom 上吧

<template>
  <div class='cc-pagination'>
    <button class="btn-prev"
            @click="handlClick(value-1)"
            // 到头了要提示用户, 显示出禁止点击的样式
            :style="{'cursor': (value === 1)?'not-allowed':'pointer'}">
      <slot name='previous'> &lt; </slot>
    </button>
    <ul class='cc-pagination__box'>
      <li @click="handlClick(1)"
          :class="{'is-active':value === 1}">1</li>
      <li v-if='showLeft'
          @click="previous">··</li>
      <li v-for="item in showPages"
          :key='item'
          @click="handlClick(item)"
          :class="{'is-active':value === item}">{{item}}</li>
      <li v-if='showRight'
          @click="next">··</li>
      <li v-if="pageTotal !== 1"
          @click="handlClick(pageTotal)"
          :class="{'is-active':value === pageTotal}">{{pageTotal}}</li>
    </ul>
    <button class="btn-prev"
            @click="handlClick(value+1)"
            // 到头了要提示用户, 显示出禁止点击的样式
            :style="{'cursor': (value === pageTotal)?'not-allowed':'pointer'}">
      <slot name="next"> &gt; </slot>
    </button>
  </div>
</template>

为了判断左右的 … 是否显示, 我们也要抽离出判断的逻辑
比如说中间的那个数组两边的元素连接上了, 就不显示.. 否则出现.

showLeft() {let { showPages, pageTotal, pageSize} = this;
      // 左边不是 2, 并且 pageTotal 超出规定数才显示, 不然 1 ... ... 2 很尴尬
      return showPages[0] !== 2 && pageTotal > pageSize;
    },
    showRight() {let { showPages, pageTotal, pageSize} = this,
        len = showPages.length;
      return showPages[len - 1] !== pageTotal - 1 && pageTotal > pageSize;
    }

至此, 功能性的东西才告一段落

五. 丰富样式与效果

  1. 用户可以传 background 以激活背景色效果, 看对比图


  1. 对比一下大量数据的美, ???? 哈哈哈, 这个组件的特色就是可以无限多


代码实现一下

// background 是关键字, 尤其涉及到 css 不要直接使用
// js 里面为了方便用户, 可以适当使用
<ul class='cc-pagination__box'
        :class="{'ground-box':background}">

css
单独抽离出 ground 样式, 为以后的扩展做准备


.ground {
        background-color: #f4f4f5;
        ;
        border-radius: 4px;
    
        &:hover {
            background-color: $--color-nomal;
            color: white;
        }
    }
    .ground-box { // 背景色是关键字
        &>li {@extend .ground;}
        &>.is-active{
            background-color: $--color-nomal;
            color: white;
        }
    }

hideOne 属性, 开启只有一页的时候不显示组件

// 最外层的父级定义就好了
 <div class='cc-pagination'
       v-if='!(hideOne && pageTotal === 1)'>

total: 开启左侧显示条数模式
我做的与别人不同, 你传了我就显示, 没传就无所谓, 没有附加的功能

<p v-if="total"
       class="total-number"> 总共 <span> {{total}}</span> 条 </p>

麻雀虽小, 五脏俱全, 做这个也花费了半天的时间, 测出好多问题, 都一一改进了.

end
另外最近计划做一个 vue,vuex, vue-router, webpack 原理解析的系列, 也是一点点从零开始, 期待大家继续一起学习, 一起进步, 实现自我价值!!
下一集准备聊聊 计数器 … 上期就这么说的????;
更多好玩的效果请关注个人博客: 链接描述
github 地址: 链接描述

退出移动版