Element Plus 级联选择器实战:仿学科网教材多级选择的完整方案

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'
checkStrictlytrue父子节点不关联勾选,任意层级都可选
multipletrue开启多选模式,显示 checkbox
checkOnClickNodetrue2.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中手动过滤,以最后选择的父路径为基准
提交时后端收不到 ListFormData 用逗号分隔的字符串改为重复 key 方式fd.append('textbookIds', id)

八、版本要求

  • Element Plus >= 2.10.5checkOnClickNodeprop
  • Vue 3v-model响应式绑定
  • Spring BootList<Long>自动绑定重复 key 参数