导航类

树形控件

树型结构列表。

代码演示

如何引入

<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';
</script>

基本用法

最简单的用法,默认为单选模式,每一级菜单项均可选择。

  • 亚洲
  • 中国
  • 北京
  • 上海
  • 北美洲
代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: '亚洲',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: '中国',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: '北京',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: '上海',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
            ],
        },
        {
            label: '北美洲',
            value: 'North America',
            key: '1',
        },
    ];
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree defaultExpandAll :style="style" :treeData="treeData" />
</template>

多选

设置 multiple,可以进行多选。多选情况下所有子项都被选择时,自动勾选显示其父项。

  • Asia
  • China
  • Beijing
  • Shanghai
  • Chengdu
  • Japan
  • Osaka
  • North America
  • United States
  • Canada
代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        { label: 'Beijing', value: 'Beijing', key: '0-0-0' },
                        { label: 'Shanghai', value: 'Shanghai', key: '0-0-1' },
                        { label: 'Chengdu', value: 'Chengdu', key: '0-0-2' },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [{ label: 'Osaka', value: 'Osaka', key: '0-1-0' }],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                { label: 'United States', value: 'United States', key: '1-0' },
                { label: 'Canada', value: 'Canada', key: '1-1' },
            ],
        },
    ];

    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree defaultExpandAll multiple :style="style" :treeData="treeData" />
</template>

可搜索的

通过设置 filterTreeNode 属性可支持搜索功能。默认对 label 值进行搜索,可通过 treeNodeFilterProp 更改。如果只希望展示过滤后的结果,可以设置 showFilteredOnly 。

showFilteredOnly

代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Switch, Tree } from '@fibrous/semi-ui-vue';

    const showFilteredOnly = ref(false);

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        { label: 'Beijing', value: 'Beijing', key: '0-0-0' },
                        { label: 'Shanghai', value: 'Shanghai', key: '0-0-1' },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [{ label: 'Osaka', value: 'Osaka', key: '0-1-0' }],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                { label: 'United States', value: 'United States', key: '1-0' },
                { label: 'Canada', value: 'Canada', key: '1-1' },
            ],
        },
    ];

    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <span id="tree-searchable-filtered-only">showFilteredOnly</span>
    <Switch v-model="showFilteredOnly" aria-labelledby="tree-searchable-filtered-only" size="small" />
    <br />
    <Tree filterTreeNode multiple :showFilteredOnly="showFilteredOnly" :style="style" :treeData="treeData" />
</template>

设置 filterTreeNode 属性开启搜索后,可以通过 #search 插槽自定义搜索框,插槽参数为 class、modelValue、placeholder、showClear 和 onChange。插槽内容为空(<template #search />)时隐藏搜索框。

Search
代码
<script setup lang="ts">
    import { Input, Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        { label: 'Beijing', value: 'Beijing', key: '0-0-0' },
                        { label: 'Shanghai', value: 'Shanghai', key: '0-0-1' },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [{ label: 'Osaka', value: 'Osaka', key: '0-1-0' }],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                { label: 'United States', value: 'United States', key: '1-0' },
                { label: 'Canada', value: 'Canada', key: '1-1' },
            ],
        },
    ];
</script>

<template>
    <Tree filterTreeNode :treeData="treeData">
        <template #search="{ class: searchClass, modelValue, placeholder, showClear, onChange }">
            <Input :class="searchClass" :modelValue="modelValue" :placeholder="placeholder" :showClear="showClear" @change="onChange">
                <template #prefix>Search</template>
            </Input>
        </template>
    </Tree>
</template>

手动触发搜索

可以通过模板 ref 获取 Tree 的实例,调用 search 方法进行搜索。注意需要同时设置 filterTreeNode 开启搜索,如果搜索框在 Tree 外部,可以用空的 #search 插槽隐藏 Tree 内部的搜索框。

简单 JSON 格式的数据

可以通过 treeDataSimpleJson 传入 JSON 形式的 treeNodes 数据。此时 key-value 键值对中的 key 值将作为 TreeNodeData 的 key 和 label,value 值将作为 TreeNodeData 的 value。返回值为包含选中节点的 JSON 数据。

  • Node2
代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const json = {
        Node1: {
            'Child Node1': '0-0-1',
            'Child Node2': '0-0-2',
        },
        Node2: '0-1',
    };

    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree multiple :style="style" :treeDataSimpleJson="json" @change="(value) => console.log('当前所有选中项: ', value)" @select="(key) => console.log('当前选项: ', key)" />
</template>

行显示节点

可以通过设置 blockNode 使节点显示为整行,此时悬浮选中高亮状态都会显示整行。默认打开。关闭时只高亮节点 label。

  • Asia
  • China
  • Beijing
  • Shanghai

  • Asia
  • China
  • Beijing
  • Shanghai
代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: 'Beijing',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: 'Shanghai',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [
                        {
                            label: 'Osaka',
                            value: 'Osaka',
                            key: '0-1-0',
                        },
                    ],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                {
                    label: 'United States',
                    value: 'United States',
                    key: '1-0',
                },
                {
                    label: 'Canada',
                    value: 'Canada',
                    key: '1-1',
                },
            ],
        },
    ];
</script>

<template>
    <div>
        <Tree :blockNode="false" defaultValue="Shanghai" :treeData="treeData" />
        <br />
        <Tree :blockNode="false" defaultValue="Shanghai" multiple :treeData="treeData" />
    </div>
</template>

自定义节点内容

TreeNodeData 的 label 只接受字符串或数字,节点的自定义内容通过 #label 插槽提供,插槽参数为 item(节点数据)、label 和 searchWord(当前搜索词)。开启 filterTreeNode 搜索时,仍按 treeData 中的 label 文本匹配。

节点内容最好只用于展示。节点里的按钮、链接虽然可以用 Tab 到达(见 Accessibility),但读屏软件会把它们当作节点名称的一部分,方向键浏览时也不会经过它们。需要对节点执行的操作请放在树外,例如 @contextMenu 打开的菜单或树旁的工具栏。

代码
<script setup lang="ts">
    import { Tag, Tree } from '@fibrous/semi-ui-vue';

    // Node content is display-only: focus stays on the tree, so buttons and links inside are keyboard-unreachable
    const treeData = [
        {
            label: '亚洲',
            value: 'yazhou',
            key: 'yazhou',
            children: [
                {
                    label: '中国',
                    value: 'zhongguo',
                    key: 'zhongguo',
                },
                {
                    label: '日本',
                    value: 'riben',
                    key: 'riben',
                },
            ],
        },
    ];
    const labelStyle = {
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
    };
    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree :style="treeStyle" :treeData="treeData">
        <template #label="{ item, label }">
            <div :style="labelStyle">
                <span>{{ label }}</span>
                <Tag size="small">{{ item.children ? '大洲' : '国家' }}</Tag>
            </div>
        </template>
    </Tree>
</template>

过长省略。可以在 #label 插槽中实现文本过长省略的效果。

