组件系列--无限层级联动选择

项目背景

使用第三方UI框架 ant vue3 的UI框架。如果了解这个框架知道,有一个组件:# Cascader,是能够实现级联选择的。控件样式:

该死的产品设计如下:

因为这个原因,所以有了联动组件的开发。

前言

做项目,我一般会优先选择一套成熟的UI组件库。除非这套库的内容不能实现相关的UI,我才会动手写。 开发该组件相关:

  • vu3 + ant design vue + ts
  • 组件的设计思考
  • 你自己的一个业务组件

先看效果

设计

  1. 设计组件,首先要考虑的是数据的交互。
  • 数据源结构:

    js 复制代码
    // 选择数据
    export type SelectEntity = {
      value: string // 标识
      label: string // 名称
      children?: SelectEntity[]
    }

    数据源是,嵌套关系。理论上支持无限层级。只要UI显示没有问题。

  • 数据变更:

    js 复制代码
    // 通知调用组件更新数据
    emit('onSelectEvent', {
    fieldNames: props.fieldNames,
    selectValues: refData.value,
    })

数据选择变更后,通过事件【onSelectEvent】通知父组件。

  1. 组件使用需要涉及到的属性
属性 说明 类型 默认值 必填
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显示满足需求。

动态数据

所谓动态数据,是第一级的数据选择后,给第二级的控件设置数据源,以此类推。这里实现的关键是两个:

  1. 数据结构

上述的【设计】章节,说到数据结构是无限嵌套的。这是实现动态数据的第一步。

  1. 选择事件

这里通过一个数组,来存储每个层级对应的子选项的集合。发生变更时,进行子项的设置。

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,
  })
}
  1. 模板中绑定
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等,各位看官,可以在评论区给出建议。谢谢。

相关推荐
耶啵奶膘1 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^3 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie3 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic4 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿4 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具5 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161775 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test6 小时前
js下载excel示例demo
前端·javascript·excel
Yaml46 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事6 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro