项目背景
使用第三方UI框架 ant vue3 的UI框架。如果了解这个框架知道,有一个组件:# Cascader,是能够实现级联选择的。控件样式:
该死的产品设计如下:
因为这个原因,所以有了联动组件的开发。
前言
做项目,我一般会优先选择一套成熟的UI组件库。除非这套库的内容不能实现相关的UI,我才会动手写。 开发该组件相关:
- vu3 + ant design vue + ts
- 组件的设计思考
- 你自己的一个业务组件
先看效果
设计
- 设计组件,首先要考虑的是数据的交互。
-
数据源结构:
js// 选择数据 export type SelectEntity = { value: string // 标识 label: string // 名称 children?: SelectEntity[] }
数据源是,嵌套关系。理论上支持无限层级。只要UI显示没有问题。
-
数据变更:
js// 通知调用组件更新数据 emit('onSelectEvent', { fieldNames: props.fieldNames, selectValues: refData.value, })
数据选择变更后,通过事件【onSelectEvent】通知父组件。
- 组件使用需要涉及到的属性
属性 | 说明 | 类型 | 默认值 | 必填 |
---|---|---|---|---|
modelValue | 绑定数据 | string[] | [] | 是 |
fieldNames | 绑定的字段名称数组 | string[] | [] | 是 |
placeholder | 输入提示 | string[] | [] | 否 |
label | 控件描述 | string | '' | 否 |
optionData | 控件数据源 | SelectEntity[] | [] | 是 |
layout | 布局模式(跟Form控件的布局模式同步) | string | 'vertical' | 否 |
required | 是否必填 | boolean | false | 否 |
rules | 控件校验规则(同ant 中 form控件的校验) | any[] | [] | 否 |
控件是根据属性【fieldNames】,来决定是多少级的联动。如果是两个字段,则是两级联动。
代码实现
代码
js
<script lang="ts" setup>
import { reactive, watch } from 'vue'
import { Col, FormItem, Row, Select } from 'ant-design-vue'
import type { SelectEntity } from '.'
type Props = {
modelValue: string[] // 绑定的数据
fieldNames: string[] // 控件绑定的字段
placeholder?: string[] // 输入描述
label?: string // 控件描述
optionData: SelectEntity[] // 获取数据
layout?: string // 表单布局模式 默认:vertical
required?: boolean // 是否必填
rules?: any[] // 验证规则
}
const props = defineProps<Props>()
const selectDataType: Record<string, SelectEntity[] | undefined> = {}
const refData = reactive({
value: props.modelValue,
options: props.optionData,
selectData: selectDataType,
})
// 获取控件的数据源
const getOptionData = (index: number) => {
const result: any[] = []
if (index === 0) {
refData.options.forEach((item) => {
result.push({
label: item.label,
value: item.value,
})
})
} else {
const key = refData.value[index - 1]
if (refData.selectData && refData.selectData[key]) {
; (refData.selectData[key] as SelectEntity[]).forEach((item) => {
result.push({
label: item.label,
value: item.value,
})
})
}
}
return result
}
const emit = defineEmits(['onSelectEvent'])
// 选项数据时
const selectChange = (value: any, index: number, isClear: boolean) => {
if (index === 0) {
if (!Object.hasOwnProperty.call(refData.selectData, value)) {
const sourceItems = refData.options.find((item) => item.value === value)
refData.selectData[value] = sourceItems?.children
}
} else {
const key = refData.value[index - 1]
if (
!Object.hasOwnProperty.call(refData.selectData, value) &&
refData.selectData &&
refData.selectData[key]
) {
const sourceItems = (refData.selectData[key] as SelectEntity[]).find(
(item) => item.value === value
)
refData.selectData[value] = sourceItems?.children
}
}
// 清除后续的选择
if (isClear) {
for (let i = index + 1; i < refData.value.length; i++) {
refData.value[i] = ''
}
}
// 通知调用组件更新数据
emit('onSelectEvent', {
fieldNames: props.fieldNames,
selectValues: refData.value,
})
}
// 设置默认值
const setDefaultVal = () => {
if (props.modelValue && props.modelValue.length > 0) {
for (let index = 0; index < props.modelValue.length; index++) {
selectChange(props.modelValue[index], index, false)
}
}
}
// 监控数据变化
watch(
() => props.modelValue,
() => {
setDefaultVal()
},
{ immediate: true }
)
</script>
<template>
<div class="flex justify-content w-[100%]">
<Row :gutter="4" class="flex-grow">
<Col :span="24 / props.fieldNames.length" v-for="(item, index) in props.fieldNames">
<FormItem :label="index === 0 && props.label ? props.label : ''" :required="props.required ? props.required : false"
:rules="props.rules
? props.rules
: [
{
required: props.required ? props.required : false,
message: `请选择${props.placeholder && props.placeholder[index]
? props.placeholder[index]
: ''
}`,
trigger: 'change',
},
]
" :class="index != 0 && (!props.layout || props.layout === 'vertical')
? 'mt-[30px]'
: ''
" :name="item">
<Select v-model:value="refData.value[index]" :placeholder="props.placeholder && props.placeholder[index]
? props.placeholder[index]
: '请选择'
" style="width: 100%" :options="getOptionData(index)"
@change="(value: any) => selectChange(value, index, true)"></Select>
</FormItem>
</Col>
</Row>
</div>
</template>
<style scoped lang="scss"></style>
设计解读
如果仔细看【template】的内容,你会发现代码很简单,就是一个【Row】中包多个【Col】,每个【Col】中,设置一个【FormItem】和一个【Select】。
从代码中看,控件是支持无限层级的,只要UI显示满足需求。
动态数据
所谓动态数据,是第一级的数据选择后,给第二级的控件设置数据源,以此类推。这里实现的关键是两个:
- 数据结构
上述的【设计】章节,说到数据结构是无限嵌套的。这是实现动态数据的第一步。
- 选择事件
这里通过一个数组,来存储每个层级对应的子选项的集合。发生变更时,进行子项的设置。
js
// 选项数据时
const selectChange = (value: any, index: number, isClear: boolean) => {
if (index === 0) {
if (!Object.hasOwnProperty.call(refData.selectData, value)) {
const sourceItems = refData.options.find((item) => item.value === value)
refData.selectData[value] = sourceItems?.children
}
} else {
const key = refData.value[index - 1]
if (
!Object.hasOwnProperty.call(refData.selectData, value) &&
refData.selectData &&
refData.selectData[key]
) {
const sourceItems = (refData.selectData[key] as SelectEntity[]).find(
(item) => item.value === value
)
refData.selectData[value] = sourceItems?.children
}
}
// 清除后续的选择
if (isClear) {
for (let i = index + 1; i < refData.value.length; i++) {
refData.value[i] = ''
}
}
// 通知调用组件更新数据
emit('onSelectEvent', {
fieldNames: props.fieldNames,
selectValues: refData.value,
})
}
- 模板中绑定
js
<Select v-model:value="refData.value[index]" :placeholder="props.placeholder && props.placeholder[index]
? props.placeholder[index]
: '请选择'
" style="width: 100%" :options="getOptionData(index)"
@change="(value: any) => selectChange(value, index, true)"></Select>
可以看到模板中,下拉选择的数据源是【getOptionData】
js
// 获取控件的数据源
const getOptionData = (index: number) => {
const result: any[] = []
if (index === 0) {
refData.options.forEach((item) => {
result.push({
label: item.label,
value: item.value,
})
})
} else {
const key = refData.value[index - 1]
if (refData.selectData && refData.selectData[key]) {
; (refData.selectData[key] as SelectEntity[]).forEach((item) => {
result.push({
label: item.label,
value: item.value,
})
})
}
}
return result
}
通过这个函数,就能动态获取变更时,设置的子选项。
调用样例
js
<BankSubBranch
:modelValue="refData.publicSelect"
:label="'开户支行'"
:fieldNames="props.branchFields"
:placeholder="['省/市', '市', '区/县']"
:optionData="selectCityOpition"
@onSelectEvent="refSelectHandle"
>
</BankSubBranch>
设置选择的数据。
js
// 联动选择的处理
const refSelectHandle = (selectData: CascadeSelectResult) => {
if (!selectData.fieldNames || selectData.fieldNames.length < 1) {
return
}
for (let i = 0; i < selectData.fieldNames.length; i++) {
const itemKey = selectData.fieldNames[i]
if (Object.hasOwnProperty.call(refData.formState, itemKey)) {
refData.formState[itemKey] = selectData.selectValues[i]
}
}
}
结语
如果看到设计是三层级联选择,就写死三级级联选择。那么,从设计角度来说,就是很失败的。本组件,实现了无限层级的级联选择。且,本组件把获取数据源的业务逻辑,让父组件完成,把级联组件变成了一个与业务无关的通用组件。
该控件还有很多需要修改的细节,如:存储子选项的对象换成weakMap等,各位看官,可以在评论区给出建议。谢谢。