网络日志

web技术分享| 基于vue3实现自己的组件库第二章:Pagination组件

大家好今天的内容是基于vue3实现自己的组件库系列第二章,本文默认你会安装和创建vue3项目,如果不会请参考vue官网

Pagination.vue Template

  • v-select 组件可以先注释掉
  • v-input 组件可以先注释掉
<div class='v-pagination'>
    <div v-for='(item, index) in layout' :key='index'>
        <div class='v-pagination-select-box' v-if='item === "switch"'>
            <v-select 
            v-model='activePageSize' 
            :options='pageSizes'
            :disabled='disabled'
            @change='handleSizeChange'>
            </v-select>
        </div>
        <v-pages
        v-if='item === "pages"'
        ref='cpages'
        :total='total'
        :pagerCount='pagerCount'
        :pageSize='pageSize'
        v-model='cCurrentPage'
        :prependText='prependText'
        :suffixText='suffixText'
        :hideOnSinglePage='hideOnSinglePage'
        :disabled='disabled'
        :background='background'
        @prev-click='handlePrevClick'
        @next-click='handleNextClick'
        ></v-pages>
        <div :class='["v-pagination-input-box", { disabled }]' v-if='item === "jump"'>
            <span>前往</span>
            <v-input placeholder='' :disabled='disabled' v-model='jumpValue' @input='handleInput' @change='handleChange'></v-input>
            <span>页</span>
        </div>
    </div>
</div>

Pagination.vue Script

import VairPagination from '../../types/pagination';
import vPages from './components/Pages';
import vSelect from '../Select/Select';
import vInput from '../Input/Input';
import { ref, computed, watchEffect, watch } from 'vue';
export default {
    name: 'pagination',
    components: {
        vPages,
        vSelect,
        vInput
    },
    props: VairPagination,
    setup (props, ctx) {
        const reg = /^\+?[1-9][0-9]*$/;
        const activePageSize = ref('');
        const oldJumpValue = ref(props.currentPage);
        const jumpValue = ref(props.currentPage);
        const cCurrentPage = ref(props.currentPage);
        const cpages = ref(null);

        const pageSizes = computed(() => {
            if (Array.isArray(props.pageSizes)) {
                let arr = [];
                props.pageSizes.forEach((item, index) => {
                    if (!reg.test(item)) {
                        throw new Error('page-sizes the item It has to be an integer greater than or equal to 1');
                    }
                    arr.push({
                        label: `${item}条 / 页`,
                        value: index,
                        number: item
                    });
                });
                return arr;
            } else {
                throw new TypeError(`page-sizes wants to receive an array, but received a ${typeof props.pageSizes}`);
            }
        });

        const handleSizeChange = (option) => {
            ctx.emit('size-change', option.number);
        };

        const handlePrevClick = (index) => {
            ctx.emit('prev-click', index);
            setJumpValue();
        };

        const handleNextClick = (index) => {
            ctx.emit('next-click', index);
            setJumpValue();
        };

        const handleInput = (value) => {
            if (!reg.test(value) && value !== '') {
                jumpValue.value = oldJumpValue.value;
            } else {
                oldJumpValue.value = value;
            }
        };

        const handleChange = (value) => {
            const max = Math.ceil(props.total / props.pageSize);
            if (value < 1) {
                jumpValue.value = 1;
                oldJumpValue.value = 1;
            } else if (value > max) {
                jumpValue.value = max;
                oldJumpValue.value = max;
            }
            cCurrentPage.value = +jumpValue.value;
        };
        
        const setJumpValue = () => {
            jumpValue.value = cCurrentPage.value;
            oldJumpValue.value = cCurrentPage.value;
        };

        watchEffect(() => {
            activePageSize.value = pageSizes.value[0].label;
        });

        watchEffect(() => {
            if (!reg.test(props.pagerCount) || props.pagerCount < 7 || props.pagerCount > 21) {
                throw new TypeError(`pager-count value of can only be an integer greater than or equal to 7 and less than or equal to 21, but received a ${props.pagerCount}`);
            }
        });

        watchEffect(() => {
            if (!reg.test(props.pageSize)) {
                throw new Error('pager-size It has to be an integer greater than or equal to 1');
            }
        });

        watch(() => props.pageSize, () => {
            setJumpValue();
        });

        watchEffect(() => {
            if (!reg.test(props.currentPage)) {
                throw new Error('current-page It has to be an integer greater than or equal to 1');
            } else {
                cCurrentPage.value = props.currentPage;
            }
        });

        watchEffect(() => {
            if (typeof props.total !== 'number' || props.total < 0) {
                throw new Error('total must be a number greater than or equal to 0');
            }
        });

        watchEffect(() => {
            ctx.emit('current-change', cCurrentPage.value);
            setJumpValue();
        });

        return {
            handleSizeChange,
            handlePrevClick,
            handleNextClick,
            handleInput,
            handleChange,
            pageSizes,
            activePageSize,
            jumpValue,
            cCurrentPage,
            cpages
        }
    }
}

Pagination.vue Props

const VairPagination = {
    background: { // 是否开启背景色
        type: Boolean,
        default: () => {
            return false;
        }
    },
    pageSize: { // 每页显示几条数据
        type: Number,
        default: () => {
            return 10;
        }
    },
    total: { // 数据总数量
        type: Number,
        default: () => {
            return 0;
        }
    },
    pagerCount: { // 当总页数超过该值时会开启折叠 最低为 7
        type: Number,
        default: () => {
            return 7;
        }
    },
    pageSizes: { // 每页显示个数选择器的选项
        type: Array,
        default: () => {
            return [10, 20, 30, 40, 50, 100];
        }
    },
    prependText: { // 后退按钮文字
        type: String,
        default: () => {
            return '';
        }
    },
    suffixText: { // 前进按钮文字
        type: String,
        default: () => {
            return '';
        }
    },
    disabled: { // 是否禁用
        type: Boolean,
        default: () => {
            return false;
        }
    },
    hideOnSinglePage: { // 只有一页时是否隐藏
        type: Boolean,
        default: () => {
            return false;
        }
    },
    currentPage: { // 当前页数
        type: Number,
        default: () => {
            return 1;
        }
    },
    layout: { // 组件布局显示顺序
        type: Array,
        default: () => {
            return ['switch', 'pages', 'jump'];
        }
    },
};


// Event
    // size-change (number)
    // current-change (index)
    // prev-click (index)
    // next-click (index)


export default VairPagination;

Pagination.vue Style

<style lang='less' scoped>
.v-pagination {
    display: flex;
    align-items: center;
    .v-pagination-select-box, .v-pagination-input-box {
        width: 120px;
        /deep/.v-select, /deep/.v-input {
            min-width: 0;
        }
        /deep/.v-input {
            height: 30px;
            .v-input-box, .input {
                height: 30px;
                .suffix {
                    height: 25px;
                }
            }
        }
    }
    .v-pagination-input-box {
        display: flex;
        align-items: center;
        width: 120px;
        /deep/.v-input {
            width: 50px;
            margin: 0 6px;
            .input {
                text-indent: 0px;
                text-align: center;
            }
        }
        span {
            font-size: 14px;
        }
    }
    .disabled {
        cursor: not-allowed;
        span {
            color: #c0c4cc;
        }
    }
}
</style>

Pages.vue Template

<div class='v-pages' ref='cPages'>
    <div :class='["prepend", { prependDisabled }, { disabled }]' 
        ref='prepend'
        @click='handlePrependClick'>
        <p v-if='prependText'>{{ prependText }}</p>
        <i v-if='!prependText' class='iconfont icon-zuojiantou'></i>
    </div>
    <ul class='v-pages-ul'>
        <li :class='["v-pages-li", { activeLi: modelValue === item }, { disabled }]' 
            :ref='el => {if (el) liList[index] = el}'
            v-for='(item, index) in calculatePagesButtonList' :key='index'
            @click='handlePagesLiClick(item)'>
            <span :class='[{ color: !background }]' v-if='item !== "suffix" && item !== "prepend"'>{{ item }}</span>
            <i  @mouseenter='handleMouseEnter(item)'
                @mouseleave='handleMouseLeave(item)' 
                @click='handleIClick(item)'
                v-else 
                :class='["iconfont", "icon-ellipsis2",
                { "icon-chevronsrightshuangyoujiantou": doubleRight && item === "suffix" },
                { "icon-chevronsleftshuangzuojiantou": doubleLeft && item === "prepend" }]'>
            </i>
        </li>
    </ul>
    <div 
        ref='suffix'
        :class='["suffix", { suffixDisabled }, { disabled }]' 
        @click='handleSuffixClick'>
        <p v-if='suffixText'>{{ suffixText }}</p>
        <i v-if='!suffixText' class='iconfont icon-youjiantou'></i>
    </div>
</div>

Pages.vue Script

