输入类

级联选择

用于选择多级分类下的某个选项。

使用场景

与 TreeSelect 组件的区别:

  • TreeSelect: 核心价值在于目标节点,层级结构是为了方便用户快速筛选出目标选项,最终的节点才是用户想要的内容,常见于文件/文件夹选择、组织架构、权限分配等场景。
  • Cascader: 核心价值在于路径,用户选择的不是一个孤立的点,而是一条从根到叶的完整路径,常用于地理位置,商品分类等场景。

代码演示

如何引入

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

基本用法

最简单的用法,默认只可以选叶子节点。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                        {
                            label: '临安区',
                            value: 'linan',
                        },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        {
                            label: '海曙区',
                            value: 'haishu',
                        },
                        {
                            label: '江北区',
                            value: 'jiangbei',
                        },
                    ],
                },
            ],
        },
    ];
    const style = { width: '300px' };
</script>

<template>
    <Cascader placeholder="请选择所在地区" :style="style" :treeData="treeData" />
</template>

多选

设置 multiple,可以进行多选。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <Cascader :defaultValue="['zhejiang', 'ningbo', 'jiangbei']" multiple placeholder="请选择所在地区" style="width: 300px" :treeData="treeData" />
</template>

可搜索的

通过设置 filterTreeNode 属性可支持搜索功能。

默认对 label 值进行搜索(使用字符串的 includes 方法进行匹配,不区分大小写),可通过 treeNodeFilterProp 指定其他属性值进行搜索。label 只接受文本,节点内容用 #label 插槽定制时,搜索仍匹配 label 文本;需要匹配其他文本时,可在 treeData 中使用其他字段存储,并通过 treeNodeFilterProp 指定该字段进行搜索。

默认搜索结果只会展示叶子结点的路径,想要显示更多的结果,可以设置 filterLeafOnly 为 false。





filterLeafOnly=false:


Label 为自定义节点,指定其他属性进行搜索
代码
<script setup lang="ts">
    import { IconMapPinStroked } from '@fibrous/semi-icons-vue';
    import { Cascader, Typography } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];

    // The label slot adds a decorative icon in the columns; labelText holds the text that search matches
    const labelNodeTreeData = [
        {
            label: '浙江省',
            labelText: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    labelText: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', labelText: '西湖区', value: 'xihu' },
                        { label: '萧山区', labelText: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', labelText: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    labelText: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', labelText: '海曙区', value: 'haishu' },
                        { label: '江北区', labelText: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <div>
        <Cascader filterTreeNode placeholder="默认对label值进行搜索" style="width: 300px" :treeData="treeData" />
        <br />
        <br />
        <Cascader filterTreeNode placeholder="对value值进行搜索" style="width: 300px" :treeData="treeData" treeNodeFilterProp="value" />
        <br />
        <br />
        <Typography.Title :heading="6">filterLeafOnly=false:</Typography.Title>
        <Cascader :filterLeafOnly="false" filterTreeNode placeholder="filterLeafOnly=false" style="width: 300px" :treeData="treeData" />
        <br />
        <br />
        <Typography.Title :heading="6">Label 为自定义节点,指定其他属性进行搜索</Typography.Title>
        <Cascader displayProp="labelText" filterTreeNode placeholder="Search for labelText" style="width: 300px" :treeData="labelNodeTreeData" treeNodeFilterProp="labelText">
            <template #label="{ item }">
                <span :style="{ display: 'inline-flex', alignItems: 'center', columnGap: '4px' }">
                    <IconMapPinStroked />
                    {{ item.label }}
                </span>
            </template>
        </Cascader>
    </div>
</template>

可搜索的多选

支持多选和搜索同时使用,在这种场景下,可以通过按下 BackSpace 键来删除对应的已选项目。

海曙区
代码
<script setup lang="ts">
    import { ref } from 'vue';
    import { Cascader, type CascaderValue } from '@fibrous/semi-ui-vue';

    const value = ref<CascaderValue>(['zhejiang', 'ningbo', 'haishu']);

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <Cascader v-model="value" filterTreeNode multiple placeholder="请选择所在地区" style="width: 300px" :treeData="treeData" />
</template>

可以使用 filterSorter 对筛选后的数据进行排序。

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

    const treeData = [
        {
            label: 'Product',
            value: 'Product',
            children: [
                { label: 'Semi-Material', value: 'Semi-Material' },
                { label: 'Semi-DSM', value: 'Semi-DSM' },
                { label: 'Semi', value: 'Semi' },
                { label: 'Semi-C2D', value: 'Semi-C2D' },
                { label: 'Semi-D2C', value: 'Semi-D2C' },
            ],
        },
    ];

    // The sorter receives the two result paths, so compare their last nodes
    const filterSorter: CascaderProps['filterSorter'] = (first, second, inputValue) => {
        const firstData = first[first.length - 1];
        const lastData = second[second.length - 1];
        if (firstData.label === inputValue) {
            return -1;
        } else if (lastData.label === inputValue) {
            return 1;
        } else {
            return firstData.label < lastData.label ? -1 : 1;
        }
    };
</script>

<template>
    <div>
        <Cascader :filterSorter="filterSorter" filterTreeNode placeholder="输入 s 查看排序效果" :style="{ width: '300px' }" :treeData="treeData" />
    </div>
</template>

如果想要自定义渲染搜索后的选项,可以使用 #filterItem 插槽实现整行的自定义渲染,插槽参数的类型为 CascaderFilterItemSlotProps:

interface CascaderFilterItemSlotProps {
    class: ClassValue; // 选项的 class,需要绑定到插槽的根元素上
    inputValue: string; // 搜索栏搜索内容
    disabled: boolean; // 是否禁用
    data: CascaderData[]; // 搜索结果数据,即这条路径上的各级节点
    selected: boolean; // 单选时的选中状态
    checkStatus: {
        // 多选时的选中状态
        checked: boolean;
        halfChecked: boolean;
    };
    onClick: (event: MouseEvent) => void; // 单选点击选中
    onCheck: (event: CheckboxEvent) => void; // 多选点击选中,绑定到 Checkbox 的 change 事件
}

插槽只渲染一个元素根时,该元素会自动获得选项的 id、role="option" 和选中状态,开启 virtualizeInSearch 时还会合并行的定位样式;渲染文本或多个节点时,组件会包一层 li。插槽根应该是普通元素。

使用示例如下

鼠标 hover 到选项可查看被省略文本完整内容



代码
<script setup lang="ts">
    import { Cascader, Checkbox, TypographyText, type CascaderData, type Ellipsis } from '@fibrous/semi-ui-vue';

    const treeData = [
        {
            label: 'Semi',
            value: 'Semi',
            children: [
                {
                    label: 'Semi-Material Semi-Material Semi-Material Semi-Material',
                    value: 'Semi-Material',
                },
                {
                    label: 'Semi-DSM Semi-DSM Semi-DSM Semi-DSM',
                    value: 'Semi-DSM',
                },
                {
                    label: 'Semi Design Semi Design Semi Design Semi Design',
                    value: 'Semi',
                },
                {
                    label: 'Semi-C2D Semi-C2D Semi-C2D Semi-C2D Semi-C2D',
                    value: 'Semi-C2D',
                },
                {
                    label: 'Semi-D2C Semi-D2C Semi-D2C Semi-D2C Semi-D2C ',
                    value: 'Semi-D2C',
                },
            ],
        },
    ];
    const ellipsis: Ellipsis = { showTooltip: { opts: { style: { wordBreak: 'break-all' } } } };
    const pathLabel = (data: CascaderData[]) => data.map((item) => item.label).join(' / ');
</script>

<template>
    <div>
        <p>鼠标 hover 到选项可查看被省略文本完整内容</p>
        <br />
        <Cascader filterTreeNode placeholder="单选,输入 s 自定义搜索选项渲染结果" :style="{ width: '320px' }" :treeData="treeData">
            <template #filterItem="{ class: className, data, onClick, selected }">
                <li :aria-selected="selected" :class="className" role="option" :style="{ justifyContent: 'flex-start' }" @click="onClick">
                    <TypographyText :ellipsis="ellipsis" :style="{ width: '270px', color: selected ? 'var(--semi-color-primary)' : undefined }">
                        {{ pathLabel(data) }}
                    </TypographyText>
                </li>
            </template>
        </Cascader>
        <br />
        <Cascader filterTreeNode multiple placeholder="多选,输入 s 自定义搜索选项渲染结果" :style="{ width: '320px', marginTop: '20px' }" :treeData="treeData">
            <template #filterItem="{ class: className, data, checkStatus, onCheck }">
                <li :aria-label="pathLabel(data)" :aria-selected="checkStatus.checked" :class="className" role="option" :style="{ justifyContent: 'flex-start' }">
                    <Checkbox :indeterminate="checkStatus.halfChecked" :modelValue="checkStatus.checked" :tabIndex="-1" @change="onCheck">
                        <TypographyText :ellipsis="ellipsis" :style="{ width: '250px' }">
                            {{ pathLabel(data) }}
                        </TypographyText>
                    </Checkbox>
                </li>
            </template>
        </Cascader>
    </div>
</template>

如果搜索结果中存在大量 Option,可以通过设置 virtualizeInSearch 开启搜索结果面板的虚拟化来优化性能,virtualizeInSearch 是一个包含下列值的对象:

  • height: Option 列表高度值(number,单位 px),缺省 180,与下拉框高度一致
  • width: Option 列表宽度值
  • itemSize: 每行 Option 的高度

限制标签展示数量

在多选的场景中,利用 maxTagCount 可以限制展示的标签数量,超出部分将以 +N 的方式展示。

使用 showRestTagsPopover 可以设置在超出 maxTagCount 后,hover +N 是否显示 Popover,默认为 false。并且,还可以在 restTagsPopoverProps 属性中配置 Popover。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
    const restTagsPopoverProps = { position: 'top' } as const;
    const defaultValue = [
        ['zhejiang', 'ningbo', 'haishu'],
        ['zhejiang', 'hangzhou', 'xihu'],
    ];
</script>

<template>
    <Cascader :defaultValue="defaultValue" :maxTagCount="1" multiple placeholder="请选择所在地区" :restTagsPopoverProps="restTagsPopoverProps" showRestTagsPopover :style="{ width: '300px' }" :treeData="treeData" />
</template>

限制选中数量

在多选的场景中,利用 max 可以限制多选选中的数量。超出 max 后将触发 exceed 事件。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
    const defaultValue = [['zhejiang', 'ningbo', 'haishu']];

    const onExceed = (v: CascaderEntity[]) => {
        Toast.warning('exceed max');
        console.log(v);
    };
</script>

<template>
    <Cascader :defaultValue="defaultValue" :max="1" multiple placeholder="请选择所在地区" :style="{ width: '300px' }" :treeData="treeData" @exceed="onExceed" />
</template>

选择即改变

在单选的情况下,还可以通过设置 changeOnSelect,允许选中父级选项。



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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                        {
                            label: '临安区',
                            value: 'linan',
                        },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        {
                            label: '海曙区',
                            value: 'haishu',
                        },
                        {
                            label: '江北区',
                            value: 'jiangbei',
                        },
                    ],
                },
            ],
        },
    ];
    const style = { width: '300px' };
