UNPKG

@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
--- 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',