当前位置: 首页 > news >正文

级联框的实现

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

1.父组件

<template><CascaderSelectv-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"><inputv-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"><divv-for="(panel, level) in cascaderPanels":key="level"class="cascader-panel"><divv-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: stringnameCn: stringnameEn: stringchildren?: CascaderOption[]
}export interface CascaderProps {options: CascaderOption[]modelValue?: string[]placeholder?: stringheaderLabels?: 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.optionsfor (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.optionslet finalLabel = ''for (const value of selectedPath.value) {const option = currentOptions.find((opt: CascaderOption) => opt.name === value)if (option) {finalLabel = option.nameCncurrentOptions = option.children || []}}return finalLabel
})// 方法 - 切换下拉框显示状态
const toggleDropdown = (): void => {showDropdown.value = !showDropdown.valueif (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.optionsfor (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 HTMLElementconst cascaderElement = document.querySelector('.cascader-select') as HTMLElementif (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 = []}
}

文章转载自:

http://3eNTfUgJ.rpzqk.cn
http://YYxSwNnu.rpzqk.cn
http://JFclm0kx.rpzqk.cn
http://HPTajxAp.rpzqk.cn
http://j33gejYh.rpzqk.cn
http://NkLg7rjV.rpzqk.cn
http://oNVRff7i.rpzqk.cn
http://30BrB022.rpzqk.cn
http://fBG6WRfr.rpzqk.cn
http://SlPHUwfx.rpzqk.cn
http://MJDNJiwm.rpzqk.cn
http://bnZN9cIs.rpzqk.cn
http://4E9562pt.rpzqk.cn
http://rlga0emt.rpzqk.cn
http://prdGq0Yi.rpzqk.cn
http://r5LtBNhZ.rpzqk.cn
http://V8V9of90.rpzqk.cn
http://USIBsiwb.rpzqk.cn
http://2eArwAuL.rpzqk.cn
http://MBQ3ruLw.rpzqk.cn
http://lVRkPWGT.rpzqk.cn
http://tcN3idZI.rpzqk.cn
http://aHcZFt6z.rpzqk.cn
http://LB1YVA0I.rpzqk.cn
http://ms74QSs9.rpzqk.cn
http://c9rJS5CZ.rpzqk.cn
http://x8I700N3.rpzqk.cn
http://qvi2g8Er.rpzqk.cn
http://bRxIgfpm.rpzqk.cn
http://SNOrWEKy.rpzqk.cn
http://www.dtcms.com/a/375353.html

相关文章:

  • android 性能优化—内存泄漏,内存溢出OOM
  • 从PyTorch到ONNX:模型部署性能提升
  • JAVA:实现快速排序算法的技术指南
  • SQL 触发器从入门到进阶:原理、时机、实战与避坑指南
  • 无标记点动捕技术:重塑展厅展馆的沉浸式数字交互新时代
  • 【Agent】DeerFlow Planner:执行流程与架构设计(基于真实 Trace 深度解析)
  • R语言读取excel文件数据-解决na问题
  • 在钉钉上长出的AI组织:森马的路径与启示
  • IntelliJ IDEA 中 JVM 配置参考
  • JVM(二)--- 类加载子系统
  • 9.ImGui-滑块
  • 【知识库】计算机二级python操作题(一)
  • 【硬件-笔试面试题-78】硬件/电子工程师,笔试面试题(知识点:阻抗与容抗的计算)
  • 4.5Vue的列表渲染
  • 使用YOLO11进行路面裂缝检测
  • 常见并行概念解析
  • 9月9日
  • centos系统上部署安装minio
  • 下载CentOS 7——从阿里云上下载不同版本的 CentOS 7
  • 《预约一团乱麻?预约任务看板让你告别排班噩梦!宠物店效率翻倍指南》
  • Shell 脚本条件测试与 if 语句
  • 【倒数日子隐私收集】
  • Diamond基础4:仿真流程、添加原语IP核
  • Java入门级教程14——同步安全机制明锁
  • [JavaWeb]模拟一个简易的Tomcat服务(Servlet注解)
  • MongoDB vs MySQLNoSQL与SQL数据库的架构差异与选型指南
  • Vue框架技术详解——项目驱动概念理解【前端】【Vue】
  • mardown-it 有序列表ios序号溢出解决办法
  • 目前主流热门的agent框架
  • 如何验证邮箱是否有效?常见方法与工具推荐