</script>

<template>
    <div>
        <Cascader changeOnSelect placeholder="选择即改变" :style="style" :treeData="treeData" />
        <br />
        <br />
        <Cascader changeOnSelect filterTreeNode placeholder="可搜索的选择即改变" :style="style" :treeData="treeData" />
    </div>
</template>

自定义显示

可以通过 displayProp 设置回填选项显示的属性值,默认为 label。

单选


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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                        {
                            label: '临安区',
                            value: 'linan',
                        },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        {
                            label: '海曙区',
                            value: 'haishu',
                        },
                        {
                            label: '江北区',
                            value: 'jiangbei',
                        },
                    ],
                },
            ],
        },
    ];
    const defaultValue = ['zhejiang', 'ningbo', 'jiangbei'];
    const style = { width: '300px' };
</script>

<template>
    <TypographyTitle :heading="6">单选</TypographyTitle>
    <Cascader :defaultValue="defaultValue" displayProp="value" placeholder="回填时显示数据的value值" :style="style" :treeData="treeData" />
    <br />
    <br />
    <TypographyTitle :heading="6">多选</TypographyTitle>
    <Cascader :defaultValue="defaultValue" displayProp="value" multiple placeholder="回填时显示数据的value值" :style="style" :treeData="treeData" />
</template>

displayRender 拆成两个插槽。

单选 (multiple 为 false) 时,用 #display="{ labels, path }" 设定回填内容,其中 labels 是路径上各级节点的 displayProp 值,path 是路径上的节点数据(值不在 treeData 中时为原始值,与开启 changeWithObject 时 change 的载荷相同);插槽内容为空时显示 placeholder。

多选 (multiple 为 true) 时,用 #tag="{ entity, index, disabled, onClose }" 整体替换每个标签,其中 entity 为节点的相关数据,onClose 取消该节点的选中,需要关闭按钮时把它绑定到自己的 Tag 上。可搜索的多选同样生效。entity 的类型为 CascaderEntity,如下:

interface Entity {
    children?: Entity[]; // 子节点列表
    data: CascaderData; // treedata
    ind: number; // index
    key: string; // key
    level: number; // 节点层级
    parent?: Entity; // 父节点数据
    parentKey?: string; // 父节点 key
    path: string[]; // key 路径
    pos: string; // 节点在 treeData 中的层级位置,如 '0-0-1'
    valuePath: string[]; // value 路径
}

数据项的 label 只接受文本。要在列视图的选项里渲染自定义内容,使用 #label="{ item }" 插槽,item 为该节点的数据;搜索结果、回填和标签仍显示节点的文本。

单选


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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
    const defaultValue = ['zhejiang', 'ningbo', 'jiangbei'];
</script>

