使用场景
与 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 的高度
代码
<script setup lang="ts">
import { Cascader, Checkbox, Typography, type Ellipsis } from '@fibrous/semi-ui-vue';
const treeData = ['通用', '场景'].map((label, m) => ({
label,
value: m,
children: Array.from({ length: 100 }, (_item, n) => ({
value: `${m}-${n}`,
label: `${m}-${n} 第二级`,
children: Array.from({ length: 20 }, (_item, o) => ({
value: `${m}-${n}-${o}`,
label: `${m}-${n}-${o} 第三级详细内容`,
})),
})),
}));
const virtualize = {
// Height = default panel 180px minus 2 * 8px padding
height: 172,
width: 320,
itemSize: 36,
};
const ellipsis: Ellipsis = { showTooltip: { opts: { style: { wordBreak: 'break-all' } } } };
</script>
<template>
<Cascader filterTreeNode multiple placeholder="输入 通用 or 场景 进行搜索" style="width: 320px" :treeData="treeData" :virtualizeInSearch="virtualize">
<template #filterItem="{ class: className, data, onCheck, checkStatus }">
<div :class="className" :style="{ justifyContent: 'start', padding: '8px 16px 8px 12px', boxSizing: 'border-box' }">
<Checkbox :indeterminate="checkStatus.halfChecked" :modelValue="checkStatus.checked" :style="{ marginRight: '8px' }" @change="onCheck" />
<Typography.Text :ellipsis="ellipsis" :style="{ maxWidth: '260px' }">
{{ data.map((item) => item.label).join(' | ') }}
</Typography.Text>
</div>
</template>
</Cascader>
</template>限制标签展示数量
在多选的场景中,利用 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 行为一致。
代码
<script setup lang="ts">
import { ref } from 'vue';
import { debounce } from 'es-toolkit';
import { Cascader, Spin, type CascaderData } from '@fibrous/semi-ui-vue';
const fetchByKeyword = (keyword: string) =>
new Promise<CascaderData[]>((resolve) => {
const delay = 200 + Math.floor(Math.random() * 800);
setTimeout(() => {
if (!keyword) {
resolve([]);
return;
}
resolve([
{ label: `${keyword} - 选项 A`, value: `${keyword}-a` },
{ label: `${keyword} - 选项 B`, value: `${keyword}-b` },
{ label: `${keyword} - 选项 C`, value: `${keyword}-c` },
]);
}, delay);
});
const treeData = ref<CascaderData[]>([]);
const loading = ref(false);
let latestToken = 0;
const handleSearch = debounce((input: string) => {
if (!input) {
treeData.value = [];
loading.value = false;
return;
}
latestToken += 1;
const token = latestToken;
loading.value = true;
void fetchByKeyword(input).then((next) => {
// Drop stale results that arrive after a newer request
if (token !== latestToken) {
return;
}
treeData.value = next;
loading.value = false;
});
}, 300);
</script>
<template>
<Spin :spinning="loading">
<Cascader filterTreeNode placeholder="输入关键词远程搜索" remote style="width: 300px" :treeData="treeData" @change="(v) => console.log('selected:', v)" @search="handleSearch" />
</Spin>
</template>超长列表
当你的数据结构层级特别深时,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 的类型随它变化;它没有全局默认值。 | Multiple | undefined |
| treeData | 节点数据,label 只接受文本,节点内容用 label 插槽。原地修改响应式数据也会被识别,内容相同的新数组不会重置展开和选中状态。 | CascaderData[] | semiDefault("Cascader", "treeData", []) |
| keyMaps | 自定义节点中 value、label、children、disabled、isLeaf 的字段名。 | KeyMapProps | semiGlobalDefault("Cascader", "keyMaps") |
| changeOnSelect | 单选时是否允许选择非叶子节点。 | boolean | semiDefault("Cascader", "changeOnSelect", false) |
| autoMergeValue | 多选时,某个父节点被选中后,值不再包含它的子孙节点。挂载后修改不会生效;与 leafOnly 同时开启时,后者优先。 | boolean | semiDefault("Cascader", "autoMergeValue", true) |
| leafOnly | 多选时,值与展示的标签只包含叶子节点。挂载后修改不会生效。 | boolean | semiDefault("Cascader", "leafOnly", false) |
| checkRelation | 多选时节点之间选中状态的关系:related 为父子联动,unRelated 为互不影响。 | "related" | "unRelated" | semiDefault("Cascader", "checkRelation", strings.RELATED) |
| disableStrictly | 是否严格禁用。开启后,禁用的节点不会因父级或子级的选中状态变化而改变。 | boolean | semiDefault("Cascader", "disableStrictly", false) |
| max | 多选时选中数量的上限,超出时触发 exceed 事件。 | number | semiGlobalDefault("Cascader", "max") |
| maxTagCount | 多选时标签的最大展示数量,超出的部分以 +N 展示。 | number | semiGlobalDefault("Cascader", "maxTagCount") |
| showRestTagsPopover | 超出 maxTagCount 后,悬停 +N 时是否用 Popover 展示剩余标签。 | boolean | semiDefault("Cascader", "showRestTagsPopover", false) |
| restTagsPopoverProps | 剩余标签 Popover 的配置,可设置 position、trigger 等,参见 Popover。 | PopoverProps | semiGlobalDefault("Cascader", "restTagsPopoverProps") |
| enableLeafClick | 多选时,点击叶子节点所在的选项是否触发勾选。 | boolean | semiDefault("Cascader", "enableLeafClick", false) |
| clickToSelect | 多选时,点击任意节点即触发选中,常与 showNext 为 hover 配合:悬停展开子菜单,点击选中当前节点。 | boolean | semiDefault("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 | 搜索结果是否只展示叶子节点的路径。 | boolean | semiDefault("Cascader", "filterLeafOnly", true) |
| treeNodeFilterProp | 搜索时用于匹配输入的节点属性,默认 label。 | string | semiDefault("Cascader", "treeNodeFilterProp", "label") |
| remote | 是否开启远程搜索。开启后输入不再做本地过滤,只触发 search 事件,由使用方异步更新 treeData。 | boolean | semiDefault("Cascader", "remote", false) |
| displayProp | 回填选项和标签显示的节点属性,默认 label。 | string | semiDefault("Cascader", "displayProp", "label") |
| separator | 分隔符,用于搜索结果和单选回填到触发器的路径。 | string | semiDefault("Cascader", "separator", " / ") |
| changeWithObject | 是否让 change 事件携带每个节点的数据而不是它的 value。开启后受控的 modelValue 与 defaultValue 也要传节点数据,并含有 value。这是布尔属性,不是事件,不要写成 @changeWithObject。 | boolean | semiDefault("Cascader", "changeWithObject", false) |
| placeholder | 选择框的占位文字。 | string | semiGlobalDefault("Cascader", "placeholder") |
| searchPlaceholder | 搜索框的占位文字,不传时使用 placeholder。 | string | semiGlobalDefault("Cascader", "searchPlaceholder") |
| searchPosition | 搜索框的位置:trigger 在触发器内,custom 由使用方自定义,此时用 search 方法传入搜索词。 | "trigger" | "custom" | TypesemiDefault("Cascader", "searchPosition", strings.SEARCH_POSITION_TRIGGER) |
| disabled | 是否禁用。禁用后不可 Tab 到达。 | boolean | semiDefault("Cascader", "disabled", false) |
| size | 选择框的尺寸。 | Size | semiDefault("Cascader", "size", "default") |
| borderless | 是否使用无边框模式。 | boolean | semiDefault("Cascader", "borderless", false) |
| validateStatus | 触发器的校验状态,只影响样式。 | "default" | "error" | "warning" | "success" | semiGlobalDefault("Cascader", "validateStatus") |
| insetLabelId | insetLabel 内容的 id,用于关联 combobox 的名称;不传时在使用 insetLabel 插槽时由组件生成。 | string | undefined |
| showClear | 是否展示清除按钮。 | boolean | semiDefault("Cascader", "showClear", false) |
| defaultOpen | 是否在挂载时展开下拉。 | boolean | semiDefault("Cascader", "defaultOpen", false) |
| motion | 是否开启下拉的弹出动画。 | boolean | semiDefault("Cascader", "motion", true) |
| autoAdjustOverflow | 下拉被边缘遮挡时是否自动调整展开方向。 | boolean | semiGlobalDefault("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 | boolean | semiDefault("Cascader", "stopPropagation", true) |
| dropdownClassName | 下拉的 class。 | string | semiGlobalDefault("Cascader", "dropdownClassName") |
| dropdownStyle | 下拉的样式。 | StyleValue | semiGlobalDefault("Cascader", "dropdownStyle") |
| preventScroll | 组件内的 focus 是否阻止浏览器滚动文档以显示获得焦点的元素。 | boolean | semiGlobalDefault("Cascader", "preventScroll") |
| virtualizeInSearch | 搜索结果的虚拟列表配置,由 height、width、itemSize 组成,用于结果很多的场景;height 缺省为 180,与下拉框高度一致。 | Virtualize | semiGlobalDefault("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 变量 | 值 | 用法 |
|---|---|---|
| $transition_duration-cascader_option-bg | var(--semi-transition_duration-none) | |
| $transition_function-cascader_option-bg | var(--semi-transition_function-easeIn) | |
| $transition_delay-cascader_option-bg | var(--semi-transition_delay-none) | |
| $radius-cascader | var(--semi-border-radius-small) | 级联选择触发器圆角 |
| $radius-cascader_option_empty | var(--semi-border-radius-medium) | 级联选择菜单空状态圆角 |
| $color-cascader_default-border-default | transparent | 级联选择触发器描边颜色 - 默认 |
| $color-cascader_default-border-hover | transparent | 级联选择触发器描边颜色 - 悬浮 |
| $color-cascader_default-border-focus | var(--semi-color-focus-border) | 级联选择触发器描边颜色 - 选中 |
| $color-cascader_default-bg-default | var(--semi-color-fill-0) | 级联选择触发器背景颜色 - 默认 |
| $color-cascader_default-bg-hover | var(--semi-color-fill-1) | 级联选择触发器背景颜色 - 悬浮 |
| $color-cascader_default-bg-active | var(--semi-color-fill-2) | 级联选择触发器背景颜色 - 按下 |
| $color-cascader_option_list-border-default | var(--semi-color-fill-0) | 级联选择各级菜单分割线颜色 |
| $color-cascader_option_main-text-default | var(--semi-color-text-0) | 级联选择菜单项文字颜色 |
| $color-cascader_option-bg-hover | var(--semi-color-fill-0) | 级联选择菜单项背景颜色 - 悬浮 |
| $color-cascader_option-bg-active | var(--semi-color-fill-1) | 级联选择菜单项背景颜色 - 按下 |
| $color-cascader_option-bg-selected | var(--semi-color-primary-light-default) | 级联选择菜单项背景颜色 - 命中 |
| $color-cascader_option_disabled-text-default | var(--semi-color-disabled-text) | 级联选择菜单项文字颜色 - 禁用 |
| $color-cascader_option_disabled-bg-hover | transparent | 级联选择菜禁用单项背景颜色 - 悬浮 |
| $color-cascader_option_disabled-bg-active | transparent | 级联选择菜禁用单项背景颜色 - 按下 |
| $color-cascader_tag_disabled-bg-default | transparent | 级联选择菜禁用Tag背景颜色 |
| $color-cascader_warning-bg-default | var(--semi-color-warning-light-default) | 级联选择触发器警告背景颜色 |
| $color-cascader_warning-border-default | var(--semi-color-warning-light-default) | 级联选择触发器警告描边颜色 |
| $color-cascader_warning-bg-hover | var(--semi-color-warning-light-hover) | 级联选择触发器警告背景颜色 - 悬浮 |
| $color-cascader_warning-border-hover | var(--semi-color-warning-light-hover) | 级联选择触发器警告描边颜色 - 悬浮 |
| $color-cascader_warning-bg-focus | var(--semi-color-warning-light-default) | 级联选择触发器警告背景颜色 - 选中 |
| $color-cascader_warning-border-focus | var(--semi-color-warning) | 级联选择触发器警告描边颜色 - 选中 |
| $color-cascader_warning-bg-active | var(--semi-color-warning-light-active) | 级联选择触发器警告背景颜色 - 按下 |
| $color-cascader_warning-border-active | var(--semi-color-warning-light-active) | 级联选择触发器警告描边颜色 - 按下 |
| $color-cascader_danger-bg-default | var(--semi-color-danger-light-default) | 级联选择触发器危险背景颜色 |
| $color-cascader_danger-border-default | var(--semi-color-danger-light-default) | 级联选择触发器危险描边颜色 |
| $color-cascader_danger-bg-hover | var(--semi-color-danger-light-hover) | 级联选择触发器危险背景颜色 - 悬浮 |
| $color-cascader_danger-border-hover | var(--semi-color-danger-light-hover) | 级联选择触发器危险描边颜色 - 悬浮 |
| $color-cascader_danger-bg-focus | var(--semi-color-danger-light-default) | 级联选择触发器危险背景颜色 - 选中 |
| $color-cascader_danger-border-focus | var(--semi-color-danger) | 级联选择触发器危险描边颜色 - 选中 |
| $color-cascader_danger-bg-active | var(--semi-color-danger-light-active) | 级联选择触发器危险背景颜色 - 按下 |
| $color-cascader_danger-border-active | var(--semi-color-danger-light-active) | 级联选择触发器危险描边颜色 - 按下 |
| $color-cascader_prefix_suffix_text-default | var(--semi-color-text-2) | 级联选择前后缀文字颜色 |
| $spacing-cascader_selection-paddingLeft | 12px | 级联选择触发器左侧内边距 - 默认 |
| $spacing-cascader_selection-paddingRight | 12px | 级联选择触发器右侧内边距 - 默认 |
| $spacing-cascader_small_selection-paddingLeft | 12px | 级联选择触发器左侧内边距 - 小尺寸 |
| $spacing-cascader_small_selection-paddingRight | 12px | 级联选择触发器右侧内边距 - 小尺寸 |
| $spacing-cascader_large_selection-paddingLeft | 12px | 级联选择触发器左侧内边距 - 大尺寸 |
| $spacing-cascader_large_selection-paddingRight | 12px | 级联选择触发器右侧内边距 - 大尺寸 |
| $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-marginY | 1px | 级联选择触发器多选时标签的垂直外边距 |
| $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-marginLeft | 8px | 级联选择菜单项图标左侧外边距 |
| $color-cascader_selection_n-text-default | var(--semi-color-text-0) | 超出 maxTagCount 后,+n 的文字默认颜色 |
| $color-cascader_selection_n-text-disabled | var(--semi-color-disabled-text) | 超出 maxTagCount 后,+n 的文字disabled颜色 |
| $color-cascader_selection_text_inactive | var(--semi-color-text-2) | 级联选择单选inpu输入框和text并存时,text颜色 |
| $color-cascader_selection-text-default | var(--semi-color-text-0) | 级联选择选中项文字颜色 |
| $color-cascader_placeholder-text-default | var(--semi-color-text-2) | 级联选择未选中项文字颜色 |
| $color-cascader-icon-default | var(--semi-color-text-2) | 级联选择触发器图标颜色 - 默认 |
| $color-cascader-icon-hover | var(--semi-color-primary-hover) | 级联选择触发器图标颜色 - 悬浮 |
| $color-cascader-icon-active | var(--semi-color-primary-active) | 级联选择触发器图标颜色 - 按下 |
| $color-cascader_option-icon-default | $color-cascader-icon-default | 级联选择菜单项图标颜色 |
| $color-cascader_input_disabled-bg-default | var(--semi-color-disabled-fill) | 级联选择禁用态背景颜色 |
| $color-cascader_input_disabled-bg-hover | var(--semi-color-disabled-fill) | 级联选择禁用态背景颜色 - 悬浮 |
| $color-cascader_input_disabled-text-default | var(--semi-color-disabled-text) | 级联选择禁用态文字颜色 |
| $color-cascader_input_disabled-icon-default | var(--semi-color-disabled-text) | 级联选择禁用态图标颜色 |
| $color-cascader_search-border-default | var(--semi-color-fill-0) | 级联选择搜索描边颜色 |
| $color-cascader_option_empty-text-default | var(--semi-color-disabled-text) | 级联选择空状态文字颜色 |
| $color-cascader_input-bg-default | transparent | 级联选择输入框背景颜色 |
| $color-cascader_input-border-default | none | 级联选择输入框描边颜色 |
| $color-cascader_select_highlight | var(--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-border | 1px | 级联选择触发器描边宽度 |
| $width-cascader_option_list-border | 1px | 级联选择各级菜单分割线宽度 |
| $width-cascader | 80px | 级联选择触发器最小宽度 |
| $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-border | 1px | 级联选择触搜索描边宽度 |
| $width-cascader-icon | 32px | 级联选择触发器图标尺寸 |
| $width-cascader-option-icon | 16px | 级联选择菜单项图标尺寸 |
| $width-cascader_option | 150px | 级联选择各级菜单宽度 |
| $height-cascader_option_list | 180px | 级联选择菜单高度 |
| $height-cascader_selection_tagInput_wrapper_small | 22px | 级联选择多选搜索时搜索框最小高度 - 小尺寸 |
| $height-cascader_selection_tagInput_wrapper_default | 30px | 级联选择多选搜索时搜索框最小高度 - 默认尺寸 |
| $height-cascader_selection_tagInput_wrapper_large | 38px | 级联选择多选搜索时搜索框最小高度 - 大尺寸 |
| $height-cascader_selection_wrapper_small | 22px | 级联选择单选搜索时搜索框高度 - 小尺寸 |
| $height-cascader_selection_wrapper | 30px | 级联选择单选搜索时搜索框高度 - 默认尺寸 |
| $height-cascader_selection_wrapper_large | 38px | 级联选择单选搜索时搜索框高度 - 大尺寸 |
| $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-paddingRight | 64px | 级联选择搜索结果菜单右侧内边距 |
| $spacing-cascader_empty_icon-marginRight | 8px | 级联选择搜索结果菜单 |
| $spacing-cacader_option_list-paddingY | 4px | 级联选择下拉菜单列表垂直内边距 |
| $spacing-cacader_option_list-paddingX | 0px | 级联选择下拉菜单列表水平内边距 |
| $spacing-cascader_search-paddingX | 12px | 级联选择搜索水平内边距 |
| $spacing-cascader_search-paddingY | 8px | 级联选择搜索垂直内边距 |
| $spacing-cascader_option_empty-paddingY | 8px | 级联选择空结果垂直内边距 |
| $spacing-cascader_option_empty-paddingX | 12px | 级联选择空结果水平内边距 |
| $spacing-cascader_option-paddingTop | 8px | 级联选择菜单项顶部内边距 |
| $spacing-cascader_option-paddingBottom | 8px | 级联选择菜单项底部内边距 |
| $spacing-cascader_option-paddingLeft | 12px | 级联选择菜单项左侧内边距 |
| $spacing-cascader_option-paddingRight | 16px | 级联选择菜单项右侧内边距 |