基于element select多分组多级下拉筛选封装

基于element select多分组多级下拉筛选封装

实现效果

1689644065468.jpg

具体需求

  • 这是个多选下拉组件
  • 组件内包含有分组分组不可点击)
  • 分组下有二级分类
  • 点击一级分类,选中该分类下所有二级分类

乍一看是挺容易的需求,正当我也以为如此时,转折发生了

难点出现

下拉框的源数据结构事例如下:

[
  {
    'code': 10,
    'desc': '分组1',
    'children': [
      {
        'code': 1010,
        'desc': '类型一级',
        'sub_type': [
          {
            'code': 1020,
            'desc': '类型二级1'
          },
        ]
      },
      {
        'code': 1011,
        'desc': '类型一级2',
        'sub_type': []
      },
    ]
  },
  {
    'code': 20,
    'desc': '分组2',
    'children': [
      {
        'code': 2010,
        'desc': '类型一级',
        'sub_type': [
          {
            'code': 1020,
            'desc': '类型二级1'
          },
        ]
      },
      {
        'code': 2011,
        'desc': '类型一级2',
        'sub_type': []
      },
    ]
  }
]

看了数据好像也没有感觉出来难在哪,仔细观摩,数据从children开始为分组下的二级分类数据,分为一级和二级,问题点:

  • 一级类型的code不会重复,但是仔细观察发现他居然有可能没有二级分类,这还选个毛啊,实际需求隐式包含了几条规则

    • 单独一级是可以选的
    • 有二级的一级分类是不可以单独选中的
  • 不同一级分类下的二级分类code是有重复

  • 由于上面说的code会重复,所以最后确定后端要的数据结构是个Object类型,如:

    const data = {
      1010: [1020], // 有二级并且选了的就是key是一级code,value是二级数组
      1010: [] // 没有二级只选了一级,value是个空数组
    }
    

    而我们熟知的select多选的数据结构都是Array类

代码实现

了解了需求分析了问题,终于到了该解决问题的时候了,俗话说有困难就解决困难,解决不掉就等着被解决就行了,人和代码有一个能跑就行了

模版的设计

如果我们按element的select分组的结构写的话就不可避免的涉及到几个问题:

  • 分组是不可选的
  • 如果一级分类作为分组,那么如果所有一级分类都没有二级分类,分组会直接不展示,也就是下拉框会变为空的
  • 一级分类是可以单独选择的

就这几个问题而言,使用默认的select组件带的分组是行不通了

为了满足以下条件:

  • 分组不可点
  • 没有二级分类一级分类可单独选
  • 二级分类一级分类点了就选择当前分类下所有二级分类

思考结构的时候太复杂的结构更不利于维护,尽量往简单了想,索性全部用el-option来处理吧,把多级的结构扁平化直接就一层还好理解,具体实现:

  • 分组可以直接用disabled+class控制不可选就完事了
  • 一级分类分为两种情况:
    • 一种是包含二级分类的这个时候一级分类不能单独选中就用div+class模拟稍小一点的option做个区分
    • 另一种呢是不存在二级分类一级分类,这个时候它可以单独选中,就用option来实现就行了
  • 二级分类就简单了直接遍历sub_type渲染option就行了

具体代码:

<template>
  <el-select v-model="selectValue" :placeholder="props.placeholder || '请选择'" multiple clearable :disabled="props.disabled" @change="handleChange">
    <template v-for="group in RELATETYPELIST">
      <!-- 分组 -->
      <el-option class="el-select-group__title group" :key="group.code" v-if="group.isTitle" :label="`${group.desc}`" :value="group.code" disabled>{{ group.desc }}</el-option>
      <template v-else>
        <!-- 两级,这个充当一级 -->
        <div
          v-if="group.sub_type && !!group.sub_type.length"
          :key="group.code"
          class="el-select-group__title group can-select"
          :class="{ 'all-selected': groupStatus[group.code] }"
          :label="`${group.desc}`"
          :value="group.code"
          @click="handleClickGroup(group)"
        >{{ group.desc }}</div>
        <!-- 只有一级的时候 -->
        <el-option v-else class="group can-select" :key="group.code" :label="`${group.desc}`" :value="group.code">{{ group.desc }}</el-option>
        <!-- 二级分类 -->
        <el-option class="pl-30" v-for="item in group.sub_type" :key="item.code" :label="`${group.desc}-${item.desc}`" :value="item.code">
          {{item.desc}}
        </el-option>
      </template>
    </template>
  </el-select>
</template>

处理源数据

首先我们确定了后端给我们的数据结构是如下结构:

interface BaseData {
  code: number
  desc: string
}
interface LvData extends BaseData {
  sub_type: BaseData[]
}
interface GroupData extends BaseData {
  children: LvData[]
}

根据我们想到的template结构,目前这个结构三级嵌套我们没法直接用

为了达到我们这个尽可能简单的实现的目的来分析这个结构:

  • 首先分组这一层本来就不能选,而且我们把它扁平化了,起码要与一级分类同级
  • 二级分类总要被遍历的,所以拆开它弊大于利