<template>
    <TypographyTitle :heading="6">单选</TypographyTitle>
    <Cascader :defaultValue="defaultValue" placeholder="自定义回填时显示数据的格式" :style="{ width: '300px' }" :treeData="treeData">
        <template #display="{ labels }">已选择:{{ labels.join(' -> ') }}</template>
    </Cascader>
    <br />
    <br />
    <TypographyTitle :heading="6">多选</TypographyTitle>
    <Cascader :defaultValue="defaultValue" multiple placeholder="自定义回填时显示数据的格式" :style="{ width: '300px' }" :treeData="treeData">
        <template #tag="{ entity }">
            <Tag color="white" :style="{ marginRight: '4px' }">{{ entity.data.label }}</Tag>
        </template>
    </Cascader>
</template>

自定义分隔符

可以使用 separator 设置分隔符, 包括:搜索时显示在下拉框的内容以及单选时回显到 Trigger 的内容的分隔符。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <Cascader :defaultValue="['zhejiang', 'ningbo', 'jiangbei']" filterTreeNode separator=" > " style="width: 300px" :treeData="treeData" />
</template>

禁用



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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                        {
                            label: '临安区',
                            value: 'linan',
                        },
                    ],
                },
            ],
        },
    ];
    const defaultValue = ['zhejiang', 'hangzhou', 'xihu'];
    const style = { width: '300px' };
</script>

<template>
    <div>
        <Cascader disabled placeholder="请选择所在地区" :style="style" :treeData="treeData" />
        <br />
        <br />
        <Cascader :defaultValue="defaultValue" disabled filterTreeNode placeholder="请选择所在地区" :style="style" :treeData="treeData" />
    </div>
</template>

严格禁用

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

以下面的 demo 为例,节点"宁波"开启了严格禁用,因此,当我们改变其父节点"浙江省"的选中状态时,也不会影响到节点"宁波"的选中状态。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                        {
                            label: '临安区',
                            value: 'linan',
                        },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    disabled: true,
                    children: [
                        {
                            label: '海曙区',
                            value: 'haishu',
                        },
                        {
                            label: '江北区',
                            value: 'jiangbei',
                        },
                    ],
                },
            ],
        },
    ];
    const style = { width: '300px' };
</script>

<template>
    <Cascader disableStrictly multiple placeholder="请选择所在地区" :style="style" :treeData="treeData" />
</template>

展示子菜单的时机

可以使用 showNext 设置展开 Dropdown 子菜单的触发时机,可选: click(默认)、hover。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <Cascader placeholder="请选择所在地区" showNext="hover" style="width: 300px" :treeData="treeData" />
</template>

点击选中

在多选模式下,默认情况下点击非叶子节点不会触发选中。你可以通过 clickToSelect 开启点击任意节点即选中的功能。

这个 API 在配合 showNext="hover" 使用时特别有用:鼠标悬浮展开子菜单,点击则选中当前节点。

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

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        {
                            label: '海曙区',
                            value: 'haishu',
                        },
                    ],
                },
            ],
        },
    ];
    const style = { width: '300px' };
</script>

<template>
    <Cascader clickToSelect multiple placeholder="请选择所在地区" showNext="hover" :style="style" :treeData="treeData" />
</template>

在顶部/底部渲染附加项

我们在级联选择器的顶部、底部分别预留了插槽,你可以通过 #topSlot 或 #bottomSlot 插槽来设置。

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

    const { Text } = Typography;

    const slotStyle = {
        height: '36px',
        display: 'flex',
        padding: '0 32px',
        alignItems: 'center',
        cursor: 'pointer',
        borderTop: '1px solid var(--semi-color-border)',
    };

    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <Cascader placeholder="请选择所在地区" style="width: 300px" :treeData="treeData">
        <template #bottomSlot>
            <div :style="slotStyle">
                <Text>找不到相关选项?</Text>
                <Text link>去新建</Text>
            </div>
        </template>
    </Cascader>
</template>

受控

用 v-model 绑定 modelValue 即为受控组件,change 事件同样会触发。modelValue 需要整体替换,就地修改受控数组不会被识别;传入 undefined 时回到非受控,并清空选中项。

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

    const value = ref<CascaderSimpleValue[]>([]);
    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                        {
                            label: '临安区',
                            value: 'linan',
                        },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        {
                            label: '海曙区',
                            value: 'haishu',
                        },
                        {
                            label: '江北区',
                            value: 'jiangbei',
                        },
                    ],
                },
            ],
        },
    ];
    const style = { width: '300px' };
</script>

<template>
    <Cascader v-model="value" placeholder="请选择所在地区" :style="style" :treeData="treeData" />
</template>

自动合并 value

在多选(multiple=true)场景中,当我们选中祖先节点时,如果希望 value 不包含它对应的子孙节点,则可以通过 autoMergeValue 来设置,默认为 true。当 autoMergeValue 和 leafOnly 同时开启时,后者优先级更高。

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

    const value = ref<CascaderSimpleValue[][]>([]);
    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        {
                            label: '西湖区',
                            value: 'xihu',
                        },
                        {
                            label: '萧山区',
                            value: 'xiaoshan',
                        },
                        {
                            label: '临安区',
                            value: 'linan',
                        },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        {
                            label: '海曙区',
                            value: 'haishu',
                        },
                        {
                            label: '江北区',
                            value: 'jiangbei',
                        },
                    ],
                },
            ],
        },
    ];
    const style = { width: '300px' };
</script>

<template>
    <Cascader v-model="value" :autoMergeValue="false" multiple placeholder="autoMergeValue 为 false" :style="style" :treeData="treeData" />
</template>

仅叶子节点

在多选时,可以通过开启 leafOnly 来设置 value 只包含叶子节点,即显示的 Tag 和 v-model、change 事件的值只包含叶子节点。

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

    const value = ref<CascaderSimpleValue[][]>([]);
    const onChange = (v: CascaderSimpleValue[][]) => {
        console.log(v);
    };
    const treeData = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <Cascader v-model="value" leafOnly multiple placeholder="开启 leafOnly" :style="{ width: '300px' }" :treeData="treeData" @change="onChange" />
</template>

节点选中关系

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

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

    const treeData = [
        {
            label: '亚洲',
            value: 'Asia',
            children: [
                {
                    label: '中国',
                    value: 'China',
                    children: [
                        {
                            label: '北京',
                            value: 'Beijing',
                        },
                        {
                            label: '上海',
                            value: 'Shanghai',
                        },
                    ],
                },
            ],
        },
        {
            label: '北美洲',
            value: 'North America',
        },
    ];
    const defaultValue = [['Asia'], ['Asia', 'China', 'Beijing']];
    const style = { width: '300px' };
</script>

<template>
    <Cascader checkRelation="unRelated" :defaultValue="defaultValue" multiple :style="style" :treeData="treeData" />
</template>

动态更新数据

treeData 可以整体替换,也可以就地修改响应式数据(如 push、给 children 赋值),都会被识别并重新计算;内容相同的新数组不会重置展开和选中状态。



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

    const treeData = ref<CascaderData[]>([]);

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

<template>
    <Cascader placeholder="请选择" :style="{ width: '300px' }" :treeData="treeData" />
    <br />
    <br />
    <Button @click="add">动态改变数据</Button>
</template>

异步加载数据

可以使用 loadData 实现异步加载数据

