滑動視窗式分頁的實現

since1986發表於2017-11-14

我的一個工程裡需要用到滑動視窗式分頁這個效果,我沒有直接使用現成的庫,而是嘗試自己實現了一下,我把實現的過程和大家分享一下吧,希望能對需要的朋友有所幫助。
先來看要實現的效果:

大家可以看到,滑動視窗式的分頁解決了分頁索引過多時顯示過長的問題,中間部分的索引超過了索引“視窗”的大小時,則會以省略號來替代索引值,從而達到減少索引顯示長度的目的;並且,我們可以觀察到,隨著使用者的點選,中間的索引視窗是會移動的;另外,對於分頁索引開頭和結尾處,滑動視窗是需要特殊處理的。
下面是我具體的分析過程:

//條件一:無論視窗怎麼移動,第一個和最後一個無論如何都需要顯示 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>複製程式碼

這樣實現起來簡單了很多。

相關文章