实现 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 组件。这个组件能够根据用户输入动态过滤树节点,并支持节点的展开与多选功能。在实际项目中,这个组件可以帮助我们更好地展示和管理层次结构复杂的数据。

相关推荐
也无晴也无风雨27 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤5 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js