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