uniapp下拉选择组件
- 背景
- 实现思路
- 代码实现
- 配置项
- 使用
- 尾巴
背景
最近遇到一个这样的需求,在输入框中输入关键字,通过接口查询到结果之后,以下拉框列表形式展现供用户选择。查询了下uni-app官网和项目中使用的uv-ui库,没找到符合条件的组件。唯一一个有点类似的就是uni官方下拉框组件,但是不支持input组件,所以我们自己来实现一个。
我们先上一张图镇楼,提供了多种模式和使用场景的情况
实现思路
那么实现这样一个组件要有哪些注意点了?我大概罗列了一下:
1、下拉框默认是不显示的,要知道是在哪个位置显示
2、初始显示的组件不能定死,可以是button,可以是view,或者是input,要灵活
3、提供丰富的自定义配置
要解决第一和第二个问题,我们的自定义组件需要使用插槽,插槽用来放我们的初始显示组件,来灵活适配各种组件。然后在插槽下面来通过绝对定位来显示下拉框组件,默认隐藏,然后暴露出显示函数给外部。
代码实现
接下来就是代码实现环节了,有了思路直接按思路撸代码就行。
知道你们不喜欢啰嗦BB,哈哈,直接上代码
(down-select.vue)组件代码:
<template>
<view class='layout-column'>
<view id="parent" style="width:fit-content;">
<slot></slot>
</view>
<view
:style="'width:'+slotW+';max-height: '+getListContentHei+'rpx;z-index: 9999;position: absolute;margin-top:'+slotH+';'+(isShow ? '' : 'display:none;')"
:class="(dataList.length > 0 ? 'data-box-shadow ' : 'data-box ') + animate">
<block v-if="dataList.length > 0">
<view class="data-box-scroll"
:style="'height:'+dataList.length*(itemHeight-1)+'rpx;max-height: '+max*(itemHeight-1)+'rpx;'">
<text v-for="(item,index) in dataList" :key="item[identifier]"
:class="'layout-row less-center list-item '+(item.enable === false ? '' : 'active')"
:style="'color:'+(item.enable === false ? '#dedede' : (checkedIndex.indexOf(index) >= 0 ? itemSelectColor : itemColor))+';font-size:'+itemFontsize+'rpx;'"
@click="handleListItem(index, item)">{{item[showKey]}}</text>
</view>
<view class="layout-row opera-btns less-center" v-if="mode == 'multiple'">
<view class="opera-cancel layout-row center" @click='handelCancel'>取消</view>
<view class="opera-sure layout-row center" @click='handelSure'>确定</view>
</view>
</block>
<view v-else :style="'width:'+slotW+';'" class='status-text'>暂无数据</view>
</view>
<view class="mask" v-show="isShow" @click="handelCancel"></view>
</view>
</template>
<script>
export default {
name: "down-select",
props: {
//要显示的字段
showKey: {
type: String,
default: '',
},
mode: {
type: String,
default: 'single', //multiple
// default: 'multiple'
},
dataList: {
type: Array,
default: []
},
//选中的列表,用作显示列表是展示已选中项
checkedDataList: {
type: Array,
default: []
},
//最多展示几项后开始滑动
max: {
type: Number,
default: 4
},
//数据项每个item高度rpx
itemHeight: {
type: Number,
default: 80
},
//唯一标识符字段,用来比对选中项和维持v-for列表中的key,不填此项无选中效果
identifier: {
type: String,
default: ''
},
itemSelectColor: {
type: String,
default: '#00aaff'
},
itemColor: {
type: String,
default: 'black'
},
itemFontsize: {
type: Number,
default: 30
}
},
computed: {
getListContentHei() {
let len = this.dataList.length
let signleH = len < this.max ? this.itemHeight * len : this.itemHeight * this.max
if (this.mode == 'single') {
return len <= 0 ? this.itemHeight : signleH
} else {
return len <= 0 ? this.itemHeight : (signleH + this.itemHeight)
}
}
},
watch: {
dataList: {
handler: function(newVal, oldVal) {
if (this.checkedDataList.length >= 0 && this.identifier) {
this.checkedIndex = []
this.checkedDataList.forEach(ele => {
let index = newVal.findIndex(ele1 => ele[this.identifier] === ele1[this
.identifier])
if (index >= 0) {
this.checkedIndex.push(index)
}
})
}
},
immediate: true, // 组件创建时立即触发
deep: true // 对象内部属性变化时也触发
},
checkedDataList: {
handler: function(newVal, oldVal) {
if (newVal.length >= 0 && this.identifier) {
this.checkedIndex = []
newVal.forEach(ele => {
let index = this.dataList.findIndex(ele1 => ele[this.identifier] === ele1[this
.identifier])
if (index >= 0) {
this.checkedIndex.push(index)
}
})
}
},
immediate: true, // 组件创建时立即触发
deep: true // 对象内部属性变化时也触发
}
},
mounted() {
this.$nextTick(() => {
uni.createSelectorQuery().in(this).select('#parent').boundingClientRect(res => {
if (res.width) {
this.slotW = `${res.width}px`
this.slotH = `${res.height+5}px`
}
}).exec()
})
},
data() {
return {
slotW: '0px',
slotH: '0px',
isShow: false,
checkedIndex: [],
animate: '',
//传进来选中项,后又改成未选中并确认,多选模式生效
checkedDels: []
};
},
methods: {
open() {
if (this.checkedDataList.length >= 0 && this.identifier) {
this.checkedIndex = []
this.checkedDataList.forEach(ele => {
let index = this.dataList.findIndex(ele1 => ele[this.identifier] === ele1[this
.identifier])
if (index >= 0) {
this.checkedIndex.push(index)
}
})
}
this.isShow = true
this.animate = 'show-animate'
},
close() {
this.animate = 'hide-animate'
this.checkedIndex = []
this.checkedDels = []
this.isShow = false
},
handleListItem(index, obj) {
if(obj.enable === false){
return
}
if (this.mode === 'single') {
this.checkedIndex = []
this.checkedIndex.push(index)
this.handelSure()
} else {
let sindex = this.checkedIndex.indexOf(index)
if (sindex >= 0) {
if (this.identifier) {
//判断未选中的项在传进来的已选项中是否存在
let contain = this.checkedDataList.filter(ele => ele[this.identifier] === this.dataList[index][
this.identifier
])
if (contain.length > 0) {
//传进来的已选项中是否存在选择为未选中的内容
let contain1 = this.checkedDels.filter(ele => ele[this.identifier] === contain[0][this
.identifier
])
if (contain1.length <= 0) {
this.checkedDels.push(contain[0])
}
}
}
this.checkedIndex.splice(sindex, 1);
} else {
if (this.identifier) {
let contain2 = this.checkedDels.filter(ele => ele[this.identifier] === this.dataList[index][
this.identifier
])
if (contain2.length > 0) {
let tempIndex = this.checkedDels.findIndex(ele => ele[this.identifier] === this.dataList[
index][this.identifier])
if (tempIndex >= 0) {
this.checkedDels.splice(tempIndex, 1)
}
}
}
this.checkedIndex.push(index)
}
}
},
handelCancel() {
this.close()
this.$emit('cancelDimss', '')
},
handelSure() {
let results = []
if (this.checkedIndex.length <= 0) {
uni.showToast({
title: '请选择至少一项',
icon: 'none'
});
return
}
this.checkedIndex.forEach(ele => {
if (this.dataList[ele]) {
results.push(this.dataList[ele])
}
})
//将本次选中结果清除
this.checkedIndex = []
this.$emit('resultBack', results, this.checkedDels)
this.close()
}
}
}
</script>
<style scoped>
.active {}
.active:active {
opacity: 0.6;
}
.layout-row {
display: flex;
flex-direction: row;
}
.layout-column {
display: flex;
flex-direction: column;
}
/* 整体方向居中 */
.center {
align-items: center;
justify-content: center;
}
/* 主轴方向居中 */
.main-center {
justify-content: center;
}
/* 侧轴方向居中 */
.less-center {
align-items: center;
}
.data-box-scroll {
width: 100%;
overflow-y: scroll;
}
.data-box-scroll::-webkit-scrollbar {
display: none
}
.data-box {
background: white;
border-radius: 8rpx;
box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1);
z-index: 9999;
}
.data-box-shadow {
background: white;
border-radius: 8rpx;
box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1);
z-index: 9999;
}
.list-item {
width: 100%;
height: 80rpx;
margin-left: 20rpx;
margin-right: 20rpx;
border-bottom: 1rpx solid #D8DFEC;
text-align: right;
}
.opera-btns {
width: 100%;
height: 80rpx;
justify-content: flex-end;
}
.opera-cancel {
width: 100rpx;
height: 50rpx;
background-color: white;
box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1);
border-radius: 5rpx;
font-size: 26rpx;
}
.opera-sure {
width: 100rpx;
height: 50rpx;
background-color: #58a2e4;
box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1);
border-radius: 5rpx;
font-size: 26rpx;
color: white;
margin-right: 30rpx;
margin-left: 30rpx;
}
.status-text {
text-align: center;
font-size: 28rpx;
font-weight: 600;
color: #c2c2c2;
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.mask {
position: fixed;
background: transparent;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.show-animate {
animation-name: open;
animation-duration: 1s;
animation-iteration-count: 1;
}
@keyframes open {
0% {
height: 0rpx;
}
100% {
height: 100%;
}
}
.hide-animate {
animation-name: close;
animation-duration: 1s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
@keyframes close {
0% {
height: 100%;
}
100% {
height: 0rpx;
}
}
</style>
(test.vue)测试页面代码:
<template>
<view class="content">
<downSelect ref='selectRef' showKey="name" mode='single' :dataList="list" @resultBack="result">
<view class="select-btn" @click="handleClick">单选默认配置</view>
</downSelect>
<view>{{select.name}}</view>
<downSelect ref='selectRef1' showKey="name" mode='single' itemColor='red' :dataList="list1" @resultBack="result1">
<view class="select-btn1" @click="handleClick1">单选字体设置,是否可点击</view>
</downSelect>
<view>{{select1.name}}</view>
<downSelect ref='selectRef2' showKey="name" mode='multiple' identifier="id" :dataList="list2"
:max="5" :itemFontsize='20' :checkedDataList="select2" @resultBack="result2">
<view class="select-btn2" @click="handleClick2">多选字体大小设置,超出设置项后滑动设置</view>
</downSelect>
<view v-for="item in select2">{{item.name}}</view>
<downSelect ref='selectRef3' showKey="name" mode='single'>
<view class="select-btn2" @click="handleClick3">空数据</view>
</downSelect>
<downSelect ref='selectRef4' showKey="name" mode='single' :dataList="list4" @resultBack="result4">
<input class="select-btn1" placeholder="请输入" @input="handleInput"/>
</downSelect>
<view>{{select4.name}}</view>
</view>
</template>
<script>
import downSelect from "@/components/down-select.vue"
export default {
components: {
downSelect
},
data() {
return {
select: {},
select1: {},
select2: [],
select4: {},
list: [{
name: '选项一'
}, {
name: '选项二'
}],
list1: [{
name: '选项一',
enable: false
}, {
name: '选项二'
}, {
name: '选项三'
}, {
name: '选项四'
}, {
name: '选项五'
}],
list2: [{
id: '0',
name: '选项一'
}, {
id: '1',
name: '选项二'
}, {
id: '2',
name: '选项三'
}, {
id: '3',
name: '选项四'
}, {
id: '4',
name: '选项五'
}, {
id: '5',
name: '选项六'
}],
list4: []
}
},
onLoad() {
},
methods: {
handleInput(e){
this.$refs.selectRef4.open()
//这里模拟接口访问获取数据
setTimeout(()=> {
this.list4 = [{
name: '选项一'
}, {
name: '选项二'
}]
},2000)
},
result(result) {
this.select = result[0]
},
result1(result){
this.select1 = result[0]
},
result2(result, dels = []){
// this.select2 = result
if(this.select2.length <= 0){
this.select2.push(...result)
}else {
result.forEach(ele => {
let contain = this.select2.filter(ele1 => ele.id === ele1.id)
if(contain.length <= 0){
this.select2.push(ele)
}
})
}
if(dels.length > 0){
dels.forEach(ele => {
let index = this.select2.findIndex(ele1 => ele1.id === ele.id)
if(index >= 0){
this.select2.splice(index, 1)
}
})
}
},
result4(result){
this.select4 = result[0]
},
handleClick(){
this.$refs.selectRef.open()
},
handleClick1() {
this.$refs.selectRef1.open()
},
handleClick2 () {
this.$refs.selectRef2.open()
},
handleClick3 () {
this.$refs.selectRef3.open()
}
},
}
</script>
<style>
.content {
width: 690rpx;
margin: 25rpx;
}
.select-btn {
width: 690rpx;
height: 60rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border: 1rpx solid #e5e5e5;
border-radius: 15rpx;
}
.select-btn1 {
width: 490rpx;
height: 60rpx;
display: flex;
margin-top: 100rpx;
flex-direction: row;
align-items: center;
justify-content: center;
border: 1rpx solid #e5e5e5;
border-radius: 15rpx;
}
.select-btn2 {
width: 690rpx;
height: 60rpx;
display: flex;
margin-top: 100rpx;
flex-direction: row;
align-items: center;
justify-content: center;
border: 1rpx solid #e5e5e5;
border-radius: 15rpx;
}
</style>
以上代码都可以直接拷贝使用,无其他关联项,vue2、vue3、小程序、h5、APP都可用。
配置项
1、showKey:传进来的数组单个对象在下拉框中要展示的值,比如传的dataList为[{name: ‘韩梅梅’}],那showKey传name就行。
2、mode:下拉框是单选还是多选模式,见镇楼效果图。
3、dataList:传入的数据列表,列表为对象,多选时对象需要指定唯一标识。
4、checkedDataList:多选选中的列表数据。
5、max:多条数据时最多显示几项后可以滑动。
6、identifier:唯一标识符,用作列表key和多选模式显示已选数据项时的标识。
7、itemSelectColor:多选模式选中后的字体颜色。
8、itemColor:字体颜色。
9、itemFontsize:字体大小。
以上配置项在代码层面没做严格限制,不同模式混用可能会有bug,如果发现可以留言。配置项不满足你需求,可以自行增删。
使用
页面引入down-select.vue,然后参考第二节代码实现中的demo。
尾巴
今天的文章就到这里了,希望能给大家帮助,如果喜欢我的文章,欢迎给我点赞,评论,关注,谢谢大家!