代码
<script setup lang="ts">
    import { Tree, Typography } from '@fibrous/semi-ui-vue';

    const { Text } = Typography;

    const treeData = [
        {
            label: '亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲亚洲',
            value: 'yazhou',
            key: 'yazhou',
            children: [
                {
                    label: '中国中国中国中国中国中国中国中国中国中国中国中国',
                    value: 'zhongguo',
                    key: 'zhongguo',
                },
                {
                    label: '日本日本日本日本日本日本日本日本日本日本日本日本',
                    value: 'riben',
                    key: 'riben',
                },
            ],
        },
    ];
    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree :style="treeStyle" :treeData="treeData">
        <template #label="{ label }">
            <Text :ellipsis="{ showTooltip: true }" :style="{ width: '100%' }">{{ label }}</Text>
        </template>
    </Tree>
</template>

自定义图标

通过 #icon 插槽可添加自定义图标,插槽参数为 item、level、expanded、isLeaf、loading、selected、checked 和 halfChecked,可按节点返回不同的图标。TreeNodeData 没有 icon 字段。

代码
<script setup lang="ts">
    import { IconMapPin } from '@fibrous/semi-icons-vue';
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                },
            ],
        },
    ];
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree :style="style" :treeData="treeData">
        <template #icon>
            <IconMapPin :style="{ color: 'var(--semi-color-text-2)' }" />
        </template>
    </Tree>
</template>

目录树模式

通过设置 directory 属性可显示为目录树模式。目录树模式下自带目录图标,可以通过自定义图标覆盖。

代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: 'Beijing',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: 'Shanghai',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [
                        {
                            label: 'Osaka',
                            value: 'Osaka',
                            key: '0-1-0',
                        },
                    ],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                {
                    label: 'United States',
                    value: 'United States',
                    key: '1-0',
                },
                {
                    label: 'Canada',
                    value: 'Canada',
                    key: '1-1',
                },
            ],
        },
    ];
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree directory :style="style" :treeData="treeData" />
</template>

禁用

可以使用 disableStrictly 来开启严格禁用。开启严格禁用后,当节点是 disabled 的时候,则不能通过子级或者父级的关系改变选中状态。

  • Asia
  • China
  • Beijing
  • Shanghai
代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: 'Beijing',
                            value: 'Beijing',
                            key: '0-0-0',
                            disabled: true,
                        },
                        {
                            label: 'Shanghai',
                            value: 'Shanghai',
                            key: '0-0-1',
                            disabled: true,
                        },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [
                        {
                            label: 'Osaka',
                            value: 'Osaka',
                            key: '0-1-0',
                        },
                    ],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                {
                    label: 'United States',
                    value: 'United States',
                    key: '1-0',
                },
                {
                    label: 'Canada',
                    value: 'Canada',
                    key: '1-1',
                },
            ],
        },
    ];
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree defaultValue="Shanghai" disableStrictly multiple :style="style" :treeData="treeData" />
</template>

节点选中关系

版本:>= 2.5.0

多选时,可以使用 checkRelation 来设置节点选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated',意味着节点之间的选中互不影响。

  • Asia
  • China
  • Beijing
  • Shanghai
  • Chengdu
  • Japan
  • Osaka
  • North America
  • United States
  • Canada
代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: 'Beijing',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: 'Shanghai',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                        {
                            label: 'Chengdu',
                            value: 'Chengdu',
                            key: '0-0-2',
                        },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [
                        {
                            label: 'Osaka',
                            value: 'Osaka',
                            key: '0-1-0',
                        },
                    ],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                {
                    label: 'United States',
                    value: 'United States',
                    key: '1-0',
                },
                {
                    label: 'Canada',
                    value: 'Canada',
                    key: '1-1',
                },
            ],
        },
    ];
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree checkRelation="unRelated" defaultExpandAll multiple :style="style" :treeData="treeData" />
</template>

默认展开

defaultExpandAll 和 expandAll 均可以设置 Tree 的默认展开/收起状态。二者的区别是,defaultExpandAll 只在初始化时生效,而 expandAll 不仅会在初始化时生效,当数据(treeData/treeDataSimpleJson)发生动态更新时,expandAll 也仍然生效。

其中,expandAll 是从 1.30.0 开始支持的。

在下面的 demo 中,点击按钮更新 TreeData 后,defaultExpandAll 失效,expandAll 仍然生效。

defaultExpandAll
  • Node1
  • Child Node1
  • Child Node2
  • Node2
expandAll
  • Node1
  • Child Node1
  • Child Node2
  • Node2
代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Button, Tree, type TreeProps } from '@fibrous/semi-ui-vue';

    const json = {
        Node1: {
            'Child Node1': '0-0-1',
            'Child Node2': '0-0-2',
        },
        Node2: '0-1',
    };
    const json2 = {
        Node3: {
            'Child Node1': '0-0-1',
            'Child Node2': '0-0-2',
            'Child Node3': '0-0-3',
            'Child Node4': '0-0-4',
        },
        Node2: '0-1',
    };
    const style = {
        marginRight: '20px',
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    const tree = ref<TreeProps['treeDataSimpleJson']>(json);
</script>

<template>
    <Button :style="{ marginBottom: '10px' }" @click="tree = json2">点击更新 TreeData</Button>
    <div :style="{ display: 'flex' }">
        <div>
            <span>defaultExpandAll</span>
            <Tree defaultExpandAll :style="style" :treeDataSimpleJson="tree" />
        </div>
        <div>
            <span>expandAll</span>
            <Tree expandAll :style="style" :treeDataSimpleJson="tree" />
        </div>
    </div>
</template>

开启搜索的展开受控

传入 expandedKeys 时即为展开受控组件,可以用 v-model:expandedKeys 绑定,也可以配合 @expand 使用。当展开受控时,如果开启 filterTreeNode 并进行搜索是不会再自动展开节点的,此时,节点的展开完全由 expandedKeys 来控制。你可以利用 @search 的第二个参数 filteredExpandedKeys 来实现展开受控时的搜索展开效果。

受控

传入 modelValue 时即为受控组件,用 v-model 绑定,也可以配合 @change 使用。

  • 亚洲
  • 中国
  • 北京
  • 上海
  • 北美洲
代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Tree, type TreeValue } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: '亚洲',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: '中国',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: '北京',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: '上海',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
            ],
        },
        {
            label: '北美洲',
            value: 'North America',
            key: '1',
        },
    ];
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    const value = ref<TreeValue>('Shanghai');
</script>

<template>
    <Tree v-model="value" :style="style" :treeData="treeData" />
</template>

自动展开父节点

在展开受控的情况下,当开启了 autoExpandParent ,如果想要收起父元素,则需要把它的所有子元素均收起后才可以。默认情况下,autoExpandParent 为 false,即:父元素收起不受到子元素的影响。

需要先将“中国”节点收起后,才能够收起“亚洲”节点

  • 亚洲
  • 中国
  • 北京
  • 上海
  • 北美洲
