哈喽,各位高贵的倔友们你们好呀,今天摸鱼摸得可还开心(^▽^)?记住加班不是福报,摸鱼才是王道,打工人加油(✪ω✪)。
那么,如果各位亲们摸累了,不如来看看文章学点小知识解解乏吧,今天小编给各位带了一篇实战类文章,一篇关于ant-design-vue
下拉框组件分页二次封装小技巧的文章。各位如果觉得小编写得还不错的话,希望能给点个赞呗,话就不多说啦,直接进入正题。
写在开头
以下代码小编使用 Vue
技术栈来演示,并且 ant-design-vue (以下简称AntV
)使用的是 1.7.8
版本,小编一直以来觉得这个版本真的非常好用,如果你项目中使用的是 Vue2
那我强烈推荐你使用它(★ᴗ★)。
安装并引入AntV
安装:
javascript
npm install ant-design-vue@1.7.8 --save
在 main.js
文件中引入:
javascript
import Vue from 'vue'
import App from './App.vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
Vue.use(Antd)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
这里我们使用全局引入的方式,更多其他引入方式可以自行去官网查看。传送门
数据准备
小编先准备了一些测试数据,下面会以这种数据结构来编写逻辑,如果你真实的数据结构与此不同,可以自行再转换一下或者适当调整一下代码逻辑。
创建 data.js
文件:
javascript
const data = [
{ label: '雷进宝', value: 1 },
{ label: '林雅南', value: 2 },
{ label: '小黑', value: 3 },
{ label: '洪振霞', value: 4 },
{ label: '蔡莉华', value: 5 },
{ label: '小蓝', value: 6 },
{ label: '李军', value: 7 },
{ label: '小橙', value: 8 },
{ label: '王军', value: 9 },
{ label: '小黄', value: 10 },
{ label: '小棕', value: 11 },
{ label: '瀚文', value: 12 },
{ label: '赵浩歌', value: 13 },
{ label: '陈浩', value: 14 },
{ label: '安鹏飞', value: 15 },
{ label: '刘茂强', value: 16 },
{ label: '邓海阳', value: 17 },
{ label: '陈婉璇', value: 18 },
{ label: '小粉粉', value: 19 },
{ label: '方一强', value: 20 },
{ label: '茹定', value: 21 },
{ label: '王汝民', value: 22 },
{ label: '郑昌梦', value: 23 },
{ label: '须承文', value: 24 },
];
export function getDataAPI({ pageNum, pageSize, keyword }) {
return new Promise(resolve => {
setTimeout(() => {
const listData = [];
data.forEach(item => {
if(keyword) {
if(item.label.indexOf(keyword) !== -1) {
listData.push(item);
}
} else {
listData.push(item);
}
})
const start = (pageNum - 1) * pageSize;
const end = pageNum * pageSize;
resolve({
rows: listData.slice(start, end),
total: listData.length,
});
}, 500)
})
}
getDataAPI
方法用来模拟数据接口分页的情况,它通过 pageNum
与 pageSize
参数来控制分页,也支持 keyword
参数来进行模糊搜索。
基础分页功能
我们先来创建一个 SelectPaging.js
文件,具体内容如下:
javascript
<template>
<a-select
class="select"
v-model="selectedValue"
v-bind="getProps"
:loading="loading"
:allowClear="!loading"
@dropdownVisibleChange="dropdownVisibleChange"
>
<a-icon @click="clear" slot="clearIcon" type="close-circle" theme="filled" />
<div slot="dropdownRender" slot-scope="menu">
<v-nodes :vnodes="menu" />
<a-divider class="divider" />
<div class="paging-box" @mousedown="e => e.preventDefault()">
<a-pagination
size="small"
:current="params.pageNum"
:pageSize="params.pageSize"
:total="total"
@change="paginationChange"
/>
</div>
</div>
<a-select-option
v-for="item in listData"
:key="item.value"
:value="item.label"
>
<!-- :value="item.label" 绑定 label 属性 -->
{{ item.label }}
</a-select-option>
</a-select>
</template>
<script>
import { getDataAPI } from './data';
/** @name 定义一些下拉框组件的默认值,为了更好的满足业务逻辑 **/
const DEFAULT_PROPS = {
mode: 'default', // 仅支持这两种模式: default/multiple
placeholder: '请选择',
defaultActiveFirstOption: false, // 是否默认高亮第一个选项, 默认是true
showArrow: false, // 不使用默认的清除按钮,使用slot插入清除按钮,更好的控制清除事件
optionLabelProp: 'children' // 回填到选择框的 option 的属性值,默认是 Option 的子元素
showSearch: false, // 是否支持输入搜索
filterOption: false, // 不使用默认的筛选规则,因为分页行为,筛选需要传递到接口层面去过滤
}
export default {
components: {
VNodes: {
functional: true,
render: (_, ctx) => ctx.props.vnodes
}
},
props: {
value: {
type: [Number, String, Array],
default: undefined
},
},
data() {
return {
loading: false,
params: {
pageNum: 1,
pageSize: 5,
keyword: ''
},
listData: [],
total: 0,
selectedValue: undefined,
open: false, // 用于处理 search 事件默认最终会输出一次空的行为, 其主要会造成与用户删除的行为冲突
}
},
computed: {
getProps () {
return {
...DEFAULT_PROPS,
...this.$attrs
}
}
},
methods: {
/** @name 分页操作 **/
paginationChange(page) {
this.params.pageNum = page;
this.getData();
},
/** @name 点击清除按钮 **/
clear () {
this.params = {
pageNum: 1,
pageSize: 5,
keyword: '',
}
this.selectedValue = undefined;
this.getData();
},
/** @name 展开下拉菜单的回调 **/
dropdownVisibleChange(open) {
this.open = open;
if (open) {
if (this.loading) return;
if (this.listData.length !== 0 && this.params.pageNum === 1 && this.params.keyword === '') return;
// 重置回第一页
this.resetData();
} else {
this.params.keyword = '';
}
},
/** @name 重置下拉菜单数据 **/
resetData() {
this.params.pageNum = 1;
this.params.keyword = '';
this.getData();
},
/** @name 获取数据 **/
getData () {
this.loading = true
getDataAPI(this.params).then(res => {
this.listData = res.rows
this.total = res.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
}
}
</script>
<style scoped>
.select {
width: 100%;
}
.divider {
margin: 2px 0;
}
.paging-box {
display: flex;
justify-content: flex-end;
box-sizing: border-box;
padding: 4px 8px 8px 0;
}
</style>
在上面代码中,小编写了一些注释,相信你看起来应该不难,我们主要是使用到了 Select 组件中的 dropdownRender
属性来完成自定义下拉框内容。
对于为什么创建 <v-nodes />
组件,你可以简单认为就是创建了一个函数式组件,用于渲染下拉框的选项内容(menu
),这样渲染能方便我们将自定义内容与下拉框选项内容结合,我们可以令自定义的内容在其上面渲染,或者在其下方渲染。
具体使用及其效果:
javascript
<template>
<select-paging v-model="form.baseValue" />
</template>
<script>
import SelectPaging from './components/SelectPaging.vue'
export default {
components: {
SelectPaging
},
data() {
return {
form: {
baseValue: undefined,
}
}
},
}
</script>
多选功能
对于多选功能,我们还是一样继承 AntV
原本的用法,直接添加 mode="multiple"
属性即可。
javascript
<template>
<select-paging v-model="form.multipleValue" mode="multiple" />
</template>
<script>
export default {
...
data() {
return {
form: {
multipleValue: [],
}
}
},
}
</script>
处理v-model
不过,到这里代码还没写完,我们还需要把 v-model
绑定的行为处理一下,让数据实现双向数据绑定。
xml
<template>
<a-select
...
@change="change"
>
...
<a-select-option
...
:option="item"
>
<!-- :option="item" 绑定整个数据项,方便后续的使用 -->
{{ item.label }}
</a-select-option>
</a-select>
</template>
<script>
import SelectPaging from './components/SelectPaging.vue'
export default {
...,
watch: {
value: {
handler: function() {
this.selectedValue = this.value
},
deep: true,
immediate: true
},
selectedValue () {
this.$emit('input', this.selectedValue)
},
},
methods: {
...,
/** @name 下拉框选择 **/
change(selectedValue, options) {
let selectedOption;
if (Array.isArray(options)) { // 处理多选
selectedOption = options.map(item => item.data.attrs.option);
} else {
// 这里取的option就是上面标签 :option="item" 绑定的数据项
selectedOption = options.data.attrs.option;
}
this.$emit('change', selectedValue, selectedOption)
},
}
}
</script>
到这里不知道你发现没有,通过 v-model
绑定的值,拿到的会是每条数据中的 label
属性,但是,很多时候后端接口需要我们提交的都是数据中的 value
属性。
<a-select-option ... :value="item.label" />
那么,这里就要强调一下了,由于下拉框组件是分页性质的,所以这意味着我们需要把 label
属性储存起来,提供给回显的时候使用。
这里有的人可能会在想,我在添加的时候,提交 value
属性,而在回显的时候,后端接口能根据 value
顺便把 label
给我查回来,这样是否可行?如果你仅仅使用该基础分页功能,是可行的,但如果你还使用后面的 "支持自定义输入的值" 功能,那这就不行了,又会引出其他一系列问题。
所以总得来说,最好是 label
和 value
一起存储,或者只需要 label
就只存 label
就行。
而对于还想获取选项中的其他属性,可以通过 change
事件来解决。
javascript
<template>
<select-paging v-model="form.baseValue" @change="change" />
</template>
<script>
...
export default {
...,
methods: {
change(selectedValue, options) {
console.log(selectedValue, options)
},
}
}
</script>
模糊搜索功能
既然需要分页行为,这意味着下拉列表数据应该是很多的,那么我们就不可能一页一页翻着去找,搜索得给它安排上才行(✪ω✪)。
javascript
<template>
<a-select
...
@search="search"
>
...
</a-select>
</template>
<script>
...
export default {
...,
methods: {
/** @name 搜索事件 **/
search(value) {
if(!this.getProps.showSearch) return;
if(this.open) {
const inputValue = value || undefined;
this.params.keyword = inputValue;
this.params.pageNum = 1;
this.getData();
}
},
}
}
</script>
具体使用:
html
<template>
<div>
<select-paging v-model="form.searchValue" :showSearch="true" />
<select-paging v-model="form.searchValueMultiple" :showSearch="true" mode="multiple" />
</div>
</template>
是否支持模糊搜索功能,我们可以直接通过 showSearch
属性来开启。
防抖优化
对于这种一旦输入就请求接口的行为,一般我们需要做一些优化策略才行,防止性能损耗,提高用户体验。
javascript
<script>
...
import _ from 'lodash';
export default {
...,
methods: {
...,
/** @name 搜索事件 **/
search: _.debounce(async function (value) {
if(!this.getProps.showSearch) return;
if(this.open) {
const inputValue = value || undefined;
this.params.keyword = inputValue;
this.params.pageNum = 1;
this.getData();
}
},
}
}
</script>
防抖函数就不多说啦,你可以下载 lodash
或者可以自己手撸一个。
javascript
npm install lodash --save
支持自定义输入的值
下拉框组件很多时候的使用场景就是用来选择下拉选项的,但是有时业务需求可能要求它既要能选择也要能输入,那么我们应该如何来完成这么一个功能,接下来就随小编一起来瞧瞧吧。
javascript
<template>
<a-select
...
@inputKeydown="inputKeydown"
>
...
</a-select>
</template>
<script>
...
export default {
...,
props: {
...,
/** @name 是否支持自定义输入的值,单选-直接输入即可,多选-需要回车操作 **/
isCustomInput: {
type: Boolean,
default: false,
},
},
methods: {
...,
/** @name 展开下拉菜单的回调 **/
dropdownVisibleChange(open) {
this.open = open;
if (open) {
if (this.loading) return;
if (this.listData.length !== 0 && this.params.pageNum === 1 && this.params.keyword === '') return;
this.resetData();
} else {
// 不支持的时候,关闭就清空
if (!this.isCustomInput) {
this.params.keyword = '';
}
}
},
/** @name 搜索事件 **/
search: _.debounce(async function (value) {
if(!this.getProps.showSearch) return;
if(this.open) {
const inputValue = value || undefined;
if (this.isCustomInput) {
// 单选-存储输入的值
if (this.getProps.mode !== 'multiple') {
this.selectedValue = inputValue;
}
}
this.params.keyword = inputValue;
this.params.pageNum = 1;
this.getData();
}
},
/** @name 键盘按下时回调 **/
inputKeydown ({ code, target }) {
// 按下回车键
if (code === 'Enter' && this.isCustomInput) {
const inputValue = target.value;
this.selectedValue.push(inputValue);
this.params.keyword = '';
}
},
}
}
</script>
功能实现起来也不难,在单选的时候,只要将用户输入的内容直接赋值到 selectedValue
变量身上就行了,多选的时候就需要我们监听 inputKeydown
事件手动处理用户输入的内容。
如果有对 AntV
比较熟的小伙伴可能知道, a-select
组件的 mode
属性还支持 tags
参数。
它支持在多选的时候随意输入的内容作为值,那么这里小编为什么不使用它呢?主要原因是用户输入的值,我们不仅仅作为值,还需要把它做为搜索内容,为此我们才需要单独来实现这块逻辑,让两者的逻辑更可控一点。
滚动加载功能
最后,我们来让分页加载的行为变得多样性一点,让它来支持滚动加载,直接来看代码:
javascript
<template>
<a-select
...
@popupScroll="popupScroll"
>
<a-icon @click="clear" slot="clearIcon" type="close-circle" theme="filled" />
<div slot="dropdownRender" slot-scope="menu">
<v-nodes :vnodes="menu" />
<template v-if="loadType === 'pagination'">
<a-divider class="divider" />
<div class="paging-box" @mousedown="e => e.preventDefault()">
<a-pagination
size="small"
:current="params.pageNum"
:pageSize="params.pageSize"
:total="total"
@change="paginationChange"
/>
</div>
</template>
<div v-if="loadType === 'scroll' && listData.length >= total && listData.length !== 0" class="empty">暂无更多数据</div>
</div>
...
</a-select>
</template>
<script>
...
export default {
...,
props: {
...
/** @name 加载方式,pagination/scroll **/
loadType: {
type: String,
default: 'pagination'
},
/** @name 分页参数 **/
pagination: {
type: Object,
default: () => ({})
}
},
created() {
this.params = Object.assign(this.params, this.pagination);
},
methods: {
...,
/** @name 点击清除按钮 **/
clear () {
this.params = {
pageNum: 1,
pageSize: 5,
keyword: '',
...this.pagination // 重置
}
this.selectedValue = undefined;
this.getData();
},
/** @name 获取数据 **/
getData (isScroll) {
this.loading = true
getDataAPI(this.params).then(res => {
this.listData = isScroll ? this.listData = [...this.listData, ...res.rows] : res.rows;
this.total = res.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** @name 下拉列表滚动时的回调 **/
popupScroll({ target }) {
if (this.loading || this.total <= this.listData.length || this.loadType !== 'scroll') return;
const isTouchBottom = (target.scrollTop + target.offsetHeight + 10) > target.scrollHeight
if (isTouchBottom) {
this.params.pageNum = this.params.pageNum + 1;
this.getData(true);
}
},
}
}
</script>
<style scoped>
...
.empty {
text-align: center;
font-size: 13px;
color: #666;
}
</style>
具体使用:
html
<template>
<select-paging v-model="form.scrollValue" loadType="scroll" :pagination="{ pageSize: 10 }" @change="change" />
</template>
写到这里,已经是晚上 22:48 分,累了。
完整源码
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。