子组件
<template>
<view class="search-combox" :class="border ? '' : 'search-combox__no-border'">
<view v-if="label" class="search-combox__label" :style="labelStyle">
<text>{{ label }}</text>
</view>
<view class="search-combox__input-box">
<input
class="search-combox__input"
type="text"
:placeholder="placeholder"
placeholder-class="search-combox__input-plac"
v-model="inputVal"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
/>
<uni-icons
:type="showSelector ? 'top' : 'bottom'"
size="14"
color="#999"
@click="toggleSelector"
></uni-icons>
</view>
<view class="search-combox__selector" v-if="showSelector">
<view class="uni-popper__arrow"></view>
<scroll-view scroll-y="true" class="search-combox__selector-scroll">
<view
class="search-combox__selector-empty"
v-if="filterCandidatesLength === 0"
>
<text>{{ emptyTips }}</text>
</view>
<view
class="search-combox__selector-item"
v-for="(item, index) in filterCandidates"
:key="index"
@click="onSelectorClick(index)"
>
<text
:style="
(
isJSON
? item[keyName]
? item[keyName] == inputVal
: false
: item == inputVal
)
? 'font-weight: bold;background-color: ' +
selectedBackground +
';color: ' +
selectedColor
: ''
"
>
{{
isJSON
? item[keyName]
? item[keyName]
: '字段' + keyName + '不存在'
: item
}}
</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script lang="ts" setup>
import { watch, computed, ref } from 'vue'
const props = defineProps({
isJSON: {
type: Boolean,
default: false,
},
// 默认取text
keyName: {
type: String,
default: 'text',
},
// 被选中的背景颜色
selectedBackground: {
type: String,
default: '#e5e5e5',
},
// 被选中的字体颜色
selectedColor: {
type: String,
default: '#3774c6',
},
// 选择输入框边框
border: {
type: Boolean,
default: true,
},
// 前缀
label: {
type: String,
default: '',
},
// 前缀宽度
labelWidth: {
type: String,
default: 'auto',
},
// 提示文案
placeholder: {
type: String,
default: '请选择',
},
candidates: {
type: Array,
/**
* default
*/
default: () => [],
},
// 无匹配项
emptyTips: {
type: String,
default: '无匹配项',
},
// modelValue
modelValue: {
type: [String, Number],
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'input', 'select'])
const isInput = ref(false)
const showSelector = ref(false)
const inputVal = ref<any>('')
const labelStyle = computed(() => {
if (props.labelWidth === 'auto') {
return ''
}
return `width: ${props.labelWidth}`
})
const filterCandidates: any = computed(() => {
if (isInput.value) {
if (props.isJSON) {
return props.candidates.filter(
(item: any) =>
item[props.keyName].toString().indexOf(inputVal.value) > -1,
)
} else {
return props.candidates.filter(
(item: any) => item.toString().indexOf(inputVal.value) > -1,
)
}
} else {
return props.candidates
}
})
const filterCandidatesLength = computed(() => filterCandidates.value.length)
watch(
() => props.modelValue,
(newVal) => {
inputVal.value = newVal
isInput.value = true
},
{ immediate: true },
)
/**
* 点击展示选项
*/
const toggleSelector = () => {
showSelector.value = !showSelector.value
isInput.value = false
}
/**
* 获得焦点
*/
const onFocus = () => {
showSelector.value = true
isInput.value = false
}
/**
* 失去焦点
*/
const onBlur = () => {
setTimeout(() => {
showSelector.value = false
isInput.value = false
}, 153)
}
/**
* 选择事件
* @param index index
*/
const onSelectorClick = (index: any) => {
const item: any = filterCandidates.value[index]
inputVal.value = props.isJSON ? item[props.keyName] : item
showSelector.value = false
emit('update:modelValue', inputVal.value)
emit('input', inputVal.value)
emit('select', item)
}
/**
* 输入事件
*/
const onInput = () => {
setTimeout(() => {
emit('input', inputVal.value)
})
}
</script>
<style lang="scss" scoped>
.search-combox {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
min-height: 70rpx;
padding: 0 15rpx 0 20rpx;
font-size: 28rpx;
border: 1px solid #e5e5e5;
border-radius: 8rpx;
}
.search-combox__label {
padding-right: 10rpx;
color: #999999;
font-size: 32rpx;
line-height: 44rpx;
}
.search-combox__input-box {
position: relative;
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
}
.search-combox__input {
flex: 1;
height: 44rpx;
font-size: 28rpx;
line-height: 44rpx;
}
.search-combox__input-plac {
color: #999;
font-size: 12px;
}
.search-combox__selector {
position: absolute;
top: calc(100% + 12px);
left: 0;
z-index: 999;
box-sizing: border-box;
width: 100%;
padding: 8rpx 0;
background-color: #ffffff;
border: 1px solid #ebeef5;
border-radius: 12rpx;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.search-combox__selector-scroll {
box-sizing: border-box;
max-height: 300rpx;
}
.search-combox__selector-empty,
.search-combox__selector-item {
display: flex;
padding: 0px 0px;
font-size: 14px;
line-height: 70rpx;
text-indent: 1rem;
cursor: pointer;
}
.search-combox__selector-empty text,
.search-combox__selector-item text {
width: 100%;
}
.search-combox__selector-item:hover {
background-color: #e5e5e5;
}
.search-combox__selector-empty:last-child,
.search-combox__selector-item:last-child {
border-bottom: none;
}
// picker 弹出层通用的指示小三角
.uni-popper__arrow,
.uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 12rpx;
}
.uni-popper__arrow {
top: -12rpx;
left: 80%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #ebeef5;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
}
.uni-popper__arrow::after {
top: 2rpx;
margin-left: -12rpx;
border-top-width: 0;
border-bottom-color: #fff;
content: ' ';
}
.search-combox__no-border {
border: none;
}
</style>
父组件
<searchCombox
:candidates="[
{ id: '1', name: '选项一' },
{ id: '2', name: '选项二' },
{ id: '3', name: '选项三' },
{ id: '4', name: '选项四' },
{ id: '5', name: '选项五' },
]"
:isJSON="true"
keyName="name"
placeholder="请选择归属部门"
v-model="form.deptId"
@select="handelSelect"
></searchCombox>
效果图
下拉
输入