自建”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)

下一篇

相关推荐
顾平安1 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网1 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工1 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染
沈剑心1 小时前
如何在鸿蒙系统上实现「沉浸式」页面?
前端·harmonyos
一棵开花的树,枝芽无限靠近你1 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器
m0_748237051 小时前
Chrome 关闭自动添加https
前端·chrome
开心工作室_kaic1 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育1 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博1 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
lv程序媛1 小时前
css让按钮放在最右侧
前端·css