代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: '亚洲',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: '中国',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: '北京',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: '上海',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
            ],
        },
        {
            label: '北美洲',
            value: 'North America',
            key: '1',
        },
    ];
    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
    const expandedKeys = ref(['0', '0-0']);
</script>

<template>
    <div>需要先将“中国”节点收起后,才能够收起“亚洲”节点</div>
    <br />
    <Tree v-model:expandedKeys="expandedKeys" autoExpandParent :style="style" :treeData="treeData" />
</template>

自定义展开 Icon

可以通过 #expandIcon 插槽自定义展开 Icon,插槽参数为 onClick、class 和 expanded。插槽只返回一个元素时,该元素自动获得 class 与展开的点击处理;也可以按参数自行绑定。

<Tree :treeData="treeData">
    <template #expandIcon="{ expanded, onClick, class: className }">
        <IconMinus v-if="expanded" :class="className" @click="onClick" />
        <IconPlus v-else :class="className" @click="onClick" />
    </template>
</Tree>

示例如下:

expandIcon 是固定的图标

  • 亚洲
  • 日本
  • 北美洲

expandIcon 随展开状态变化

  • 亚洲
  • 日本
  • 北美洲
代码
<script setup lang="ts">
    import { IconChevronDown, IconMinus, IconPlus } from '@fibrous/semi-icons-vue';
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: '亚洲',
            key: 'yazhou',
            children: [
                {
                    label: '中国',
                    key: 'zhongguo',
                    children: [
                        {
                            label: '北京',
                            key: 'beijing',
                        },
                        {
                            label: '上海',
                            key: 'shanghai',
                        },
                    ],
                },
                {
                    label: '日本',
                    key: 'riben',
                },
            ],
        },
        {
            label: '北美洲',
            key: 'beimeizhou',
        },
    ];
    const style = {
        width: '260px',
        height: '200px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <p>expandIcon 是固定的图标</p>
    <Tree :defaultExpandedKeys="['yazhou']" multiple :style="style" :treeData="treeData">
        <template #expandIcon>
            <IconChevronDown class="testCls" size="small" />
        </template>
    </Tree>
    <br />
    <p>expandIcon 随展开状态变化</p>
    <Tree :defaultExpandedKeys="['yazhou']" multiple :style="style" :treeData="treeData">
        <template #expandIcon="{ expanded, onClick, class: className }">
            <IconMinus v-if="expanded" :class="className" size="small" @click="onClick" />
            <IconPlus v-else :class="className" size="small" @click="onClick" />
        </template>
    </Tree>
</template>

连接线

通过 showLine 设置节点之间的连接线,默认为 false,从 2.50.0 开始支持

showLine
  • parent-0
  • leaf-0-0
  • leaf-0-0-0
  • leaf-0-0-1
  • leaf-0-0-2
  • leaf-0-1
  • parent-1
代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Switch, Tree } from '@fibrous/semi-ui-vue';

    const show = ref(true);

    const treeData = [
        {
            label: 'parent-0',
            key: 'parent-0',
            children: [
                {
                    label: 'leaf-0-0',
                    key: 'leaf-0-0',
                    children: [
                        { label: 'leaf-0-0-0', key: 'leaf-0-0-0' },
                        { label: 'leaf-0-0-1', key: 'leaf-0-0-1' },
                        { label: 'leaf-0-0-2', key: 'leaf-0-0-2' },
                    ],
                },
                { label: 'leaf-0-1', key: 'leaf-0-1' },
            ],
        },
        { label: 'parent-1', key: 'parent-1' },
    ];

    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <div :style="{ display: 'flex', alignItems: 'center', columnGap: '5px', marginBottom: '5px' }">
        <strong id="tree-line-show-line">showLine </strong>
        <Switch v-model="show" aria-labelledby="tree-line-show-line" />
    </div>
    <Tree defaultExpandAll :showLine="show" :style="style" :treeData="treeData" />
</template>

虚拟化

列表虚拟化,用于大量树节点的情况。开启后,动画效果将被关闭。

virtualize 是一个包含下列值的对象:

  • height: 高度值,如果为 string 必须有计算高度才能被渲染出来,即其父节点有 offsetHeight
  • width: 宽度值,默认 100%
  • itemSize: 每行的treeNode的高度,必传

如果带搜索框,建议开启 showFilteredOnly 减少多余节点的渲染。

共 0 个节点

代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Button, Tree, type TreeNodeData } from '@fibrous/semi-ui-vue';

    const style = {
        width: '260px',
        border: '1px solid var(--semi-color-border)',
    };

    const virtualize = {
        // A set Tree height fills the remaining space
        height: 300,
        itemSize: 28,
    };

    function generateData(x = 5, y = 4, z = 3, data: TreeNodeData[] = []) {
        // x: nodes per level. y: nodes with children per level. z: depth (0 is one level)
        function fill(level: number, preKey: string, nodes: TreeNodeData[]) {
            const parents: string[] = [];
            for (let i = 0; i < x; i++) {
                const key = `${preKey}-${i}`;
                nodes.push({ label: `${key}-标签`, key: `${key}-key`, value: `${key}-value` });
                if (i < y) {
                    parents.push(key);
                }
            }
            if (level < 0) {
                return;
            }
            parents.forEach((key, index) => {
                const children: TreeNodeData[] = [];
                nodes[index].children = children;
                fill(level - 1, key, children);
            });
        }
        fill(z, '0', data);

        let total = 0;
        for (let n = z + 1; n >= 0; n--) {
            total += x * y ** n;
        }
        return { gData: data, total };
    }

    const gData = ref<TreeNodeData[]>([]);
    const total = ref(0);

    const onGen = () => {
        const data = generateData();
        gData.value = data.gData;
        total.value = data.total;
    };
</script>

<template>
    <div :style="{ padding: '0 20px' }">
        <Button @click="onGen">生成数据: </Button>
        <span>共 {{ total }} 个节点</span>
        <br />
        <br />
        <Tree v-if="gData.length" filterTreeNode showFilteredOnly :style="style" :treeData="gData" :virtualize="virtualize" />
    </div>
</template>

动态更新数据

treeData 只支持整体替换:传入新数组才会更新,原地修改数组或节点不会更新树。

  • item-0

代码
<script setup lang="ts">
    import { shallowRef } from 'vue';
    import { Button, Tree, type TreeNodeData } from '@fibrous/semi-ui-vue';

    const style = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };

    const treeData = shallowRef<TreeNodeData[]>([
        {
            key: '0',
            label: 'item-0',
            value: '0',
        },
    ]);

    function add() {
        const itemLength = Math.floor(Math.random() * 5) + 1;
        treeData.value = Array.from({ length: itemLength }).map((_, i) => {
            const length = Math.floor(Math.random() * 3);
            const children = Array.from({ length }).map((_, ci) => ({
                key: `${i}-${ci}`,
                label: `Leaf-${i}-${ci}`,
                value: `${i}-${ci}`,
            }));
            return {
                key: `${i}`,
                label: `Item-${i}`,
                value: `${i}`,
                children,
            };
        });
    }
