likes
comments
collection
share

二次封装 el-select 组件:提升灵活性与功能性的实践

作者站长头像
站长
· 阅读数 60

在前端开发中,我们经常需要使用到 Element UI 这样的 UI 框架来快速构建界面。其中,el-select 是一个非常常用的组件,用于实现下拉选择框的功能。然而,在实际项目中,我们可能需要对 el-select 进行一些定制化处理,以满足特定的设计需求或功能要求。本文将介绍如何对 el-select 组件进行二次封装,以增加其灵活性和功能性。

一、组件设计与核心功能

二次封装的核心目标在于提升组件的性能和灵活性。为此,我们引入了动态组件切换、自定义选项展示、全选功能以及分页机制等关键特性,旨在创造一个既高效又高度可定制的 el-select 替代方案。

二、二次封装的核心思路

二次封装 el-select 的核心在于理解其内部结构和工作原理,然后在此基础上添加或修改必要的功能。以下是我们封装的主要目标:

  • 动态组件切换:根据 useVirtual prop 的值动态切换组件类型,支持虚拟滚动版本。
  • 自定义选项:通过插槽和 customLabel prop 允许用户自定义选项的显示内容。
  • 全选/取消全选功能:在多选模式下提供全选/取消全选的能力。
  • 分页支持:当选项过多时,提供分页功能,避免性能问题。
  • 事件处理:正确处理输入和选择变化事件,确保数据的同步和更新。

三、具体实现

我们的二次封装主要集中在 <template><script setup> 部分,通过使用 Vue 3 的组合式 API 来实现上述目标。下面是对关键部分的解析:

1. 动态组件切换

通过 :is 绑定属性,根据 useVirtual 的值动态决定使用 el-selectel-select-v2(虚拟滚动版本)。

<component
  :is="!useVirtual ? 'el-select' : 'el-select-v2'"
  ...
  />

通过 :is 属性,组件根据 useVirtual 属性动态选择是否使用虚拟滚动版本,有效提升了大数据量场景下的性能。

2. 自定义选项与插槽

利用 Vue 的插槽机制,允许用户自定义选项的显示内容,并通过 customLabelHandler 函数处理自定义标签。

<template v-for="(index, name) in slots" v-slot:[name]="data">
  <slot :name="name" v-bind="data" />
</template>

3. 全选/取消全选功能

通过计算属性和事件处理器实现全选功能,并结合分页配置,确保了组件在多选和大数据场景下的高效和友好。

<el-checkbox v-if="multiple && !isShowPagination" v-model="selectChecked" @change="selectAll">全选</el-checkbox>
<div class="t_select__pagination" v-if="isShowPagination && filteredOptionsCount > 0">
  <el-pagination
    v-model:current-page="paginationOption.currentPage"
    ...
    />
</div>

4. 完整代码

<template>
  <!-- 根据 useVirtual prop 的值动态切换组件类型 -->
  <component
    :is="!useVirtual ? 'el-select' : 'el-select-v2'"
    popper-class="t_select"
    ref="tselectRef"
    v-model="childSelectedValue"
    :options="!useVirtual ? null : optionSource"
    :style="{ width: width || '100%' }"
    @change="handlesChange"
    @input="handlesSelectInput"
    v-bind="{
      clearable: true,
      filterable: filterable,
      multiple: multiple,
      ...$attrs
    }"
  >
    <!-- 使用插槽来自定义组件内容 -->
    <template v-for="(index, name) in slots" v-slot:[name]="data">
      <slot :name="name" v-bind="data" />
    </template>
    <!-- 当不使用虚拟列表时,显示传统的选项列表和分页 -->
    <template v-if="!useVirtual">
      <!-- 在多选模式下显示全选复选框 -->
      <el-checkbox
        v-if="multiple && !isShowPagination"
        v-model="selectChecked"
        @change="selectAll"
        class="all_checkbox"
        >全选</el-checkbox
      >
      <!-- 遍历选项源数据,生成选项组件 -->
      <el-option
        v-for="(item, index) in optionSource"
        :key="index + 'i'"
        :label="customLabel ? customLabelHandler(item) : item[labelCustom]"
        :value="item[valueCustom]"
        :disabled="item.disabled"
      >
        <!-- 使用插槽来自定义选项的显示内容 -->
        <template v-for="(index, name) in slots" v-slot:[name]="data">
          <slot :name="name" v-bind="data" />
        </template>
      </el-option>
      <!-- 显示分页组件,用于分批加载选项 -->
      <div class="t_select__pagination" v-if="isShowPagination && filteredOptionsCount > 0">
        <el-pagination
          v-model:current-page="paginationOption.currentPage"
          v-model:page-size="paginationOption.pageSize"
          :layout="paginationOption.layout || 'total, prev, pager, next, jumper'"
          :pager-count="paginationOption.pagerCount"
          :total="paginationOption.total"
          @input.stop="(e: any) => emits('input', e.target.value)"
          v-bind="{
            small: true,
            background: true,
            ...$attrs,
            ...paginationOption.bind
          }"
        />
      </div>
    </template>
  </component>
</template>

<script setup lang="ts" name="TSelect">
import { computed, ref, useSlots } from "vue"

// 定义组件 props
const props: any = defineProps({
  modelValue: {
    type: [String, Number, Array]
  },
  // 是否多选
  multiple: {
    type: Boolean,
    default: false
  },
  // 选择框宽度
  width: {
    type: String
  },
  // 传入的option数组中,要作为最终选择项的键值key
  valueCustom: {
    type: String,
    default: "key"
  },
  // 传入的option数组中,要作为显示项的键值名称
  labelCustom: {
    type: String,
    default: "label"
  },
  // 是否自定义设置下拉label
  customLabel: {
    type: String
  },
  // 下拉框组件数据源
  optionSource: {
    type: Array as unknown as any[],
    default: () => []
  },
  // 是否过滤默认开启
  filterable: {
    type: Boolean,
    default: true
  },
  // 是否显示分页
  isShowPagination: {
    type: Boolean,
    default: false
  },
  // 分页配置
  paginationOption: {
    type: Object,
    default: () => {
      return {
        pageSize: 6, // 每页显示条数
        currentPage: 1, // 当前页
        pagerCount: 5, // 按钮数,超过时会折叠
        total: 0 // 总条数
      }
    }
  },
  // 是否开启虚拟列表
  useVirtual: {
    type: Boolean,
    default: false
  }
})

// 定义组件的 ref 参考
const tselectRef = ref()
const filteredOptionsCount = ref(1)
const slots = useSlots()

// 抛出事件
const emits = defineEmits(["update:modelValue", "change", "input", "select-input"])

// 处理选项输入事件
const handlesSelectInput = (e: any) => {
  if (props.filterable) {
    filteredOptionsCount.value = tselectRef.value.filteredOptionsCount
  }
  emits("select-input", e.target.value)
}

// vue3 v-model简写
let childSelectedValue: any = computed({
  get() {
    return props.modelValue
  },
  set(val) {
    emits("update:modelValue", val)
  }
})

// 处理选择值变化事件
const handlesChange = (val: any) => {
  emits("change", val)
}

// 设置全选
const selectChecked = computed({
  get() {
    const _deval: any = props.modelValue
    const list = props.optionSource.filter((item: { disabled: any }) => {
      return !item.disabled
    })
    return _deval?.length === list.length
  },
  set(val: any) {
    const list = props.optionSource.filter((item: { disabled: any }) => {
      return !item.disabled
    })
    return val?.length === list.length
  }
})

// 处理全选操作
const selectAll = (val: any) => {
  let options = JSON.parse(JSON.stringify(props.optionSource))
  // 数据源过滤禁用选项
  options = options.filter((item: { disabled: any }) => {
    return !item.disabled
  })
  if (val) {
    const selectedAllValue = options.map((item: { [x: string]: any }) => {
      return item[props.valueCustom]
    })
    emits("update:modelValue", selectedAllValue)
  } else {
    emits("update:modelValue", null)
  }
}

// 自定义label显示
const customLabelHandler = (_item: any) => {
  return eval(props.customLabel)
}
</script>
<style lang="scss" scoped>
.t_select {
  .el-select-dropdown {
    .all_checkbox {
      margin-left: 20px;
    }
  }
}
</style>

四、组件使用

单选

