Vue3级联选择(Cascader)

简介: 这是一个可定制的级联选择器组件,支持多级下拉选项。主要属性包括:数据源、文本字段、值字段、后代字段、占位符文本、选择行为、间距、宽度、高度、禁用状态、清除功能、搜索功能及过滤函数等。组件可根据需求灵活配置,并支持动态更新与事件回调。在线预览和详细示例可见[这里](https://themusecatcher.github.io/vue-amazing-ui/guide/components/cascader.html)。

可自定义设置以下属性:

  • 可选项数据源(options),类型:Option[],默认 [],其中 Option 类型:{ label?: string, value?: string | number, disabled?: boolean, children?: Option[], [propName: string]: any }

  • 下拉字典项的文本字段名(label),类型:string,默认 'label'

  • 下拉字典项的值字段名(value),类型:string,默认 'value'

  • 下拉字典项的后代字段名(children),类型:string,默认 'children'

  • 三级下拉各自占位文本(placeholder),类型:string | string[],默认 '请选择'

  • 当此项为 true 时,点选每级菜单选项值 (v-model) 都会发生变化;否则只有选择第三级选项后选项值才会变化(changeOnSelect),类型:boolean,默认 false

  • 级联下拉框相互间隙宽度(gap),类型:number,单位 px,默认 8

  • 三级下拉各自宽度(width),类型:'auto' | number | number[],默认 'auto'

  • 下拉框高度(height),类型:number,单位 px,默认 32

  • 三级各自是否禁用(disabled),类型:boolean | boolean[],默认 false

  • 是否支持清除(allowClear),类型:boolean,默认 false

  • 是否支持搜索(search),使用搜索时请设置 width,类型:boolean,默认 false

  • 过滤条件函数(filter),仅当支持搜索时生效,类型:Function | true;根据输入项进行筛选,默认为 true 时,筛选每个选项的文本字段 label 是否包含输入项,包含返回 true,反之返回 false;当其为函数 Function 时,接受 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false

  • 下拉面板最多能展示的下拉项数,超过后滚动显示(maxDisplay),类型:number,默认 6

  • 级联选中项(v-model:modelValue),类型:number[] | string[],默认 []

效果如下图:在线预览

展开图:

①创建级联选择组件Cascader.vue:

<script setup lang="ts">
import Select from '../select'
import { ref, watchEffect } from 'vue'
interface Option {
  label?: string // 选项名
  value?: string | number // 选项值
  disabled?: boolean // 是否禁用选项,默认 false
  children?: Option[] // 选项 children 数组
  [propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
}
interface Props {
  options?: Option[] // 可选项数据源
  label?: string // 下拉字典项的文本字段名
  value?: string // 下拉字典项的值字段名
  children?: string // 下拉字典项的后代字段名
  placeholder?: string | string[] // 三级下拉各自占位文本
  changeOnSelect?: boolean // 当此项为 true 时,点选每级菜单选项值(v-model)都会发生变化;否则只有选择第三级选项后选项值才会变化
  gap?: number // 级联下拉框相互间隙宽度,单位 px
  width?: 'auto' | number | number[] // 三级下拉各自宽度
  height?: number // 下拉框高度
  disabled?: boolean | boolean[] // 三级各自是否禁用
  allowClear?: boolean // 是否支持清除
  search?: boolean // 是否支持搜索,使用搜索时请设置 width
  /*
    根据输入项进行筛选,默认为 true 时,筛选每个选项的文本字段 label 是否包含输入项,包含返回 true,反之返回 false
    当其为函数 Function 时,接受 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false
  */
  filter?: Function | true // 过滤条件函数,仅当支持搜索时生效
  maxDisplay?: number // 下拉面板最多能展示的下拉项数,超过后滚动显示
  modelValue?: number[] | string[] //(v-model)级联选中项
}
const props = withDefaults(defineProps<Props>(), {
  options: () => [],
  label: 'label',
  value: 'value',
  children: 'children',
  placeholder: '请选择',
  changeOnSelect: false,
  gap: 8,
  width: 'auto',
  height: 32,
  disabled: false,
  allowClear: false,
  search: false,
  filter: true,
  maxDisplay: 6,
  modelValue: () => []
})
const values = ref<(string | number)[]>([]) // 级联value值数组
const labels = ref<string[]>([]) // 级联label文本数组
const firstOptions = ref<Option[]>([])
const secondOptions = ref<Option[]>([])
const thirdOptions = ref<Option[]>([])
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)[]) {
  // 获取二级/三级下拉项
  secondOptions.value = findChildren(firstOptions.value, 0)
  thirdOptions.value = []
  if (values.length > 1) {
    thirdOptions.value = findChildren(secondOptions.value, 1)
  }
}
function findLabel(options: Option[], index: number): any {
  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)[]) {
  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)
  }
}
const emits = defineEmits(['update:modelValue', 'change'])
function onFirstChange(value: string | number, label: string) {
  // 一级下拉回调
  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) {
  // 二级下拉回调
  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) {
  // 三级下拉回调
  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 class="m-cascader" :style="`height: ${height}px; gap: ${gap}px;`">
    <Select
      :options="firstOptions"
      :label="label"
      :value="value"
      :placeholder="Array.isArray(placeholder) ? placeholder[0] : placeholder"
      :disabled="Array.isArray(disabled) ? disabled[0] : disabled"
      :allow-clear="allowClear"
      :search="search"
      :filter="filter"
      :width="Array.isArray(width) ? width[0] : width"
      :height="height"
      :max-display="maxDisplay"
      v-model="values[0]"
      @change="onFirstChange"
    />
    <Select
      :options="secondOptions"
      :label="label"
      :value="value"
      :placeholder="Array.isArray(placeholder) ? placeholder[1] : placeholder"
      :disabled="Array.isArray(disabled) ? disabled[1] : disabled"
      :allow-clear="allowClear"
      :search="search"
      :filter="filter"
      :width="Array.isArray(width) ? width[1] : width"
      :height="height"
      :max-display="maxDisplay"
      v-model="values[1]"
      @change="onSecondChange"
    />
    <Select
      :options="thirdOptions"
      :label="label"
      :value="value"
      :placeholder="Array.isArray(placeholder) ? placeholder[2] : placeholder"
      :disabled="Array.isArray(disabled) ? disabled[2] : disabled"
      :allow-clear="allowClear"
      :search="search"
      :filter="filter"
      :width="Array.isArray(width) ? width[2] : width"
      :height="height"
      :max-display="maxDisplay"
      v-model="values[2]"
      @change="onThirdChange"
    />
  </div>
</template>
<style lang="less" scoped>
.m-cascader {
  display: inline-flex;
}
</style>

②在要使用的页面引入:

<script setup lang="ts">
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 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)
}
function onAntChange(values: (number | string)[], selectedOptions: any) {
  console.log('values:', values)
  console.log('selectedOptions:', selectedOptions)
}
// 自定义过滤函数,当选项的 value 值大于 输入项时返回 true
function filter(inputValue: string, option: any) {
  return option.value > inputValue
}
</script>
<template>
  <div>
    <h1>{
  { $route.name }} {
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Cascader :options="options" v-model="selectedValue" />
    <h2 class="mt30 mb10">禁用</h2>
    <Cascader :options="options" v-model="selectedValue" disabled />
    <h2 class="mt30 mb10">禁用某一级</h2>
    <h3 class="mb10">只禁用第一级:disabled: [true]</h3>
    <h3 class="mb10">禁用前两级:disabled: [true, true]</h3>
    <Cascader :options="options" v-model="selectedValue" :disabled="[true]" @change="onChange" />
    <h2 class="mt30 mb10">禁用选项</h2>
    <h3 class="mb10">只需指定 options 里的 disabled 字段</h3>
    <Cascader :options="optionsDisabled" v-model="selectedValue" @change="onChange" />
    <h2 class="mt30 mb10">选择即改变</h2>
    <Cascader :options="options" v-model="selectedValue" change-on-select @change="onChange" />
    <h2 class="mt30 mb10">支持清除</h2>
    <Cascader :options="options" v-model="selectedValue" allow-clear @change="onChange" />
    <h2 class="mt30 mb10">支持搜索</h2>
    <Cascader :options="options" :width="100" v-model="selectedValue" search @change="onChange" />
    <h2 class="mt30 mb10">自定义搜索过滤函数</h2>
    <Cascader :options="options" :width="100" v-model="selectedValue" search :filter="filter" @change="onChange" />
    <h2 class="mt30 mb10">自定义样式</h2>
    <Cascader :options="options" v-model="selectedValue" :width="120" :height="36" :gap="12" @change="onChange" />
    <h2 class="mt30 mb10">自定义字段名</h2>
    <Cascader
      :options="optionsCustom"
      v-model="selectedValue"
      label="name"
      value="code"
      children="items"
      @change="onChange"
    />
    <h2 class="mt30 mb10">Ant Design Vue 级联选择</h2>
    <a-cascader
      :options="options"
      style="width: 200px"
      placeholder="Please select"
      :disabled="false"
      allowClear
      v-model:value="selectedValue"
      @change="onAntChange"
    />
  </div>
</template>
相关文章
|
16天前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
164 11
|
5月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
760 5
|
2月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
282 1
|
2月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
151 0
|
3月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
101 0
|
5月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
435 17
|
6月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点
|
6月前
|
存储 JavaScript 前端开发
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
425 6
|
5月前
|
JavaScript 前端开发 API
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
1517 0
|
7月前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
455 2