</script>

<template>
    <div :style="style">
        <Tree :treeData="treeData" />
        <br />
        <Button :style="{ margin: '20px' }" @click="add">动态改变数据</Button>
    </div>
</template>

异步加载数据

通过设置 loadData 可以动态加载数据,此时需要在数据中传入 isLeaf 标明叶子节点。

  • Leaf Node
代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-vue';

    const treeData = ref<TreeNodeData[]>([
        { label: 'Expand to load', value: '0', key: '0' },
        { label: 'Expand to load', value: '1', key: '1' },
        { label: 'Leaf Node', value: '2', key: '2', isLeaf: true },
    ]);

    function updateTreeData(list: TreeNodeData[], key: string | undefined, children: TreeNodeData[]): TreeNodeData[] {
        return list.map((node) => {
            if (node.key === key) {
                return { ...node, children };
            }
            if (node.children) {
                return { ...node, children: updateTreeData(node.children, key, children) };
            }
            return node;
        });
    }

    function onLoadData(treeNode?: TreeNodeData) {
        const key = treeNode?.key;
        return new Promise<void>((resolve) => {
            if (treeNode?.children) {
                resolve();
                return;
            }
            setTimeout(() => {
                treeData.value = updateTreeData(treeData.value, key, [
                    { label: 'Child Node', key: `${key}-0` },
                    { label: 'Child Node', key: `${key}-1` },
                ]);
                resolve();
            }, 1000);
        });
    }
</script>

<template>
    <Tree :loadData="onLoadData" :treeData="treeData" />
</template>

可拖拽的Tree

通过设置 draggable 配合 @drop 可以实现 Tree 节点的拖拽。

拖拽事件的载荷是一个对象,参数如下:

- @dragEnd: { event, node: TreeDragNode }
- @dragEnter: { event, node: TreeDragNode, expandedKeys?: string[] }
- @dragLeave: { event, node: TreeDragNode }
- @dragOver: { event, node: TreeDragNode }
- @dragStart: { event, node: TreeDragNode }
- @drop: { event, node: TreeDragNode, dragNode: TreeDragNode, dragNodesKeys: string[], dropPosition: number, dropToGap: boolean }

数据类型 TreeDragNode,除了包含 TreeNodeData 所有属性外,还包含 expanded 和 pos 属性,

TreeDragNode {
    expanded: boolean,
    pos: string
    value?: string | number;
    label?: string | number;
    disabled?: boolean;
    isLeaf?: boolean;
    [key: string]: any;
}
  • pos 指的是当前节点在整个 treeData 中的位置关系,如第0层第1个节点的第2个节点的第0个节点:'0-1-2-0'
  • dropPosition 指的是被拖拽节点在当前层级中被 drop 的位置,如插入在同级的第0个节点前则为 -1,在第0个节点后则为 1,落在其上则为 0,以此类推。配合 dropToGap 可以得到更完整的判断。
  • dropToGap 指的是被拖拽节点是否被 drop 在节点之间的位置,如果为 false 则是 drop 在某个节点上方
  • Europe
代码
<script setup lang="ts">
    import { shallowRef } from 'vue';
    import { Tree, type TreeNodeData, type TreeOnDragProps } from '@fibrous/semi-ui-vue';

    type LoopCallback = (item: TreeNodeData, ind: number, arr: TreeNodeData[]) => void;

    const treeData = shallowRef<TreeNodeData[]>([
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: 'Beijing',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: 'Shanghai',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [
                        {
                            label: 'Osaka',
                            value: 'Osaka',
                            key: '0-1-0',
                        },
                    ],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                {
                    label: 'United States',
                    value: 'United States',
                    key: '1-0',
                },
                {
                    label: 'Canada',
                    value: 'Canada',
                    key: '1-1',
                },
            ],
        },
        {
            label: 'Europe',
            value: 'Europe',
            key: '2',
        },
    ]);

    function onDrop(info: TreeOnDragProps) {
        const { dropToGap, node, dragNode } = info;
        const dropKey = node.key;
        const dragKey = dragNode.key;
        const dropPos = node.pos.split('-');
        const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);

        const data = [...treeData.value];
        const loop = (data: TreeNodeData[], key: string | undefined, callback: LoopCallback) => {
            data.forEach((item, ind, arr) => {
                if (item.key === key) return callback(item, ind, arr);
                if (item.children) return loop(item.children, key, callback);
            });
        };
        let dragObj: TreeNodeData = {};
        loop(data, dragKey, (item, ind, arr) => {
            arr.splice(ind, 1);
            dragObj = item;
        });

        if (!dropToGap) {
            loop(data, dropKey, (item) => {
                item.children = item.children || [];
                item.children.push(dragObj);
            });
        } else if (dropPosition === 1 && node.children && node.expanded) {
            // Expanded with children and dropped in the bottom gap: insert at the top
            loop(data, dropKey, (item) => {
                item.children = item.children || [];
                item.children.unshift(dragObj);
            });
        } else {
            let dropNodeInd = 0;
            let dropNodePosArr: TreeNodeData[] = [];
            loop(data, dropKey, (_item, ind, arr) => {
                dropNodePosArr = arr;
                dropNodeInd = ind;
            });
            if (dropPosition === -1) {
                dropNodePosArr.splice(dropNodeInd, 0, dragObj);
            } else {
                dropNodePosArr.splice(dropNodeInd + 1, 0, dragObj);
            }
        }
        treeData.value = data;
    }
</script>

<template>
    <Tree draggable :treeData="treeData" @drop="onDrop" />
</template>

高级定制

版本 v>=1.7.0

Tree 组件的 api 支持了大部分的渲染需求,但是如果有非常特殊的定制要求的话,可以使用 #fullLabel 插槽来接管整行的内容。

带有 id、role="treeitem" 和行状态的行元素(li)由 Tree 自己提供,虚拟化时的定位样式也在这个元素上,插槽只写行内的内容。把插槽参数中的 class 绑到内容元素上,该元素就获得与内置行相同的缩进、选中、焦点等样式和行内边距。

插槽参数类型如下:

type TreeFullLabelSlotProps = {
    /* 节点数据 */
    data: TreeNodeData;
    /* 层级 */
    level: number;
    /* 样式类名,包括内置样式,如缩进、选中、焦点、拖拽状态等 */
    class: string;
    /* 内置的展开按钮 */
    expandIcon: VNode;
    /* 选中状态 */
    checkStatus: {
        /* 是否选中 */
        checked: boolean;
        /* 是否半选 */
        halfChecked: boolean;
    };
    /* 展开状态 */
    expandStatus: {
        /* 是否展开 */
        expanded: boolean;
        /* 是否加载中 */
        loading: boolean;
    };
    /* 该节点是否符合筛选条件 */
    filtered: boolean;
    /* 当前搜索框输入值 */
    searchWord: string;
    /* 点击回调:单选时选中,多选时勾选 */
    onClick: (event: MouseEvent) => void;
    /* 多选点击回调 */
    onCheck: (event: MouseEvent) => void;
    /* 展开回调 */
    onExpand: (event: MouseEvent) => void;
    /* 右键点击回调 */
    onContextmenu: (event: MouseEvent) => void;
    /* 二次点击回调 */
    onDblclick: (event: MouseEvent) => void;
};
<Tree :treeData="treeData">
    <template #fullLabel="{ data, class: className, onClick }">
        <div :class="className" @click="onClick">{{ data.label }}</div>
    </template>