import { ref, computed, watchEffect, onMounted } from 'vue';
export default {
    name: 'pages',
    props: {
        total: Number,
        pagerCount: Number,
        pageSize: Number,
        prependText: String,
        suffixText: String,
        disabled: Boolean,
        hideOnSinglePage: Boolean,
        background: Boolean || String,
        modelValue: Number
    },
    setup (props, ctx) {
        const medianButtonList = ref([]);
        const prependDisabled = ref(true);
        const suffixDisabled = ref(true);
        const doubleLeft = ref(false);
        const doubleRight = ref(false);
        const liList = ref([]);
        const prepend = ref(null);
        const suffix = ref(null);
        const cPages = ref(null);

        onMounted(() => {
            watchEffect(() => {
                liList.value.forEach(item => {
                    !props.background && (item.style.backgroundColor = 'transparent');
                });
                !props.background && (prepend.value.style.backgroundColor = 'transparent');
                !props.background && (suffix.value.style.backgroundColor = 'transparent');
            });
            watchEffect(() => {
                if (calculatePagesButtonList.value.length <= 1 && props.hideOnSinglePage) {
                    cPages.value.style.display = 'none';
                } else {
                    cPages.value.style.display = 'flex';
                }
            });
        });

        const handlePagesLiClick = (index) => {
            if (props.disabled) return
            if (typeof index === 'number' && props.modelValue !== index) {
                ctx.emit('update:modelValue', index);
                ctx.emit('current-change', index);
            }
        };

        const handleMouseEnter = (item) => {
            if (props.disabled) return
            if (item === 'prepend') {
                doubleLeft.value = true;
            } else if (item === 'suffix') {
                doubleRight.value = true;
            }
        };

        const handleMouseLeave = (item) => {
            if (props.disabled) return
            if (item === 'prepend') {
                doubleLeft.value = false;
            } else if (item === 'suffix') {
                doubleRight.value = false;
            }
        };

        const handleIClick = (item) => {
            if (props.disabled) return
            if (item === 'prepend') {
                const num = props.modelValue - 3;
                ctx.emit('update:modelValue', num < 1? 1 : num);
            } else if (item === 'suffix') {
                const maxPages = Math.ceil(props.total / props.pageSize);
                const num = props.modelValue + 3;
                ctx.emit('update:modelValue', num > maxPages? maxPages : num);
            }
            ctx.emit('current-change', props.modelValue);
        };

        const handlePrependClick = () => {
            if (props.disabled) return
            if (props.modelValue <= 1) return;
            ctx.emit('update:modelValue', props.modelValue - 1);
            ctx.emit('prev-click', props.modelValue);
        };

        const handleSuffixClick = () => {
            if (props.disabled) return
            const value = calculatePagesButtonList.value;
            const item = value[value.length - 1];
            if (props.modelValue >= item) return;
            ctx.emit('update:modelValue', props.modelValue + 1);
            ctx.emit('next-click', props.modelValue);
        };

        const calculatePagesButtonList = computed(() => {
            const value = props.modelValue;
            const maxPages = Math.ceil(props.total / props.pageSize);
            const bool = (maxPages - props.pagerCount) > 0;
            const maxCurrentPage = (value - maxPages) > 0? maxPages : value;
            const pagerCountHalf = Math.ceil((props.pagerCount - 2) / 2);
            const a = (maxCurrentPage + pagerCountHalf) < maxPages;
            const b = (maxCurrentPage - pagerCountHalf) <= 2;
            let pagesButtonList = [];
            if (bool) {
                if (b) {
                    for(let i = 1; i < props.pagerCount; i++) {
                        pagesButtonList.push(i);
                    }
                    pagesButtonList.push('suffix');
                    pagesButtonList.push(maxPages);
                } else if (a) {
                    pagesButtonList.push(1);
                    pagesButtonList.push('prepend');
                    for(let i = (maxCurrentPage - pagerCountHalf + 1); i < (maxCurrentPage + pagerCountHalf); i++) {
                        pagesButtonList.push(i);
                    }
                    pagesButtonList.push('suffix');
                    pagesButtonList.push(maxPages);
                } else if (!a) {
                    pagesButtonList.push(1);
                    pagesButtonList.push('prepend');
                    for(let i = (maxPages - props.pagerCount + 2); i <= maxPages; i++) {
                        pagesButtonList.push(i);
                    }
                }
            } else {
                for (let i = 1; i <= maxPages; i++) {
                    pagesButtonList.push(i);
                }
            }
            return pagesButtonList;
        });

        const calculateCurrentPage = () => {
            const value = calculatePagesButtonList.value;
            const item = value[value.length - 1];
            ctx.emit('update:modelValue', props.modelValue > item? item : props.modelValue);
        };

        watchEffect(() => {
            calculateCurrentPage();
        });

        watchEffect(() => {
            const value = calculatePagesButtonList.value;
            const item = value[value.length - 1];
            prependDisabled.value = props.modelValue === 1;
            suffixDisabled.value = props.modelValue >= item;
        });

        return {
            calculatePagesButtonList,
            medianButtonList,
            prependDisabled,
            suffixDisabled,
            handlePagesLiClick,
            handlePrependClick,
            handleSuffixClick,
            handleMouseEnter,
            handleMouseLeave,
            handleIClick,
            doubleRight,
            doubleLeft,
            liList,
            prepend,
            cPages,
            suffix
        }
    }
}

