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>
效果展示