所以最终的结构如下:

const RELATETYPELIST = [
  { code: 10, desc: '分组1', isTitle: true },
  { code: 1010, desc: '类型一级'sub_type: [] }
]
RELATETYPELIST处理

为了实现点击一级分类快速找到对应的二级分类,并且选中我们还需要一份数据,此处我选择Map类型的数据,好处在于Object类型的数据如果用code做key会被重新排序,为了偷懒(少处理一次数据),所以在处理渲染数组RELATETYPELIST之前直接造出Map,然后取Map的value就行了,毕竟Map的顺序不会被重排

/**
 * 下拉框渲染options的源数据
 * @returns
 */
const localListToMap = () => {
  const localList = [
    {
      'code': 10,
      'desc': '分组1',
      'children': [
        {
          'code': 1010,
          'desc': '类型一级',
          'sub_type': [
            {
              'code': 1020,
              'desc': '类型二级1'
            },
          ]
        },
        {
          'code': 1011,
          'desc': '类型一级2',
          'sub_type': []
        },
      ]
    },
    {
      'code': 20,
      'desc': '分组2',
      'children': [
        {
          'code': 2010,
          'desc': '类型一级',
          'sub_type': [
            {
              'code': 1020,
              'desc': '类型二级1'
            },
          ]
        },
        {
          'code': 2011,
          'desc': '类型一级2',
          'sub_type': []
        },
      ]
    }
  ]
  const localListMap = new Map()
  // 把最外层的分组跟第一级类型摊平
  localList.forEach((parent) => {
    localListMap.set(`${parent.code}`, { code: `${parent.code}`, desc: parent.desc, isTitle: true })
    parent.children.forEach(d => {
      localListMap.set(`${d.code}`, { ...d, code: `${d.code}`, sub_type: d.sub_type?.map(child => ({ ...child, code: `${d.code}.${child.code}` })) })
    })
  })
  // 根据一级code和分组code 组成Map
  return localListMap
}

// Map(6) {'10' => {…}, '1010' => {…}, '1011' => {…}, '20' => {…}, '2010' => {…}, …}
// {
//     "key": "10",
//     "value": {
//         "code": "10",
//         "desc": "分组1",
//         "isTitle": true
//     }
// }
// {
//     "key": "1010",
//     "value": {
//         "code": "1010",
//         "desc": "类型一级",
//         "sub_type": [
//             {
//                 "code": "1010.1020",
//                 "desc": "类型二级1"
//             }
//         ]
//     }
// }
一级类型选中状态处理

除此之外我们还需要考虑div做一级分类的时候如何准确的加上class,当然在处理Map的时候加上属性控制也可以,但是这样又会让结构变得不再纯粹,所以我使用一个新的对象存状态,这个时候就不需要考虑顺序问题了,只需要存一级类型的code和一个Boolean值就可以了,所以结构如下:

const groupStatus = {
  1010: false,
  // ...
}
双向绑定的值处理

下拉框的渲染和一级分类选中的问题解决了,我么该考虑下select组件v-model的数据怎么处理了,这里我才用computed来作为绑定值,具体为啥用computed可以实现看官网吧,下面分析如何写getter和setter:

  • 输入和输出需要保持数据结构一致
  • 为了满足后端的要求,我们需要将emit的输出处理成对象
  • 为了满足element组件的要求,我们还需要对getter的输出处理为数组

如下为具体实现:

/**
 * select 组件内部绑定的value的getter函数
 * @param {{[key as Stirng]: Array}} value 外部传入的双向绑定对象
 * @returns
 * @example
 * // value demo
 * const value = {
 *  1010: [1101, 1102, 1103]
 * }
 * const val = getValue(value) // ['1010.1101', '1010.1102', '1010.1103']
 */
const getValue = (value = {}) => {
  const v = []
  // 将对象转为数组进行处理
  Object.entries(value).forEach(([key, subVal]) => {
    // 如果存在二级分类则便利二级分类处理为以 . 链接的字符串满足option的对应关系
    if (subVal.length) {
      subVal.forEach((val) => {
        v.push(`${key}.${val}`)
      })
    } else {
      // 否则直接加进去,为不包含二级分类只有一级分类的option
      v.push(`${key}`)
    }
  })
  return v
}
/**
 * 响应式返回给上一层的数据
 * @param {Array<String>} value
 * @returns {{[key as String]: Array}}
 */
const setValue = (value) => {
  const vMap = new Map() // 这里也可以直接用一个空对象进行添加
  value.forEach((val) => {
    // 将已选的数据每项用 . 拆开表示一级code和二级code
    const [key, subVal] = val.split('.')
    // 判断是否存在这个一级code作为key的值,如果vMap用对象的话可以使用hasOwnProperty判断
    if (!vMap.has(key)) {
      vMap.set(key, [])
    }
    // 如果存在二级code则加入数组里
    if (subVal) {
      vMap.get(key).push(subVal)
    }
  })
  // 如果没选中任何值则然后undefined
  if (!vMap.size) {
    return undefined
  }
  // 返回一个对象,如{ 1010: [1020] }
  return Object.fromEntries([...vMap.entries()])
}

/**
 * 双向绑定的处理
 */
const selectValue = computed({
  get: () => getValue(props.value),
  set: (value) => {
    const v = setValue(value)
    emit('update:value', v)
    emit('changeSelect', v)
  }
})

v-model的处理: cn.vuejs.org/guide/compo…

事件处理

至此我们解决了页面渲染的问题

我们已有的数据如下:

const useSelectData = (props, emit) => {
  // map结构,一级code做key
  const RELATETYPELISTMAP = localListToMap()
  // 所有一级对应的数据,包含二级数据
  const RELATETYPELIST = [...RELATETYPELISTMAP.values()]
  // 所有一级分组的选中态, filter过滤掉最外层只展示的分组
  const groupStatus = reactive(
    Object.fromEntries([...RELATETYPELISTMAP.entries()]
      .filter(([key, val]) => !val.isTitle)
      .map(([key, val]) => [key, false]))
  )
  /**
   * 双向绑定的处理
   */
  const selectValue = computed({
    get: () => getValue(props.value),
    set: (value) => {
      const v = setValue(value)
      emit('update:value', v)
      emit('changeRelateType', v)
    }
  })

    return {
    RELATETYPELIST,
    groupStatus,
    selectValue
  }
}

我们还需要解决的问题是:

  • 点击二级分类时判断一级分类是否被选中
  • 点击一级分类时选中所有当前一级分类下的所有二级分类
处理正常点击option

处理二级分类点击时一级分类是否选中,我们可以直接监听select组件的change事件,具体思路如下:

  • 当清空select时传入的value会是一个空数组,这个时候把所有的一级分类选中状态的对象都赋值为false
  • 当选择了某个option时,把已选的数据处理成一个Map或者Object,这样对比源数据和已选数据比较容易,直接比对code一致的一级分类下的二级分类数量就行了,一致的就是全选,此时一级分类变色,不一致就不变

如下是代码实现:

/**
 * 选项改变时
 */
const handleChange = (value) => {
  // 如果清空选项把group的选中态清空
  if (!value.length) {
    Object.keys(groupStatus).forEach(key => {
      groupStatus[key] = false
    })
    return
  }
  const selectedMap = new Map();
  // 将已选项处理成一个Map
  value.forEach((v) => {
    const [key, val] = v.split('.')
    if (!selectedMap.has(key)) {
      selectedMap.set(key, [])
    }
    if (val) {
      selectedMap.get(key).push(val)
    }
  });
  // 这个selectedMap的结构是
  // Map<{
  //   key: 一级code,
  //   value: [二级code]
  // }>
  // 获取已选项的一级的code
  const selectKeys = [...selectedMap.keys()]
  selectKeys.forEach((key) => {
    // 比较已选的二级和源数据二级的长度
    // 长度相等则改变一级分类的选中态
    if (selectedMap.get(key).length === (RELATETYPELISTMAP.get(key)?.sub_type ?? []).length) {
      groupStatus[key] = true
    } else {
      groupStatus[key] = false
    }
  })
}
处理点击一级分类

当点击一级分类的时候需要选中它下面的所有二级分类

点击的时候判断点击的这个一级分类是否已经全选,分两种情况处理具体实现思路如下:

  • 如果已经全选则取消全选,同时删除他下面二级分类的选中态
  • 如果不是全选,则把当前一级分类下面的没有被选择的二级分类加到v-model的数据里,同时一级分类状态切换为已选

具体代码实现如下:

/**
 * 点击一级分类时
 * @param {*} group
 */
const handleClickGroup = (group) => {
  // 获取子项, 如果不存在二级分类就把当前的一级分类拿出来
  const childrens = !RELATETYPELISTMAP.get(group.code).sub_type?.length ? [group] : RELATETYPELISTMAP.get(group.code).sub_type
  // 获取子项code
  const childrensCode = childrens.map((val) => val.code)
  // 获取已选的code
  const selected = new Set(selectValue.value)
  // 如果当前组已经是全选了,则删除已选的
  if (groupStatus[group.code]) {
    // 删除已选的里面包含的子项
    childrensCode.forEach(code => {
      selected.delete(code)
    })
    // 重新给select赋值
    selectValue.value = [...selected]
    // 改变当前组的选中态
    groupStatus[group.code] = false
    return;
  }

  // 往里塞当前组的code
  childrensCode.forEach(code => {
    selected.add(code)
  })
  // 重新给select赋值
  selectValue.value = [...selected]
  // 改变当前组的选中态
  groupStatus[group.code] = true
}

完整代码请查看我的个人网站:wangblogs.top/2023/07/18/…

全部评论

相关推荐

数学转码崽:一直给我推,投了又不理,理了又秒挂
点赞 评论 收藏
分享
01-23 19:12
门头沟学院 Java
榨出爱国基因:你还差 0.1% 就拿到校招礼盒,快叫朋友给你砍一刀吧
投递拼多多集团-PDD等公司10个岗位
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务