我的一个工程里需要用到滑动窗口式分页这个效果,我没有直接使用现成的库,而是尝试自己实现了一下,我把实现的过程和大家分享一下吧,希望能对需要的朋友有所帮助。
先来看要实现的效果:






大家可以看到,滑动窗口式的分页解决了分页索引过多时显示过长的问题,中间部分的索引超过了索引“窗口”的大小时,则会以省略号来替代索引值,从而达到减少索引显示长度的目的;并且,我们可以观察到,随着用户的点击,中间的索引窗口是会移动的;另外,对于分页索引开头和结尾处,滑动窗口是需要特殊处理的。
下面是我具体的分析过程:

//条件一:无论窗口怎么移动,第一个和最后一个无论如何都需要显示 1   100
//窗口起始位置=当前number-1,窗口末尾位置=(窗口起始位置+窗口大小)-1 如果当前number-1=0则窗口起始位置=number 如果当前number+1>当前数列最后一个值(也就是等于当前数列的size)则窗口末尾=当前数列的size 窗口起始=(窗口末尾位置-窗口长度)+1
//条件二:如果整个数列中只有0个或1个'...'则下一轮需要remove操作,而如果数列中有2个'...'则无需remove只需要窗口中所有数+1或-1即可(这里的remove是逻辑上的,实际操作就是splice)
//条件三:如果窗口的起始位置的值=1或=2 则remove窗口左侧的'...'(其实也就是remove数列中第一个'...')
//条件四:如果窗口的结尾位置=数列最后一个值或=数列最后一个值-1 则remove窗口右侧的'...'(其实也就是remove数列中第二个'...')

//设定窗口大小为3
var windowSize = 3;


//向右移动的情况

//当前初始值:
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100];
//↓
//↓ 输入参数(inputNumber)为1 窗口位置(windowStart-windowEnd)为1-3 现在数列中有0个'...'满足条件1 remove除了第一个和最后一个以外的窗口以外的所有值并向窗口末尾和数列末尾之间填充一个"..."
//↓
var numberWindow1 = [1, 2, 3, '...', 100];
//↓
//↓ 当前点击(inputNumber)为2 窗口位置(windowStart-windowEnd)为1-3 没有到达窗口末尾不用右移 现在数列中有1个'...'满足条件1 remove除了第一个和最后一个以外的窗口以外的所有值并向窗口末尾和数列末尾之间填充一个"..."
//↓
var numberWindow2 = [1, 2, 3, '...', 100];
//↓
//↓ 当前点击(inputNumber)为3 窗口位置(windowStart-windowEnd)为1-3 已到达窗口末尾 窗口右移1位(其实就是窗口中所有值 + 1) 新的窗口位置为2-4 现在数列中有1个'...'满足条件一 remove除了第一个和最后一个以外的窗口以外的所有值并向窗口末尾和数列末尾之间填充一个"..."
//↓
var numberWindow3 = [1, 2, 3, 4, '...', 100];
//↓
//↓ 当前点击(inputNumber)为4 窗口位置(windowStart-windowEnd)为2-4 已到达窗口末尾 窗口右移1位(其实就是窗口中所有值 + 1) 新的窗口位置为3-5 现在数列中有1个'...'满足条件一 remove除了第一个和最后一个以外的窗口以外的所有值并向窗口末尾和数列末尾之间填充一个"..."
//↓
var numberWindow4 = [1, '...', 3, 4, 5, '...', 100];
//↓
//↓ 当前点击(inputNumber)为5 窗口位置(windowStart-windowEnd)为3-5 已到达窗口末尾 窗口右移1位(其实就是窗口中所有值 + 1) 新的窗口位置为4-6 现在数列中有2个'...'满足条件二 无需remove
//↓
var numberWindow5 = [1, '...', 4, 5, 6, '...', 100];


//向左移动的情况

