项目场景:
element-plus Select组件 远程搜索 ios时无法正常聚焦获取软键盘,需要点多次,百度了很多说需要删除input readonly 属性才可以,试了多次也没有很好的办法解决,所以就自己封装个远程搜索Select 选择器组件,方便以后复制粘贴
组件预览:
输入框用的是防抖,也可以用节流
组件传值:
组件传值可以自行根据组件代码修改
v-model | 选中项绑定值 | string / number |
v-model:selectList | select下拉数据 | array(数组项为props配置格式) |
loading | 是否正在从远程获取数据 | boolean |
props | selectList数组每项的配置 | object 默认值: { key: "key", label: "text", value: "value" } |
@inputChange | 远程搜索输入框值改变触发 | (value: any) => {value为输入框值} |
使用案例:
使用的时候初次先请求一下列表接口展示
<script setup lang="ts">
import SearchSelect from "@/components/SearchSelect/index.vue";
import { ref, onMounted } from "vue";
/* 请求列表接口 */
import { getCityAgencyList } from "@/api/channelManage/cityAgency";
defineOptions({
name: "ceshi"
});
const val = ref("");
const list = ref([]);
const loading = ref(false);
/* 输入框改变 */
const inputChange = async value => {
val.value = "";
loading.value = true;
const http_data = {
key: "name",
value,
page: 1,
limit: 10
};
const { data, code } = await getCityAgencyList(http_data);
if (code != 200000) return;
list.value = data.data;
loading.value = false;
};
/* 初始先请求数据 */
onMounted(() => {
inputChange("");
});
</script>
<template>
<div>
<h1>远程搜索下拉框组件</h1>
<div style="margin-top: 30px">
<SearchSelect
v-model="val"
v-model:selectList="list"
:loading="loading"
:props="{ key: 'id', label: 'name', value: 'id' }"
@inputChange="inputChange"
style="width: 200px"
/>
</div>
</div>
</template>
<style lang="scss" scoped></style>
组件完整代码:
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
defineOptions({
name: "SearchSelect"
});
const emits = defineEmits([
"update:modelValue",
"update:selectList",
"inputChange"
]); // 此处需写'update'
const props = defineProps({
/* 选中值 */
modelValue: {
required: true,
default() {
return "";
}
},
/* 下拉数据 */
selectList: {
required: true,
default() {
return [];
}
},
loading: {
required: true,
default() {
return false;
}
},
props: {
required: true,
default() {
return {
key: "key",
label: "text",
value: "value"
};
}
}
});
const select_data = reactive({
status: false, //下拉框主体是否显示
value: props.modelValue, //下拉框选中值
inputVal: ""
});
const searchSelect = ref(); //下拉框总体实例
/* 下拉框body显示 */
const showSelect = () => {
select_data.status = !select_data.status;
};
/* 下拉框子项点击 */
const rowConfirm = row => {
select_data.value = row[props.props.key];
emits("update:modelValue", row[props.props.key]);
select_data.status = false;
};
/* 输入框改变 防抖 */
const inputChange = debounce(() => {
emits("inputChange", select_data.inputVal);
}, 500);
/* 防抖函数 */
function debounce(fun, time) {
let timer;
return function (...val) {
clearTimeout(timer);
timer = setTimeout(() => {
fun.apply(this, val);
}, time);
};
}
/* 显示选中的text方法 */
const showText = val => {
const objs = props.selectList.find(obj => obj[props.props.key] == val);
if (objs) {
return objs[props.props.label];
} else {
return val;
}
};
/* 生命周期 */
onMounted(() => {
document.addEventListener("click", function (event) {
const dropdown = searchSelect.value;
const target = event.target;
if (target !== dropdown && !dropdown?.contains(target)) {
// 点击的不是下拉框或者下拉框的子元素,关闭下拉框
select_data.status = false;
}
});
});
</script>
<template>
<div ref="searchSelect" class="search_select_wrapper">
<div
:class="['search_select', select_data.status ? 'search_select_ok' : '']"
@click="showSelect"
>
<div class="search_select_text">
<span v-if="props.modelValue">{{ showText(props.modelValue) }}</span>
<span v-else style="color: #a8abb2">请选择</span>
</div>
<div
:class="[
'search_select_icon',
select_data.status ? 'search_select_icon_ok' : ''
]"
>
<svg
t="1702445263166"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4219"
width="13"
height="13"
>
<path
d="M73.4 328.1L509.6 764l439-435.8-45.1-45.5-393.8 390.9-391.1-390.8z"
p-id="4220"
fill="#a8abb2"
/>
</svg>
</div>
</div>
<div
v-show="select_data.status"
:class="[
'search_select_list_cont',
select_data.status ? 'search_select_list_cont_show' : ''
]"
>
<div class="search_select_input">
<input
type="text"
v-model="select_data.inputVal"
placeholder="请输入"
@input="inputChange"
/>
</div>
<div v-loading="props.loading" class="search_select_list">
<div
class="search_select_list_scrollbar"
v-show="props.selectList.length > 0"
>
<div
v-for="item in props.selectList"
:key="item[props.props.key]"
:class="[
'search_select_list_item',
props.modelValue == item[props.props.value]
? 'search_select_list_item_ok'
: ''
]"
@click="rowConfirm(item)"
>
{{ item[props.props.label] }}
</div>
</div>
<div v-show="props.selectList.length <= 0" class="no_list">
暂无数据
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.search_select_wrapper {
width: 100%;
position: relative;
font-size: 14px;
.search_select {
width: 100%;
height: 32px;
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 11px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.search_select_text {
flex: 1;
margin-right: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #606266;
}
.search_select_icon {
transition: all 0.3s;
}
.search_select_icon_ok {
transform: rotate(-180deg);
}
}
.search_select_ok {
border: 1px solid #409eff;
}
.search_select_list_cont {
position: absolute;
top: 45px;
left: 0;
width: 100%;
background-color: #fff;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
z-index: 1;
.search_select_input {
padding: 5px;
border-bottom: 1px solid #dcdfe6;
input {
outline: none;
width: 100%;
height: 30px;
border-radius: 4px;
border: 1px solid #dcdfe6;
padding: 0 5px;
box-sizing: border-box;
}
input:focus {
outline: none;
border: 1px solid #409eff;
}
}
.search_select_list {
height: 230px;
padding: 5px 0;
box-sizing: border-box;
.search_select_list_scrollbar {
width: 100%;
height: 100%;
overflow-y: scroll;
}
.search_select_list_scrollbar::-webkit-scrollbar {
display: none;
}
.search_select_list_item {
padding: 0 15px;
box-sizing: border-box;
line-height: 36px;
cursor: pointer;
}
.search_select_list_item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.search_select_list_item_ok {
font-weight: 700;
color: #409eff;
background-color: #f5f7fa;
}
.no_list {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
}
}
}
.search_select_list_cont_show {
// transform: translateY(0);
// opacity: 1;
}
.search_select_list_cont::before {
content: "";
position: absolute;
top: -14px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border: 7px solid;
border-color: transparent transparent #dcdfe6 transparent;
z-index: 2;
}
.search_select_list_cont::after {
content: "";
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border: 6.5px solid;
border-color: transparent transparent #ffffff transparent;
z-index: 3;
}
}
</style>