不能与搜索同时使用

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

    const data = ref<CascaderData[]>([
        { label: 'Node1', value: '0-0' },
        { label: 'Node2', value: '0-1' },
        { label: 'Node3', value: '0-2', isLeaf: true },
    ]);

    const updateTreeData = (list: CascaderData[], value: CascaderData['value'], children: CascaderData[]): CascaderData[] =>
        list.map((node) => {
            if (node.value === value) {
                return { ...node, children };
            }
            if (node.children) {
                return { ...node, children: updateTreeData(node.children, value, children) };
            }
            return node;
        });

    const onLoadData = (selectedOpt: CascaderData[]) => {
        const targetOpt = selectedOpt[selectedOpt.length - 1];
        const { label, value } = targetOpt;
        return new Promise<void>((resolve) => {
            if (targetOpt.children) {
                resolve();
                return;
            }

            setTimeout(() => {
                data.value = updateTreeData(data.value, value, [
                    {
                        label: `${label} - 1`,
                        value: `${label}-1`,
                        isLeaf: selectedOpt.length > 1,
                    },
                    {
                        label: `${label} - 2`,
                        value: `${label}-2`,
                        isLeaf: selectedOpt.length > 1,
                    },
                ]);
                resolve();
            }, 1000);
        });
    };
</script>

<template>
    <Cascader :loadData="onLoadData" placeholder="Please select" :style="{ width: '300px' }" :treeData="data" />
</template>

远程搜索

v>=2.97.0 起 Cascader 支持远程搜索。设置 remote 后,搜索输入不再走本地匹配,而是仅触发 search 事件,由你根据输入异步拉取 treeData。这与 Select 的 remote 行为一致。

超长列表

当你的数据结构层级特别深时,Cascader下拉菜单可能会超出屏幕,此时我们建议为下拉菜单设置 overflow-x: auto 以及一个合适的 width 宽度( 建议以N+0.5列的宽度为准,最右侧显示半列,以给用户一种右侧尚有待展开项,可以水平方向滚动的视觉暗示)

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

    const treeData = [
        {
            label: 'A',
            value: 'A',
            children: [
                {
                    label: 'B',
                    value: 'B',
                    children: [
                        {
                            label: 'C',
                            value: 'C',
                            children: [
                                {
                                    label: 'D',
                                    value: 'D',
                                    children: [
                                        {
                                            label: 'E',
                                            value: 'E',
                                            children: [{ label: 'F', value: 'F' }],
                                        },
                                    ],
                                },
                            ],
                        },
                    ],
                },
            ],
        },
    ];
</script>

<template>
    <Cascader dropdownClassName="components-cascader-demo" placeholder="请选择所在地区" :style="{ width: '300px' }" :treeData="treeData" />
</template>
.components-cascader-demo {
    .semi-cascader-option-lists {
        max-width: 510px;
        overflow-x: auto;
    }
}

自定义 Trigger

如果默认的触发器样式满足不了你的需求,可以用 #trigger 插槽自定义选择框的展示

#trigger 的插槽参数如下,类型为 CascaderTriggerSlotProps

interface CascaderTriggerSlotProps {
    /* Cascader 的 props */
    componentProps: CascaderProps;
    /* 是否禁用 Cascader */
    disabled: boolean;
    /**
     * 已选中的 node 在 treeData 中的层级位置,如下例子,
     * 当选中浙江省-杭州市-萧山区时,此处 value 为 '0-0-1';多选时为 Set<string>
     */
    value: string | undefined | Set<string>;
    /* 展开时为搜索词;可搜索的单选收起时为已选路径的展示文本(各级文本用 separator 连接) */
    inputValue: string;
    /**
     * 用于更新 input 框值的函数,当你在 #trigger 自定义的
     * Input 组件值更新时,你应该调用该函数,用于向 Cascader 内部
     * 同步状态, 使用时需要设置 filterTreeNode 参数非 false
     */
    onSearch: (inputValue: string) => void;
    /* 用于清空值的函数;绑定在触发器内的控件上时传入点击事件,避免这次点击同时打开下拉 */
    onClear: (event?: MouseEvent) => void;
    /* Placeholder */
    placeholder?: string;
    /* 用于删除单个 item,入参为该节点在 treeData 中的层级位置(与 value 的格式相同) */
    onRemove: (pos: string) => void;
}

代码
<script setup lang="ts">
    import { IconChevronDown, IconClose } from '@fibrous/semi-icons-vue';
    import { Button, ButtonGroup, Cascader, Tag, TagInput, type CascaderData } from '@fibrous/semi-ui-vue';

    const treeData: CascaderData[] = [
        {
            label: '浙江省',
            value: 'zhejiang',
            children: [
                {
                    label: '杭州市',
                    value: 'hangzhou',
                    children: [
                        { label: '西湖区', value: 'xihu' },
                        { label: '萧山区', value: 'xiaoshan' },
                        { label: '临安区', value: 'linan' },
                    ],
                },
                {
                    label: '宁波市',
                    value: 'ningbo',
                    children: [
                        { label: '海曙区', value: 'haishu' },
                        { label: '江北区', value: 'jiangbei' },
                    ],
                },
            ],
        },
    ];

    const getLabelFromValue = (value: string) => {
        const [first, ...rest] = value.split('-').map((item) => Number(item));
        let node: CascaderData | undefined = treeData[first];
        rest.forEach((item) => {
            node = node?.children?.[item];
        });
        return node?.label;
    };
</script>

<template>
    <Cascader placeholder="Custom Trigger" :treeData="treeData">
        <template #trigger="{ value, placeholder, onClear }">
            <!-- The clear button is a sibling of the trigger button: buttons cannot nest -->
            <ButtonGroup theme="light">
                <Button iconPosition="right">
                    <template #icon>
                        <IconChevronDown />
                    </template>
                    {{ value ? getLabelFromValue(value) : placeholder }}
                </Button>
                <Button v-if="value" aria-label="清除" @click.stop="onClear($event)">
                    <template #icon>
                        <IconClose />
                    </template>
                </Button>
            </ButtonGroup>
        </template>
    </Cascader>
    <br />
    <Cascader filterTreeNode multiple placeholder="Custom Trigger" style="width: 300px" :treeData="treeData">
        <template #trigger="{ value, onSearch, onRemove }">
            <TagInput :modelValue="Array.from(value)" @inputChange="onSearch">
                <template #tag="{ value: pos }">
                    <Tag closable :style="{ marginLeft: '2px' }" :tagKey="pos" @close="onRemove(pos)">
                        {{ getLabelFromValue(pos) }}
                    </Tag>
                </template>
            </TagInput>
        </template>
    </Cascader>
</template>

API 参考

Cascader

Props

