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.实现效果

最后 展示一下实现效果吧

模糊搜索:

相关推荐
祈澈菇凉2 小时前
Webpack的基本功能有哪些
前端·javascript·vue.js
小纯洁w2 小时前
Webpack 的 require.context 和 Vite 的 import.meta.glob 的详细介绍和使用
前端·webpack·node.js
想睡好3 小时前
css文本属性
前端·css
qianmoQ3 小时前
第三章:组件开发实战 - 第五节 - Tailwind CSS 响应式导航栏实现
前端·css
zhoupenghui1683 小时前
golang时间相关函数总结
服务器·前端·golang·time
White graces3 小时前
正则表达式效验邮箱格式, 手机号格式, 密码长度
前端·spring boot·spring·正则表达式·java-ee·maven·intellij-idea
庸俗今天不摸鱼3 小时前
Canvas进阶-4、边界检测(流光,鼠标拖尾)
开发语言·前端·javascript·计算机外设
bubusa~>_<4 小时前
解决npm install 出现error,比如:ERR_SSL_CIPHER_OPERATION_FAILED
前端·npm·node.js
风清云淡_A4 小时前
【react18】如何使用useReducer和useContext来实现一个todoList功能
react.js
流烟默5 小时前
vue和微信小程序处理markdown格式数据
前端·vue.js·微信小程序