Vue3级联选择(Cascader)

发布时间:2026/8/29 13:11:13
Vue3级联选择(Cascader) Vue2级联选择Cascader可自定义设置以下属性可选项数据源options类型Option[]默认 []下拉字典项的文本字段名label类型string默认 label下拉字典项的值字段名value类型string默认 value下拉字典项的后代字段名children类型string默认 children三级选择器各自占位文本placeholder类型string | string[]默认 请选择是否禁用disabled可全部禁用或单独禁用某一级选择器类型boolean | boolean[]默认 false三级选择器各自宽度width单位 px类型auto | number | number[]默认 auto级联选择高度height单位 px类型number默认 undefined级联选择大小size类型small | middle | large默认 middle级联选择器相互间隙宽度gap单位 px类型number默认 undefined当此项为 true 时点选每级菜单选项值 (v-model) 都会发生变化否则只有选择第三级选项后选项值才会变化changeOnSelect类型boolean默认 false是否支持清除allowClear类型boolean默认 false是否支持搜索search类型boolean默认 false下拉面板弹出位置placement类型bottom | top默认 bottom下拉面板被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置flip类型boolean默认 true下拉面板挂载的容器节点to类型string | HTMLElement | false默认body可选元素标签名 (例如body) 或者元素本身false会待在原地过滤条件函数filter仅当支持搜索时生效类型Function | true根据输入项进行筛选默认为 true 时筛选每个选项的文本字段 label 是否包含输入项包含返回 true反之返回 false当其为函数 Function 时接受 inputValue option 两个参数当 option 符合筛选条件时应返回 true反之则返回 false下拉面板最多能展示的项数maxDisplay超过后滚动显示类型number默认 6下拉面板滚动条scrollbar组件属性配置scrollbarProps类型object默认{}参考 Scrollbar Props级联选中项v-model:modelValue类型number[] | string[]默认 []效果如下图展开图在线预览①创建级联选择组件Cascader.vue其中引入使用了以下组件Vue3选择器Selectscript setup langts import { ref, computed, watchEffect } from vue import Select from components/select export interface Option { label?: string // 选项名 value?: string | number // 选项值 disabled?: boolean // 是否禁用选项默认 false children?: Option[] // 选项 children 数组 [propName: string]: any // 添加一个字符串索引签名用于包含带有任意数量的其他属性 } export interface Props { options?: Option[] // 可选项数据源 label?: string // 字典项的文本字段名 value?: string // 字典项的值字段名 children?: string // 字典项的后代字段名 placeholder?: string | string[] // 三级选择器各自占位文本 disabled?: boolean | boolean[] // 是否禁用可全部禁用或单独禁用某一级选择器 width?: auto | number | number[] // 三级选择器各自宽度单位 px height?: number // 级联选择高度单位 px size?: small | middle | large // 级联选择大小 gap?: number // 级联选择器相互间隙宽度单位 px changeOnSelect?: boolean // 当此项为 true 时点选每级菜单选项值v-model都会发生变化否则只有选择第三级选项后选项值才会变化 allowClear?: boolean // 是否支持清除 search?: boolean // 是否支持搜索 placement?: bottom | top // 下拉面板弹出位置 flip?: boolean // 下拉面板被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置 to?: string | HTMLElement | false // 下拉面板挂载的容器节点可选元素标签名 (例如 body) 或者元素本身false 会待在原地 /* 根据输入项进行筛选默认为 true 时筛选每个选项的文本字段 label 是否包含输入项包含返回 true反之返回 false 当其为函数 Function 时接受 inputValue option 两个参数当 option 符合筛选条件时应返回 true反之则返回 false */ filter?: Function | true // 过滤条件函数仅当支持搜索时生效 maxDisplay?: number // 下拉面板最多能展示的项数超过后滚动显示 scrollbarProps?: object // 下拉面板滚动条 scrollbar 组件属性配置 modelValue?: number[] | string[] //v-model级联选中项 } const props withDefaults(definePropsProps(), { options: () [], label: label, value: value, children: children, placeholder: 请选择, disabled: false, width: auto, height: undefined, size: middle, gap: undefined, changeOnSelect: false, allowClear: false, search: false, placement: bottom, flip: true, to: body, filter: true, maxDisplay: 6, scrollbarProps: () ({}), modelValue: () [] }) const values ref(string | number)[]([]) // 级联 value 值数组 const labels ref(string | number)[]([]) // 级联 label 文本数组 const firstOptions refOption[]([]) const secondOptions refOption[]([]) const thirdOptions refOption[]([]) const emits defineEmits([update:modelValue, change]) const selectGap computed(() { const gapMap { small: 4, middle: 6, large: 8 } if (props.gap ! undefined) { return ${props.gap}px } return ${gapMap[props.size]}px }) watchEffect(() { firstOptions.value [...props.options] }) watchEffect(() { values.value [...props.modelValue] }) watchEffect(() { initCascader(values.value) initLabels(values.value) }) function findChildren(options: Option[], index: number): Option[] { const len options.length for (let i 0; i len; i) { if (options[i][props.value] values.value[index]) { return options[i][props.children] || [] } } return [] } // 获取二级/三级下拉项 function initCascader(values: (string | number)[]): void { secondOptions.value findChildren(firstOptions.value, 0) thirdOptions.value [] if (values.length 1) { thirdOptions.value findChildren(secondOptions.value, 1) } } function findLabel(options: Option[], index: number): string | number { const len options.length for (let i 0; i len; i) { if (options[i][props.value] values.value[index]) { return options[i][props.label] } } return values.value[index] } function initLabels(values: (string | number)[]): void { labels.value[0] findLabel(firstOptions.value, 0) if (values.length 1) { labels.value[1] findLabel(secondOptions.value, 1) } if (values.length 2) { labels.value[2] findLabel(thirdOptions.value, 2) } } // 一级下拉回调 function onFirstChange(value: string | number, label: string): void { if (props.changeOnSelect) { emits(update:modelValue, [value]) emits(change, [value], [label]) } else { values.value [value] labels.value [label] } } // 二级下拉回调 function onSecondChange(value: string | number, label: string): void { if (props.changeOnSelect) { emits(update:modelValue, [values.value[0], value]) emits(change, [values.value[0], value], [labels.value[0], label]) } else { values.value [values.value[0], value] labels.value [labels.value[0], label] } } // 三级下拉回调 function onThirdChange(value: string | number, label: string): void { emits(update:modelValue, [...values.value.slice(0, 2), value]) emits(change, [...values.value.slice(0, 2), value], [...labels.value.slice(0, 2), label]) } /script template div classcascader-wrap :style--cascader-select-gap: ${selectGap}; Select :optionsfirstOptions :labellabel :valuevalue :placeholderArray.isArray(placeholder) ? placeholder[0] : placeholder :disabledArray.isArray(disabled) ? disabled[0] : disabled :widthArray.isArray(width) ? width[0] : width :heightheight :sizesize :allow-clearallowClear :searchsearch :placementplacement :flipflip :toto :filterfilter :max-displaymaxDisplay :scrollbar-propsscrollbarProps v-modelvalues[0] changeonFirstChange / Select :optionssecondOptions :labellabel :valuevalue :placeholderArray.isArray(placeholder) ? placeholder[1] : placeholder :disabledArray.isArray(disabled) ? disabled[1] : disabled :widthArray.isArray(width) ? width[1] : width :heightheight :sizesize :allow-clearallowClear :searchsearch :placementplacement :flipflip :toto :filterfilter :max-displaymaxDisplay :scrollbar-propsscrollbarProps v-modelvalues[1] changeonSecondChange / Select :optionsthirdOptions :labellabel :valuevalue :placeholderArray.isArray(placeholder) ? placeholder[2] : placeholder :disabledArray.isArray(disabled) ? disabled[2] : disabled :widthArray.isArray(width) ? width[2] : width :heightheight :sizesize :allow-clearallowClear :searchsearch :placementplacement :flipflip :toto :filterfilter :max-displaymaxDisplay :scrollbar-propsscrollbarProps v-modelvalues[2] changeonThirdChange / /div /template style langless scoped .cascader-wrap { display: inline-flex; gap: var(--cascader-select-gap); } /style②在要使用的页面引入​其中引入使用了以下组件Vue3间距Spacescript setup langts import Cascader from ./Cascader.vue import { ref, watchEffect } from vue const options ref([ { value: 1, label: 北京, children: [ { value: 11, label: 北京市, children: [ { value: 111, label: 东城区 }, { value: 112, label: 西城区 } ] } ] }, { value: 2, label: 浙江, children: [ { value: 21, label: 杭州市, children: [ { value: 211, label: 西湖区 }, { value: 212, label: 余杭区 } ] }, { value: 22, label: 湖州市, children: [ { value: 221, label: 吴兴区 }, { value: 222, label: 安吉区 } ] } ] } ]) const optionsDisabled ref([ { value: 1, label: 北京, disabled: true, children: [ { value: 11, label: 北京市, children: [ { value: 111, label: 东城区 }, { value: 112, label: 西城区 } ] } ] }, { value: 2, label: 浙江, children: [ { value: 21, label: 杭州市, children: [ { value: 211, label: 西湖区 }, { value: 212, label: 余杭区 } ] }, { value: 22, label: 湖州市, children: [ { value: 221, label: 吴兴区 }, { value: 222, label: 安吉区 } ] } ] } ]) const optionsCustom ref([ { code: 1, name: 北京, items: [ { code: 11, name: 北京市, items: [ { code: 111, name: 东城区 }, { code: 112, name: 西城区 } ] } ] }, { code: 2, name: 浙江, items: [ { code: 21, name: 杭州市, items: [ { code: 211, name: 西湖区 }, { code: 212, name: 余杭区 } ] }, { code: 22, name: 湖州市, items: [ { code: 221, name: 吴兴区 }, { code: 222, name: 安吉区 } ] } ] } ]) const sizeOptions [ { label: small, value: small }, { label: middle, value: middle }, { label: large, value: large } ] const size ref(large) const selectedValue ref([2, 21, 212]) watchEffect(() { console.log(selectedValue, selectedValue.value) }) function onChange(values: (number | string)[], labels: string[]) { console.log(values, values) console.log(labels, labels) } // 自定义过滤函数当选项的 value 值大于 输入项时返回 true function filter(inputValue: string, option: any) { return option.value inputValue } /script template div h1{{ $route.name }} {{ $route.meta.title }}/h1 h2 classmt30 mb10基本使用/h2 Cascader :optionsoptions v-modelselectedValue / h2 classmt30 mb10禁用/h2 Cascader :optionsoptions v-modelselectedValue disabled / h2 classmt30 mb10禁用某一级/h2 h3 classmb10只禁用第一级disabled: [true]/h3 h3 classmb10禁用前两级disabled: [true, true]/h3 Cascader :optionsoptions v-modelselectedValue :disabled[true] changeonChange / h2 classmt30 mb10禁用选项/h2 h3 classmb10只需指定 options 里的 disabled 字段/h3 Cascader :optionsoptionsDisabled v-modelselectedValue changeonChange / h2 classmt30 mb10自定义字段名/h2 Cascader :optionsoptionsCustom v-modelselectedValue labelname valuecode childrenitems changeonChange / h2 classmt30 mb10自定义样式/h2 Cascader :optionsoptions v-modelselectedValue :width100 :height36 :gap12 changeonChange / h2 classmt30 mb10三种尺寸/h2 Space vertical Radio :optionssizeOptions v-model:valuesize button button-stylesolid / Cascader :optionsoptions v-modelselectedValue :sizesize changeonChange / Cascader :optionsoptions v-modelselectedValue :sizesize allow-clear search changeonChange / /Space h2 classmt30 mb10选择即改变/h2 Cascader :optionsoptions v-modelselectedValue change-on-select changeonChange / h2 classmt30 mb10支持清除/h2 Cascader :optionsoptions v-modelselectedValue allow-clear changeonChange / h2 classmt30 mb10支持搜索/h2 Cascader :optionsoptions v-modelselectedValue search changeonChange / h2 classmt30 mb10自定义搜索过滤函数/h2 Cascader :optionsoptions v-modelselectedValue allow-clear search :filterfilter changeonChange / /div /template