属性说明类型默认值
modelValue当前选中的条目,用 v-model 绑定。传入后即为受控组件,传入 undefined 回到非受控并清空选中项;只在整体替换时识别变化,就地修改受控数组不会生效。单选时是节点或节点路径,多选时可以是路径数组。TypeByMultiple<Multiple, CascaderSimpleValue | CascaderSimpleValue[], CascaderValue>undefined
defaultValue指定默认选中的条目,只在挂载时读取。TypeByMultiple<Multiple, CascaderSimpleValue | CascaderSimpleValue[], CascaderValue>semiGlobalDefault("Cascader", "defaultValue")
multiple是否多选。modelValue、defaultValue、change 与 select 的类型随它变化;它没有全局默认值。Multipleundefined
treeData节点数据,label 只接受文本,节点内容用 label 插槽。原地修改响应式数据也会被识别,内容相同的新数组不会重置展开和选中状态。CascaderData[]semiDefault("Cascader", "treeData", [])
keyMaps自定义节点中 value、label、children、disabled、isLeaf 的字段名。KeyMapPropssemiGlobalDefault("Cascader", "keyMaps")
changeOnSelect单选时是否允许选择非叶子节点。booleansemiDefault("Cascader", "changeOnSelect", false)
autoMergeValue多选时,某个父节点被选中后,值不再包含它的子孙节点。挂载后修改不会生效;与 leafOnly 同时开启时,后者优先。booleansemiDefault("Cascader", "autoMergeValue", true)
leafOnly多选时,值与展示的标签只包含叶子节点。挂载后修改不会生效。booleansemiDefault("Cascader", "leafOnly", false)
checkRelation多选时节点之间选中状态的关系:related 为父子联动,unRelated 为互不影响。"related" | "unRelated"semiDefault("Cascader", "checkRelation", strings.RELATED)
disableStrictly是否严格禁用。开启后,禁用的节点不会因父级或子级的选中状态变化而改变。booleansemiDefault("Cascader", "disableStrictly", false)
max多选时选中数量的上限,超出时触发 exceed 事件。numbersemiGlobalDefault("Cascader", "max")
maxTagCount多选时标签的最大展示数量,超出的部分以 +N 展示。numbersemiGlobalDefault("Cascader", "maxTagCount")
showRestTagsPopover超出 maxTagCount 后,悬停 +N 时是否用 Popover 展示剩余标签。booleansemiDefault("Cascader", "showRestTagsPopover", false)
restTagsPopoverProps剩余标签 Popover 的配置,可设置 position、trigger 等,参见 Popover。PopoverPropssemiGlobalDefault("Cascader", "restTagsPopoverProps")
enableLeafClick多选时,点击叶子节点所在的选项是否触发勾选。booleansemiDefault("Cascader", "enableLeafClick", false)
clickToSelect多选时,点击任意节点即触发选中,常与 showNext 为 hover 配合:悬停展开子菜单,点击选中当前节点。booleansemiDefault("Cascader", "clickToSelect", false)
showNext展开子菜单的时机:click 为点击,hover 为悬停。"hover" | "click"semiDefault("Cascader", "showNext", strings.SHOW_NEXT_BY_CLICK)
loadData异步加载数据,参数为从根到当前节点的路径,需要返回 Promise;reject 后节点不再显示加载态,可以再次展开重试。不能与搜索同时使用。保留为函数 prop,因为需要读取返回的 Promise。Type((selectOptions: CascaderData[]) => Promise<void>)undefined
filterTreeNode是否开启搜索:true 时按 treeNodeFilterProp 对应的属性值匹配输入,不区分大小写;也可以传入自定义的匹配函数。保留为函数 prop,因为需要读取返回值。Typeboolean | ((inputValue: string, treeNodeString: string, data?: CascaderData | undefined) => boolean)undefined
filterSorter对筛选后的结果排序,参数为两条结果路径(从根节点到匹配节点的节点数组)与输入内容。保留为函数 prop,因为需要读取返回值。Type((first: CascaderData[], second: CascaderData[], inputValue: string) => number)undefined
filterLeafOnly搜索结果是否只展示叶子节点的路径。booleansemiDefault("Cascader", "filterLeafOnly", true)
treeNodeFilterProp搜索时用于匹配输入的节点属性,默认 label。stringsemiDefault("Cascader", "treeNodeFilterProp", "label")
remote是否开启远程搜索。开启后输入不再做本地过滤,只触发 search 事件,由使用方异步更新 treeData。booleansemiDefault("Cascader", "remote", false)
displayProp回填选项和标签显示的节点属性,默认 label。stringsemiDefault("Cascader", "displayProp", "label")
separator分隔符,用于搜索结果和单选回填到触发器的路径。stringsemiDefault("Cascader", "separator", " / ")
changeWithObject是否让 change 事件携带每个节点的数据而不是它的 value。开启后受控的 modelValue 与 defaultValue 也要传节点数据,并含有 value。这是布尔属性,不是事件,不要写成 @changeWithObject。booleansemiDefault("Cascader", "changeWithObject", false)
placeholder选择框的占位文字。stringsemiGlobalDefault("Cascader", "placeholder")
searchPlaceholder搜索框的占位文字,不传时使用 placeholder。stringsemiGlobalDefault("Cascader", "searchPlaceholder")
searchPosition搜索框的位置:trigger 在触发器内,custom 由使用方自定义,此时用 search 方法传入搜索词。"trigger" | "custom"TypesemiDefault("Cascader", "searchPosition", strings.SEARCH_POSITION_TRIGGER)
disabled是否禁用。禁用后不可 Tab 到达。booleansemiDefault("Cascader", "disabled", false)
size选择框的尺寸。SizesemiDefault("Cascader", "size", "default")
borderless是否使用无边框模式。booleansemiDefault("Cascader", "borderless", false)
validateStatus触发器的校验状态,只影响样式。"default" | "error" | "warning" | "success"semiGlobalDefault("Cascader", "validateStatus")
insetLabelIdinsetLabel 内容的 id,用于关联 combobox 的名称;不传时在使用 insetLabel 插槽时由组件生成。stringundefined
showClear是否展示清除按钮。booleansemiDefault("Cascader", "showClear", false)
defaultOpen是否在挂载时展开下拉。booleansemiDefault("Cascader", "defaultOpen", false)
motion是否开启下拉的弹出动画。booleansemiDefault("Cascader", "motion", true)
autoAdjustOverflow下拉被边缘遮挡时是否自动调整展开方向。booleansemiGlobalDefault("Cascader", "autoAdjustOverflow")
position下拉的展开方向,默认 bottomLeft,RTL 下为 bottomRight。Type"top" | "bottom" | "left" | "right" | "topLeft" | "topRight" | "leftTop" | "leftBottom" | "rightTop" | "rightBottom" | "bottomLeft" | "bottomRight" | "leftTopOver" | "rightTopOver" | "leftBottomOver" | "rightBottomOver"semiGlobalDefault("Cascader", "position")
stopPropagation是否阻止下拉上的点击事件冒泡。string | booleansemiDefault("Cascader", "stopPropagation", true)
dropdownClassName下拉的 class。stringsemiGlobalDefault("Cascader", "dropdownClassName")
dropdownStyle下拉的样式。StyleValuesemiGlobalDefault("Cascader", "dropdownStyle")
preventScroll组件内的 focus 是否阻止浏览器滚动文档以显示获得焦点的元素。booleansemiGlobalDefault("Cascader", "preventScroll")
virtualizeInSearch搜索结果的虚拟列表配置,由 height、width、itemSize 组成,用于结果很多的场景;height 缺省为 180,与下拉框高度一致。VirtualizesemiGlobalDefault("Cascader", "virtualizeInSearch")

