自建”IT兵器库”,你值得一看!第四篇

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!

常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。

接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步

需求:

自定义树,使用antd树组件的checkbox的时候,会发现选中某父级下的子级节点时候,其父级均为半选中状态,当我们获取其选中的key时,只有把其父级下所有子级全部选中,才能拿到其父级。自定义树,就是基于这个弊端,进行改造,选中某父级的子级,其父级均被选中。

antd tree(用于对比)

选中某一父级,下的子级

选中的key

js 复制代码
const treeData: DataNode[] = [
  {
    title: '0-0',
    key: '0-0',
    children: [
      {
        title: '0-0-0',
        key: '0-0-0',
        children: [
          { title: '0-0-0-0', key: '0-0-0-0' },
          { title: '0-0-0-1', key: '0-0-0-1' },
          { title: '0-0-0-2', key: '0-0-0-2' },
        ],
      },
      {
        title: '0-0-1',
        key: '0-0-1',
        children: [
          { title: '0-0-1-0', key: '0-0-1-0' },
          { title: '0-0-1-1', key: '0-0-1-1' },
          { title: '0-0-1-2', key: '0-0-1-2' },
        ],
      },
      {
        title: '0-0-2',
        key: '0-0-2',
      },
    ],
  },
  {
    title: '0-1',
    key: '0-1',
    children: [
      { title: '0-1-0-0', key: '0-1-0-0' },
      { title: '0-1-0-1', key: '0-1-0-1' },
      { title: '0-1-0-2', key: '0-1-0-2' },
    ],
  },
  {
    title: '0-2',
    key: '0-2',
  },
];

自定义树-实现效果

选中子级,默认勾选所有父级

选中父级,默认勾选所有子级

高亮搜索

默认选中值(外部传入默认选中的key)

支持清除模式

功能点划分

  • 自定义树组件选中子级,默认勾选所有父级
  • 自定义树组件选中父级,默认勾选所有子级
  • 自定义树组件,搜索关键字高亮
  • 自定义树组件,默认选中
  • 自定义树组件,支持清除模式

使用到组件(Antd 组件库哈)

  • Tree
  • Input
  • Button

自定义树组件区域划分

  • 搜索框
  • 树组件

自定义树组件最终可支持配置项

js 复制代码
interface SuCustomTreeSelectProps {
  basePropName?: any // 转换的配置
  emitTreeData?: any // 传递的树数据
  defaultCheckKey?: any // 默认选中的key
  treeOnCheck?: (checkKeys) => void // 树节点选中回调
  maxTitleWidth?: number // 树展示宽度限制 默认280
  delTreeMode?: boolean // 是否开启删除模式
  delKeys?: any // 删除的key
  setDelKeys?: any // set删除的key
}

自定义树组件布局

js 复制代码
<CustomTreeSelectWrapper className="flex flex-col">
      <div className="container">
        <div className="flex w-full">
          <div className="box-border flex-1">
            <div>
              <div className="flex items-center box-border py-3">
                <div className="flex items-center mt-2 flex-1">
                  <Search
                    style={{ marginBottom: 8 }}
                    placeholder="请输入"
                    onSearch={onSearch}
                    allowClear
                    className="flex-1"
                  />
                </div>
              </div>
            </div>
            <div style={{ height: '500px' }} className="mt-1 overflow-y-auto">
              <Tree
                checkable={!delTreeMode}
                onExpand={onExpand}
                expandedKeys={expandedKeys}
                autoExpandParent={autoExpandParent}
                onCheck={onCheck}
                checkedKeys={checkedKeys}
                onSelect={onSelect}
                selectedKeys={selectedKeys}
                treeData={treeData}
                switcherIcon={<DownOutlined />}
                blockNode
                checkStrictly
              />
            </div>
          </div>
        </div>
      </div>
    </CustomTreeSelectWrapper>

自定义树组件-树节点数据(用于搜索高亮以及树数据处理)

