elementPlus 支持“可输入+多选+自定义”三者并存

9 阅读1分钟

elementPlus 支持“可输入+多选+自定义”三者并存

✅ 效果需求:

  1. 支持多选

  2. 支持选择已有选项

  3. 支持自定义输入(点击“自定义”后可输入)

自定义输入后可确认添加

调用如下

<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>