#Tree 树
按层级浏览、选择或勾选内容。
#浏览与选择
设计资源
研发项目
basic.tsx
import { Space, Text, Tree } from '@ifhover/leaf-ui';
import { useState } from 'react';
import { treeData } from './data';
export function TreeBasic() {
const [selected, setSelected] = useState<readonly string[]>([]);
return (
<Space direction="vertical" align="stretch">
<Tree
data={treeData}
defaultExpandedKeys={['design']}
showIcon
selectedKeys={selected}
onSelect={setSelected}
/>
<output>
<Text tone="muted">已选: {selected.join(', ') || '暂无'}</Text>
</output>
</Space>
);
}
#父子勾选联动
设计资源
研发项目
checkable.tsx
import { Space, Text, Tree } from '@ifhover/leaf-ui';
import { useState } from 'react';
import { treeData } from './data';
export function TreeCheckable() {
const [checked, setChecked] = useState<readonly string[]>(['components']);
return (
<Space direction="vertical" align="stretch">
<Tree
data={treeData}
checkable
selectable={false}
defaultExpandAll
showLine
checkedKeys={checked}
onCheck={setChecked}
/>
<output>
<Text tone="muted">勾选节点: {checked.join(', ') || '暂无'}</Text>
</output>
</Space>
);
}
#独立勾选
父节点和子节点分别勾选,互不影响。
设计资源
研发项目
strict.tsx
import { Tree } from '@ifhover/leaf-ui';
import { treeData } from './data';
export function TreeStrict() {
return <Tree data={treeData} checkable checkStrictly selectable={false} defaultExpandAll />;
}
#搜索节点
设计资源
研发项目
search.tsx
import { Input, Space, Tree } from '@ifhover/leaf-ui';
import { Search } from 'lucide-react';
import { useState } from 'react';
import { treeData } from './data';
export function TreeSearch() {
const [query, setQuery] = useState('');
return (
<Space direction="vertical" align="stretch">
<Input
aria-label="搜索节点"
placeholder="搜索节点"
prefix={<Search />}
value={query}
onChange={(event) => setQuery(event.target.value)}
allowClear
/>
<Tree data={treeData} searchValue={query} showIcon />
</Space>
);
}
#扩展用法
通过手柄拖动节点,移到目标行的上方、中央或下方,分别放在目标之前、内部或之后。键盘操作时,空格开始或放置,上下选择目标,左/右选择前方/后方,Escape 取消。onDrop 提供新数据,由调用方更新 data。
展开项目加载子项,通过手柄移动节点。
项目
归档
10000 个节点,仅挂载可视行。
节点 1
节点 2
节点 3
节点 4
节点 5
节点 6
节点 7
节点 8
节点 9
节点 10
节点 11
extensions.tsx
import type { TreeNode } from '@ifhover/leaf-ui';
import { Space, Tree } from '@ifhover/leaf-ui';
import { useState } from 'react';
const initial: TreeNode[] = [
{ key: 'projects', title: '项目', isLeaf: false },
{ key: 'archive', title: '归档', children: [{ key: 'draft', title: '草稿', isLeaf: true }] },
];
export function ExtensionDemo() {
const [data, setData] = useState(initial);
return (
<Space direction="vertical" align="stretch">
<p>展开项目加载子项,通过手柄移动节点。</p>
<Tree
data={data}
loadData={async (node, signal) => {
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, 500);
signal.addEventListener(
'abort',
() => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
},
{ once: true },
);
});
return [
{ key: `${node.key}-design`, title: '设计', isLeaf: true },
{ key: `${node.key}-dev`, title: '开发', isLeaf: true },
];
}}
draggable
onDrop={(info) => setData(info.data)}
/>
<p>10000 个节点,仅挂载可视行。</p>
<Tree
virtual
height={240}
data={Array.from({ length: 10000 }, (_, i) => ({
key: String(i),
title: `节点 ${i + 1}`,
}))}
/>
</Space>
);
}
#刷新异步数据
更新 cacheKey 可以刷新同一组节点的懒加载缓存。更换 data 或 loadData 也会清除缓存;保持它们的引用稳定,避免因无关渲染重复加载。过期请求被取消后,不会覆盖新数据。
文档
capabilities.tsx
import { Button, Space, Text, Tree, type TreeNode } from '@ifhover/leaf-ui';
import { useCallback, useMemo, useState } from 'react';
export function TreeCacheRefresh() {
const [version, setVersion] = useState(1);
const data = useMemo<readonly TreeNode[]>(
() => [{ key: 'root', title: '文档', isLeaf: false }],
[false],
);
const loadData = useCallback(
async (node: TreeNode, signal: AbortSignal): Promise<readonly TreeNode[]> => {
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 400);
signal.addEventListener(
'abort',
() => {
clearTimeout(timer);
resolve();
},
{ once: true },
);
});
if (signal.aborted) return [];
return [
{
key: `${node.key}-${version}`,
title: `${'版本'} ${version}`,
isLeaf: true,
},
];
},
[false, version],
);
return (
<Space direction="vertical" align="stretch">
<div>
<Button variant="outline" onClick={() => setVersion((value) => value + 1)}>
{'刷新数据'}
</Button>
</div>
<Tree
aria-label={'可刷新文档'}
data={data}
loadData={loadData}
cacheKey={version}
defaultExpandedKeys={['root']}
/>
<Text tone="muted">
{'根节点 key 保持不变,数据版本变化后会清除旧子节点并取消未完成的请求。'}
</Text>
</Space>
);
}
#Tree API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
data | readonly TreeNode[] | 必填 | 必需。树节点 |
selectedKeys | readonly string[] | [] | 受控 / 初始选中节点 |
defaultSelectedKeys | readonly string[] | [] | 受控 / 初始选中节点 |
onSelect | (keys: string[], info: TreeSelectInfo) => void | — | 选择变化 |
multiple | boolean | false | 多选 / 允许选择 |
selectable | boolean | true | 多选 / 允许选择 |
checkable | boolean | false | 展示复选框 / 禁止父子联动 |
checkStrictly | boolean | false | 展示复选框 / 禁止父子联动 |
checkedKeys | readonly string[] | [] | 受控 / 初始勾选节点 |
defaultCheckedKeys | readonly string[] | [] | 受控 / 初始勾选节点 |
onCheck | (keys: string[], info: TreeCheckInfo) => void | — | 勾选变化,包含联动结果 |
expandedKeys | readonly string[] | [] | 受控 / 初始展开节点 |
defaultExpandedKeys | readonly string[] | [] | 受控 / 初始展开节点 |
defaultExpandAll | boolean | false | 初始展开全部节点 |
onExpand | (keys: string[], info: TreeExpandInfo) => void | — | 展开变化 |
disabled | boolean | false | 禁用交互。 |
showLine | boolean | false | 禁用操作 / 显示图标 / 显示层级线 |
showIcon | boolean | false | 禁用操作 / 显示图标 / 显示层级线 |
searchValue | string | '' | 过滤节点,保留匹配项和祖先 |
filterTreeNode | (node: TreeNode, query: string) => boolean | — | 自定义搜索规则 |
notFoundContent | ReactNode | 暂无选项 | 无匹配内容 |
loadData | (node: TreeNode, signal: AbortSignal) => Promise<readonly TreeNode[]> | — | 展开时加载子项,signal 用于取消 |
cacheKey | string | number | — | 数据版本改变时清空懒加载缓存,并取消旧请求。 |
nodeRender | (node: TreeNode) => ReactNode | — | 自定义节点展示内容。 |
onLoad | (node: TreeNode, children: readonly TreeNode[]) => void | — | 子项加载成功 |
onLoadError | (error: unknown, node: TreeNode) => void | — | 加载失败,再次展开可重试 |
virtual | boolean | false | 仅渲染可视节点;大量节点时建议启用。 |
height | number | 320 | 高度,数字按像素计算 |
itemHeight | number | 42 | 跟随区域控件高度和密度的预估行高,包含行间距;实际行会被测量。 |
draggable | boolean | ((node: TreeNode) => boolean) | false | 允许通过手柄移动节点 |
allowDrop | (info: Omit<TreeDropInfo, 'data'>) => boolean | — | 限制放置位置 |
onDrop | (info: TreeDropInfo) => void | — | 移动结果,调用方更新 data |
#TreeNode
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key | string | — | 必需。唯一标识,必填 |
title | ReactNode | — | 必需。节点内容,必填 |
children | readonly TreeNode[] | — | 子节点;使用同一结构 |
icon | ReactNode | — | 自定义图标 |
disabled | boolean | — | 禁用交互。 |
selectable | boolean | — | 该节点能否选择 / 是否展示复选框,默认可用 |
checkable | boolean | — | 该节点能否选择 / 是否展示复选框,默认可用 |
disableCheckbox | boolean | — | 禁止勾选并阻断父子勾选联动 |
searchLabel | string | — | 标题为节点时提供用于搜索和读屏的文字 |
isLeaf | boolean | — | false 允许异步加载子项 |
#TreeSelectInfo
#TreeCheckInfo
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
node | TreeNode | — | 必需。本次操作节点 |
checked | boolean | — | 必需。操作后是否勾选 |
checkedNodes | readonly TreeNode[] | — | 必需。完整勾选节点 |
halfCheckedKeys | readonly string[] | — | 必需。半选节点标识 |
#TreeExpandInfo
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
node | TreeNode | — | 必需。本次操作节点 |
expanded | boolean | — | 必需。操作后是否展开 |
#类型
可导入以下类型定义属性、组装数据或处理回调。
import type { TreeProps, TreeNode, TreeSelectInfo, TreeCheckInfo, TreeExpandInfo } from '@ifhover/leaf-ui';#使用说明
每个节点的 key 应唯一且稳定。示例数据结构:
import type { TreeNode } from '@ifhover/leaf-ui';
export const treeData: readonly TreeNode[] = [
{
key: 'design',
title: '设计资源',
children: [
{ key: 'components', title: '组件规范' },
{ key: 'colors', title: '品牌颜色' },
{ key: 'archive', title: '归档资源(不可选)', disabled: true },
],
},
{
key: 'engineering',
title: '研发项目',
children: [
{ key: 'web', title: '网站' },
{ key: 'mobile', title: '移动应用' },
],
},
];
勾选父节点会勾选可用子节点,禁用节点不会被联动修改。子节点部分选中时父节点呈半选;checkStrictly 让各节点独立勾选。多选时点击切换选择,Shift 可连续选择可见节点。
方向键上下移动,左右展开或收起,Home / End 跳至首尾,Enter 选择,空格勾选。输入文字可快速定位节点。搜索时会暂时展开匹配路径,清空后恢复原展开状态。
#扩展类型
import type { TreeDropInfo } from '@ifhover/leaf-ui';#TreeDropInfo API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
node | TreeNode | 必填 | 必需。操作节点 |
target | TreeNode | 必填 | 必需。接收拖放的目标节点。 |
position | 'before' | 'inside' | 'after' | 必填 | 必需。放在目标节点之前、内部或之后。 |
data | TreeNode[] | 必填 | 必需。层级数据 |
#moveTreeNode
需要自行调整树数据时可导入此纯函数。返回新树,不修改原数据;目标不存在、移动到自身或后代节点时返回 null。Tree 的 onDrop 已直接提供调整后的 data。
import { moveTreeNode } from '@ifhover/leaf-ui';
const next = moveTreeNode(data, 'design', 'team', 'inside');| 参数 | 类型 | 说明 |
|---|---|---|
data | readonly TreeNode[] | 原始树数据 |
key | string | 待移动的节点 key |
target | string | 目标节点 key |
position | 'before' | 'inside' | 'after' | 相对目标的放置位置 |
| 返回值 | TreeNode[] | null | 新的树;无法移动时返回 null |