js 复制代码
  /**
   * @description: 根据搜索值的变化,处理树节点数据,完成高亮展示关键词文本
   * @param {*} useMemo
   * @return {*}
   */
  const treeData = useMemo(() => {
    const handleCustomTreeData = (list: any, count = 1) => {
      const arr = []
      // eslint-disable-next-line array-callback-return, consistent-return
      list?.forEach(it => {
        const key = keepIdProp ? it?.[idPropName] : it?.[keyPropName]
        const value = keepIdProp ? it?.[idPropName] : it?.[keyPropName]
        const strTitle = it[titlePropName] as string
        const index = strTitle.indexOf(searchValue)
        const beforeStr = strTitle.substring(0, index)
        const afterStr = strTitle.slice(index + searchValue.length)
        const title =
          index > -1 ? (
            <div className="flex justify-between w-full items-center">
              <span
                className="flex-1 text-ellipsis overflow-hidden whitespace-nowrap"
                style={{ width: `${maxTitleWidth}px` }}
              >
                {beforeStr}
                <span className="site-tree-search-value">{searchValue}</span>
                {afterStr}
              </span>
              {delTreeMode && (
                <Button
                  type="text"
                  className="text-right"
                  onClick={() => deleteRightData(it.key, customTreeOriginData)}
                  size="small"
                >
                  <DeleteOutlined />
                </Button>
              )}
            </div>
          ) : (
            <span className="flex-1">
              <div className="flex justify-between w-full items-center">
                <span
                  className="flex-1 text-ellipsis overflow-hidden whitespace-nowrap"
                  style={{ width: `${maxTitleWidth}px` }}
                >
                  {strTitle}
                </span>
                {delTreeMode && (
                  <Button
                    type="text"
                    className="text-right"
                    onClick={() => deleteRightData(it.key, customTreeOriginData)}
                    size="small"
                  >
                    <DeleteOutlined />
                  </Button>
                )}
              </div>
            </span>
          )
        if (it?.[childrenPropName] && it?.[childrenPropName]?.length) {
          arr.push({
            ...it,
            key,
            title,
            value,
            children: handleCustomTreeData(it?.[childrenPropName], count + 1),
            level: count,
          })
        } else {
          arr.push({
            ...it,
            key,
            title,
            value,
            children: it?.[childrenPropName],
            level: count,
          })
        }
      })

      setCustomTreeData(arr)
      const finallyMapArr = handleTreeToFlatList(arr)
      if (delTreeMode) {
        const keys = finallyMapArr?.map(it => it?.key)
        setExpandedKeys(keys)
      }
      return arr
    }
    return handleCustomTreeData(customTreeOriginData)

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [searchValue, customTreeOriginData])

功能点逐一拆解

自定义树组件选中子级,默认勾选所有父级

  • 首先,我们需要获取当前选中节点信息(每次只会选中一个节点
  • 其次,需要准备好当前树数据的平铺数组,根据当前key的parentId依次寻找
  • 注意的是,我们怎么知道到底上面还有没有父级呢?
  • 我的做法时,前端处理树的时候,自己存一个level字段,表示当前层级(或许你后端会给你。。。笑嘻嘻)
  • 原理就是,递归的时候,以count计数,每执行一次,代表层级加1,赋值给每一个树节点即可
  • 最后,一个for循环,以level为起始值,i>1为结束,i--,去遍历平铺的树,找到便记录下来
  • 拿捏
js 复制代码
 /**
   * @description: 寻找当前节点的所有父级
   * @param {*} key 当前选中的key
   * @return {*} 所找到的父级keys
   */
  const loopGetParentKey = key => {
    // 当前key 信息
    const curNodeInfo = customTreeFlatData?.filter(it => it?.key === key)
    if (!curNodeInfo?.length) return []
    const { level, parentId } = curNodeInfo[0]
    const allParentKeys = []
    const allParentNodes = []
    let shouldFindParentKey = parentId
    for (let i = level; i > 1; i--) {
      // 找到当前符合的key
      // eslint-disable-next-line no-loop-func
      const findParentKeyInfo = customTreeFlatData?.find(it => it?.key === shouldFindParentKey)
      // eslint-disable-next-line @typescript-eslint/no-shadow
      const { key, parentId } = findParentKeyInfo
      // 记录其父级并更新以及父级节点信息
      allParentNodes.push(findParentKeyInfo)
      allParentKeys.push(key)
      shouldFindParentKey = parentId
    }
    return allParentKeys?.filter(it => it)
  }

自定义树组件选中父级,默认勾选所有子级

  • 首先,我们需要获取当前选中节点信息(每次只会选中一个节点
  • 其次,根据当前节点信息,判断是否有子级嘛
  • 逐级找到,set一下当前选中的key
  • 拿捏
js 复制代码
  /**
   * @description: 寻找当前节点的所有子级
   * @param {*} key 当前选中的key
   * @return {*} 所找到的子级keys
   */
  const loopGetChildKey = key => {
    // 当前key 信息
    const curNodeInfo = customTreeFlatData?.filter(it => it?.key === key)
    if (!curNodeInfo?.length) return []
    const { children } = curNodeInfo[0]
    if (!children || !children?.length) return []
    // 以该节点作为顶层 往下找自己的子级
    const handleLoopChild = (childList = []) => {
      return childList?.reduce((val, cur) => {
        if (cur?.children && cur?.children?.length) {
          const loopArr = handleLoopChild(cur?.children || [])
          return [...val, ...loopArr, cur]
        }
        return [...val, cur]
      }, [])
    }
    const resNodes = handleLoopChild(children)
    const resKeys = resNodes?.map(it => it?.key)
    return resKeys
  }

自定义选中-选中的key,需要自己去处理

  • 当我们选中某一节点的时候,我们需要关注当前节点的选中状态
  • 如果点击选中,需要关注的key有:先前已选的,以及当前节点所关联的父级以及其所有的子级,以及当前节点本身
  • 如果是取消选中,需要关注的key有:只取消当前节点以及该节点下存在子级
  • 最后去重处理 避免存在key重复
js 复制代码
 /**
   * @description: 自定义选中树节点
   * @param {*} curCheckKey 当前选中的key
   * @param {*} isCheck 选中状态
   * @param {*} preCheckKey 前面已选key
   * @return {*}
   */
  // eslint-disable-next-line consistent-return
  const handleCustomCheck = (curCheckKey, isCheck, preCheckKey = []) => {
    // 从平铺当中找到符合的要求的key
    let needCheckParentKeys = customTreeFlatData
      // eslint-disable-next-line array-callback-return, consistent-return
      .map(item => {
        if (!isCheck) {
          return null
        }

        // 从符合要求的key 当中寻找其所有父级 (向上寻找)
        // 从符合要求的key 当中寻找其所有子级 (向下寻找)
        if (item?.key === curCheckKey) {
          const allChildKey = loopGetChildKey(curCheckKey)
          const allParentKey = loopGetParentKey(curCheckKey)
          return [...allChildKey, ...allParentKey]
        }
        return null
      })
      ?.filter(it => it)

    needCheckParentKeys = needCheckParentKeys?.flat()

    if (isCheck) {
      // 之前选中的 以及当前选中节点所关联父级以及其所有的子级 以及节点本身
      const tempArr = preCheckKey
      tempArr.push(...needCheckParentKeys, curCheckKey)
      // 去重
      const finallyArr = new Set(tempArr)
      return Array.from([...finallyArr])
    }
    // 取消选中 只取消当前节点以及该节点下存在子级 一并取消
    const allChildKey = loopGetChildKey(curCheckKey)
    const allDelChildKey = [...allChildKey, curCheckKey]
    const delCurKeyArr = preCheckKey.filter(it => !allDelChildKey.includes(it))
    // 去重
    const finallyArr = new Set(delCurKeyArr)
    return Array.from([...finallyArr])
  }

自定义树组件,搜索关键字高亮(基于前面useMemo 缓存树数据监听搜索值变化实现高亮)

  • 搜索逻辑为,当title里面存在的搜索关键字时,展开当前节点,其余节点不展开,并关键字高亮
  • 首先,需要从平铺的树中,去找到符合的树节点
  • 其次,把这些符合的key,寻找其父级展开 antd 的展开key,存在某一子级,关联父级便可自动展开
js 复制代码
  /**
   * @description: 只会寻找一次 antd 的展开key,存在某一子级,关联父级便可自动展开
   * @param {React} key 符合条件key || 当前选中的key
   * @param {any} tree 树数据
   * @return {*}
   */
  const getParentKey = (key: React.Key, tree: any[]): React.Key => {
    let parentKey: React.Key

    // 一层一层进行遍历
    for (let i = 0; i < tree.length; i++) {
      const node = tree[i]
      // 如果当前节点存在孩子
      if (node.children) {
        // 如果孩子当中 找到其传进来的key 即当前节点 就是所需要找到父级节点
        if (node.children.some(item => item.key === key)) {
          parentKey = node.key
        } else if (getParentKey(key, node.children)) {
          // 否则循环往复
          parentKey = getParentKey(key, node.children)
        }
      }
    }
    return parentKey!
  }

自定义树组件,支持清除模式

  • 这里主要是为了后续的自定义树穿梭框做的准备
  • 往往右侧数据是,通过点击删除按钮,即可删除当前数据
  • 开启删除模式,需要外部传入delKey,setDelKey,并且会更新选中的key ===> checkbox模式的选中与未选中
js 复制代码
  useEffect(() => {
    const { checkedKeys, setCheckedKeys } = leftTreeRef.current || {}
    const filterKeys = checkedKeys?.filter(it => !delKeys.includes(it))
    setCheckedKeys(filterKeys)
  }, [delKeys])
删除使用示例 复制代码
<CustomTreeSelect
   defaultCheckKey={[]}
   treeOnCheck={onCheckBoxChange}
   emitTreeData={rightOriginData}
   delKeys={delKeys}
   setDelKeys={setDelKeys}
   delTreeMode
   ref={rightTreeRef}
   basePropName={{
      ...basePropName,
      // 转换以后的字段叫做children
      childrenPropName: 'children',
      }}
 />

核心

自定义组件最核心的逻辑:

1.选中与未选中时,需要追加或删减的key

2.选中父级时,寻找子级(向下寻找)

3.选中子级时,寻找其父级(向上寻找)

结束

都看到这里了,不留点痕迹,是怕我发现么?

上一篇

自建"IT兵器库",你值得一看!第三篇 - 掘金 (juejin.cn)

下一篇

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax