级联框的实现

下面的代码能够实现如下效果:
1.只展示最小级在输入框中;
2.可以任意选中一个层级;
3.自定义组件;

1.父组件

<template>
  <CascaderSelect
    v-model="selectedValue"
    :options="options"
    placeholder="请选择分类"
    :header-labels="['分类', '子分类', '组件']"
    @change="handleChange"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import CascaderSelect from './CascaderSelect.vue'
import type { CascaderOption } from './CascaderSelect.vue'

const selectedValue = ref<string[]>([])

const options: CascaderOption[] = [
  {
    name: 'component',
    nameCn: '组件',
    nameEn: 'Component',
    children: [
      {
        name: 'basic',
        nameCn: '基础组件',
        nameEn: 'Basic',
        children: [
          { name: 'button', nameCn: '按钮', nameEn: 'Button' },
          { name: 'input', nameCn: '输入框', nameEn: 'Input' }
        ]
      }
    ]
  }
]

const handleChange = (value: string[], selectedOptions: CascaderOption[]) => {
  console.log('选中值:', value)
  console.log('选中选项:', selectedOptions)
}
</script>

2.子组件

<template>
  <div class="cascader-select">
    <!-- 输入框 -->
    <div 
      class="input-wrapper"
      @click="toggleDropdown"
    >
      <input
        v-model="displayValue"
        :placeholder="placeholder"
        class="cascader-input"
        readonly
      />
      <span 
        class="arrow-icon"
        :class="{ 'arrow-up': showDropdown }"
      ></span>
    </div>

    <!-- 级联下拉框 -->
    <div 
      v-show="showDropdown" 
      class="cascader-dropdown"
    >
      <div class="cascader-content">
        <!-- 级联头部标注 -->
        <div class="cascader-header">
          <div 
            v-for="(panel, level) in cascaderPanels"
            :key="`header-${level}`"
            class="cascader-header-item"
          >
            {{ getHeaderLabel(level) }}
          </div>
        </div>
        
        <!-- 级联面板 -->
        <div class="cascader-panels">
          <div
            v-for="(panel, level) in cascaderPanels"
            :key="level"
            class="cascader-panel"
          >
            <div
              v-for="option in panel"
              :key="option.name"
              class="cascader-option"
              :class="{
                'is-active': isOptionActive(option, level),
                'is-selected': isOptionSelected(option, level),
                'has-children': option.children && option.children.length > 0
              }"
              @click="handleOptionClick(option, level)"
            >
              <span class="option-label">{{ option.nameCn }}</span>
              <span 
                v-if="option.children && option.children.length > 0"
                class="option-arrow"
              ></span>
            </div>
          </div>
        </div>
      </div>

      <!-- 操作按钮 -->
      <div class="cascader-footer">
        <button 
          class="btn btn-cancel"
          @click="handleCancel"
        >
          取消
        </button>
        <button 
          class="btn btn-confirm"
          @click="handleConfirm"
          :disabled="!tempSelectedPath.length"
        >
          确定
        </button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'

// 类型定义
export interface CascaderOption {
  name: string
  nameCn: string
  nameEn: string
  children?: CascaderOption[]
}

export interface CascaderProps {
  options: CascaderOption[]
  modelValue?: string[]
  placeholder?: string
  headerLabels?: string[]
}

export interface CascaderEmits {
  (event: 'update:modelValue', value: string[]): void
  (event: 'change', value: string[], selectedOptions: CascaderOption[]): void
}

// 定义props,带默认值
const props = withDefaults(defineProps<CascaderProps>(), {
  modelValue: () => [],
  placeholder: '请选择',
  headerLabels: () => ['分类', '子分类', '组件', '详细', '子项']
})

// 定义emits
const emit = defineEmits<CascaderEmits>()

// 响应式数据
const showDropdown = ref<boolean>(false)
const tempSelectedPath = ref<string[]>([...props.modelValue])
const selectedPath = ref<string[]>([...props.modelValue])

// 计算属性 - 级联面板数据
const cascaderPanels = computed<CascaderOption[][]>(() => {
  const panels: CascaderOption[][] = [props.options]
  
  let currentOptions: CascaderOption[] = props.options
  for (let i = 0; i < tempSelectedPath.value.length; i++) {
    const selectedValue = tempSelectedPath.value[i]
    const selectedOption = currentOptions.find((option: CascaderOption) => option.name === selectedValue)
    
    if (selectedOption && selectedOption.children && selectedOption.children.length > 0) {
      panels.push(selectedOption.children)
      currentOptions = selectedOption.children
    } else {
      break
    }
  }
  
  return panels
})

// 计算属性 - 显示值(只显示最后一级的label)
const displayValue = computed<string>(() => {
  if (!selectedPath.value.length) return ''
  
  let currentOptions: CascaderOption[] = props.options
  let finalLabel = ''
  
  for (const value of selectedPath.value) {
    const option = currentOptions.find((opt: CascaderOption) => opt.name === value)
    if (option) {
      finalLabel = option.nameCn
      currentOptions = option.children || []
    }
  }
  
  return finalLabel
})

// 方法 - 切换下拉框显示状态
const toggleDropdown = (): void => {
  showDropdown.value = !showDropdown.value
  if (showDropdown.value) {
    // 打开时重置临时选中路径
    tempSelectedPath.value = [...selectedPath.value]
  }
}

// 方法 - 检查选项是否处于激活状态
const isOptionActive = (option: CascaderOption, level: number): boolean => {
  return tempSelectedPath.value[level] === option.name
}

// 方法 - 检查选项是否已选中
const isOptionSelected = (option: CascaderOption, level: number): boolean => {
  return selectedPath.value[level] === option.name
}

// 方法 - 处理选项点击
const handleOptionClick = (option: CascaderOption, level: number): void => {
  const newPath = tempSelectedPath.value.slice(0, level)
  newPath.push(option.name)
  tempSelectedPath.value = newPath
}

// 方法 - 确认选择
const handleConfirm = (): void => {
  selectedPath.value = [...tempSelectedPath.value]
  
  // 获取选中的完整选项对象
  const selectedOptions: CascaderOption[] = []
  let currentOptions: CascaderOption[] = props.options
  
  for (const value of selectedPath.value) {
    const option = currentOptions.find((opt: CascaderOption) => opt.name === value)
    if (option) {
      selectedOptions.push(option)
      currentOptions = option.children || []
    }
  }
  
  // 触发事件
  emit('update:modelValue', selectedPath.value)
  emit('change', selectedPath.value, selectedOptions)
  
  // 关闭下拉框
  showDropdown.value = false
}

// 方法 - 取消选择
const handleCancel = (): void => {
  tempSelectedPath.value = [...selectedPath.value]
  showDropdown.value = false
}

// 方法 - 获取头部标签
const getHeaderLabel = (level: number): string => {
  return props.headerLabels[level] || `级别 ${level + 1}`
}

// 监听外部值变化
watch(() => props.modelValue, (newValue: string[]) => {
  selectedPath.value = [...newValue]
  tempSelectedPath.value = [...newValue]
}, { deep: true })

// 点击外部关闭下拉框
const handleClickOutside = (event: Event): void => {
  const target = event.target as HTMLElement
  const cascaderElement = document.querySelector('.cascader-select') as HTMLElement
  
  if (cascaderElement && !cascaderElement.contains(target)) {
    showDropdown.value = false
  }
}

// 添加/移除全局点击监听
watch(showDropdown, (isShow: boolean) => {
  if (isShow) {
    document.addEventListener('click', handleClickOutside)
  } else {
    document.removeEventListener('click', handleClickOutside)
  }
})

// 组件卸载时清理事件监听
import { onUnmounted } from 'vue'
onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside)
})
</script>

<style scoped>
.cascader-select {
  position: relative;
  display: inline-block;
  width: 100%;
}

.input-wrapper {
  position: relative;
  cursor: pointer;
}

.cascader-input {
  width: 100%;
  padding: 12px 40px 12px 16px;
  border: 2px solid #e9ecef;
  border-radius: 8px;
  background-color: #fff;
  cursor: pointer;
  transition: all 0.3s ease;
  font-size: 14px;
  line-height: 1.5;
  box-sizing: border-box;
}

.cascader-input:hover {
  border-color: #667eea;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

.cascader-input:focus {
  outline: none;
  border-color: #667eea;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}

.arrow-icon {
  position: absolute;
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: #adb5bd;
  font-size: 12px;
  transition: transform 0.3s ease;
  pointer-events: none;
}

.arrow-icon.arrow-up {
  transform: translateY(-50%) rotate(180deg);
}

.cascader-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  border: 2px solid #e9ecef;
  border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
  z-index: 2000;
  margin-top: 8px;
  max-height: 450px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.cascader-content {
  flex: 1;
  overflow: hidden;
}

.cascader-header {
  display: flex;
  border-bottom: 2px solid #e9ecef;
  background: #fff;
}

.cascader-header-item {
  flex: 1;
  min-width: 100px;
  width: 100px;
  padding: 8px 12px;
  font-weight: 600;
  font-size: 13px;
  color: #667eea;
  text-align: left;
  border-right: 1px solid #dee2e6;
  background: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

.cascader-header-item:last-child {
  border-right: none;
}

.cascader-panels {
  display: flex;
  max-height: 350px;
  overflow: auto;
}

.cascader-panel {
  flex: 1;
  min-width: 100px;
  width: 100px;
  border-right: 1px solid #e9ecef;
  background: #fff;
}

.cascader-panel:last-child {
  border-right: none;
}

.cascader-option {
  padding: 8px 12px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 13px;
  color: #495057;
  transition: all 0.2s ease;
  position: relative;
}

.cascader-option:hover {
  background-color: #f8f9fa;
  color: #667eea;
}

.cascader-option.is-active {
  color: #667eea;
  background-color: #e7f3ff;
  font-weight: 500;
}

.cascader-option.is-selected {
  color: #667eea;
  font-weight: 600;
}

.cascader-option.has-children {
  padding-right: 40px;
}

.option-label {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.option-arrow {
  color: #adb5bd;
  font-size: 12px;
  margin-left: 8px;
  font-weight: bold;
}

.cascader-footer {
  border-top: 1px solid #e9ecef;
  padding: 16px 20px;
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  background: #fff;
}

.btn {
  padding: 8px 20px;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  border: 2px solid;
  transition: all 0.3s ease;
}

.btn-cancel {
  background: #fff;
  color: #6c757d;
  border-color: #dee2e6;
}

.btn-cancel:hover {
  color: #495057;
  border-color: #adb5bd;
  background-color: #f8f9fa;
}

.btn-confirm {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
  border-color: transparent;
}

.btn-confirm:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.btn-confirm:disabled {
  background: #adb5bd;
  border-color: #adb5bd;
  cursor: not-allowed;
  transform: none;
}

@media (max-width: 768px) {
  .cascader-panel {
    min-width: 100px;
    width: 100px;
  }
  
  .cascader-header-item {
    min-width: 100px;
    width: 100px;
    font-size: 12px;
    padding: 6px 8px;
  }
  
  .cascader-option {
    padding: 6px 8px;
    font-size: 12px;
  }
}
</style>


3.说明

## 📋 API

### Props

| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| `options` | 级联数据源 | `CascaderOption[]` | `[]` |
| `modelValue` | 绑定值 | `string[]` | `[]` |
| `placeholder` | 占位符 | `string` | `'请选择'` |
| `headerLabels` | 头部标注 | `string[]` | `['分类', '子分类', '组件', '详细', '子项']` |

### Events

| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| `update:modelValue` | 值变更 | `(value: string[]) => void` |
| `change` | 选择变更 | `(value: string[], selectedOptions: CascaderOption[]) => void` |

4.表格请求

// 计算属性 - 请求参数(最深层级的name值)
const requestParam = computed(() => {
  if (!selectedValue.value.length) return ''
  return selectedValue.value[selectedValue.value.length - 1]
})

// 方法 - 处理级联选择器变更
const handleCascaderChange = (value: string[], selectedOptions: CascaderOption[]) => {
  selectedLabels.value = selectedOptions.map(opt => opt.nameCn)
  
  // 添加操作日志
  addLog(`级联选择器值变更: ${selectedLabels.value.join(' → ')}`)
  addLog(`提取请求参数: ${requestParam.value}`)
  
  // 触发表格数据请求
  if (requestParam.value) {
    fetchTableData(requestParam.value)
  } else {
    tableData.value = []
  }
}
### 级联选择实现方式 在使用 Element UI 实现级联选择时,可以通过 `el-cascader` 组件来完成。该组件允许用户从一个层级结构的数据中选择值,适用于需要多级联动选择的场景,比如地区选择、分类选择等。以下是实现的基本方式和相关配置。 #### 基本用法 `el-cascader` 的基本实现方式包括以下几个关键属性和配置: - `v-model`: 用于绑定选中项的值,通常是一个数组,表示选中的各级值。 - `:options`: 数据源,是一个嵌套的数组结构,每一级的数据通过 `children` 字段进行嵌套。 - `:props`: 用于定义数据中的字段名,如 `label`、`value` 和 `children`。 - `@change`: 当选择发生变化时触发的回调函数,可以用来处理选中值的逻辑。 例如,以下代码展示了如何配置 `el-cascader`: ```html <el-cascader expand-trigger="hover" v-model="addForm.goods_cat" :options="cateList" @change="handleChange" :props="cateProps" > </el-cascader> ``` 在 `data` 中定义的 `cateProps` 可以如下所示: ```javascript cateProps: { label: "cat_name", value: "cat_id", children: "children" } ``` 这种方式可以实现基本的级联选择功能,用户在选择时会触发 `@change` 回调,并可以通过 `v-model` 获取当前选中的值 [^2]。 #### 多选功能的实现 如果需要实现多选功能,Element UI 本身并没有直接支持,但可以通过一些扩展来实现。例如,可以在 `@change` 回调中动态管理选中的值,或者使用第三方库来增强 `el-cascader` 的功能。 一种常见的做法是在每次选择时,将选中的值(通常是最后一级的值)保存到一个数组中,并通过标签(Tag)的形式显示在界面上。这样可以实现类似多选的效果,用户可以选择多个子集值并显示在输入中 [^1]。 #### 自定义扩展 对于更复杂的需求,比如需要支持多选、默认选中项、或者只获取某一级的值,可以通过自定义组件或使用社区提供的扩展库来实现。这些扩展通常保留了 Element UI 的样式和功能,并在此基础上增加了更多特性,例如支持多选、级联选择器的大部分功能等 [^1]。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛夏天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值