elementPlus 支持“可输入+多选+自定义”三者并存
✅ 效果需求:
-
支持多选
-
支持选择已有选项
-
支持自定义输入(点击“自定义”后可输入)
自定义输入后可确认添加
调用如下
<CustomMultiSelect
v-model="form.exits"
:options="existResult"
></CustomMultiSelect>
实现如下
<template>
<el-form-item :label="label" :prop="prop">
<el-select
ref="selectRef"
v-model="modelValueLocal"
:placeholder="placeholder"
multiple
filterable
collapse-tags
:filter-method="customFilter"
@change="handleChange"
@visible-change="handleDropdownVisible"
>
<template #default>
<!-- 普通选项 -->
<el-option
v-for="(dict, index) in filteredOptions"
:key="index"
:label="dict.inExitName"
:value="dict.inExitName"
/>
<!-- 固定的“自定义”选项 -->
<el-option key="__custom__" label="自定义" :value="'__custom__'" />
<!-- 👇 自定义输入区域,直接挂在下拉内容底部,不用 teleport -->
<div v-if="showCustomInput" class="custom-input-wrapper" @click.stop>
<el-input
v-model="customInput"
placeholder="请输入"
style="width: 100%"
@keydown.enter.prevent="confirmCustom"
/>
<div class="opt">
<el-button @click="cancelCustom">取消</el-button>
<el-button type="primary" @click="confirmCustom">确定</el-button>
</div>
</div>
</template>
</el-select>
</el-form-item>
</template>
<script setup>
// props
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
label: {
type: String,
default: "封闭的出入口",
},
prop: {
type: String,
default: "exits",
},
placeholder: {
type: String,
default: "请选择封闭的出入口",
},
options: {
type: Array,
default: () => [
{ inExitName: "交通枢纽" },
{ inExitName: "商圈" },
{ inExitName: "景区公园" },
{ inExitName: "祭扫相关" },
{ inExitName: "景区商圈" },
],
},
});
const emit = defineEmits(["update:modelValue", "change"]);
const selectRef = ref();
const customInput = ref("");
const showCustomInput = ref(false);
const searchKeyword = ref("");
const modelValueLocal = ref([...props.modelValue]);
const existResult = ref([...props.options]);
// 当父组件的 options 发生变化时,同步到本地
watch(
() => props.options,
(newVal) => {
existResult.value = [...newVal];
},
{ deep: true }
);
const filteredOptions = computed(() => {
if (!searchKeyword.value) return existResult.value;
return existResult.value.filter((item) =>
item.inExitName.includes(searchKeyword.value)
);
});
const customFilter = (val) => {
searchKeyword.value = val;
};
const handleChange = (val) => {
const idx = val.indexOf("__custom__");
if (idx !== -1) {
val.splice(idx, 1);
showCustomInput.value = true;
}
modelValueLocal.value = val;
emit("update:modelValue", val);
emit("change", val);
};
const confirmCustom = () => {
const val = customInput.value.trim();
if (!val) return;
if (!modelValueLocal.value.includes(val)) {
modelValueLocal.value.push(val);
emit("update:modelValue", modelValueLocal.value);
emit("change", modelValueLocal.value);
}
// 加入自定义项到 options 中
const exists = existResult.value.some((item) => item.inExitName === val);
if (!exists) {
existResult.value.push({ inExitName: val });
}
cancelCustom();
};
const cancelCustom = () => {
customInput.value = "";
showCustomInput.value = false;
};
const handleDropdownVisible = (visible) => {
if (!visible) {
cancelCustom(); // 关闭下拉时清除
}
};
// 同步外部 v-model
watch(
() => props.modelValue,
(val) => {
modelValueLocal.value = [...val];
},
{ deep: true }
);
</script>
<style scoped lang="scss">
.custom-input-wrapper {
padding: 10px 12px;
border-top: 1px solid #f0f0f0;
background-color: #fff;
display: flex;
flex-direction: column;
gap: 8px;
.opt {
display: flex;
justify-content: flex-end;
gap: 8px;
}
}
</style>