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

项目背景

使用第三方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等,各位看官,可以在评论区给出建议。谢谢。

相关推荐
码事漫谈11 分钟前
解决 Anki 启动器下载错误的完整指南
前端
im_AMBER31 分钟前
Web 开发 27
前端·javascript·笔记·后端·学习·web
蓝胖子的多啦A梦1 小时前
低版本Chrome导致弹框无法滚动的解决方案
前端·css·html·chrome浏览器·版本不同造成问题·弹框页面无法滚动
玩代码1 小时前
vue项目安装chromedriver超时解决办法
前端·javascript·vue.js
訾博ZiBo1 小时前
React 状态管理中的循环更新陷阱与解决方案
前端
StarPrayers.1 小时前
旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)
前端·人工智能·python·算法·pycharm·启发式算法
一壶浊酒..2 小时前
ajax局部更新
前端·ajax·okhttp
DoraBigHead3 小时前
React 架构重生记:从递归地狱到时间切片
前端·javascript·react.js
彩旗工作室3 小时前
WordPress 本地开发环境完全指南:从零开始理解 Local by Flywhee
前端·wordpress·网站
iuuia3 小时前
02--CSS基础
前端·css