二次封装 el-select 组件:提升灵活性与功能性的实践
在前端开发中,我们经常需要使用到 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-select
或 el-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>
四、组件使用
单选
<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
中
<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>
单选分页
在组件中配置:isShowPagination
及 paginationOption
<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>
多选
<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>
多选--隐藏多余标签的多选
<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>
多选分页
在组件中配置:isShowPagination
及 paginationOption
;多选不支持翻页选中功能
<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
即可
<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
即可
<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