ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Element 表单自定义 label:插槽、星号与悬浮提示实战

Element 表单自定义 label:插槽、星号与悬浮提示实战 上周帮一个做后台系统的同事看问题他们有个审批表单产品要求在 label 后面挂一个问号图标鼠标悬浮给出填写说明某些字段的 label 还要根据当前选中的数据类型动态变化。他一开始图省事直接把label属性写成模板字符串拼 HTML结果标签渲染成了一串转义字符后来又试着把图标塞进 label 字符串里同样不生效。问题的根在于el-form-item的label属性只接受纯文本想往里塞任何结构必须走label插槽。这篇就把 Element UI 和 Element Plus 里el-form-item自定义 label 这件事从头到尾讲清楚label 元素到底是怎么渲染出来的、两代框架写法差在哪、星号和对齐为什么会崩、超长文案悬浮提示怎么做、样式穿透怎么写、出问题按什么顺序排查。不管你是刚接手一个老 Vue 2 项目还是在写新的 Vue 3 后台这里面的坑基本都会碰上。1. 为什么原生 label 属性不够用先把渲染链路看清1.1 el-form-item 里 label 到底是哪一段 DOM很多人改了半天样式没生效第一步就错了——不知道要改哪个元素。el-form-item渲染出来大致是这么个结构div classel-form-item label classel-form-item__label stylewidth: 120px;用户名/label div classel-form-item__content div classel-input.../div /div /div关键点有三个外层是一个原生label元素宽度是通过内联style打上去的来源就是label-width而右侧的内容区是独立的.el-form-item__content。你写的label用户名只是被塞进这个label的文本节点里而已。理解这一点后面所有问题的答案都能自己推出来。比如 label 宽度为什么用 CSS 覆盖不掉——因为它是内联样式比如为什么点击 label 里的按钮会引发奇怪行为——因为它是原生 label带的语义和默认行为都在。1.2 label 属性、label 插槽、两者共存时的渲染规则el-form-item内部判断是否渲染 label 元素条件大致是有label属性或者有 label 插槽。这个条件决定了三种情况只写labelxxx渲染 label 元素内容是xxx。只写 label 插槽渲染 label 元素内容是你的插槽内容。两者都写label 元素仍然渲染但插槽内容会覆盖掉label属性的文本。第三条特别值得记一下它是个非常好用的技巧。很多人为了保留必填星号或者提示语会把label属性和插槽一起写结果是属性里的文本根本不显示。所以别指望属性写纯文本兜底、插槽写富文本这种并存方案两者只能二选一。注意如果label属性传空字符串且没有提供插槽label 元素整个不会渲染连带挂在它上面的必填星号也一起消失。这是星号不见了最常见的根因。1.3 必须上插槽的五类典型场景不是所有需求都要动插槽。判断标准很简单只要 label 需要表达属性字符串装不下的信息就得上插槽。我梳理了一下实际项目里最常出现的五类带提示图标label 后面跟一个问号或感叹号悬浮给出填写规则、字段来源、单位说明。超长文案字段名特别长比如近 12 个月累计含税销售金额容器宽度又不够需要截断加悬浮。动态文案label 内容依赖表单里的其他值比如币种切了label 从金额人民币变成金额美元。带状态标记label 旁边挂一个红色的圆点表示未填写、灰色的勾表示已同步这类视觉标记属性表达不了。带轻交互label 里放一个复制字段名的按钮、一个跳到字段字典文档的链接。这五类里前三类是高频第四类看业务第五类要额外小心点击事件后面会专门讲。2. 两代 Element 的写法差异别在新项目里抄老代码2.1 Vue 2 项目里的几种等价写法与各自的坑Element UI 是 Vue 2 时代的产物label 插槽的写法有好几种功能等价但细节不同。最常见的是在template上标slotel-form-item propname template slotlabel 项目名称 el-tooltip content不超过 30 个字符 placementtop i classel-icon-question/i /el-tooltip /template el-input v-modelform.name / /el-form-itemVue 2.6 之后也可以用v-slot指令但必须写在template上template v-slot:label项目名称/template !-- 或者缩写 -- template #label项目名称/template还有一种写法是直接把slotlabel写在普通元素上比如div slotlabel。这种写法能跑但它会额外包一层 div这层 div 会继承 label 元素的text-align: right和line-height还可能在 flex 布局里变成一个不受控的子项。除了极少数需要给插槽根节点加 class 的场景我一律建议用template。还有一个特别容易踩的坑Element UI 的 label 插槽不提供作用域参数。有些同学从其他组件库迁移过来习惯性写template slotlabel slot-scopescope然后发现scope是undefined又去找文档找半天。这里没有作用域变量需要的数据从组件实例的data/computed里取就行。2.2 Element Plus 下的写法细节Vue 3 的 Element Plus 只保留了v-slot语法slotlabel这种老写法已经不认了。标准写法el-form-item propname template #label span classlabel-text项目名称/span el-tooltip content不超过 30 个字符 placementtop el-icon classlabel-iconQuestionFilled //el-icon /el-tooltip /template el-input v-modelform.name / /el-form-item两个变化要注意。第一图标从字体图标el-icon-question换成了组件el-iconi classel-icon-xxx在 Element Plus 里不会渲染出任何图形。第二Element Plus 的el-form-item的 label 插槽目前也不提供可靠的作用域参数别指望从插槽里拿到 label 文本需要文案就从外部变量传。另外 Element Plus 较新版本在el-form上加了require-asterisk-position和hide-required-asterisk两个属性前者能把星号挪到 label 右侧后者直接隐藏星号。Vue 2 的 Element UI 没有这两个开关要调星号位置只能靠 CSS 或者干脆自己在插槽里画一个星号。2.3 从 Vue 2 迁到 Vue 3 的对照表老项目升级或者两套代码并存时这张表能省不少时间关注点Element UIVue 2Element PlusVue 3插槽写法slotlabel/v-slot:label#label/v-slot:label图标i classel-icon-questionel-iconQuestionFilled //el-icon星号位置只能靠 CSS 调整require-asterisk-position属性隐藏星号CSS 或全局覆盖hide-required-asterisk属性插槽根元素建议用template必须用templatetooltip 挂载:append-to-bodytrue默认已 teleport 到 body表格里最后一行是个隐藏差异。Element UI 的 tooltip 默认挂在当前位置需要手动append-to-bodyElement Plus 的 tooltip 默认就传送到 body 了。这导致同一个tooltip 被裁剪的问题在两代里修复方式完全不同迁移的时候特别容易改错方向。3. 星号与对齐自定义 label 之后最容易崩的三件事3.1 必填星号为什么会跑位甚至消失先把原理讲清楚必填星号不是 DOM 节点而是挂在 label 元素上的伪元素。el-form-item在检测到required来自rules或者显式的required属性之后会给外层容器加上is-required类样式表里再用.el-form-item__label::before把星号画出来。它跟 label 里写了什么内容毫无关系。由此可以推出几个结论只要 label 元素被渲染出来星号就在跟你用没用插槽没关系。星号消失一定是 label 元素没了或者被hide-required-asteriskElement Plus之类的方式屏蔽了。星号位置看着别扭多半是因为你改了 label 元素本身的布局模式。最后一条是最常见的。为了让 label 里的图标和文字垂直居中对齐有人直接在 label 元素上写display: flex。这一改伪元素星号立刻变成第一个 flex 子项参与justify-content的空间分配。如果你写的是justify-content: space-between星号会被推到最左边文字被推到最右边看起来就像星号跑到火星去了。正确的做法是不要动 label 元素本身在插槽内部再包一层容器做 flextemplate #label span classlabel-inner span classlabel-text手机号/span el-tooltip content用于接收审批通知 el-icon classlabel-iconWarning //el-icon /el-tooltip /span /template.label-inner { display: inline-flex; align-items: center; } .label-icon { margin-left: 4px; }这里用margin-left而不是gap是有原因的。如果项目还要兼容老版本浏览器很多 Element UI 项目就是 IE 时代的遗产flex 的gap属性在旧内核上不生效图标会贴着文字看起来像是没写间距。3.2 label-width 的三种取值怎么选自定义 label 之后宽度问题会比原来更突出因为插槽内容的高度、宽度都不可控了。label-width实际可用的取值就三类各有明确的适用面取值实际表现适合场景固定值如120px所有 label 等宽超出部分挤压或换行字段名长度接近的常规表单auto取最宽的 label 宽度随内容自适应文案长短差异大、有多语言0label-positiontoplabel 独占一行左对齐移动端、窄容器、表格内嵌表单label-widthauto需要较新的版本支持早期的 Element UI 是没这个能力的那时候大家的做法是写一段 JS 手动测量所有 label 的宽度取最大值再统一设置。如果你接手的老项目里有这种代码先确认一下版本能升上去就升手动测量那套在动态文案面前基本是失效的。固定值和 auto 之间还有一个隐性权衡auto 会在文案变化时重新计算宽度这意味着切换语言、切换业务类型导致 label 变长时整个表单的布局会抖动一下。字段不多的话无所谓字段几十个的表单用户会明显感觉到页面跳了一下。这种情况我一般选固定值 截断提示牺牲一点完整展示换稳定性。3.3 嵌套元素引发的点击穿透与焦点跳转这是自定义 label 里最隐蔽的一类问题。前面说过外层是个原生label元素。原生 label 有个默认行为如果它通过for关联到了某个表单控件点击 label 内的任意位置浏览器都会把焦点交给那个控件。于是就会出现这种诡异现象用户只是想点一下 label 里的问号图标看看提示结果输入框被聚焦了甚至因为上一次输入触发了blur校验页面啪地弹出一条红色报错。用户一脸懵你查半天也查不出所以然。处理办法有两个层次。轻量方案是给 label 内的可点击元素加事件修饰符click.stop.prevent一起上只.stop是不够的.prevent才能拦住原生的激活行为。更稳的方案是别用button或a这类语义元素用span或者el-icon加click从源头上避免浏览器把它当成可交互控件。还有一个小细节如果 label 内的元素需要被键盘访问别用div用span tabindex0并补上role和键盘事件。表单的无障碍访问在 To B 系统里越来越被重视别在这里留缺陷。4. 把 label 做厚截断提示、图标、动态文案的落地写法4.1 超长文案截断加悬浮显示完整内容这是后台系统里最高频的需求。实现分两步CSS 负责截断tooltip 负责展示。template #label el-tooltip :contentlabelText placementtop :disabled!isOverflow effectdark span classlabel-ellipsis mouseentercheckOverflow {{ labelText }} /span /el-tooltip /template.label-ellipsis { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }methods: { checkOverflow(e) { const el e.target this.isOverflow el.scrollWidth el.clientWidth } }几个要点值得展开说。为什么用mouseenter事件源来判断而不是用ref因为在弹窗、折叠面板这类场景里label 可能在挂载时还没有实际宽度父容器display: none这时候测量出来的scrollWidth和clientWidth都是 0判断结果永远是没溢出tooltip 就再也不显示了。用事件源元素来判断测量发生在用户真正把鼠标移上去的那一刻数据一定是准的。:disabled!isOverflow这个绑定不能省。不省的话短文案也会弹出 tooltip用户移过每一个 label 都弹一次体验很差。加上之后只有真正被截断的才提示。字体加载会影响测量结果。如果项目用了自定义中文字体首屏渲染时字体还没加载完浏览器用的是兜底字体字符宽度和最终宽度不一样可能导致判断错误。严谨一点的做法是在document.fonts.ready之后再触发一次重测或者在字体加载完成后给表单加个key让它整体重渲染。4.2 图标与状态标记嵌入 label 的布局处理图标的问题主要在垂直居中。Element UI 的 label 元素默认line-height: 40px字体图标用行内对齐勉强能看但换成 Element Plus 的el-icon组件之后尺寸和基线的处理变了很容易出现图标比文字高一点点、或者明显下沉的情况。我的经验是别依赖 line-height直接用 flex 做居中但记住前面说的——flex 加在内部容器上不要加在 label 元素上。另外图标尺寸建议显式指定比如font-size: 14px或者el-icon :size14跟 label 的字号保持一致不然图标会比文字大一圈。状态标记稍微复杂一点。如果要在 label 旁边挂一个红点表示未填写常见做法是用伪元素或者一个绝对定位的小圆点。这里要注意 label 元素的overflow和padding——Element UI 的 label 默认有padding-right: 12px如果你的红点用position: absolute; right: 0定位正好会落在 padding 区域里看起来它会贴近文字跟设计稿不一致。这时候要么调整定位偏移要么把红点放进内部容器用 flex 排布。4.3 动态 label 与多语言computed 的正确用法动态 label 最容易写错的地方不是渲染而是配套文案没跟着改。computed: { amountLabel() { const unitMap { CNY: 人民币, USD: 美元, EUR: 欧元 } return 金额${unitMap[this.form.currency]} }, amountRules() { const unit unitMap[this.form.currency] return [ { required: true, message: 请输入${unit}金额, trigger: blur } ] } }看到了吗label 变了校验提示语也得跟着变。我见过太多只改 label 不改 message 的表单用户看到的画面是金额美元配着请输入人民币金额的报错一眼就露馅。这两个应该绑在同一个数据源上别各写各的。多语言场景下label 内容来自$t(xxx)同样放进 computed 里。切换语言后 label 宽度会变如果用的是固定label-width长语言的 label 会换行导致表单错位。这时候有两条路一是改用label-widthauto二是给el-form绑一个:keycurrentLocale强制重建让它重新测量一次。后者是我在老项目里常用的兜底手段代价是表单状态要保证能被重建否则用户填了一半的内容会丢。需要注意的是用:key重建表单时如果表单是v-model双向绑在父组件的 data 上的值不会丢但如果有些字段是子组件内部维护的临时状态重建就会清空。切换语言这种操作一般发生在填写前风险可控但心里要有数。4.4 tooltip 被裁剪、被遮挡的两个修复手段label 里的 tooltip 出问题原因基本是两个被父级的overflow: hidden裁掉或者z-index 被其他层盖住。Element UI 的解法是加:append-to-bodytrue把 tooltip 的弹出层挂到body下面脱离父级的裁剪上下文。Element Plus 默认就是传送到 body一般不用管。但挂到 body也不是万能的。如果 tooltip 出现在弹窗el-dialog里而弹窗的 z-index 比 tooltip 高你依然看不到提示。这时候要手动指定popper-class然后在全局样式里把该类的 z-index 提上去.form-label-tooltip { z-index: 3000 !important; }数值不要瞎写。合理的做法是先看一眼弹窗的 z-index 是多少往上加两三百就行不然后面再有其他浮层出现你又要往上加最后加到 99999 这种完全不讲道理的值后面接手的人会很痛苦。5. 样式穿透与窄屏适配让自定义 label 不翻车5.1 深层选择器的三种写法与选择依据要改 Element 的内部样式绕不开样式穿透。Vue 2 和 Vue 3 的语法不同预处理器不同写法也不同/* Vue 2 sass / less推荐 */ ::v-deep .el-form-item__label { color: #303133; } /* Vue 2 纯 CSS */ /deep/ .el-form-item__label { color: #303133; } /* Vue 3 */ :deep(.el-form-item__label) { color: #303133; }这种写法在scoped的纯 CSS 里能用但只要用了 sass 或 less就会被当成选择器解析符报错能不用就不用。/deep/在新版构建工具里会收到废弃警告能用::v-deep就用::v-deep。比语法更重要的是作用范围。千万别在全局样式文件里直接改.el-form-item__label那会影响整个项目里所有表单的 label。正确做法是给表单外层加一个包装类穿透从包装类开始写.approval-form ::v-deep .el-form-item__label { color: #303133; }这样一来你的改动只作用于这一个表单别的页面该什么样还什么样。我见过项目里因为全局改了 label 的line-height导致某个复杂表单的 label 换行后整体错位排查了整整一个下午。5.2 窄屏和移动端下的 label 折行策略窄屏下固定宽度的 label 会挤爆内容区这时候基本只有一个正解改成上下布局。media (max-width: 768px) { .filter-form ::v-deep .el-form-item__label { width: auto !important; text-align: left; line-height: 1.5; padding-bottom: 4px; } .filter-form ::v-deep .el-form-item { display: block; } }这里必须用!important原因前面提过——label-width是内联样式打上去的CSS 选择器再具体也压不过内联样式。这是个硬约束不是你写得不规范。更彻底的方案是用 JS 监听窗口宽度动态切换label-position和:label-widthel-form :label-positionisNarrow ? top : right :label-widthisNarrow ? auto : 120px 这种方案的好处是不用跟!important打交道样式逻辑全在模板里可读性好。代价是要引入一个媒体查询的监听组件销毁时记得解绑不然会有内存泄漏和看不到的组件还在响应 resize的问题。5.3 表单嵌在表格单元格或弹窗里的特殊处理后台系统里经常出现表格里嵌表单或者弹窗里放表单的形态自定义 label 在这两种环境里会遇到额外的麻烦。先说表格。单元格宽度本来就很窄label 基本只能走截断加 tooltip 的方案。另外如果表格有固定列固定列会创建新的层叠上下文tooltip 挂到 body 之后z-index 不够高的话会被固定列盖住表现就是鼠标移上去明明该有提示但什么都看不到。这个问题跟表格固定列自身在某些渲染时机下出现的异常属于同一类层叠上下文问题处理思路一样先确认层级关系再针对性提升浮层的层级而不是盲目加 z-index。再说弹窗。弹窗里最大的坑是首次打开时测量失效。el-dialog在打开动画完成之前内部元素的尺寸信息可能是不完整的这时候如果 label 的截断判断依赖clientWidth结果会全是 0判断成没溢出tooltip 永远不弹。第二次打开就正常了因为此时元素已经渲染过一次。修复方式是把测量时机往后挪。要么用opened事件Element UI 和 Element Plus 都提供等动画结束再测要么就像前面说的把测量挂在mouseenter上天然规避这个问题。我更推荐后者因为它顺手把文案动态变化的场景也一起覆盖了。6. 出问题时的排查顺序从 DOM 倒推别瞎猜6.1 按这五步走基本不会跑偏自定义 label 出问题我固定按这个顺序查效率最高label 元素在不在。打开开发者工具看.el-form-item__label这个节点有没有被渲染。没有的话八成是label属性是空的而且插槽没写对位置。类名对不对。检查外层el-form-item上有没有is-required有没有被加上is-no-asterisk之类的类。星号相关的异常基本在这步就定位了。伪元素在不在。在样式面板里看::before看它的content有没有值color是不是被覆盖成了背景色有些全局主题会不小心把::before的color改掉星号就隐身了。宽度从哪来。看 label 的内联style是什么再决定是改label-width属性还是只能靠!important覆盖。层叠上下文。tooltip、图标被挡住、被裁剪都在这一步查。看父级有没有overflow: hidden、transform、filter这些会创建新层叠上下文的属性。顺序不要乱。我见过有人一上来就调 z-index调了一个小时最后发现根本是 label 元素压根没渲染。先确定元素存在再谈样式。6.2 常见异常对照表把上面这些整理成一张速查表遇到问题直接对号入座现象大概率原因处理方向label 完全不显示label属性为空且未提供插槽补上template #label必填星号不见了label 元素未渲染或被隐藏星号的属性/全局样式屏蔽检查第 1、2 步星号和文字间距异常在 label 元素本身上开了 flex在插槽内部再包一层容器图标和文字不垂直居中依赖 line-height 对齐内层容器用 flex align-items: center图标和文字贴在一起用了gap但浏览器不支持改用margin-left文案被截断但没有提示tooltip 被:disabled关掉了或测量时机过早用mouseenter事件源测量tooltip 被裁掉或看不到父级overflow: hidden或层级不够append-to-body/popper-class提层级CSS 改 label 宽度无效内联样式优先级更高用!important或改属性点击 label 里的图标触发校验原生 label 的激活行为click.stop.prevent或改用span切换语言后 label 换行错位固定宽度放不下新文案用auto宽度或加:key重建6.3 我自己踩过的三个坑一个比一个疼第一个坑是把 HTML 写进 label 属性。当时的想法很简单label接受字符串那我就拼个字符串带标签呗。结果页面上明晃晃地显示出了span stylecolor:red名称/span这串字符。浏览器不会把属性值当 HTML 解析这个道理现在看很简单但当时确实花了十几分钟才反应过来。第二个坑是在 label 里放了可点击的图标点击时会闪出校验错误。用户点问号想看提示结果上一次输入触发的blur校验被触发红色提示闪一下又消失。定位花了很久最后发现是原生 label 的激活行为把焦点交给了输入框。加上.prevent之后彻底解决。第三个坑是弹窗里首次打开时截断判断失效。现象特别迷惑第一次打开弹窗长文案被截断了但不显示提示关掉再打开提示就正常了。当时怀疑是 tooltip 的 bug查了半天源码最后发现是自己写的溢出判断在元素尺寸为 0 的时候跑了一遍。改成在mouseenter时判断问题消失。这三个坑的共同点是都不是组件库的问题而是对渲染时机和原生行为的理解不到位。多看两眼渲染出来的 DOM比翻文档快得多。后来我把这套逻辑抽成了一个小的 label 组件接收文案、图标、提示内容、是否需要截断这几个参数内部统一处理 flex 布局、溢出判断和 tooltip。项目里有二十多个表单用到自定义 label抽出来之后改一次全局生效也不用每个表单重复写一遍mouseenter判断。如果你手上的系统也有类似需求这个抽法值得试试——成本很低收益在后期维护时体现得特别明显。
返回列表