</Tree>

这里给几个常见的高级用法的 demo。

第一个是针对 “希望只有叶子节点可以选中,父节点只起到分组作用” 的场景。

  • 你只需要渲染叶子节点前的 Checkbox,并且点击父节点时不触发选中,点击叶子节点触发。
  • 同时开启 leafOnly 可以使 change 事件的载荷都是叶子节点。
    注意:#fullLabel 只接管了渲染效果,并不影响内部的数据逻辑。但是你可以选取需要的逻辑进行渲染,或者配合受控来实现更复杂的需求。
代码
<script setup lang="ts">
    import { Checkbox, Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: 'Beijing',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: 'Shanghai',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [
                        {
                            label: 'Osaka',
                            value: 'Osaka',
                            key: '0-1-0',
                        },
                    ],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                {
                    label: 'United States',
                    value: 'United States',
                    key: '1-0',
                },
                {
                    label: 'Canada',
                    value: 'Canada',
                    key: '1-1',
                },
            ],
        },
    ];

    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree leafOnly multiple :style="treeStyle" :treeData="treeData">
        <!-- Focus stays on the tree (arrows expand, Enter/Space check), so the inline checkbox is display-only -->
        <template #fullLabel="{ class: cls, data, checkStatus, expandIcon, onCheck, onExpand }">
            <div :class="cls" @click="data.children?.length ? onExpand($event) : onCheck($event)">
                <component :is="expandIcon" v-if="data.children?.length" />
                <Checkbox v-else aria-hidden="true" :indeterminate="checkStatus.halfChecked" :modelValue="checkStatus.checked" :style="{ marginRight: '8px' }" tabindex="-1" />
                <span>{{ data.label }}</span>
            </div>
        </template>
    </Tree>
</template>

第二个是针对 “希望只有叶子节点可以单选,父节点只起到分组作用” 的场景。

  • 你只需要点击父节点时不触发选中,点击叶子节点触发。
代码
<script setup lang="ts">
    import { Tree } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        { label: 'Beijing', value: 'Beijing', key: '0-0-0' },
                        { label: 'Shanghai', value: 'Shanghai', key: '0-0-1' },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [{ label: 'Osaka', value: 'Osaka', key: '0-1-0' }],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                { label: 'United States', value: 'United States', key: '1-0' },
                { label: 'Canada', value: 'Canada', key: '1-1' },
            ],
        },
    ];

    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree :style="treeStyle" :treeData="treeData" @change="(...args) => console.log('change', ...args)">
        <template #fullLabel="{ class: rowClass, data, expandIcon, onClick, onExpand }">
            <div :class="rowClass" @click="data.children?.length ? onExpand($event) : onClick($event)">
                <component :is="expandIcon" />
                <span>{{ data.label }}</span>
            </div>
        </template>
    </Tree>
</template>

第三个是针对 “单选选中父节点同时也高亮子节点” 的场景。

  • 黑色固定按钮
  • 模块
  • 可自由摆放的组件
  • 分栏容器
  • 按钮组件
  • 按钮组件
  • 模块
  • 自定义组件
代码
<script setup lang="ts">
    import { shallowRef, type Component } from 'vue';
    import { IconAbsoluteStroked, IconComponentStroked, IconFixedStroked, IconInnerSectionStroked, IconSectionStroked } from '@fibrous/semi-icons-vue';
    import { Tree, type TreeNodeData } from '@fibrous/semi-ui-vue';

    const icons: Record<string, Component> = {
        'fix-btn-0': IconFixedStroked,
        'module-0': IconSectionStroked,
        'free-compo-0': IconAbsoluteStroked,
        'split-col-0': IconInnerSectionStroked,
        'btn-0': IconComponentStroked,
        'btn-1': IconComponentStroked,
        'module-1': IconSectionStroked,
        'cus-0': IconComponentStroked,
    };
    const iconStyle = { marginRight: '8px', color: 'var(--semi-color-text-2)' };

    const selected = shallowRef(new Set<string | undefined>());
    const selectedThroughParent = shallowRef(new Set<string | undefined>());
    const treeData = [
        {
            label: '黑色固定按钮',
            key: 'fix-btn-0',
        },
        {
            label: '模块',
            key: 'module-0',
            children: [
                {
                    label: '可自由摆放的组件',
                    key: 'free-compo-0',
                },
                {
                    label: '分栏容器',
                    key: 'split-col-0',
                    children: [
                        {
                            label: '按钮组件',
                            key: 'btn-0',
                        },
                        {
                            label: '按钮组件',
                            key: 'btn-1',
                        },
                    ],
                },
            ],
        },
        {
            label: '模块',
            key: 'module-1',
            children: [
                {
                    label: '自定义组件',
                    key: 'cus-0',
                },
            ],
        },
    ];

    function findDescendantKeys(node: TreeNodeData) {
        const res = [node.key];
        const findChild = (item?: TreeNodeData) => {
            if (!item) return;
            const { children } = item;

            if (children && children.length) {
                children.forEach((child) => {
                    res.push(child.key);
                    findChild(child);
                });
            }
        };
        findChild(node);
        return res;
    }

    function onSelect(key: string, _selected: boolean, node: TreeNodeData) {
        selected.value = new Set([key]);
        selectedThroughParent.value = new Set(findDescendantKeys(node));
    }

    function rowBackground(key: string | undefined) {
        if (selected.value.has(key)) return 'var(--semi-blue-0)';
        if (selectedThroughParent.value.has(key)) return 'rgb(from var(--semi-blue-0) r g b / .5)';
        return 'transparent';
    }

    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree defaultExpandAll :style="treeStyle" :treeData="treeData" @select="onSelect">
        <template #fullLabel="{ class: cls, data, expandIcon, onClick }">
            <div :class="cls" :style="{ backgroundColor: rowBackground(data.key) }" @click="onClick">
                <span v-if="!data.children?.length" :style="{ width: '24px' }" />
                <component :is="expandIcon" v-else />
                <component :is="icons[data.key ?? '']" :style="iconStyle" />
                <span>{{ data.label }}</span>
            </div>
        </template>
    </Tree>
</template>

可拖拽的高级定制

