二十一.vue3之el-select封装集成

前言

最近写表单的时候被测试提了好几个bug,最后排查发现主要是el-select引起的。

  • 第一种情况是远程搜索,页面初始化的时候默认获取50条下拉,然后远程搜索了一个值,保存后再进详情,由于远程搜索的该值不在默认获取的50条里面,所以导致显示的code
  • 第二个问题类似,选择了一个值后,然后从字典里面把这个值删了,导致回显的code
  • 第三个问题 和后台约定存储时下拉的namecode都需要存储,由于el-select默认只能绑定一个code,所以导致每次都需要change然后赋值,比较麻烦;
  • 第四个问题如果页面有好多个select,初始化的时候就问请求n个接口,导致页面变慢,性能上不友好。

所以这里就对el-select进行一次二次封装,彻底解决掉这些问题,提升开发效率,这里记录下解决过程和思路。

回显问题

关于这个回显问题,也进行搜索了很久,但是基本上都不符合预期,要么就是初始化拿数据的时候把当前数据push到下拉的options里面去,要么就是通过cachedOptions进行push,或者直接说让用element-plus中的虚拟select(element-ui:那我走?)。

鉴于网上搜索的都不行,就只能自己想了,这个时候发现下拉选项都是通过options配置的,如果我给他加一个默认的option,是不是就能解决值无法回显的问题了?对现有代码进行改造。

<el-select v-model="form.code" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
<el-option :label="form.name" :value="form.code" v-if="(form.name||form.code)&&(options.length === 0 || !options.find(item => item.value === form.code)"></el-option>
</el-select>
    <el-select v-model="form.code" placeholder="请选择">
        <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
        <el-option :label="form.name" :value="form.code" v-if="(form.name||form.code)&&(options.length === 0 || !options.find(item => item.value === form.code)"></el-option>
    </el-select>
<el-select v-model="form.code" placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> <el-option :label="form.name" :value="form.code" v-if="(form.name||form.code)&&(options.length === 0 || !options.find(item => item.value === form.code)"></el-option> </el-select>

当然这个默认的option也不是时候都显示的,例如当默认下拉选项中包含这个code时候,就不需要显示,但是如果每个select这样写代码复用性太差了,所以这里需要对select进行一个封装。

封装select

封装之前我们需要考虑以下几个问题:

  • options下拉选项是通过props传递进来的,需要考虑options的值不为labelvalue的情况。
  • 需要兼容同时绑定labelvalue值,不需要再change赋值;
  • change有时候想要当前选中的下拉项,而不仅仅是value值;

基于上面的考虑,我们开始对select进行封装:

# base-select.vue
<template>
<el-select v-model="selectValue" :multiple="multiple" placeholder="请选择" :value-key="fieldNames.value" @change="changeSelect" v-bind="$attrs">
<el-option v-for="item in options" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" />
<el-option v-for="item in defaultOption" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" />
</el-select>
</template>
<script setup>
const props = defineProps({
value: {
type: [String, Array]
},
label: {
type: [String, Array]
},
options: {
type: Array,
default: () => []
},
// 是否change的时候返回当前下拉项值
labelInValue: {
type: Boolean,
default: true
},
// 配置option中的想绑定的label和value字段
fieldNames: {
type: Object,
default: () => ({ label: 'label', value: 'value' })
},
//value是否是当前item
valueObject: {
type: Boolean,
default: false
},
//是否多选
multiple: {
type: Boolean,
default: false
}
});
const emits = defineEmits(['update:value', 'update:label', 'change']);
//这里使用vueuse中的useVModel,也可以不使用,自己写也行。
const selectValue = useVModel(props, 'value', emits, { passive: true, defaultValue: props.value });
//value绑定的值,看是否需要绑定整个字段
const valueCompute = computed(() => {
return (val) => {
return props.valueObject ? val : val[props.fieldNames.value];
};
});
//默认下拉框需要出现的逻辑,之所以这里改成是一个数组,是因为他可能多选。
const defaultOption = computed(() => {
const { multiple, valueObject, label, value } = props;
if(!value) return [];
if (multiple) {
if (valueObject) {
return value.filter((item) => !hasItem(item));
}
const arr = [];
value.forEach((item, index) => {
if (!hasItem(item)) {
arr.push(joinArr(label[index], item));
}
});
return arr;
} else if ((label || value) && !hasItem(value)) {
return [joinArr(label, value)];
}
return [];
});
const hasItem = (value) => {
const val = props.valueObject ? value[props.fieldNames.value] : value;
return props.options && props.options.find((item) => item[props.fieldNames.value] === val);
};
const joinArr = (label,value) => {
const { fieldNames } = props;
return {
[fieldNames.label]: label,
[fieldNames.value]: value
};
};
const changeSelect = (val) => {
const options = props.options;
if (props.valueObject) {
emits('change', val);
return;
}
const currentSelect = options.find(item => item[props.fieldNames.value] === val);
emits('update:label', currentSelect?.[props.fieldNames.label]);
emits('change', currentSelect || {});
};
</script>
# base-select.vue


