Element Plus 级联选择器实战:仿学科网教材多级选择的完整方案
基于 Element Plus 2.10+ 的
el-cascader,实现一个「前两级单选 + 后续层级同父多选」的教材选择器,涵盖 props 配置、选中逻辑、UI 定制和数据提交。
一、需求背景
在资源上传场景中,教材数据是一个多级树结构:
出版社(第1级) └── 教材(第2级) └── 章(第3级) └── 节(第4级)业务要求:
- 第1、2级(出版社、教材):单选,用于定位教材
- 第3级起(章、节):同父多选,用于关联具体章节
- 点击节点文字即可选中,不需要精确点击 checkbox
- 前两级隐藏 checkbox,UI 更简洁
二、Cascader Props 配置
<el-cascader v-model="textbookCascaderValue" :options="textbookCascaderList" :props="{ expandTrigger: 'click', checkStrictly: true, multiple: true, checkOnClickNode: true }" popper-class="textbook-cascader-popper" clearable filterable placeholder="请选择" @change="onTextbookCascaderChange" />Props 逐项说明
| Prop | 值 | 作用 |
|---|---|---|
expandTrigger | 'click' | 点击节点展开子级(默认是'hover') |
checkStrictly | true | 父子节点不关联勾选,任意层级都可选 |
multiple | true | 开启多选模式,显示 checkbox |
checkOnClickNode | true | 2.10.5+点击节点文字即勾选,不用精确点 checkbox |
checkOnClickNode是 Element Plus 2.10.5 新增的 prop,解决了「必须点击 checkbox 才能选中」的痛点。在此之前需要通过 CSS hack 或 JS 模拟实现。
三、选中逻辑:前两级单选 + 后续同父多选
核心处理函数
constonTextbookCascaderChange=(meta:any)=>{constval=meta.textbookCascaderValueif(!val||val.length===0){meta.textbookIds=[]return}constlastPath=val[val.length-1]// 前两级(出版社、教材):单选,只保留最后选择的项if(lastPath.length<=2){meta.textbookCascaderValue=[lastPath]meta.textbookIds=[lastPath[lastPath.length-1]]return}// 第三级起(章节):同级 + 同父多选constparentKey=JSON.stringify(lastPath.slice(0,-1))constsameParent=val.filter((path:any[])=>JSON.stringify(path.slice(0,-1))===parentKey)if(sameParent.length!==val.length){meta.textbookCascaderValue=sameParent}meta.textbookIds=sameParent.map((path:any[])=>path[path.length-1])}数据结构说明
el-cascader多选模式下,v-model的值是二维数组,每个元素是一条从根到选中节点的路径:
// 选中了「北京大学出版社 → 必修一 → 第一章」和「北京大学出版社 → 必修一 → 第二章」textbookCascaderValue=[[1,10,100],// 路径:出版社ID=1, 教材ID=10, 章ID=100[1,10,101],// 路径:出版社ID=1, 教材ID=10, 章ID=101]逻辑分两段
1. 前两级(path.length <= 2):单选
if(lastPath.length<=2){meta.textbookCascaderValue=[lastPath]// 只保留最后选的meta.textbookIds=[lastPath[lastPath.length-1]]return}用户选了出版社A,再选出版社B → 清掉A,只保留B。
2. 第三级起(path.length >= 3):同父多选
constparentKey=JSON.stringify(lastPath.slice(0,-1))constsameParent=val.filter((path:any[])=>JSON.stringify(path.slice(0,-1))===parentKey)- 取最后一次选择的父路径(去掉最后一个元素)作为基准
- 过滤掉父路径不一致的旧选项
- 例如:选了「必修一 → 第一章」,再选「必修二 → 第三章」→ 清掉旧的,只保留后者
四、UI 定制:隐藏前两级 Checkbox
问题
multiple: true会在所有层级显示 checkbox,但前两级(出版社、教材)是单选,显示 checkbox 会让用户困惑。
方案
通过popper-class给教材级联的下拉面板加一个专属 class,再用 CSS 隐藏前两级的 checkbox:
<el-cascader popper-class="textbook-cascader-popper" ... />/* 教材级联:前两级(出版社、教材)隐藏 checkbox */.textbook-cascader-popper .el-cascader-menu:nth-child(1) .el-checkbox, .textbook-cascader-popper .el-cascader-menu:nth-child(2) .el-checkbox{display:none;}为什么用popper-class?
el-cascader的下拉面板是teleport 到<body>的,不在组件 DOM 树内。直接用组件的 class(如.l-cascader)选择器够不到弹窗内的元素。popper-class会把自定义 class 加到弹窗根元素上,是定位 teleported 内容的标准做法。
DOM 结构
<!-- teleport 到 body 的弹窗 --><divclass="el-popper textbook-cascader-popper el-cascader__dropdown"><divclass="el-cascader-panel"><divclass="el-cascader-menu"><!-- :nth-child(1) = 出版社 --><ul><liclass="el-cascader-node"><spanclass="el-checkbox">...</span><!-- 隐藏 --><spanclass="el-cascader-node__label">北京大学出版社</span><iclass="el-cascader-node__postfix">→</i></li></ul></div><divclass="el-cascader-menu"><!-- :nth-child(2) = 教材 --><ul><liclass="el-cascader-node"><spanclass="el-checkbox">...</span><!-- 隐藏 --><spanclass="el-cascader-node__label">必修一</span><iclass="el-cascader-node__postfix">→</i></li></ul></div><divclass="el-cascader-menu"><!-- :nth-child(3) = 章 --><!-- checkbox 正常显示 --></div></div></div>五、数据提交
后端接收List<Long>类型,FormData 提交时用重复 key:
// 教材ID列表if(meta.textbookIds?.length){meta.textbookIds.forEach((id:number)=>{fd.append('textbookIds',String(id))})}Spring Boot 会自动将多个同名参数绑定到List<Long>:
@Schema(description="教材ID列表(多选)")privateList<Long>textbookIds;六、完整流程图
用户操作 cascaderValue 变化 提交数据 ───────────────────────────────────────────────────────────────────── 点击出版社 A → [[A]] → textbookIds: [] 点击教材 B → [[A, B]] → textbookIds: [] 点击章节 1 → [[A, B, 1]] → textbookIds: [1] 点击章节 2 → [[A,B,1], [A,B,2]] → textbookIds: [1,2] 点击章节 3(不同教材) → [[A,C,3]] ← 清掉旧的 → textbookIds: [3] 点击出版社 D → [[D]] ← 清掉旧的 → textbookIds: []七、踩坑记录
| 问题 | 原因 | 解决 |
|---|---|---|
| 点击 label 无法选中 | checkStrictly模式下点击 label 只高亮不勾选 | 升级 Element Plus 2.10.5+,使用checkOnClickNode: true |
| 前两级 checkbox 隐藏不生效 | dropdown 被 teleport 到 body,组件 class 选择器够不到 | 用popper-class定位弹窗 |
| 跨父级选择时旧数据未清空 | 多选模式下新选择是追加而非替换 | 在@change中手动过滤,以最后选择的父路径为基准 |
| 提交时后端收不到 List | FormData 用逗号分隔的字符串 | 改为重复 key 方式fd.append('textbookIds', id) |
八、版本要求
- Element Plus >= 2.10.5:
checkOnClickNodeprop - Vue 3:
v-model响应式绑定 - Spring Boot:
List<Long>自动绑定重复 key 参数
