实现 SearchTree 组件

需求

业务开发中,需要实现一个具有搜索功能的树组件 SearchTree。 主要功能有:

  • 显示树形结构的数据。
  • 支持节点的展开与收起。
  • 支持节点的多选。
  • 实现搜索功能,能够根据输入动态过滤树节点。

效果预览

数据结构

json 复制代码
[
    {
        "label": "0-0",
        "value": "0-0",
        "children": [
            {
                "label": "0-0-0",
                "value": "0-0-0",
                "children": [
                    {
                        "label": "0-0-0-0",
                        "value": "0-0-0-0"
                    },
                    {
                        "label": "0-0-0-1",
                        "value": "0-0-0-1"
                    }
                ]
            },
            {
                "label": "0-0-1",
                "value": "0-0-1",
                "children": [
                    {
                        "label": "0-0-1-0",
                        "value": "0-0-1-0"
                    }
                ]
            }
        ]
    }
]

组件实现

引入依赖和定义类型

首先,需要引入所需的依赖,并定义类型。

javascript 复制代码
import { Empty, Input, Tree } from 'antd'
import type { DataNode, TreeProps } from 'antd/es/tree'
import { debounce } from 'lodash-es'
import React, { ReactNode, useEffect, useMemo, useState } from 'react'

const { Search } = Input

interface TreeData {
  value: string
  label: string
  children?: TreeData[]
}

interface SearchOptions {
  allowClear?: boolean | { clearIcon: ReactNode }
  bordered?: boolean
  defaultValue?: string
  disabled?: boolean
  placeholder?: string
  style?: React.CSSProperties
}

interface SearchTreeProps {
  checkedKeys?: React.Key[]
  treeData: TreeData[]
  setCheckedKeys?: React.Dispatch<React.SetStateAction<React.Key[]>>
  searchOptions?: SearchOptions
  treeOptions?: TreeProps
  style?: React.CSSProperties
}

树形数据处理函数

定义几个辅助函数,用于处理树形数据。

判断节点是否匹配搜索条件

javascript 复制代码
const isMatch = (label = '', matchKey: string) => {
  return label.includes(matchKey)
}

生成树节点

javascript 复制代码
const generateTree = (data: TreeData[], searchValue: string): DataNode[] => {
  const treeNode: DataNode[] = []
  data.forEach((item) => {
    const match = !item.children || (item.children && item.children.length === 0) ? isMatch(item.label, searchValue) : false
    const children = item.children ? generateTree(item.children, searchValue) : []
    if (match || children.length) {
      treeNode.push({
        ...item,
        children,
        title: searchValue && match ? (
          <span style={{ fontWeight: '500' }}>{item.label}</span>
        ) : (
          item.label
        ),
        key: item.label,
      })
    }
  })
  return treeNode
}

生成的结构:

json 复制代码
[
    {
        "label": "0-0",
        "value": "0-0",
        "children": [
            {
                "label": "0-0-0",
                "value": "0-0-0",
                "children": [
                    {
                        "label": "0-0-0-0",
                        "value": "0-0-0-0",
                        "children": [],
                        "title": "0-0-0-0",
                        "key": "0-0-0-0"
                    },
                    {
                        "label": "0-0-0-1",
                        "value": "0-0-0-1",
                        "children": [],
                        "title": "0-0-0-1",
                        "key": "0-0-0-1"
                    }
                ],
                "title": "0-0-0",
                "key": "0-0-0"
            },
            {
                "label": "0-0-1",
                "value": "0-0-1",
                "children": [
                    {
                        "label": "0-0-1-0",
                        "value": "0-0-1-0",
                        "children": [],
                        "title": "0-0-1-0",
                        "key": "0-0-1-0"
                    }
                ],
                "title": "0-0-1",
                "key": "0-0-1"
            }
        ],
        "title": "0-0",
        "key": "0-0"
    }
]

生成节点列表

javascript 复制代码
const generateList = (data: TreeData[]): DataListItem[] => {
  const dataList: DataListItem[] = []
  const recurse = (nodes: TreeData[]) => {
    nodes.forEach((node) => {
      dataList.push({ key: node.value, label: node.label })
      if (node.children) {
        recurse(node.children)
      }
    })
  }
  recurse(data)
  return dataList
}

获取父节点的 Key

javascript 复制代码
const getParentKey = (key: string, tree: TreeData[]): string => {
  for (let i = 0; i < tree.length; i++) {
    const node = tree[i]
    if (node.children) {
      const found = node.children.find((item) => item.label === key)
      if (found) return node.label
      const parentKey = getParentKey(key, node.children)
      if (parentKey) return parentKey
    }
  }
  return ''
}

主组件实现

实现 SearchTree 组件的主体部分。

javascript 复制代码
const defaultValue: TreeData[] = []

export const SearchTree: React.FC<SearchTreeProps> = (props) => {
  const {
    treeData = defaultValue,
    searchOptions,
    treeOptions,
    style,
  } = props
  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([])
  const [searchValue, setSearchValue] = useState('')
  const [autoExpandParent, setAutoExpandParent] = useState(true)
  const [treeNodes, setTreeNodes] = useState<DataNode[]>([])
  const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([])

  const dataList = useMemo(() => generateList(treeData), [treeData])

  useEffect(() => {
    setTreeNodes(generateTree(treeData, searchValue))
  }, [treeData])

  const onExpand = (newExpandedKeys: React.Key[]) => {
    setExpandedKeys(newExpandedKeys)
    setAutoExpandParent(false)
  }

  const debounceGenTree = useMemo(() => {
    return debounce((searchValue) => {
      const newExpandedKeys = dataList
        .map((item) => {
          if (searchValue && item.label.indexOf(searchValue) > -1) {
            const parentKey = getParentKey(item.label, treeData)
            return parentKey
          }
          return null
        })
        .filter((item, i, self) => item && self.indexOf(item) === i)

      setAutoExpandParent(true)
      setExpandedKeys(newExpandedKeys as React.Key[])

      const tree = generateTree(treeData, searchValue)
    
      setTreeNodes(tree)
    }, 200)
  }, [dataList, treeData])

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target
    setSearchValue(value)
    debounceGenTree(value)
  }

  const onCheck = (checkedKeys: React.Key[], info: any) => {
    const leafKeys = info.checkedNodes
      .filter((node: any) => !node.children || node.children.length === 0)
      .map((node: any) => node.key)

    setCheckedKeys((prevCheckedKeys) => {
      if (!searchValue) return leafKeys
      const handledPrevCheckedKeys = prevCheckedKeys.filter(
        (item) =>
          !isMatch(item as string, searchValue) || leafKeys.includes(item),
      )
      return [...handledPrevCheckedKeys, ...leafKeys]
    })
  }

  return (
    <div style={style} className="search-tree">
      <Search onChange={onChange} {...searchOptions} className="search" />
      {treeNodes.length > 0 ? (
        <Tree
          rootClassName="search-tree"
          checkable
          onExpand={onExpand}
          expandedKeys={expandedKeys}
          autoExpandParent={autoExpandParent}
          treeData={treeNodes}
          onCheck={onCheck}
          checkedKeys={checkedKeys}
          {...treeOptions}
        />
      ) : (
        <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
      )}
    </div>
  )
}

结论

通过上述步骤,实现了一个功能齐全的 SearchTree 组件。这个组件能够根据用户输入动态过滤树节点,并支持节点的展开与多选功能。在实际项目中,这个组件可以帮助我们更好地展示和管理层次结构复杂的数据。

相关推荐
@大迁世界2 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路2 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug2 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121382 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中2 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路2 小时前
GDAL 实现矢量合并
前端
hxjhnct2 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星2 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子3 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗3 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全