//现在的数列:
var numberWindow6 = [1, '...', 4, 5, 6, '...', 100];
//↓
//↓ 当前点击(inputNumber)为4 已到达窗口起始 窗口左移一位(其实就是窗口中所有值 - 1) 新的窗口位置为3-5 现在数列中有2个'...'满足条件二 无需remove
//↓
var numberWindow7 = [1, '...', 3, 4, 5, '...', 100];
//↓
//↓ 当前点击(inputNumber)为3 已到达窗口起始 窗口左移一位(其实就是窗口中所有值 - 1) 新的窗口位置为2-4 现在数列中有2个'...'满足条件二 无需remove 满足条件三 remove数列中第一个'...'
//↓
var numberWindow8 = [1, 2, 3, 4, '...', 100];
//↓
//↓ 当前点击(inputNumber)为2 已到达窗口起始 窗口左移一位(其实就是窗口中所有值 - 1) 新的窗口位置为1-3 现在数列中有1个'...'满足条件二 remove除了第一个和最后一个以外的窗口以外的所有值并向窗口起始和数列起始之间填充一个"..." 满足条件三 remove数列中第一个'...'
//↓
var numberWindow9 = [1, 2, 3, '...', 100];复制代码

上边的分析过程是我想通过前端实现时进行的分析过程,后来感觉实现起来会很繁琐,要考虑很多细节,然后我想了想,要在后端实现,会简单很多,把这些分页索引看做数据从后端暴露接口,然后前端模板循环输出就好了,这样在逻辑上比较简单。思路有了,来看具体实现吧:

后端定义一个输出分页列表信息和分页索引信息的VO(基本上是从spring-data中摘出来的,getPageNumbers()这个是我自己加的):

import java.util.LinkedList;
import java.util.List;

public class JsonPage<T> {

    private long totalElements;
    private int number;
    private int size;
    private List<T> content = new LinkedList<>();

    private static final int PAGE_NUMBERS_WINDOW_SIZE = 3; //分页索引列表窗口大小(只能为奇数,否则下面方法中计算窗口半长会时会引起迷惑)

    public long getTotalElements() {
        return totalElements;
    }

    public int getNumber() {
        return number;
    }

    public int getSize() {
        return size;
    }

    public List<T> getContent() {
        return content;
    }

    public int getTotalPages() {
        return getSize() == 0 ? 1 : (int) Math.ceil((double) totalElements / (double) getSize());
    }

    //计算窗口化分页列表
    public List<Object> getPageNumbers() {

        List<Object> pageNumbers = new LinkedList<>();
        for (int i = 0; i < getTotalPages(); i++) { //注意这里为了逻辑清晰 pageIndexes 是0 base的
            pageNumbers.add(i);
        }

        //是否达到了窗口化的条件
        if (pageNumbers.size() <= PAGE_NUMBERS_WINDOW_SIZE) return pageNumbers; //不满足窗口化条件直接返回原数列

        // 注意number(当前是第几页)是0 base的
        // 窗口起始位置 = number - 窗口半长  (窗口半长 = (PAGE_NUMBERS_WINDOW_SIZE - 1) / 2)
        // 窗口末尾位置 = number + 窗口半长
        // 如果 number - 窗口半长 <= 0 则 窗口起始位置 = 0
        // 如果 number + 窗口半长 >= 数列末尾值 则 窗口末尾位值 = 数列末尾值
        int pageNumbersStartPosition = 0; //数列起始(注意是索引不是元素,当然如果是0 base的话索引和元素是值相同的)
        int pageNumbersEndPosition = pageNumbers.size() - 1; //数列末尾
        int windowHalfSize = (PAGE_NUMBERS_WINDOW_SIZE - 1) / 2;

        int windowStartPosition = number - windowHalfSize <= pageNumbersStartPosition ? pageNumbersStartPosition : number - windowHalfSize;
        int windowEndPosition = number + windowHalfSize >= pageNumbersEndPosition ? pageNumbersEndPosition : number + windowHalfSize;

        //remove 除了 窗口中元素、数列首、尾元素 以外的所有元素(也就是从 数列起始 + 1 到 窗口起始 - 1 以及 窗口末尾 + 1 到 数列末尾 - 1 的所有元素),并在窗口两边填补"..."
        List<Object> removeElementLeft = new LinkedList<>(), removeElementRight = new LinkedList<>();
        if ((windowStartPosition - 1) - (pageNumbersStartPosition + 1) >= 1) //如果 从 数列起始 + 1 到 窗口起始 - 1 之间有元素
            removeElementLeft.addAll(pageNumbers.subList(1, windowStartPosition)); //注意sublist(from, to)是不包含to的,想要连to一起的话需要sublist(from, to + 1)
        if ((pageNumbersEndPosition - 1) - (windowEndPosition + 1) >= 1) //如果 从窗口末尾 + 1 到 数列末尾 - 1 之间有元素
            removeElementRight.addAll(pageNumbers.subList(windowEndPosition + 1, pageNumbersEndPosition));

        if (removeElementLeft.size() != 0) {
            pageNumbers.removeAll(removeElementLeft);
            pageNumbers.add(pageNumbersStartPosition + 1, "..."); // 虽然在remove后pageNumbers的大小已经变化了,但是pageNumbersEndPosition是不变的,这一点与pageNumbersEndPosition不同
        }
        if (removeElementRight.size() != 0) {
            pageNumbers.removeAll(removeElementRight);
            pageNumbers.add(pageNumbers.size() - 1, "..."); //注意 在remove后pageNumbers的大小已经变化了就不能用原来的pageNumbersEndPosition了 ;add(index, element) 中 index指的是插入前的,插入后整个size会+1
        }

        return pageNumbers;
    }

    public boolean hasPrevious() {
        return getNumber() > 0;
    }

    public boolean hasNext() {
        return getNumber() + 1 < getTotalPages();
    }

    public boolean isFirst() {
        return !hasPrevious();
    }

    public boolean isLast() {
        return !hasNext();
    }

    public boolean hasContent() {
        return !content.isEmpty();
    }

    public void setTotalElements(long totalElements) {
        this.totalElements = totalElements;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public void setContent(List<T> content) {
        this.content = content;
    }
}复制代码

前端用handlebars循环输出就好了:

<div class="page-box">
    {{#if (-gt? totalPages 1)}}
    {{#if first}}
    <a class="page-btn1 page-item" disabled="disabled" href="javascript:"
       data-page="{{n-subtract number 1}}">
        <span><i class="iconfont icon-zuoyouqiehuan-"></i></span>
        上一页
    </a>
    {{else}}
    <a class="page-btn1 page-item" href="javascript:" data-page="{{n-subtract number 1}}">
        <span><i class="iconfont icon-zuoyouqiehuan-"></i></span>
        上一页
    </a>
    {{/if}}

    {{#each pageNumbers}}
    {{#if (-equal? this ../number)}}
    <a href="javascript:" class="cur" data-page="{{this}}">{{n-add this 1}}</a>
    {{else}}
    {{#if (-equal? this '...')}}
    <i class="iconfont icon-hengsangedian"></i>
    {{else}}
    <a class="page-item" href="javascript:" data-page="{{this}}">{{n-add this 1}}</a>
    {{/if}}
    {{/if}}
    {{/each}}

    {{#if last}}
    <a class="page-btn2 page-item" disabled="disabled" href="javascript:" data-page="{{n-add number 1}}">
        下一页
        <span><i class="iconfont icon-zuoyouqiehuan-"></i></span>
    </a>
    {{else}}
    <a class="page-btn2 page-item" href="javascript:" data-page="{{n-add number 1}}">
        下一页
        <span><i class="iconfont icon-zuoyouqiehuan-"></i></span>
    </a>
    {{/if}}
    {{/if}}
</div>复制代码

这样实现起来简单了很多。