可拖拽(draggable)和高级定制(#fullLabel)可以同时使用。

  • 黑色固定按钮
  • 模块
  • 可自由摆放的组件
  • 分栏容器
  • 按钮组件
  • 按钮组件
  • 模块
  • 自定义组件
代码
<script setup lang="ts">
    import { shallowRef, type Component } from 'vue';
    import { IconAbsoluteStroked, IconComponentStroked, IconFixedStroked, IconSectionStroked } from '@fibrous/semi-icons-vue';
    import { Tree, type TreeNodeData, type TreeOnDragProps } from '@fibrous/semi-ui-vue';

    type LoopCallback = (item: TreeNodeData, ind: number, arr: TreeNodeData[]) => void;

    const icons: Record<string, Component> = {
        'fix-btn-0': IconFixedStroked,
        'module-0': IconSectionStroked,
        'free-compo-0': IconAbsoluteStroked,
        'split-col-0': IconSectionStroked,
        'btn-0': IconComponentStroked,
        'btn-1': IconComponentStroked,
        'module-1': IconSectionStroked,
        'cus-0': IconComponentStroked,
    };
    const iconStyle = { marginRight: '8px', color: 'var(--semi-color-text-2)' };

    const selected = shallowRef(new Set<string | undefined>());
    const selectedThroughParent = shallowRef(new Set<string | undefined>());
    const treeData = shallowRef<TreeNodeData[]>([
        {
            label: '黑色固定按钮',
            key: 'fix-btn-0',
        },
        {
            label: '模块',
            key: 'module-0',
            children: [
                {
                    label: '可自由摆放的组件',
                    key: 'free-compo-0',
                },
                {
                    label: '分栏容器',
                    key: 'split-col-0',
                    children: [
                        {
                            label: '按钮组件',
                            key: 'btn-0',
                        },
                        {
                            label: '按钮组件',
                            key: 'btn-1',
                        },
                    ],
                },
            ],
        },
        {
            label: '模块',
            key: 'module-1',
            children: [
                {
                    label: '自定义组件',
                    key: 'cus-0',
                },
            ],
        },
    ]);

    function onDrop(info: TreeOnDragProps) {
        const { dropToGap, node, dragNode } = info;
        const dropKey = node.key;
        const dragKey = dragNode.key;
        const dropPos = node.pos.split('-');
        const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);

        const data = [...treeData.value];
        const loop = (data: TreeNodeData[], key: string | undefined, callback: LoopCallback) => {
            data.forEach((item, ind, arr) => {
                if (item.key === key) return callback(item, ind, arr);
                if (item.children) return loop(item.children, key, callback);
            });
        };

        let dragObj: TreeNodeData = {};
        loop(data, dragKey, (item, ind, arr) => {
            arr.splice(ind, 1);
            dragObj = item;
        });

        if (!dropToGap) {
            loop(data, dropKey, (item) => {
                item.children = item.children || [];
                item.children.push(dragObj);
            });
        } else if (dropPosition === 1 && node.children && node.expanded) {
            loop(data, dropKey, (item) => {
                item.children = item.children || [];
                item.children.unshift(dragObj);
            });
        } else {
            let dropNodeInd = 0;
            let dropNodePosArr: TreeNodeData[] = [];
            loop(data, dropKey, (_item, ind, arr) => {
                dropNodePosArr = arr;
                dropNodeInd = ind;
            });
            if (dropPosition === -1) {
                dropNodePosArr.splice(dropNodeInd, 0, dragObj);
            } else {
                dropNodePosArr.splice(dropNodeInd + 1, 0, dragObj);
            }
        }
        treeData.value = data;
    }

    function findDescendantKeys(node: TreeNodeData) {
        const res = [node.key];
        const findChild = (item?: TreeNodeData) => {
            if (!item) return;
            const { children } = item;

            if (children && children.length) {
                children.forEach((child) => {
                    res.push(child.key);
                    findChild(child);
                });
            }
        };
        findChild(node);
        return res;
    }

    function onSelect(key: string, _selected: boolean, node: TreeNodeData) {
        selected.value = new Set([key]);
        selectedThroughParent.value = new Set(findDescendantKeys(node));
    }

    function rowBackground(key: string | undefined) {
        if (selected.value.has(key)) return 'var(--semi-blue-0)';
        if (selectedThroughParent.value.has(key)) return 'rgb(from var(--semi-blue-0) r g b / .5)';
        return 'transparent';
    }

    const treeStyle = {
        width: '260px',
        height: '420px',
        border: '1px solid var(--semi-color-border)',
    };
</script>

<template>
    <Tree defaultExpandAll draggable :style="treeStyle" :treeData="treeData" @drop="onDrop" @select="onSelect">
        <template #fullLabel="{ class: cls, data, expandIcon, onClick }">
            <div :class="cls" :style="{ backgroundColor: rowBackground(data.key) }" @click="onClick">
                <span v-if="!data.children?.length" :style="{ width: '24px' }" />
                <component :is="expandIcon" v-else />
                <component :is="icons[data.key ?? '']" :style="iconStyle" />
                <span>{{ data.label }}</span>
            </div>
        </template>
    </Tree>
</template>

API参考

Tree

Props

