@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
1,842 lines (1,691 loc) • 85.4 kB
Markdown
---
localeCode: zh-CN
order: 36
category: 输入类
title: Cascader 级联选择
icon: doc-cascader
brief: 用于选择多级分类下的某个选项。
---
## 使用场景
与 TreeSelect 组件的区别:
- TreeSelect: 核心价值在于**目标节点**,层级结构是为了方便用户快速筛选出目标选项,最终的节点才是用户想要的内容,常见于文件/文件夹选择、组织架构、权限分配等场景。
- Cascader: 核心价值在于**路径**,用户选择的不是一个孤立的点,而是一条从根到叶的完整路径,常用于地理位置,商品分类等场景。
## 代码演示
### 如何引入
```jsx import
import { Cascader } from '@douyinfe/semi-ui';
```
### 基本用法
最简单的用法,默认只可以选叶子节点。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
/>
);
};
```
### 多选
设置 `multiple`,可以进行多选。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<Cascader
defaultValue={['zhejiang', 'ningbo', 'jiangbei']}
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
multiple
/>
);
};
```
### 可搜索的
通过设置 `filterTreeNode` 属性可支持搜索功能。
默认对 `label` 值进行搜索(使用字符串的 includes 方法进行匹配,不区分大小写),可通过 `treeNodeFilterProp` 指定其他属性值进行搜索。
如 `label` 为 ReactNode,可在 treeData 中使用其他字段存储纯文本,并通过 `treeNodeFilterProp` 指定该字段进行搜索。
默认搜索结果只会展示叶子结点的路径,想要显示更多的结果,可以设置 `filterLeafOnly` 为 `false`。
```jsx live=true
import React, { useState } from 'react';
import { Cascader, Typography } from '@douyinfe/semi-ui';
() => {
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 labelNodeTreeData = [
{
label: <Tooltip content="说明">浙江省</Tooltip>,
labelText: '浙江省',
value: 'zhejiang',
children: [
{
label: <Tooltip content="说明">杭州市</Tooltip>,
labelText: '杭州市',
value: 'hangzhou',
children: [
{
label: <Tooltip content="说明">西湖区</Tooltip>,
labelText: '西湖区',
value: 'xihu',
},
{
label: <Tooltip content="说明">萧山区</Tooltip>,
labelText: '萧山区',
value: 'xiaoshan',
},
{
label: <Tooltip content="说明">临安区</Tooltip>,
labelText: '临安区',
value: 'linan',
},
],
},
{
label: <Tooltip content="说明">宁波市</Tooltip>,
labelText: '宁波市',
value: 'ningbo',
children: [
{
label: <Tooltip content="说明">海曙区</Tooltip>,
labelText: '海曙区',
value: 'haishu',
},
{
label: <Tooltip content="说明">江北区</Tooltip>,
labelText: '江北区',
value: 'jiangbei',
}
]
},
],
}
];
return (
<div>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="默认对label值进行搜索"
filterTreeNode
/>
<br/>
<br/>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="对value值进行搜索"
filterTreeNode
treeNodeFilterProp='value'
/>
<br/>
<br/>
<Typography.Title heading={6}>filterLeafOnly=false:</Typography.Title>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="filterLeafOnly=false"
filterTreeNode
filterLeafOnly={false}
/>
<br/>
<br/>
<Typography.Title heading={6}>Label 为 ReactNode,指定其他属性进行搜索</Typography.Title>
<Cascader
style={{ width: 300 }}
treeData={labelNodeTreeData}
placeholder="Search for labelText"
filterTreeNode
treeNodeFilterProp='labelText'
/>
</div>
);
};
```
### 可搜索的多选
支持多选和搜索同时使用,在这种场景下,可以通过按下 BackSpace 键来删除对应的已选项目。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const [value, setValue] = useState(['zhejiang', 'ningbo', 'haishu']);
const onChange = (val) => { setValue(val); };
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
value={value}
multiple
filterTreeNode
onChange={e => onChange(e)}
/>
);
};
```
可以使用 `filterSorter` 对筛选后的数据进行排序, `filterSorter` 于 v2.28.0 开始提供。
```jsx live=true
import React, { useState } from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
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',
},
],
}
];
return (
<div>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="输入 s 查看排序效果"
filterTreeNode
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;
}
}}
/>
</div>
);
};
```
如果想要自定义渲染搜索后的选项,可以使用 `filterRender` 实现整行的自定义渲染,`filterRender` 于 v2.28.0 开始提供,函数参数如下:
``` tsx
interface FilterRenderProps {
className: string;
inputValue: string; // 搜索栏搜索内容
disabled: boolean; // 是否禁用
data: CascaderData[]; // 搜索结果数据
selected: boolean; // 单选时的选中状态
checkStatus: { // 多选时的选中状态
checked: boolean;
halfChecked: boolean;
};
onClick: (e: React.MouseEvent) => void; // 单选点击选中回调
onCheck: (e: React.MouseEvent) => void; // 多选点击选中回调
}
```
使用示例如下
```jsx live=true
import React, { useState } from 'react';
import { Cascader, Typography, Checkbox } from '@douyinfe/semi-ui';
() => {
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 { Text } = Typography;
const renderSearchOptionSingle = (props) => {
const { className, data, selected, onClick } = props;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<li
className={className}
style={{ justifyContent: 'flex-start' }}
role="treeitem"
onClick={onClick}
>
<Text
ellipsis={{ showTooltip: { opts: { style: { wordBreak: 'break-all' } } } }}
style={{ width: 270, color: selected ? 'var(--semi-color-primary)': undefined }}
>
{data.map(item => item.label ).join(' / ')}
</Text>
</li>
);
};
const renderSearchOptionMultiple = (props) => {
const { className, data, checkStatus, onCheck } = props;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<li
className={className}
style={{ justifyContent: 'flex-start' }}
role="treeitem"
onClick={onCheck}
>
<Checkbox
onChange={onCheck}
indeterminate={checkStatus.halfChecked}
checked={checkStatus.checked}
style={{ marginRight: 8 }}
/>
<Text
ellipsis={{ showTooltip: { opts: { style: { wordBreak: 'break-all' } } } }}
style={{ width: 250 }}
>
{data.map(item => item.label).join(' / ')}
</Text>
</li>
);
};
return (
<div>
<p>鼠标 hover 到选项可查看被省略文本完整内容</p>
<br />
<Cascader
style={{ width: 320 }}
treeData={treeData}
placeholder="单选,输入 s 自定义搜索选项渲染结果"
filterTreeNode
filterRender={renderSearchOptionSingle}
/>
<br />
<Cascader
multiple
style={{ width: 320, marginTop: 20 }}
treeData={treeData}
placeholder="多选,输入 s 自定义搜索选项渲染结果"
filterTreeNode
filterRender={renderSearchOptionMultiple}
/>
</div>
);
};
```
如果搜索结果中存在大量 Option,可以通过设置 virtualizeInSearch 开启搜索结果面板的虚拟化来优化性能,virtualizeInSearch 自 v2.44.0 提供。virtualizeInSearch 是一个包含下列值的对象:
- height: Option 列表高度值
- width: Option 列表宽度值
- itemSize: 每行 Option 的高度
```jsx live=true
import React from 'react';
import { Cascader, Checkbox, Typography } from '@douyinfe/semi-ui';
() => {
const treeData = useMemo(() => (
['通用', '场景'].map((label, m) => ({
label: label,
value: m,
children: new Array(100).fill(0).map((item, n)=> ({
value: `${m}-${n}`,
label: `${m}-${n} 第二级`,
children: new Array(20).fill(0).map((item, o)=> ({
value: `${m}-${n}-${o}`,
label: `${m}-${n}-${o} 第三级详细内容`,
})),
}))
}))
), []);
let virtualize = {
// 高度为面板默认高度为 180px 减去上下padding 2 * 8px
height: 172,
width: 320,
itemSize: 36,
};
const filterRender = useCallback((props) => {
const { data, onCheck, checkStatus, className } = props;
return (
<div
key={data.value}
className={className}
style={{ justifyContent: 'start', padding: '8px 16px 8px 12px', boxSizing: 'border-box' }}
>
<Checkbox
onChange={onCheck}
indeterminate={checkStatus.halfChecked}
checked={checkStatus.checked}
style={{ marginRight: 8 }}
/>
<Typography.Text
ellipsis={{ showTooltip: { opts: { style: { wordBreak: 'break-all' } } } }}
style={{ maxWidth: 260 }}
>
{data.map(item => item.label).join(' | ')}
</Typography.Text>
</div>
);
}, []);
return (
<Cascader
multiple
filterTreeNode
style={{ width: 320 }}
treeData={treeData}
placeholder="输入 通用 or 场景 进行搜索"
virtualizeInSearch={virtualize}
filterRender={filterRender}
/>
);
};
```
### 限制标签展示数量
在多选的场景中,利用 maxTagCount 可以限制展示的标签数量,超出部分将以 +N 的方式展示。
使用 showRestTagsPopover 可以设置在超出 maxTagCount 后,hover +N 是否显示 Popover,默认为 false。并且,还可以在 restTagsPopoverProps 属性中配置 Popover。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
multiple
showRestTagsPopover={true}
restTagsPopoverProps={{ position: 'top' }}
maxTagCount={1}
defaultValue={[
['zhejiang', 'ningbo', 'haishu'],
['zhejiang', 'hangzhou', 'xihu']
]}
/>
);
};
```
### 限制选中数量
在多选的场景中,利用 max 可以限制多选选中的数量。超出 max 后将触发 onExceed 回调。
```jsx live=true
import React from 'react';
import { Cascader, Toast } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
multiple
max={1}
onExceed={v=>{
Toast.warning('exceed max');
console.log(v);
}}
defaultValue={['zhejiang', 'ningbo', 'haishu']}
/>
);
};
```
### 选择即改变
在单选的情况下,还可以通过设置 `changeOnSelect`,允许选中父级选项。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<div>
<Cascader
style={{ width: 300 }}
treeData={treeData}
changeOnSelect
placeholder="选择即改变"
/>
<br/>
<br/>
<Cascader
style={{ width: 300 }}
treeData={treeData}
changeOnSelect
placeholder="可搜索的选择即改变"
filterTreeNode
/>
</div>
);
};
```
### 自定义显示
可以通过 `displayProp` 设置回填选项显示的属性值,默认为 `label`。
```jsx live=true
import React from 'react';
import { Cascader, Typography } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<>
<Typography.Title heading={6}>单选</Typography.Title>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="回填时显示数据的value值"
displayProp='value'
defaultValue={['zhejiang', 'ningbo', 'jiangbei']}
/>
<br />
<br />
<Typography.Title heading={6}>多选</Typography.Title>
<Cascader
multiple
style={{ width: 300 }}
treeData={treeData}
defaultValue={['zhejiang', 'ningbo', 'jiangbei']}
placeholder="回填时显示数据的value值"
displayProp='value'
/>
</>
);
};
```
可以通过设置 `displayRender` 可以设定返回格式。
单选 (`multiple=false`) 时, `displayRender((labelPath: string[]) => ReactNode)`, 其中 labelPath 是由 label 构成的 path 数组。
多选 (`multiple=true`) 时, `displayRender((item: Entity, index: number) => ReactNode)`, 其中 item 为节点的相关数据。
```typescript
interface Entity {
children?: Entity[]; // children list
data: CascaderData; // treedata
ind: number; // index
key: string; // key
level: number; // node level
parent?: Entity; // parent data
parentKey?: string; // parent key
path: string[]; // key path
valuePath: string[]; // value path
}
```
```jsx live=true
import React from 'react';
import { Cascader, Tag, Typography } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<>
<Typography.Title heading={6}>单选</Typography.Title>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="自定义回填时显示数据的格式"
displayRender={list => '已选择:' + list.join(' -> ')}
defaultValue={['zhejiang', 'ningbo', 'jiangbei']}
/>
<br />
<br />
<Typography.Title heading={6}>多选</Typography.Title>
<Cascader
multiple
style={{ width: 300 }}
treeData={treeData}
defaultValue={['zhejiang', 'ningbo', 'jiangbei']}
placeholder="自定义回填时显示数据的格式"
displayRender={(item, idx) => (
<Tag
style={{ marginRight: 4 }}
color='white'
key={`${idx}-${item.data.label}`}
>
{item.data.label}
</Tag>
)}
/>
</>
);
};
```
### 自定义分隔符
版本: >=2.2.0
可以使用 `separator` 设置分隔符, 包括:搜索时显示在下拉框的内容以及单选时回显到 Trigger 的内容的分隔符。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
defaultValue={['zhejiang', 'ningbo', 'jiangbei']}
filterTreeNode
separator=' > '
/>
);
};
```
### 禁用
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
}
],
}
];
return (
<div>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
disabled
/>
<br />
<br />
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
defaultValue={['zhejiang', 'hangzhou', 'xihu']}
filterTreeNode
disabled
/>
</div>
);
};
```
### 严格禁用
可以使用 disableStrictly 来开启严格禁用。开启严格禁用后,当节点是 disabled 的时候,则不能通过子级或者父级的关系改变选中状态。
以下面的 demo 为例,节点"宁波"开启了严格禁用,因此,当我们改变其父节点"浙江省"的选中状态时,也不会影响到节点"宁波"的选中状态。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
multiple
placeholder="请选择所在地区"
disableStrictly
/>
);
};
```
### 展示子菜单的时机
可以使用 `showNext` 设置展开 Dropdown 子菜单的触发时机,可选: `click`(默认)、`hover`。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
}
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
showNext="hover"
/>
);
};
```
### 点击选中
在多选模式下,默认情况下点击非叶子节点不会触发选中。你可以通过 `clickToSelect` 开启点击任意节点即选中的功能。
这个 API 在配合 `showNext="hover"` 使用时特别有用:鼠标悬浮展开子菜单,点击则选中当前节点。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
],
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
multiple
placeholder="请选择所在地区"
showNext="hover"
clickToSelect
/>
);
};
```
### 在顶部/底部渲染附加项
我们在级联选择器的顶部、底部分别预留了插槽,你可以通过 `topSlot` 或 `bottomSlot` 来设置。
```jsx live=true
import React from 'react';
import { Cascader, Typography } from '@douyinfe/semi-ui';
() => {
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
bottomSlot={
<div style={slotStyle}>
<Text>找不到相关选项?</Text>
<Text link>去新建</Text>
</div>
}
/>
);
};
```
### 受控
传入 `value` 时即为受控组件,可以配合 `onChange` 使用。
```jsx live=true hideInDSM
import React, { useState } from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const [value, setValue] = useState([]);
const onChange = (newValue) => {
setValue(newValue);
};
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择所在地区"
value={value}
onChange={onChange}
/>
);
};
```
### 自动合并 value
在多选(multiple=true)场景中,当我们选中祖先节点时,如果希望 value 不包含它对应的子孙节点,则可以通过 `autoMergeValue` 来设置,默认为 true。当 autoMergeValue 和 leafOnly 同时开启时,后者优先级更高。
```jsx live=true
import React, { useState } from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const [value, setValue] = useState([]);
const onChange = value => {
console.log(value);
setValue(value);
};
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="autoMergeValue 为 false"
value={value}
multiple
autoMergeValue={false}
onChange={e => onChange(e)}
/>
);
};
```
### 仅叶子节点
版本: >=2.2.0
在多选时,可以通过开启 leafOnly 来设置 value 只包含叶子节点,即显示的 Tag 和 onChange 的参数 value 只包含 value。
```jsx live=true
import React, { useState } from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const [value, setValue] = useState([]);
const onChange = value => {
console.log(value);
setValue(value);
};
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',
}
]
},
],
}
];
return (
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="开启 leafOnly"
value={value}
multiple
leafOnly
onChange={e => onChange(e)}
/>
);
};
```
### 节点选中关系
版本:>= 2.71.0
多选时,可以使用 `checkRelation` 来设置节点之间选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated' 时,意味着节点之间的选中互不影响。
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const treeData = [
{
label: '亚洲',
value: 'Asia',
children: [
{
label: '中国',
value: 'China',
children: [
{
label: '北京',
value: 'Beijing',
},
{
label: '上海',
value: 'Shanghai',
},
],
},
],
},
{
label: '北美洲',
value: 'North America',
}
];
return (
<Cascader
multiple
defaultValue={[
['Asia'],
['Asia', 'China', 'Beijing']
]}
checkRelation='unRelated'
style={{ width: 300 }}
treeData={treeData}
/>
);
};
```
### 动态更新数据
```jsx live=true hideInDSM
import React, { useState } from 'react';
import { Cascader, Button } from '@douyinfe/semi-ui';
() => {
const [treeData, setTreeData] = useState([]);
const add = () => {
let itemLength = Math.floor(Math.random() * 3) + 1;
let newTreeData = new Array(itemLength).fill(0).map((v, i) => {
let length = Math.floor(Math.random() * 3);
let children = new Array(length).fill(0).map((cv, ci) => {
let child = {
key: `${i}-${ci}`,
label: `Item-${i}-${ci}`,
value: `${i}-${ci}`
};
return child;
});
let item = {
key: `${i}`,
label: `Item-${i}`,
value: `${i}`,
children
};
return item;
});
setTreeData(newTreeData);
};
return (
<>
<Cascader
style={{ width: 300 }}
treeData={treeData}
placeholder="请选择"
/>
<br/>
<br/>
<Button onClick={add}>
动态改变数据
</Button>
</>
);
};
```
### 异步加载数据
可以使用 loadData 实现异步加载数据
**不能与搜索同时使用**
```jsx live=true hideInDSM
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const initialData = [
{
label: 'Node1',
value: '0-0',
},
{
label: 'Node2',
value: '0-1',
},
{
label: 'Node3',
value: '0-2',
isLeaf: true
},
];
const [data, setData] = useState(initialData);
const updateTreeData = (list, value, children) => {
return 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 => {
const targetOpt = selectedOpt[selectedOpt.length - 1];
const { label, value } = targetOpt;
return new Promise(resolve => {
if (targetOpt.children) {
resolve();
return;
}
setTimeout(() => {
setData(origin =>
updateTreeData(origin, value, [
{
label: `${label} - 1`,
value: `${label}-1`,
isLeaf: selectedOpt.length > 1
},
{
label: `${label} - 2`,
value: `${label}-2`,
isLeaf: selectedOpt.length > 1
},
]),
);
resolve();
}, 1000);
});
};
return (
<Cascader
style={{ width: 300 }}
treeData={data}
loadData={onLoadData}
placeholder="Please select"
/>
);
};
```
### 远程搜索
**v>=2.97.0** 起 Cascader 支持远程搜索。设置 `remote` 后,搜索输入不再走本地匹配,而是仅触发 `onSearch` 回调,由你根据输入异步拉取 `treeData`。这与 Select 的 `remote` 行为一致。
<Notice title='使用说明'>
搜索时建议自行处理以下几点:
- **防抖(debounce)**:避免每次按键都打请求,常见做法是用 `lodash.debounce` 包裹搜索逻辑(如 200~300ms)。
- **竞态保护**:用一个递增 token 或 `AbortController` 丢弃过期请求结果,避免后发先到时旧响应覆盖新响应。
- **loading 提示**:在请求期间通过外层 `Spin` 或简单的提示元素告知用户「加载中」,否则用户会看到「暂无数据」误以为无结果。
- **初始 / 空输入处理**:输入被清空时把 `treeData` 还原为初始数据,避免空查询拉数据。
</Notice>
```jsx live=true
import React, { useRef, useState, useCallback } from 'react';
import { Cascader, Spin } from '@douyinfe/semi-ui';
import { debounce } from 'lodash';
() => {
const [treeData, setTreeData] = useState([]);
const [loading, setLoading] = useState(false);
// 用 ref 保存最新一次请求的 token,丢弃过期响应
const reqTokenRef = useRef(0);
// 模拟远程搜索接口
const fetchByKeyword = (keyword) =>
new Promise((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 handleSearch = useCallback(
debounce((input) => {
if (!input) {
setTreeData([]);
setLoading(false);
return;
}
const token = ++reqTokenRef.current;
setLoading(true);
fetchByKeyword(input).then((next) => {
// 后发先到时直接丢弃过期结果
if (token !== reqTokenRef.current) {
return;
}
setTreeData(next);
setLoading(false);
});
}, 300),
[]
);
return (
<Spin spinning={loading}>
<Cascader
style={{ width: 300 }}
placeholder="输入关键词远程搜索"
filterTreeNode
remote
treeData={treeData}
onSearch={handleSearch}
onChange={(v) => console.log('selected:', v)}
/>
</Spin>
);
};
```
### 超长列表
当你的数据结构层级特别深时,Cascader下拉菜单可能会超出屏幕,此时我们建议为下拉菜单设置 overflow-x: auto 以及一个合适的 width 宽度( 建议以N+0.5列的宽度为准,最右侧显示半列,以给用户一种右侧尚有待展开项,可以水平方向滚动的视觉暗示)
```jsx live=true
import React from 'react';
import { Cascader } from '@douyinfe/semi-ui';
() => {
const treeData = [
{
label: 'A',
value: 'A',
children: [
{
label: 'B',
value: 'B',