Pages.vue Style

<style lang='less' scoped>
.v-pages {
    display: flex;
    align-items: center;
    margin: 0 14px;
    .prepend, .suffix {
        display: flex;
        align-items: center;
        justify-content: center;
        min-width: 30px;
        min-height: 28px;
        box-sizing: border-box;
        padding: 0 6px;
        margin: 0 5px;
        background-color:#F4F4F5;
        cursor: pointer;
        p, i {
            font-size: 12px;
            color: #333;
            font-weight: 600;
        }
        &:hover {
            p, i {
                color: #409EFF;
            }
        }
    }
    .prependDisabled, .suffixDisabled {
        p, i {
            color: #CDC9CC !important;
        }
        cursor: not-allowed;
    }
    .v-pages-ul {
        display: flex;
        align-items: center;
        .v-pages-li {
            margin: 0 5px;
            background-color:#F4F4F5;
            cursor: pointer;
            span, i {
                display: block;
                min-width: 30px;
                box-sizing: border-box;
                padding: 0 6px;
                line-height: 28px;
                text-align: center;
                font-size: 12px;
                color: #333;
                font-weight: 600;
            }
            &:hover {
                span, i {
                    color: #409EFF;
                }
            }
        }
        .activeLi {
            background-color:#409EFF;
            span {
                color: #fff;
            }
            .color {
                color: #409EFF !important;
            }
            &:hover {
                span {
                    color: #fff;
                }
            }
        }
    }
    .disabled {
        p, i, span {
            color: #c0c4cc !important;
        }
        cursor: not-allowed !important;
    }
}
</style>

index.js 出口文件中引入组件

// Pagination 分页
import Pagination from './components/Pagination/Pagination.vue';
import Pages from './components/Pagination/components/Pages.vue';

const Vair = function(Vue) {
    Vue.component(`v-${Pagination.name}`, Pagination);
    Vue.component(`v-${Pages.name}`, Pages);
}

export default Vair;

使用组件

  • 在main.js中引入
import { createApp } from 'vue'; 
import App from './App.vue'; 
import Vair from './libs/vair/index.js'; 
const app = createApp(App); 
app.use(Vair).mount('#app');

App.vue中调用

<template>
    <div>
        <v-pagination
        :page-sizes='pageSizes'
        :pager-count='pagerCount'
        :total='total'
        :page-size='pageSize'
        :current-page='currentPage'
        :background='background'
        :hideOnSinglePage='false'
        :disabled='disabled'
        @current-change='handleCurrentChange'
        @prev-click='handlePrevClick'
        @next-click='handleNextClick'
        @size-change='handleSizeChange'
        ></v-pagination>
    </div>
</template>

<script>
import { ref } from 'vue';
export default {
    setup () {
        const pageSizes = ref([10, 20, 30, 40, 50, 100]);
        const pageSize = ref(10);
        const total = ref(50500);
        const currentPage = ref(10);
        const pagerCount = ref(7);
        const background = ref(true);
        const prependText = ref('prepre');
        const suffixText = ref('nextnext');
        const disabled = ref(false);

        const handleCurrentChange = (index) => {
            console.log(index)
        };

        const handlePrevClick = (index) => {
            console.log('handlePrevClick 触发了', index);
        };

        const handleNextClick = (index) => {
            console.log('handleNextClick 触发了', index);
        };

        const handleSizeChange = (number) => {
            pageSize.value = number;
        };

        return {
            pageSizes,
            pageSize,
            total,
            disabled,
            currentPage,
            pagerCount,
            handleCurrentChange,
            handlePrevClick,
            handleNextClick,
            handleSizeChange,
            background,
            prependText,
            suffixText
        }
    }
}
</script>

<style lang='less' scoped>
div {
    margin-top: 20px;
}
</style>

效果展示