<template>
  <el-select v-model="selectValue" :multiple="multiple" placeholder="请选择" :value-key="fieldNames.value" @change="changeSelect" v-bind="$attrs">
  <el-option v-for="item in options" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" />
        <el-option v-for="item in defaultOption" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" />
  </el-select>
</template>

<script setup>
  const props = defineProps({
    value: {
      type: [String, Array]
    },
    label: {
      type: [String, Array]
    },
    options: {
      type: Array,
      default: () => []
    },
    // 是否change的时候返回当前下拉项值
    labelInValue: {
      type: Boolean,
      default: true
    },
    // 配置option中的想绑定的label和value字段
    fieldNames: {
      type: Object,
      default: () => ({ label: 'label', value: 'value' })
    },
    //value是否是当前item
    valueObject: {
      type: Boolean,
      default: false
    },
    //是否多选
    multiple: {
      type: Boolean,
      default: false
    }
  });
  
  const emits = defineEmits(['update:value', 'update:label', 'change']);
  
  //这里使用vueuse中的useVModel,也可以不使用,自己写也行。
  const selectValue = useVModel(props, 'value', emits, { passive: true, defaultValue: props.value });
  
 //value绑定的值,看是否需要绑定整个字段
  const valueCompute = computed(() => {
    return (val) => {
      return props.valueObject ? val : val[props.fieldNames.value];
    };
  });
  
  //默认下拉框需要出现的逻辑,之所以这里改成是一个数组,是因为他可能多选。
  const defaultOption = computed(() => {
       const { multiple, valueObject, label, value } = props;
        if(!value) return [];
        if (multiple) {
            if (valueObject) {
               return value.filter((item) => !hasItem(item));
            }
            const arr = [];
            value.forEach((item, index) => {
               if (!hasItem(item)) {
                  arr.push(joinArr(label[index], item));
               }
            });
            return arr;
       } else if ((label || value) && !hasItem(value)) {
          return [joinArr(label, value)];
       }
       return [];
  });
  
  const hasItem = (value) => {
       const val = props.valueObject ? value[props.fieldNames.value] : value;
       return props.options && props.options.find((item) => item[props.fieldNames.value] === val);
  };
  
  const joinArr = (label,value) => {
      const { fieldNames } = props;
      return {
          [fieldNames.label]: label,
          [fieldNames.value]: value
      };
  };
  
  const changeSelect = (val) => {
    const options = props.options;
    if (props.valueObject) {
      emits('change', val);
      return;
    }
    const currentSelect = options.find(item => item[props.fieldNames.value] === val);
    emits('update:label', currentSelect?.[props.fieldNames.label]);
    emits('change', currentSelect || {});
  };
</script>
# base-select.vue <template> <el-select v-model="selectValue" :multiple="multiple" placeholder="请选择" :value-key="fieldNames.value" @change="changeSelect" v-bind="$attrs"> <el-option v-for="item in options" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" /> <el-option v-for="item in defaultOption" :key="item[fieldNames.label]" :label="item[fieldNames.label]" :value="valueCompute(item)" /> </el-select> </template> <script setup> const props = defineProps({ value: { type: [String, Array] }, label: { type: [String, Array] }, options: { type: Array, default: () => [] }, // 是否change的时候返回当前下拉项值 labelInValue: { type: Boolean, default: true }, // 配置option中的想绑定的label和value字段 fieldNames: { type: Object, default: () => ({ label: 'label', value: 'value' }) }, //value是否是当前item valueObject: { type: Boolean, default: false }, //是否多选 multiple: { type: Boolean, default: false } }); const emits = defineEmits(['update:value', 'update:label', 'change']); //这里使用vueuse中的useVModel,也可以不使用,自己写也行。 const selectValue = useVModel(props, 'value', emits, { passive: true, defaultValue: props.value }); //value绑定的值,看是否需要绑定整个字段 const valueCompute = computed(() => { return (val) => { return props.valueObject ? val : val[props.fieldNames.value]; }; }); //默认下拉框需要出现的逻辑,之所以这里改成是一个数组,是因为他可能多选。 const defaultOption = computed(() => { const { multiple, valueObject, label, value } = props; if(!value) return []; if (multiple) { if (valueObject) { return value.filter((item) => !hasItem(item)); } const arr = []; value.forEach((item, index) => { if (!hasItem(item)) { arr.push(joinArr(label[index], item)); } }); return arr; } else if ((label || value) && !hasItem(value)) { return [joinArr(label, value)]; } return []; }); const hasItem = (value) => { const val = props.valueObject ? value[props.fieldNames.value] : value; return props.options && props.options.find((item) => item[props.fieldNames.value] === val); }; const joinArr = (label,value) => { const { fieldNames } = props; return { [fieldNames.label]: label, [fieldNames.value]: value }; }; const changeSelect = (val) => { const options = props.options; if (props.valueObject) { emits('change', val); return; } const currentSelect = options.find(item => item[props.fieldNames.value] === val); emits('update:label', currentSelect?.[props.fieldNames.label]); emits('change', currentSelect || {}); }; </script>

这里核心逻辑点 的是这个默认下拉是否出现的场景,由于我们下拉框分为单选、多选value对象,多选普通value等情况。所以这里要进行一个区分,至于为什么返回的是一个数组,是因为可能是多选,如果多选里面多个值无法正常回显,这里就需要添加多个。

至于同时绑定labelvalue,其实就是change的时候updatelabel绑定的值即可。

change想获取当前下拉项,其实和上面绑定label一样,我们重写这个change事件,1.更新label 2.将当前下拉项次emit出去。

至于el-select一些其他属性例如clearable这些,或者el-select的一些其他事件,这里我们使用了v-bind,他会自动将没有被props接受的参数向下传递,所以这里就不需要写其他的属性。在vue3v-bindv-on进行了合并,只需要写v-bind即可。

使用:

<template>
//如果是vue2,可以使用label.sync
<base-select v-model:label="form.name" v-model:value="form.code" :options="options" :fieldNames="{label:'fieldName',value:'fieldCode'}" labelInValue/>
</template>
<template>

//如果是vue2,可以使用label.sync

 <base-select v-model:label="form.name" v-model:value="form.code" :options="options" :fieldNames="{label:'fieldName',value:'fieldCode'}" labelInValue/>
</template>
<template> //如果是vue2,可以使用label.sync <base-select v-model:label="form.name" v-model:value="form.code" :options="options" :fieldNames="{label:'fieldName',value:'fieldCode'}" labelInValue/> </template>

select扩展

其实上面select还可以进一步扩展,这个之所以独立出来,是因人而异,有的人喜欢基础版觉得简单可进一步定制,有人喜欢功能强大pro版,开箱即用,所以这里单独独立出来。例如对select的下拉,远程搜索进行扩展,扩展成 apiSelect等。

我们可以给select再加几个参数:apiparamsremotequeryField

  • api:请求optionsurl 地址;
  • params:请求url的其他参数;
  • remote:是否开启远程搜索;
  • queryField:远程搜索的关键字参数,默认为keyword
# base-select.vue
....其他代码
<script setup>
....其他代码
onMounted(){
if(props.api){
getOptions()
}
}
//远程搜索也调用这个方法即可
const getOptions=(keyword)=>{
const {api,params,queryField}=props
const requestParams={
[queryField]:keyword,
...params,
}
request(api,{...requestParams}).then(res=>{
})
}
</script>
# base-select.vue

....其他代码
<script setup>
....其他代码
 
 onMounted(){
    if(props.api){
        getOptions()
    }
 }
 
 //远程搜索也调用这个方法即可
 const getOptions=(keyword)=>{
    const {api,params,queryField}=props
    const requestParams={
      [queryField]:keyword,
      ...params,
    }
   
    request(api,{...requestParams}).then(res=>{
    
    }) 
 }

</script>
# base-select.vue ....其他代码 <script setup> ....其他代码 onMounted(){ if(props.api){ getOptions() } } //远程搜索也调用这个方法即可 const getOptions=(keyword)=>{ const {api,params,queryField}=props const requestParams={ [queryField]:keyword, ...params, } request(api,{...requestParams}).then(res=>{ }) } </script>

使用:

<template>
//如果是vue2,可以使用label.sync
<base-select v-model:label="form.name" v-model:value="form.code" :fieldNames="{label:'fieldName',value:'fieldCode'}" :api="xxxx" />
</template>
<template>

//如果是vue2,可以使用label.sync

 <base-select v-model:label="form.name" v-model:value="form.code" :fieldNames="{label:'fieldName',value:'fieldCode'}" :api="xxxx" />
</template>
<template> //如果是vue2,可以使用label.sync <base-select v-model:label="form.name" v-model:value="form.code" :fieldNames="{label:'fieldName',value:'fieldCode'}" :api="xxxx" /> </template>

性能优化

由于页面可能有多个select,初始化的时候都会获取接口,导致页面初始化整体性能不好,所以这里我们可以进行一次优化,只有在focus的时候,如果这个时候没有初始化,就去请求获取options的接口。

伪代码:

<el-select @focus="focusSelect">
</el-select>
<script setup>
const isInit=ref(false)
const focusSelect=()=>{
if(!isInit.value){
getOptions()
}
}
</scipt>
<el-select @focus="focusSelect">
</el-select>


<script setup>
const isInit=ref(false)

const focusSelect=()=>{
   if(!isInit.value){
     getOptions()
   }
}
</scipt>
<el-select @focus="focusSelect"> </el-select> <script setup> const isInit=ref(false) const focusSelect=()=>{ if(!isInit.value){ getOptions() } } </scipt>

最后

经过这个select组件的封装,原本代码中大量的change事件都没有了,整体代码看起来清晰舒服了,希望该文章对你有帮助。

其他文章

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYvtFM8h' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片