属性说明类型默认值
modelValue当前选中的节点的 value(没有 value 时为 key),用 v-model 绑定;多选时为数组。传入后即为受控组件。TreeValueundefined
defaultValue指定默认选中的条目,只在挂载时读取。TreeValuesemiGlobalDefault("Tree", "defaultValue")
treeData节点数据,key 在整棵树内唯一。只有传入新数组才会更新,原地修改数组或节点不会生效。TreeNodeData[]semiGlobalDefault("Tree", "treeData")
treeDataSimpleJson简单 JSON 形式的节点数据,返回值为包含选中节点的 JSON 数据。TreeDataSimpleJsonsemiGlobalDefault("Tree", "treeDataSimpleJson")
keyMaps自定义节点中 key、label、value 的字段名。映射了 label 且开启搜索时,需把 treeNodeFilterProp 设为 treeData 的键之一,或自定义 filterTreeNode。KeyMapPropssemiGlobalDefault("Tree", "keyMaps")
expandedKeys展开的节点,用 v-model:expandedKeys 绑定。传入后展开状态完全受控。string[]semiGlobalDefault("Tree", "expandedKeys")
defaultExpandedKeys默认展开的节点,显示其直接子级。string[]semiGlobalDefault("Tree", "defaultExpandedKeys")
defaultExpandAll初始化时展开所有节点。之后 treeData 或 treeDataSimpleJson 的变化不再影响展开状态,需要时使用 expandAll。booleansemiDefault("Tree", "defaultExpandAll", false)
expandAll展开所有节点,数据变化后仍然生效。booleansemiDefault("Tree", "expandAll", false)
autoExpandParentexpandedKeys 变化时是否自动展开其中节点的父节点;挂载时总会展开。开启后,展开受控时要先收起所有已展开的子节点才能收起父节点。booleansemiDefault("Tree", "autoExpandParent", false)
autoExpandWhenDragEnter拖拽到收起的节点上时是否自动展开该节点。booleansemiDefault("Tree", "autoExpandWhenDragEnter", true)
autoMergeValue父节点被选中时,值不再包含它的子孙节点;leafOnly 为 false 时生效。booleansemiDefault("Tree", "autoMergeValue", true)
blockNode节点显示为整行,悬浮和选中高亮整行;关闭时只高亮节点的 label。booleansemiDefault("Tree", "blockNode", true)
checkRelation多选时节点之间选中状态的关系:related 为父子联动,unRelated 为互不影响。CheckRelationsemiDefault("Tree", "checkRelation", "related")
directory目录树模式,自带目录图标,可用 icon 插槽覆盖。booleansemiDefault("Tree", "directory", false)
disabled禁用整棵树。节点数据中的 disabled: false 不会解除禁用。booleansemiDefault("Tree", "disabled", false)
disableStrictly严格禁用:禁用的节点不能通过父级或子级的选中关系改变状态。booleansemiDefault("Tree", "disableStrictly", false)
draggable是否允许拖拽节点,配合 drop 事件使用。不能与 virtualize 同时使用。booleansemiDefault("Tree", "draggable", false)
hideDraggingNode拖拽时隐藏被拖拽节点的拖拽图像。booleansemiDefault("Tree", "hideDraggingNode", false)
expandAction展开逻辑:false 仅点击展开箭头时展开,click 单击节点展开,doubleClick 双击节点展开。键盘不触发该逻辑。ExpandActionsemiDefault("Tree", "expandAction", false)
leafOnly多选时,值和 change 事件的载荷只包含叶子节点。booleansemiDefault("Tree", "leafOnly", false)
loadedKeys已加载的节点,配合 loadData 使用,用 v-model:loadedKeys 绑定。string[]semiGlobalDefault("Tree", "loadedKeys")
motion是否开启展开收起的动画;虚拟化时动画关闭。booleansemiDefault("Tree", "motion", true)
multiple是否多选。多选时所有子项都被选择会自动勾选其父项。booleansemiDefault("Tree", "multiple", false)
changeWithObject布尔属性而不是事件:设为 true 时,change 的载荷由 value 变为包含 value、label 及其他属性的节点对象;受控时 modelValue 与 defaultValue 也要传对象,且必须含 value。booleansemiDefault("Tree", "changeWithObject", false)
showClear搜索框是否显示清除按钮。booleansemiDefault("Tree", "showClear", true)
showFilteredOnly搜索时只展示过滤后的结果。booleansemiDefault("Tree", "showFilteredOnly", false)
showLine显示节点之间的连接线。booleansemiDefault("Tree", "showLine", false)
treeNodeFilterProp搜索时用于匹配输入的节点属性,默认 label。stringsemiDefault("Tree", "treeNodeFilterProp", "label")
labelEllipsislabel 超出时是否省略;虚拟化时默认开启。booleansemiGlobalDefault("Tree", "labelEllipsis")
virtualize列表虚拟化,用于大量节点:height、width、itemSize(必传)。开启后动画关闭。VirtualizesemiGlobalDefault("Tree", "virtualize")
searchClassName搜索框容器的类名。stringsemiGlobalDefault("Tree", "searchClassName")
searchPlaceholder搜索框的占位文字。stringsemiGlobalDefault("Tree", "searchPlaceholder")
searchStyle搜索框容器的样式。StyleValuesemiGlobalDefault("Tree", "searchStyle")
filterTreeNode是否开启搜索。传 true 时按 treeNodeFilterProp 匹配;传函数时自定义匹配,data 为当前节点数据。Typeboolean | ((inputValue: string, treeNodeString: string, data?: TreeNodeData | undefined) => boolean)undefined
loadData异步加载子节点,返回 Promise。需要在数据中用 isLeaf 标明叶子节点;设置后未标明 isLeaf 的节点显示展开箭头。Type((treeNode?: TreeNodeData | undefined) => Promise<void>)undefined

Events

事件说明回调参数
update:modelValue选中状态变化时触发,v-model 使用。[value: TreeValue]
change选中状态变化时触发,载荷与 update:modelValue 相同。会遮住根元素上同名的原生事件。[value: TreeValue]
update:expandedKeys展开的节点变化时触发,v-model:expandedKeys 使用。[expandedKeys: string[]]
expand展开或收起节点时触发,info 含 expanded 与 node。[expandedKeys: string[], info: TreeExpandedOtherProps]
update:loadedKeys已加载的节点变化时触发,v-model:loadedKeys 使用。[loadedKeys: string[]]
load节点加载完毕时触发,loadedKeys 为已加载节点的集合。Type[loadedKeys: Set<string>, node: TreeNodeData]
select节点被选中或取消选中时触发,载荷为该节点的 key、是否选中和节点数据。会遮住根元素上同名的原生事件。[selectedKey: string, selected: boolean, node: TreeNodeData]
search搜索框的值变化时触发;filteredExpandedKeys 是因搜索或 modelValue、defaultValue 而展开的节点,可在展开受控时用来展开搜索结果。[input: string, filteredExpandedKeys: string[]]
contextMenu在节点上点击右键时触发。[event: MouseEvent, node: TreeNodeData]
doubleClick双击节点时触发。[event: MouseEvent, node: TreeNodeData]
dragStart开始拖拽节点时触发。[info: TreeDragProps]
dragEnter拖拽进入节点时触发;进入收起的节点使其展开时,载荷含 expandedKeys。[info: TreeDragEnterProps]
dragOver拖拽经过节点时触发。[info: TreeDragProps]
dragLeave拖拽离开节点时触发。[info: TreeDragProps]
dragEnd拖拽结束时触发。[info: TreeDragProps]
drop节点被放下时触发,载荷含 dragNode、dragNodesKeys、dropPosition、dropToGap。会遮住根元素上同名的原生事件。[info: TreeOnDragProps]

Slots

插槽说明作用域参数
label自定义节点的 label 内容。开启搜索时仍按 treeData 中的 label 文本匹配。TreeLabelSlotProps
icon自定义节点图标,按节点数据返回不同内容;内容为空时保留目录模式的内置图标。TreeIconSlotProps
expandIcon自定义展开图标。返回单个元素时自动获得 class 与展开的点击处理;也可以用参数自行绑定。TreeExpandIconSlotProps
fullLabel接管整行的内容。带 id、role 与状态的行元素由 Tree 提供,插槽只写行内内容;虚拟化时定位样式也在行元素上。TreeFullLabelSlotProps
search替换搜索框;插槽内容为空时隐藏搜索框。TreeSearchSlotProps
emptyContent没有节点,或开启 showFilteredOnly 后搜索无结果时的内容;插槽内容为空时隐藏空状态。any
renderDraggingNode拖拽时浏览器显示的元素,参数为节点元素 nodeInstance 与节点数据 node;插槽内容在 dragstart 时渲染到游离的容器中作为拖拽图像,只能读到应用级的 provide。TreeDraggingNodeSlotProps

Exposes