二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <t-select
        placeholder="请选择工序"
        v-model="selectVlaue"
        :optionSource="stepList"
        valueCustom="label"
        @change="selectChange"
        width="200px"
      />
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts" name="Single">
import { ref } from "vue"
const selectVlaue = ref<any>()
const stepList = [
  { label: "开始" },
  { label: "POSUI" },
  { label: "11" },
  { label: "GX123" },
  { label: "烘干破碎" },
  { label: "车间仓库" },
  { label: "ui3333" },
  { label: "hhh333" }
]
const selectChange = (val: any) => {
  console.log("selectChange", val, selectVlaue.value)
}
</script>

自定义显示下拉项 label

设置 customLabel 字符串表达式:${_item.label}(${_item.id});注意:表达式必须以_item开头,且后面的属性必须存在optionSource

二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <t-select
        placeholder="自定义显示下拉项label"
        v-model="selectVlaue"
        :optionSource="stepList"
        valueCustom="label"
        customLabel="`${_item.label}(${_item.id})`"
        @change="selectChange"
      ></t-select>
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts">
import { ref } from "vue"
const selectVlaue = ref<any>()
const stepList = ref([
  { label: "开始", id: 1 },
  { label: "POSUI", id: 2 },
  { label: "11", id: 3 },
  { label: "GX123", id: 4 },
  { label: "烘干破碎", id: 5 },
  { label: "车间仓库", id: 6 },
  { label: "ui3333", id: 7 },
  { label: "hhh333", id: 8 }
])
const selectChange = (val: any) => {
  console.log("selectChange", val, selectVlaue.value)
}
</script>

单选分页

在组件中配置:isShowPaginationpaginationOption

二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <t-select
        placeholder="请选择工序(单选分页)"
        v-model="selectVlaue"
        :optionSource="stepList"
        labelCustom="materialName"
        valueCustom="id"
        @current-change="currentChange"
        @change="selectChange"
        @input="selectinput"
        @select-input="selectinput1"
        isShowPagination
        :paginationOption="paginationOption"
      />
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts" name="Pagination">
import { onMounted, ref } from "vue"
import data from "./data.json"
import data1 from "./data1.json"
const selectVlaue = ref<any>()
const stepList = ref([])
const paginationOption = ref({
  pageSize: 6, // 每页显示条数
  currentPage: 1, // 当前页
  pagerCount: 7, // 按钮数,超过时会折叠
  total: 0 // 总条数
})
const selectinput = (val: any) => {
  console.log("分页器-input", val)
}
const selectinput1 = (val: any) => {
  console.log("select-input", val)
}
onMounted(() => {
  getList(1)
})
const getList = async pageNum => {
  let res
  if (pageNum === 1) {
    res = await data
  } else {
    res = await data1
  }
  if (res.success) {
    stepList.value = res.data.records
    paginationOption.value.total = res.data.total
    // console.log('获取数据', paginationOption.value)
  }
}
// 切换分页
const currentChange = (val: any) => {
  console.log("切换分页current-change事件", val)
  getList(val)
}
const selectChange = (val: any) => {
  console.log(`change返回值${val};v-model值${selectVlaue.value}`)
}
</script>

多选

二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <t-select
        placeholder="请选择工序"
        v-model="selectVlaue"
        :optionSource="stepList"
        valueCustom="label"
        @change="selectChange"
        multiple
      />
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts" name="Multiple">
import { ref } from "vue"
const selectVlaue = ref<any>()
const stepList = [
  { label: "开始" },
  { label: "POSUI" },
  { label: "11" },
  { label: "GX123" },
  { label: "烘干破碎" },
  { label: "车间仓库" },
  { label: "ui3333" },
  { label: "hhh333" }
]
const selectChange = (val: any) => {
  console.log("selectChange", val, selectVlaue.value)
}
</script>

多选--隐藏多余标签的多选

