antd3.x实现 cascader多选功能

1.应用背景

项目技术栈背景: react + antd3.x

产品要求要有一个cascader多选的功能,类似于以下

功能看起来很简单,但是该功能只支持antd5.x的项目,但项目为antd3.x不支持该功能,所以需要重新封装一下该组件。

ps:如果是antd5的项目,可以直接使用,代码如下

ini 复制代码
<Cascader
    style={{ width: '100%' }}
    options={options}
    onChange={onChange}
    multiple
    maxTagCount="responsive"
  />

所以对于antd3.x的项目需要重新手写一下该组件,不能直接在此基础上进行封装

2.实现思路

首先先理一下基本的实现思路,可以分为两部分,第一部分为上面的选择框,里面存放初始的placeholder以及选择后的tags; 第二部分为下面的多选项处理,处理为联级多选的功能。

初始的时候 是实现了该功能,使用trigger进行包裹。上面是显示自己写的选择框是一个
,没有选中值得时候显示placeholder的值,当选中后显示选中的标签tags列表。

ini 复制代码
<Trigger
       popup={
         <Popup {...props} onCancel={handleCancel} onConfirm={handleConfirm} />
       }
/>........</Trigger>

下面的联级选择框是一个自定义组件,为Trigger组件的children

ini 复制代码
<Trigger ..........>
<Selector
         forwardRef={selectorRef}
         onRemove={handleItemRemove}
         onClear={handleClear}
         {...props}
       />
     </Trigger>

然后当我基本写完的时候才发现有一个问题,那就是因为这个选择框中的数据可能会较多且名称相似(十几条或几十条),产品肯定会需要模糊搜索功能,火速去钉钉上一问,果然这个功能是必须拥有的。此时就处于进退两难的底部,一方面是不可或缺的功能,另一方面是无法实现的功能。因为上面是一个
包裹的内容,所以就不好实现输入框的功能,只能加个输入框,那么输入框中还要放选中的标签,这个就是怎么想都不好实现。

还好就是这个应用背景是,第一级选择项只有两个,所以灵机一动,改用treeSelect实现

虽然没有cascader看起来轻巧,但是产品还是同意了。然后接下来又来了一个类似的需求,此时的父级可能有多个,所以必须要实现该功能了。逃是逃不过去的。


整理了一下思路,重新设计,头部可以使用封装好的select选择框,它已经封装好了input + tags的展示模式,要渲染的联级多选可以使用

dropdownRender属性,实现自定义的展示内容。

3.实现方式

主体是一个函数组件,react hooks + antd

返回一个Select

ini 复制代码
  return <Select
    // maxTagCount={10}
    showArrow
    disabled={disabled}
    getPopupContainer={triggerNode => triggerNode}
    dropdownMatchSelectWidth={false}
    mode="multiple"
    value={showSelectValueLabel}
    allowClear={true}
    onDeselect={removeTag}
    dropdownRender={menu}
    className="mn-style-multiple-cascader-container"
    placeholder={placeholder}
    showSearch={true}
    clearIcon={<Icon type="close-circle" onClick={handleClear}/> }
    onSearch={changeInput}
    onBlur={onBlur}
  />

底部的联级多选也就是上面的 dropdownRender={menu} 分为两部分的展示,一个是输入框中无值时,展示cascader联级多选样式,二是输入框中有值时,展示过滤后的选项(样式我是参考的antd5.x过滤的样式,如下)

没有搜索内容

ini 复制代码
{!inputValue && caseTree.map((caseItem, index) => { // 没有搜索内容则展示
        return <div className="cascader-container-levelone" key={index}>
          {caseItem.map((item) => {
            return <div className={`check-item ${item.value === activeSelected ? 'activeBackground' : ''}`} onClick={e => {
              e.preventDefault()
              // console.log(index, item,childrenKey, 123);
              if (index === 0) { // 表示是父级元素
                setActiveSelected(item.value) // 表示为当前点击的父元素,为其增加背景色标识
              }
              spreadNext(item[childrenKey], index)
            }}
            key={item.value}
            onMouseDown={e => e.preventDefault()}>
              <Checkbox
                onClick={e => {
                  e.stopPropagation()
                  spreadNext(item[childrenKey], index)
                  if (index === 0) { // 表示是父级元素
                    setActiveSelected(item.value)
                  }
                }}
                checked={item.checked}
                indeterminate={item.indeterminate} // 表示checkbox的半选状态checked-true,indeterminate-false,表示全选;checked-false,indeterminate-false表示为空;其他表示半选
                onChange={e => onCheckChange(item, e.target.checked)}
              >{item.label}</Checkbox>
              {item.children && item.children.length > 0 && <Icon type="right" />}
            </div>
          })}
        </div>
      })}

有搜索功能时

ini 复制代码
{ // 有搜索内容时展示过滤内容
        inputValue && <div className="filter-container">
        {
          filterOptions.map((filteritem, index) => {
            let currentObj
            caseTree.forEach(item => {
            item.forEach(item1 => {
                // (item1.children) && (item1.value === 'fuzhou')
                if (item1.children) {
                    item1.children.forEach(itemchild => {
                        if (itemchild.value === filteritem.value) {
                          currentObj = itemchild
                        }
                    })
                }
              })
            })
            return <div key={index} className="filter-items" onClick={e => e.preventDefault()} onMouseDown={e => e.preventDefault()}>
              <Checkbox
                onClick={e => { e.stopPropagation() }}
                checked={currentObj.checked}
                indeterminate={currentObj.indeterminate}
                onChange={e => { onCheckChange(currentObj, e.target.checked) }}
              >{filteritem.fatherLabel}/{filteritem.label}</Checkbox>
            </div>
          })
        }
        </div>
      }

这边虽然代码不算很多,总体来说300来行以及样式代码,这边就挑一些重要的函数实现来说明一下吧,不贴全部代码了。

点击父级时,展开下一级

scss 复制代码
  // 展开下一级
  const spreadNext = (children, index) => {
    if ((index || index === 0) && caseTree.indexOf(children) === -1) { //caseTree options的树形结构,[[.....], [.....]]第一个数组为父级元素,第二个数组为此时展开的父级元素的子级数组
      if (children && children.length > 0) {
        caseTree.splice(index + 1, caseTree.length - 1, children)
        setCaseTree([...caseTree])
      } else {
        caseTree.splice(index + 1, caseTree.length - 1)
        setCaseTree([...caseTree])
      }
    }
  }

递归options数据,为其打上相关标记

多选框有未选,全选,半选状态,所以需要 indeterminate , checked两个状态控制

ini 复制代码
  /**
  * 递归option数据
  * 标记数据树形层级 parent
  * 打上初始状态 checked indeterminate
  */
  const recursiveOpt = (nodeArr, parent) => {
    nodeArr.forEach((node) => {
      if (parent) {
        node.parent = parent
      }
      node.indeterminate = false
      node.checked = false // 是否被选中
      if (selectedValues && selectedValues.some(val => val === getLevel(node, 'value'))) {
        node.checked = true
      }
      markChildrenChecked(node)
      markParentChecked(node)
      if (hasArrayChild(node, childrenKey)) {
        recursiveOpt(node[childrenKey], node)
      }
    })
  }

另外调试cascader样式的时候,只要一点击弹窗就会关闭,所以需先在点击事件中增加debbuger,防止弹窗关闭。

4.实现效果

最后 展示一下实现效果吧

模糊搜索:

相关推荐
麻花201315 分钟前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习
.54815 分钟前
提取双栏pdf的文字时 输出文件顺序混乱
前端·pdf
jyl_sh23 分钟前
WebKit(适用2024年11月份版本)
前端·浏览器·客户端·webkit
zhanghaisong_20151 小时前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
Eric_见嘉1 小时前
真的能无限试(白)用(嫖)cursor 吗?
前端·visual studio code
DK七七2 小时前
多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码
开发语言·前端·微信小程序·小程序·php
老赵的博客2 小时前
QSS 设置bug
前端·bug·音视频
Chikaoya2 小时前
项目中用户数据获取遇到bug
前端·typescript·vue·bug
南城夏季2 小时前
蓝领招聘二期笔记
前端·javascript·笔记
Huazie2 小时前
来花个几分钟,轻松掌握 Hexo Diversity 主题配置内容
前端·javascript·hexo