名称说明类型
search手动触发搜索,效果同在搜索框中输入;需要同时设置 filterTreeNode。Type(value: string) => void
scrollTo在虚拟化的 Tree 中,把指定的已展开节点滚动到视图内;align 默认 center。Type(scrollData: TreeScrollData) => void

Vue 版的 Tree 没有 className 属性:class、style 和 data-* 等未声明的属性落在根元素 .semi-tree-wrapper 上,aria-* 落在 role="tree" 的元素上。空状态下没有 role="tree" 的元素,aria-* 被丢弃。

TreeNodeData

不同 TreeNodeData 的 key 值要求必填且唯一。label 允许重复。value 值非必填,不填时 change 事件、modelValue、defaultValue 及 changeWithObject 中所取的 value 属性值将改为 key 值。为了保证行为的符合预期,treeData 中的 value 值或者全部不填写,或者全部填写且唯一,不建议混写。

属性说明类型默认值
value属性值string|number-
label展示的文本,自定义内容使用 #label 插槽string|number-
disabled是否禁用,Tree 的 disabled 为 true 时整棵树都禁用,节点的 disabled: false 不会解除booleanfalse
keyrequired且要求唯一string-
isLeaf设置节点为叶子节点,在异步加载数据的情况即传入 loadData 时有效boolean-

Virtualize Object

itemSize 必传。

属性说明类型默认值
height高度值,如果为 string 必须保证有计算高度,即其父节点有 offsetHeightnumber|string'100%'
itemSize每行的treeNode的高度,必传number-
width宽度值number|string'100%'

Methods

Tree 通过模板 ref 暴露 search 和 scrollTo 两个方法,说明见上方 Tree 表格的 Exposes 部分,类型为 TreeHandle。

<script setup lang="ts">
    import { useTemplateRef } from 'vue';
    import { Tree } from '@fibrous/semi-ui-vue';
    import type { TreeHandle } from '@fibrous/semi-ui-vue';

    const tree = useTemplateRef<TreeHandle>('tree');
    const search = (value: string) => tree.value?.search(value);
</script>

<template>
    <Tree ref="tree" filterTreeNode :treeData="treeData" />
</template>

Accessibility

ARIA

  • Tree 支持传入 aria-label、aria-labelledby、aria-describedby 来命名和描述该 Tree,它们都设置在 role="tree" 的元素上;
  • Tree 会自动为每个节点设置 aria-level、aria-setsize、aria-posinset、aria-disabled,可展开的节点设置 aria-expanded;单选时用 aria-selected 表示选中,多选时用 aria-checked 表示勾选(半选为 mixed);
  • Tree 会自动为对应部分分别设置 role 为 tree、treeitem。
  • #fullLabel 的内容由 Tree 自己的 li 包住,这个 li 带有 id、role="treeitem" 和上述状态。
  • 节点前的展开箭头和多选复选框只是状态的图示,对读屏软件隐藏,也不接收焦点。

键盘交互

Tree 按 WAI-ARIA Tree View 处理键盘:整棵树是一个 Tab 停靠点,焦点停在树上,aria-activedescendant 指向当前节点,当前节点显示焦点环。

  • ArrowDown、ArrowUp:移到下一个、上一个可见节点;Home、End:移到第一个、最后一个可见节点
  • ArrowRight:展开收起的节点;节点已展开时移到它的第一个子节点(RTL 下与 ArrowLeft 互换)
  • ArrowLeft:收起展开的节点;否则移到父节点
  • Enter、Space:选中当前节点,多选时勾选或取消勾选;禁用节点不响应
  • 输入字符:移到文本以输入内容开头的下一个节点

用鼠标选中、勾选、展开、右键或双击的节点,同时成为键盘的当前节点,之后的方向键从它开始移动。

键盘不触发 expandAction,展开、收起只用方向键。

节点内容里的按钮、链接(如 #label 插槽中的控件)可以用 Tab 到达,焦点在它们上面时,按键由控件自己处理,树不响应。

示例:

<Tree aria-label="example tree" :treeData="treeData" />

文案规范

  • 尽量使用短语,首字母大写
  • 平级之间保持用语形式一致,例如全是地名或者是国家名

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$transition_duration-tree_option-bgvar(--semi-transition_duration-none)树选项-背景色-动画持续时间
$transition_function-tree_option-bgvar(--semi-transition_function-easeIn)树选项-背景色-过渡曲线
$transition_delay-tree_option-bgvar(--semi-transition_delay-none)树选项-背景色-延迟时间
$transform_scale-tree-optionvar(--semi-transform_scale-none)树选项-放大
$color-tree_option-text-defaultvar(--semi-color-text-0)树选项文字颜色
$color-tree_option-icon-defaultvar(--semi-color-text-2)树选项图标颜色
$color-tree_option-bg-hovervar(--semi-color-fill-0)树选项背景色 - 悬停
$color-tree_option_selected-bg-defaultvar(--semi-color-fill-1)树选项背景色 - 按下
$color-tree_option-bg-activevar(--semi-color-primary-light-default)树选项背景色 - 选中
$color-tree_option_disabled-text-defaultvar(--semi-color-disabled-text)禁用树选项文字颜色
$color-tree_option-icon-hovervar(--semi-color-text-0)树选项图标颜色 - 悬浮
$color-tree_option-icon-activevar(--semi-color-black)树选项图标颜色 - 按下
$color-tree_option_loading-icon-defaultvar(--semi-color-primary)树选项加载 spin 颜色
$color-tree_option_draggable_insert-border-defaultvar(--semi-color-primary)树选项加载 spin 颜色
$font-tree_option_hightlight-fontWeight$font-weight-bold树选项高亮文本字重
$color-tree_option_hightlight-textvar(--semi-color-primary)树选项高亮文本颜色
$radius-tree_checkbox_addonvar(--semi-border-radius-small)多选树 checkbox 圆角
$spacing-tree_search_wrapper-paddingX12px树搜索框水平内边距
$spacing-tree_search_wrapper-paddingY8px树搜索框垂直内边距
$spacing-tree_option_level-paddingLeft20px树选项缩进增量
$spacing-tree_option_level1-paddingLeft8px树选项最上层左侧内边距
$spacing-tree_optionList-paddingX0树选项列表水平内边距
$spacing-tree_optionList-paddingY8px树选项列表垂直内边距
$spacing-tree_option-paddingTop4px树选项顶部内边距
$spacing-tree_option-paddingBottom4px树选项底部内边距
$spacing-tree_icon-marginRight8px树选项图标右侧外边距
$spacing-tree_label_withIcon-marginRight8px树选项图标右侧外边距
$spacing-tree_option_draggable-paddingY2px可拖拽的树选项垂直内边距
$spacing-tree_option_draggable-paddingX0可拖拽的树选项水平内边距
$width-tree_emptyIcon$width-icon-small树选项空图标宽度
$width-tree_spinIcon$width-icon-small树选项加载 spin 宽度
$width-tree_option_draggable-border2px可拖拽的树标示线宽度
$width-tree_option_line1pxshowline展示线宽度