二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <div>use collapse-tags</div>
      <t-select
        placeholder="请选择(多选)"
        v-model="selectVlaue1"
        :optionSource="stepList"
        valueCustom="label"
        collapse-tags
        multiple
        @change="selectChange($event, '1')"
      />
    </t-layout-page-item>
    <t-layout-page-item>
      <div>use collapse-tags-tooltip</div>
      <t-select
        placeholder="请选择(多选)"
        v-model="selectVlaue2"
        :optionSource="stepList"
        valueCustom="label"
        collapse-tags
        collapse-tags-tooltip
        multiple
        @change="selectChange($event, '2')"
      />
    </t-layout-page-item>
    <t-layout-page-item>
      <div>use max-collapse-tags</div>
      <t-select
        placeholder="请选择(多选)"
        v-model="selectVlaue3"
        :optionSource="stepList"
        valueCustom="label"
        collapse-tags
        collapse-tags-tooltip
        :max-collapse-tags="3"
        multiple
        @change="selectChange($event, '3')"
      />
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts" name="multipleCollapseTags">
import { ref } from "vue"
const selectVlaue1 = ref<any>()
const selectVlaue2 = ref<any>()
const selectVlaue3 = ref<any>()
const stepList = [
  { label: "开始" },
  { label: "POSUI" },
  { label: "11" },
  { label: "GX123" },
  { label: "烘干破碎" },
  { label: "车间仓库" },
  { label: "ui3333" },
  { label: "hhh333" }
]
const selectChange = (val: any, type) => {
  console.log(`selectChange--selectVlaue${type}`, val)
}
</script>

多选分页

在组件中配置:isShowPaginationpaginationOption多选不支持翻页选中功能

二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <t-select
        placeholder="请选择工序(多选分页)"
        v-model="selectVlaue"
        :optionSource="stepList"
        labelCustom="materialName"
        valueCustom="id"
        @current-change="currentChange"
        @change="selectChange"
        isShowPagination
        multiple
        :paginationOption="paginationOption"
      />
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts" name="Pagination">
import { onMounted, ref } from "vue"
import data from "./data.json"
import data1 from "./data1.json"
const selectVlaue = ref<any>()
const stepList = ref([])
const paginationOption = ref({
  pageSize: 6, // 每页显示条数
  currentPage: 1, // 当前页
  pagerCount: 7, // 按钮数,超过时会折叠
  total: 0 // 总条数
})
onMounted(() => {
  getList(1)
})
const getList = async pageNum => {
  let res
  if (pageNum === 1) {
    res = await data
  } else {
    res = await data1
  }
  if (res.success) {
    stepList.value = res.data.records
    paginationOption.value.total = res.data.total
    // console.log('获取数据', paginationOption.value)
  }
}
// 切换分页
const currentChange = (val: any) => {
  console.log("切换分页current-change事件", val)
  getList(val)
}
const selectChange = (val: any) => {
  console.log(`change返回值${val};v-model值${selectVlaue.value}`)
}
</script>

虚拟列表--单选

在组件中配置:use-virtual 即可

二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <t-select
        placeholder="请选择(虚拟列表--单选)"
        v-model="selectVlaue"
        :optionSource="stepList"
        useVirtual
        @change="selectChange"
      />
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts" name="useVirtual">
import { ref } from "vue"
const selectVlaue = ref<any>()
const initials = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
const stepList = Array.from({ length: 1000 }).map((_, idx) => ({
  value: `Option ${idx + 1}`,
  label: `${initials[idx % 10]}${idx}`
}))
const selectChange = (val: any) => {
  console.log("selectChange", val, selectVlaue.value)
}
</script>

虚拟列表--多选

在组件中配置:use-virtual 即可 二次封装 el-select 组件:提升灵活性与功能性的实践

<template>
  <t-layout-page>
    <t-layout-page-item>
      <t-select
        placeholder="请选择(虚拟列表--多选)"
        v-model="selectVlaue"
        :optionSource="stepList"
        useVirtual
        multiple
        @change="selectChange"
      />
    </t-layout-page-item>
  </t-layout-page>
</template>
<script setup lang="ts" name="useVirtual">
import { ref } from "vue"
const selectVlaue = ref<any>()
const initials = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
const stepList = Array.from({ length: 1000 }).map((_, idx) => ({
  value: `Option ${idx + 1}`,
  label: `${initials[idx % 10]}${idx}`
}))
const selectChange = (val: any) => {
  console.log("selectChange", val, selectVlaue.value)
}
</script>

更多使用示例,可以看看文档 TSelect 下拉选择组件

五、总结

二次封装 el-select 不仅提升了组件的灵活性和功能性,还增强了代码的可维护性和复用性。通过深入理解组件的工作原理并结合 Vue 的最新特性,我们可以轻松地创建出满足特定需求的定制化组件。希望本文能为你在实际项目中的组件封装提供一些参考和灵感

往期组件封装文章

转载自:https://juejin.cn/post/7396332696659689481
评论
请登录