Events

事件说明回调参数
update:modelValue选中值变化时触发,是 v-model 的更新事件,载荷与 change 相同。Type[value: ByMultiple<Multiple, CascaderSimpleValue[], CascaderSimpleValue[][]>]
change选中值变化时触发。单选为所选路径,多选为各选中路径的数组;开启 changeWithObject 时为节点数据。与原生 change 同名,声明后会遮住根元素上冒泡的同名原生事件。Type[value: ByMultiple<Multiple, CascaderSimpleValue[], CascaderSimpleValue[][]>]
select选中某个节点时触发,载荷是该节点的 value;多选时只勾选了一个节点时是它的 value,勾选多个时是 value 数组。Type[value: ByMultiple<Multiple, string | number, string | number | (string | number)[]>]
search搜索输入变化时触发,载荷是输入的文本。[value: string]
focus点击或按键打开下拉时触发,载荷是那次 MouseEvent 或 KeyboardEvent。不是 DOM 焦点事件,Tab 进出不会触发。[event: MouseEvent | KeyboardEvent]
blur关闭下拉时触发:点击外部时载荷为 MouseEvent,Esc 为 KeyboardEvent,通过方法 close 关闭时为 undefined。不是 DOM 失焦事件。[event?: MouseEvent | KeyboardEvent | undefined]
dropdownVisibleChange下拉展开或收起时触发,载荷是是否展开。[visible: boolean]
clear点击清除按钮清空选中值时触发。[]
exceed多选时选中数量超出 max 时触发,载荷是已选节点的 Entity 数组。[checkedItem: CascaderEntity[]]
load节点加载完毕时触发,载荷是已加载的 key 集合与该节点的数据。Type[newLoadedKeys: Set<string>, data: CascaderData]
listScroll滚动列视图中的某一列时触发,载荷是滚动事件与该列的信息。搜索结果的滚动不触发。[event: Event, panel: CascaderScrollPanelProps]
keydown在 Cascader 处理按键之前触发,每次按键只触发一次;在其中调用 preventDefault 不会阻止 Cascader 的处理。除清除按钮与自定义触发器内控件上的按键外,事件在触发器的捕获阶段发出,此时调用 stopPropagation 会让搜索框收不到该键,并跳过触发器对它的处理(打开下拉、Esc 关闭),选项仍会处理该键。与原生 keydown 同名,会遮住根元素上冒泡的原生事件。[event: KeyboardEvent]

Slots

插槽说明作用域参数
label列视图中选项的内容;搜索结果、回显和标签仍显示节点的文本。CascaderLabelSlotProps
display单选时已选路径的回显,参数为各级节点的 displayProp 与节点数据,值不在 treeData 中时 path 为原始值;内容为空时显示 placeholder。CascaderDisplaySlotProps
tag多选时整体替换每个标签,onClose 取消该节点的选中。CascaderTagSlotProps
filterItem替换搜索结果中的一行;只有一个元素根时,该元素自动获得选项的 id、role 与状态,否则组件包一层 li。CascaderFilterItemSlotProps
trigger替换整个触发器内容。TypeCascaderTriggerSlotProps<Multiple>
expandIcon自定义展开图标;内容为空时使用默认图标。any
arrowIcon自定义右侧下拉箭头;空插槽隐藏箭头。有清除按钮可见时,悬停优先显示清除按钮。any
clearIcon自定义清除按钮的图标,showClear 为 true 时有效;内容为空时使用默认图标。any
emptyContent没有可选数据或搜索无结果时展示的内容;未提供时显示语言包中的默认文案,空插槽隐藏空状态。any
prefix前缀内容。any
suffix后缀内容。any
insetLabel嵌在选择框内的标签;同时提供 prefix 时不渲染。any
topSlot下拉顶部的附加内容。any
bottomSlot下拉底部的附加内容。any

Exposes

名称说明类型
无

Vue 版的 Cascader 没有 className 属性:class、style 和 data-* 等未声明的属性落在根元素(触发器)上,aria-label、aria-labelledby、aria-describedby、aria-errormessage、aria-invalid、aria-required 落在当前的 combobox 上(见 ARIA)。change、select、focus、blur、keydown 是组件的事件,会遮住根元素上同名的原生事件;focus、blur 在打开、关闭下拉时触发,不是 DOM 焦点事件。

CascaderData

属性说明类型默认值
children子节点CascaderData[]-
disabled不可选状态boolean-
isLeaf叶子节点boolean-
label展示的文本(必填),自定义内容使用 #label 插槽string|number-
loading正在加载boolean-
value属性值(必填)string|number-

Methods

Cascader 通过模板 ref 暴露 open()、close()、focus()、blur() 和 search(value),类型为 CascaderHandle。search 按输入了该值来搜索,需同时设置 filterTreeNode 开启搜索,searchPosition 为 custom 时自定义展示搜索框。

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

    const cascader = useTemplateRef<CascaderHandle>('cascader');
    const open = () => cascader.value?.open();
</script>

<template>
    <Cascader ref="cascader" :treeData="treeData" />
</template>

Accessibility

ARIA

  • Cascader 支持传入 aria-label、aria-describedby、aria-errormessage、aria-invalid、aria-labelledby、aria-required 来表示该 Cascader 的相关信息;
  • 这些 aria-* 属性落在当前的 combobox 上:不可搜索时是根元素(触发器),可搜索时是搜索框,多选且搜索框在触发器内时是标签输入框;
  • combobox 的名称按优先级取:aria-labelledby、aria-label、实际渲染出来的 insetLabel(combobox 的 aria-labelledby 指向它,未传 insetLabelId 时组件生成 id;有 prefix 时 insetLabel 不渲染,也不作名称),都没有时为语言包中的「级联选择」。下拉中的 tree 或搜索结果 listbox 使用同一个名称来源;
  • 按 ARIA 1.2 的 combobox 写法,任一时刻只有一个元素是 combobox,aria-controls 指向下拉中的列表,下拉与 combobox 同名。不可搜索时触发器是 select-only combobox,已选路径是它的值;可搜索时搜索框本身是 combobox,带 aria-autocomplete="list",触发器不再有角色和 Tab 停靠点。单选的搜索框只在搜索时出现,未搜索时触发器仍是 combobox;搜索框在输入前为空,已选路径以 aria-describedby 关联为它的描述;搜索中选中结果后焦点回到触发器;
  • 各列合起来是一棵 role="tree":选项为 treeitem,带 aria-level(所在列)、aria-setsize、aria-posinset、aria-disabled,可展开的选项带 aria-expanded;单选时用 aria-selected,多选时用 aria-checked(半选为 mixed);
  • 搜索结果是 role="listbox",选项为 option,用 aria-selected 表示选中,多选时表示勾选。#filterItem 只渲染一个元素根时,该元素会被加上 id、role="option" 和上述状态,否则组件包一层带这些属性的 li;
  • 选项前的勾选图标和多选复选框只是状态的图示,对读屏软件隐藏,也不接收焦点。

键盘交互

Cascader 按 WAI-ARIA Combobox 处理键盘:焦点始终留在触发器或搜索框上,aria-activedescendant 指向当前选项,当前选项显示焦点环。下拉刚打开时没有当前选项。

  • 下拉关闭时:ArrowDown、ArrowUp、Enter 打开下拉;不可搜索时,Space 也打开下拉,Home、End 打开下拉并移到第一列的第一个、最后一个选项。可搜索时,Space、Home、End 用于编辑文字
  • Enter:有当前选项时等同点击它,多选时勾选或取消勾选;禁用选项不响应
  • ArrowDown、ArrowUp:在当前选项所在的列(或搜索结果)中移到下一个、上一个选项,第一次按 ArrowDown 移到第一列的第一个选项;Home、End:移到这一列的第一个、最后一个选项
  • ArrowRight:展开当前选项,显示它的下一列;已展开时移到下一列的第一个选项(RTL 下与 ArrowLeft 互换)
  • ArrowLeft:收起展开的当前选项;否则移到上一列的父选项
  • Space:有当前选项时与 Enter 相同(焦点在搜索框里时用于输入)
  • 输入字符:下拉打开时,从当前选项往后,移到已显示的各列中第一个以输入内容开头的选项,各列保持不变;快速连续输入的字符合成一个搜索词。焦点在搜索框里时字符用于输入;下拉关闭时不响应
  • Esc:关闭下拉

焦点在搜索框里且已输入文字时,只有 ArrowDown、ArrowUp、Enter 用于移动和选择,其余按键用于编辑文字。方向键只移动当前选项,不像悬停那样自动展开下一列,也不触发 loadData;ArrowRight 展开时才加载。

showClear 开启、有已选值或搜索文本且未禁用时,触发器内渲染清除按钮。它是原生 button,名称为语言包中的「清空已选值」,在 hover、下拉打开或焦点位于 Cascader 内时显示。从触发器或搜索框按 Tab 移到清除按钮,Enter 或 Space 清空,效果与鼠标点击相同,清空后焦点回到触发器或搜索框。多选时也可在搜索框中用 Backspace 删除标签。

文案规范

  • 选择器选项
    • 如果没有默认选项,就使用 "Select" 做占位文案
    • 选项要按首字母顺序或者其他有逻辑的排列顺序,使用户更好地找到选项
    • 使用语句书写规范(首字母大写,其余小写),避免在句尾使用逗号和分号
    • 清晰表达出选项所表示的选择目的

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$transition_duration-cascader_option-bgvar(--semi-transition_duration-none)
$transition_function-cascader_option-bgvar(--semi-transition_function-easeIn)
$transition_delay-cascader_option-bgvar(--semi-transition_delay-none)
$radius-cascadervar(--semi-border-radius-small)级联选择触发器圆角
$radius-cascader_option_emptyvar(--semi-border-radius-medium)级联选择菜单空状态圆角
$color-cascader_default-border-defaulttransparent级联选择触发器描边颜色 - 默认
$color-cascader_default-border-hovertransparent级联选择触发器描边颜色 - 悬浮
$color-cascader_default-border-focusvar(--semi-color-focus-border)级联选择触发器描边颜色 - 选中
$color-cascader_default-bg-defaultvar(--semi-color-fill-0)级联选择触发器背景颜色 - 默认
$color-cascader_default-bg-hovervar(--semi-color-fill-1)级联选择触发器背景颜色 - 悬浮
$color-cascader_default-bg-activevar(--semi-color-fill-2)级联选择触发器背景颜色 - 按下
$color-cascader_option_list-border-defaultvar(--semi-color-fill-0)级联选择各级菜单分割线颜色
$color-cascader_option_main-text-defaultvar(--semi-color-text-0)级联选择菜单项文字颜色
$color-cascader_option-bg-hovervar(--semi-color-fill-0)级联选择菜单项背景颜色 - 悬浮
$color-cascader_option-bg-activevar(--semi-color-fill-1)级联选择菜单项背景颜色 - 按下
$color-cascader_option-bg-selectedvar(--semi-color-primary-light-default)级联选择菜单项背景颜色 - 命中
$color-cascader_option_disabled-text-defaultvar(--semi-color-disabled-text)级联选择菜单项文字颜色 - 禁用
$color-cascader_option_disabled-bg-hovertransparent级联选择菜禁用单项背景颜色 - 悬浮
$color-cascader_option_disabled-bg-activetransparent级联选择菜禁用单项背景颜色 - 按下
$color-cascader_tag_disabled-bg-defaulttransparent级联选择菜禁用Tag背景颜色
$color-cascader_warning-bg-defaultvar(--semi-color-warning-light-default)级联选择触发器警告背景颜色
$color-cascader_warning-border-defaultvar(--semi-color-warning-light-default)级联选择触发器警告描边颜色
$color-cascader_warning-bg-hovervar(--semi-color-warning-light-hover)级联选择触发器警告背景颜色 - 悬浮
$color-cascader_warning-border-hovervar(--semi-color-warning-light-hover)级联选择触发器警告描边颜色 - 悬浮
$color-cascader_warning-bg-focusvar(--semi-color-warning-light-default)级联选择触发器警告背景颜色 - 选中
$color-cascader_warning-border-focusvar(--semi-color-warning)级联选择触发器警告描边颜色 - 选中
$color-cascader_warning-bg-activevar(--semi-color-warning-light-active)级联选择触发器警告背景颜色 - 按下
$color-cascader_warning-border-activevar(--semi-color-warning-light-active)级联选择触发器警告描边颜色 - 按下
$color-cascader_danger-bg-defaultvar(--semi-color-danger-light-default)级联选择触发器危险背景颜色
$color-cascader_danger-border-defaultvar(--semi-color-danger-light-default)级联选择触发器危险描边颜色
$color-cascader_danger-bg-hovervar(--semi-color-danger-light-hover)级联选择触发器危险背景颜色 - 悬浮
$color-cascader_danger-border-hovervar(--semi-color-danger-light-hover)级联选择触发器危险描边颜色 - 悬浮
$color-cascader_danger-bg-focusvar(--semi-color-danger-light-default)级联选择触发器危险背景颜色 - 选中
$color-cascader_danger-border-focusvar(--semi-color-danger)级联选择触发器危险描边颜色 - 选中
$color-cascader_danger-bg-activevar(--semi-color-danger-light-active)级联选择触发器危险背景颜色 - 按下
$color-cascader_danger-border-activevar(--semi-color-danger-light-active)级联选择触发器危险描边颜色 - 按下
$color-cascader_prefix_suffix_text-defaultvar(--semi-color-text-2)级联选择前后缀文字颜色
$spacing-cascader_selection-paddingLeft12px级联选择触发器左侧内边距 - 默认
$spacing-cascader_selection-paddingRight12px级联选择触发器右侧内边距 - 默认
$spacing-cascader_small_selection-paddingLeft12px级联选择触发器左侧内边距 - 小尺寸
$spacing-cascader_small_selection-paddingRight12px级联选择触发器右侧内边距 - 小尺寸
$spacing-cascader_large_selection-paddingLeft12px级联选择触发器左侧内边距 - 大尺寸
$spacing-cascader_large_selection-paddingRight12px级联选择触发器右侧内边距 - 大尺寸
$spacing-cascader_selection_multiple-paddingLeft$spacing-extra-tight级联选择触发器多选时左侧内边距 - 默认
$spacing-cascader_selection_multiple-paddingRight$spacing-extra-tight级联选择触发器多选时右侧内边距 - 默认
$spacing-cascader_small_selection_multiple-paddingLeft$spacing-extra-tight级联选择触发器多选时左侧内边距 - 小尺寸
$spacing-cascader_small_selection_multiple-paddingRight$spacing-extra-tight级联选择触发器多选时左侧内边距 - 小尺寸
$spacing-cascader_large_selection_multiple-paddingLeft$spacing-extra-tight级联选择触发器多选时左侧内边距 - 大尺寸
$spacing-cascader_large_selection_multiple-paddingRight$spacing-extra-tight级联选择触发器多选时左侧内边距 - 大尺寸
$spacing-cascader_selection_tag-marginLeft$spacing-none级联选择触发器多选时标签的水平左外边距
$spacing-cascader_selection_tag-marginRight$spacing-super-tight级联选择触发器多选时标签的水平右外边距
$spacing-cascader_selection_tag-marginY1px级联选择触发器多选时标签的垂直外边距
$spacing-cascader_selection_tagInput-marginLeft-$spacing-extra-tight级联选择触发器多选搜索时 TagInput 的左外边距
$spacing-cascader_selection_input-marginLeft$spacing-extra-tight级联选择触发器多选搜索时输入框的左外边距
$spacing-cascader_selection_n-marginRight$spacing-extra-tight超出 maxTagCount 后,+n 的右外边距
$spacing-cascader_option-icon-marginLeft8px级联选择菜单项图标左侧外边距
$color-cascader_selection_n-text-defaultvar(--semi-color-text-0)超出 maxTagCount 后,+n 的文字默认颜色
$color-cascader_selection_n-text-disabledvar(--semi-color-disabled-text)超出 maxTagCount 后,+n 的文字disabled颜色
$color-cascader_selection_text_inactivevar(--semi-color-text-2)级联选择单选inpu输入框和text并存时,text颜色
$color-cascader_selection-text-defaultvar(--semi-color-text-0)级联选择选中项文字颜色
$color-cascader_placeholder-text-defaultvar(--semi-color-text-2)级联选择未选中项文字颜色
$color-cascader-icon-defaultvar(--semi-color-text-2)级联选择触发器图标颜色 - 默认
$color-cascader-icon-hovervar(--semi-color-primary-hover)级联选择触发器图标颜色 - 悬浮
$color-cascader-icon-activevar(--semi-color-primary-active)级联选择触发器图标颜色 - 按下
$color-cascader_option-icon-default$color-cascader-icon-default级联选择菜单项图标颜色
$color-cascader_input_disabled-bg-defaultvar(--semi-color-disabled-fill)级联选择禁用态背景颜色
$color-cascader_input_disabled-bg-hovervar(--semi-color-disabled-fill)级联选择禁用态背景颜色 - 悬浮
$color-cascader_input_disabled-text-defaultvar(--semi-color-disabled-text)级联选择禁用态文字颜色
$color-cascader_input_disabled-icon-defaultvar(--semi-color-disabled-text)级联选择禁用态图标颜色
$color-cascader_search-border-defaultvar(--semi-color-fill-0)级联选择搜索描边颜色
$color-cascader_option_empty-text-defaultvar(--semi-color-disabled-text)级联选择空状态文字颜色
$color-cascader_input-bg-defaulttransparent级联选择输入框背景颜色
$color-cascader_input-border-defaultnone级联选择输入框描边颜色
$color-cascader_select_highlightvar(--semi-color-primary)级联选择器搜索命中后文字高亮的颜色
$font-cascader_select-fontWeight$font-weight-bold级联选择菜单项字重 - 选中
$font-cascader-fontWeight$font-weight-regular级联选择菜单项字重 - 未选中
$font-cascader_selection_n-fontSize$font-size-small超出 maxTagCount 后,+n 的文字尺寸
$font-cascader_prefix_suffix_fontWeight$font-weight-bold级联选择 prefix/suffix 文字字重
$width-cascader-border1px级联选择触发器描边宽度
$width-cascader_option_list-border1px级联选择各级菜单分割线宽度
$width-cascader80px级联选择触发器最小宽度
$height-cascader_default$height-control-default级联选择触发器高度 - 默认
$height-cascader_small$height-control-small级联选择触发器高度 - 小尺寸
$height-cascader_large$height-control-large级联选择触发器高度 - 大尺寸
$width-cascader_hover-border$width-cascader-border级联选择触发器描边宽度 - 悬浮
$width-cascader_focus-border$border-thickness-control-focus级联选择触发器描边宽度 - 选中态
$width-cascader_search-border1px级联选择触搜索描边宽度
$width-cascader-icon32px级联选择触发器图标尺寸
$width-cascader-option-icon16px级联选择菜单项图标尺寸
$width-cascader_option150px级联选择各级菜单宽度
$height-cascader_option_list180px级联选择菜单高度
$height-cascader_selection_tagInput_wrapper_small22px级联选择多选搜索时搜索框最小高度 - 小尺寸
$height-cascader_selection_tagInput_wrapper_default30px级联选择多选搜索时搜索框最小高度 - 默认尺寸
$height-cascader_selection_tagInput_wrapper_large38px级联选择多选搜索时搜索框最小高度 - 大尺寸
$height-cascader_selection_wrapper_small22px级联选择单选搜索时搜索框高度 - 小尺寸
$height-cascader_selection_wrapper30px级联选择单选搜索时搜索框高度 - 默认尺寸
$height-cascader_selection_wrapper_large38px级联选择单选搜索时搜索框高度 - 大尺寸
$spacing-cascader_text-marginX$spacing-base-tight级联选择 prefix/suffix 文字水平内间距
$spacing-cascader_icon-marginX$spacing-tight级联选择 prefix/suffix 图标水平内间距
$spacing-cascader_label_checkbox-marginRight$spacing-tight级联选择 checkbox 的右间距
$spacing-cascader_selection_n-paddingX$spacing-tight级联选择 +N 的水平间距
$spacing-cascader_flatten_list-paddingRight64px级联选择搜索结果菜单右侧内边距
$spacing-cascader_empty_icon-marginRight8px级联选择搜索结果菜单
$spacing-cacader_option_list-paddingY4px级联选择下拉菜单列表垂直内边距
$spacing-cacader_option_list-paddingX0px级联选择下拉菜单列表水平内边距
$spacing-cascader_search-paddingX12px级联选择搜索水平内边距
$spacing-cascader_search-paddingY8px级联选择搜索垂直内边距
$spacing-cascader_option_empty-paddingY8px级联选择空结果垂直内边距
$spacing-cascader_option_empty-paddingX12px级联选择空结果水平内边距
$spacing-cascader_option-paddingTop8px级联选择菜单项顶部内边距
$spacing-cascader_option-paddingBottom8px级联选择菜单项底部内边距
$spacing-cascader_option-paddingLeft12px级联选择菜单项左侧内边距
$spacing-cascader_option-paddingRight16px级联选择菜单项右侧内边距