资讯动态

el-tag 标签使用进阶:从基础属性到业务状态映射的完整实践

发布时间:2026/10/16 22:56:33 来源:尧图企业网站定制
标题看起来平平无奇el-tag标签使用这件事搁在很多刚上手 Vue 的开发者眼里无非就是写一行组件、传个 type 属性就收工。但真在后台系统里泡过一段时间之后你会发现这个小组件是 Element 家族里最被低估的成员之一——它经常出现在表格状态列、筛选条件栏、动态录入框、权限标识、接口回显这些位置而且每一次出现都不只是“显示一段文字”那么简单。这篇文章我就围绕 el-tag 标签使用从基础属性、动态增删、业务状态映射到实际项目里的组合玩法与样式坑位完整梳理一遍。适合已经在用 Element UI 或 Element Plus 做项目的开发者也适合刚把组件摸熟、想搞懂更多细节的人。1. 先搞清楚 el-tag 在页面里到底扮演什么角色1.1 标签的四种典型存在形式我在做后台管理类项目时发现 el-tag 几乎不会单独出现在页面上。它通常以四种形态存在纯展示型只读状态标记比如“已完成”“已启用”不需要用户做任何操作可操作型作为筛选条件或待确认项旁边带一个关闭按钮用户点击后触发删除或取消业务绑定型后端返回一个状态值前端把它映射成不同颜色和文案比如订单状态、审核状态聚合容器型多个标签聚集在一起形成标签集合配合输入框变成长标签录入器。这四种形态看着简单实际决定了你要用哪些属性、绑哪些事件。纯展示型只给一个type就够了可操作型必须处理closable和close事件业务绑定型需要维护一套状态映射聚合容器型则要考虑数组更新、key 唯一性和动画反馈。很多时候项目里出问题不是 el-tag 本身坏了而是没想清楚当前场景属于哪一种形态导致属性乱传、事件乱绑。1.2 基础属性速查与新旧版本差异先把最常用的属性理一遍后面所有折腾都建立在这张表上属性可选值作用typeprimary / success / info / warning / danger控制标签语义颜色sizelarge / default / small控制标签尺寸closabletrue / false是否显示关闭按钮disable-transitionstrue / false是否禁用过渡动画roundtrue / false是否启用胶囊圆角effectdark / light / plain控制主题风格color任意颜色值自定义背景色hittrue / false是否显示边框描边这里有个特别值得注意的版本差异。如果你从 Element UI 老项目升级到 Element Plustype的可选值和size的取值都会变。老版本里没有primary这个 type 选项size用的是medium、small、mini新版本里size收敛成large、default、small。我见过一次升级后标签全部变矮变胖的案例排查到最后就是sizemini不生效导致的。另外 Element Plus 的默认圆角更大了同样写法的标签视觉效果会明显不同升级上线前一定要走查一遍所有用到 el-tag 的页面。1.3 一个常见的“无意识复杂化”案例很多人刚开始写状态标签时喜欢在模板里堆条件判断比如el-tag :classstatus 1 ? tag-active : tag-inactive {{ status 1 ? 启用 : 停用 }} /el-tag这样写确实能跑但页面一多就麻烦了。同样的状态判断在列表页写一遍在详情页写一遍在筛选面板又写一遍后面改一个枚举名要全局搜索替换漏一处就出问题。el-tag 本身不复杂复杂的是它要承担的状态映射逻辑没有收敛。与其在模板里到处散落判断不如先建一张映射表把状态枚举、显示文案、标签颜色放在一起后面我会专门讲这块。2. 动态标签的增删交互从展示到可操作的关键两步2.1 动态标签最常见的业务场景动态标签在实际项目里主要有两类场景。第一类是标签录入器用户可以自己输入一段文字回车生成标签也可以点击关闭删掉常见于给商品、文章、活动打分类标识。第二类是筛选条件面板用户在前台设置完条件后页面顶部生成一排已选条件标签点击标签末尾的“×”就移除该条件并触发重新查询。这两种场景的底层逻辑是一致的维护一个数组添加时把新项 push 进去删除时把对应项过滤掉。差别只在于添加按钮是输入框还是下拉选择删除之后要不要额外触发数据请求。先把最核心的增删闭环写出来后面再往里面补条件判断。2.2 从输入框到标签集合的完整实现以 Vue 3 Element Plus 为例一个最精简的标签录入器长这样template div el-input v-modelinputText placeholder输入标签后回车 keyup.enter.preventhandleAdd / div classtag-list el-tag v-fortag in tags :keytag.id closable close.stophandleRemove(tag) {{ tag.text }} /el-tag /div /div /template script setup import { ref } from vue const tags ref([ { id: 1, text: 前端 }, { id: 2, text: Vue }, ]) const inputText ref() function handleAdd() { const text inputText.value.trim() if (!text) return tags.value.push({ id: ${Date.now()}-${Math.random().toString(16).slice(2)}, text, }) inputText.value } function handleRemove(tag) { const index tags.value.findIndex((item) item.id tag.id) if (index -1) tags.value.splice(index, 1) } /script这段代码里有两个容易被忽略的细节。第一close.stop里的.stop不是随手写的它阻止了关闭按钮的点击事件冒泡到 el-tag 根节点否则一旦外层再绑一个click就会出现“点删除按钮却触发了标签点击跳转”的诡异现象。第二添加时我用Date.now()加随机数生成唯一 id而不是直接用数组下标当 key删除中间项时才能保证 Vue 能准确找到对应的 DOM。2.3 数组操作与事件绑定里的几个暗坑把标签数组传给 el-tag 渲染只是第一步实际操作时踩过不少坑。最典型的是用数组下标当 key。v-for里写:keyindex短时间没事一旦删除中间的标签后面的标签会因为 key 位置变化被 Vue 复用界面上可能出现“标签内容错位”“关闭状态残留”这类问题。解决方式就是给每个标签一个稳定的唯一 id删除时通过 id 而不是下标去定位。第二个坑和组件间的数据通信有关。如果标签数组是从父组件传下来的千万不要在子组件里直接props.tags.push(...)。Vue 3 里虽然能 push 成功但这类直接修改 props 的做法既违反单向数据流又容易让父组件无法感知变化。正确方式是子组件通过事件把新的数组抛出去上面加一层defineEmits然后用v-model:tags同步script setup const props defineProps({ tags: { type: Array, default: () [] }, }) const emit defineEmits([update:tags]) function handleRemove(tag) { emit(update:tags, props.tags.filter((item) item.id ! tag.id)) } function handleAdd(text) { const trimText text.trim() if (!trimText) return emit(update:tags, [ ...props.tags, { id: ${Date.now()}-${Math.random().toString(16).slice(2)}, text: trimText }, ]) } /script还有一个中文输入法导致的经典问题中文输入法在拼音候选状态下按回车会触发keyup.enter结果用户还没选好汉字就把半个拼音添加成标签了。这个问题需要在输入框上监听compositionstart和compositionend用一个标志位判断当前是否处于组词状态如果正在组词就忽略回车新增。2.4 批量操作时给标签补一点动画Element Plus 的 el-tag 默认自带进入和离开动画但如果你用v-for渲染整个标签集合删除其中一个标签时其他标签是一瞬间弹上去的视觉上有点生硬。想让动画更顺滑可以用TransitionGroup包一层TransitionGroup nametag-fade tagdiv classtag-list el-tag v-fortag in tags :keytag.id closable close.stophandleRemove(tag) {{ tag.text }} /el-tag /TransitionGroup对应的 CSS 至少要覆盖进入和离开两个阶段.tag-fade-enter-active, .tag-fade-leave-active { transition: all 0.2s ease; } .tag-fade-enter-from, .tag-fade-leave-to { opacity: 0; transform: scale(0.92); }需要注意使用TransitionGroup时每个子元素还是必须有稳定的 keykey 一变动画就会失效。如果只是单个标签的显示隐藏用v-if配合 el-tag 默认动画就好没必要额外包一层。项目里如果对动画要求不高保留默认效果也完全够用。3. 把标签和业务数据捆在一起状态映射、颜色规范与点击行为3.1 用一张映射表替换散落的模板判断后台项目里最常见的需求是“后端返回状态数字或枚举字符串前端显示对应文案和颜色”。比如订单有pending、approved、rejected三种状态我通常会在文件顶部定义一份映射表const statusMap { pending: { label: 待处理, type: warning }, approved: { label: 已通过, type: success }, rejected: { label: 已驳回, type: danger }, closed: { label: 已关闭, type: info }, }模板里就一行el-tag :typestatusMap[row.status]?.type ?? info {{ statusMap[row.status]?.label ?? row.status ?? 未知 }} /el-tag这里用了空值合并运算符??不是因为写法炫技而是为了避免两个实际问题一是后端偶尔会返回一个映射表里没有的状态此时至少要显示原始值而不是空白二是status可能为null或undefined直接访问属性会报错用可选链就能安全兜底。映射表的好处在于状态枚举是全局收敛的。后端新增一个枚举值只需要改这一处多个页面复用同一份状态文案不会出现同一个状态在列表页叫“通过”、在详情页叫“已完成”这种不一致。条件判断复杂时甚至可以基于映射表生成一个计算属性模板里只依赖计算属性。3.2 数据更新后标签不同步的常见原因标签本身没有独立状态它只是把数据渲染出来。数据变了但标签没变多半是数据引用问题。我用reactive维护列表时如果直接list[0].status approved视图通常会更新但如果后端返回的是全新对象而你用Object.assign(existingItem, newData)时没注意嵌套字段可能出现标签文案变了但颜色没变。更稳妥的做法是整体替换列表项const newData await fetchDetail(id) const idx list.value.findIndex((item) item.id id) if (idx -1) { list.value[idx] { ...list.value[idx], ...newData } }这样 Vue 能明确侦测到对象引用变化强制触发重渲染。另外还要注意接口返回的状态值类型后端用0、1表示枚举前端映射表如果用字符串0、1做 key一定要先String(row.status)再取值否则会踩严格相等判断失败的坑。3.3 标签点击事件与关闭按钮的冲突处理el-tag 可以绑定原生click事件用在点击标签跳转详情、切换筛选条件等场景。但前面说过closable时关闭按钮的点击事件会冒泡到根节点。如果同时绑了click和close会出现“点关闭按钮删除标签同时也触发了标签点击跳转页面”的后果。最省心的解决办法是给close加.stop修饰符让关闭事件不再冒泡也可以保留冒泡在 click 回调里判断点击目标是不是关闭图标function handleTagClick(event, tag) { if (event.target.closest(.el-tag__close)) return // 正常处理标签点击行为 }第二种方式更适合那些“点击标签就要处理逻辑且关闭按钮也需要被一起感知”的场景。如果项目里有多个位置用到了带关闭按钮且带点击跳转的标签我建议封装成一个公共组件把冲突处理逻辑集中在一起。3.4 与服务端枚举的对应规范服务端返回的枚举前端不要直接用魔法值散落在模板里。我一般会把状态枚举单独抽成常量文件比如constants/status.js里面同时定义原始枚举值、显示文案、标签配置。这样前后端联调时接口文档里的枚举定义和前端代码是同一份认知。还有个小细节后端接口可能返回PENDING大写也可能返回pending小写。为了统一取值映射前先做一次String(value).toLowerCase()再查映射表。这样即使后端某个接口的大小写风格不一致前端也不会出现一半状态能显示、一半状态显示原始值的情况。4. 在真实页面里组合使用表格列、表单回显与筛选条件4.1 表格列里的状态标签表格列是 el-tag 出现频率最高的地方。用el-table-column的默认插槽取出当前行数据渲染标签el-table-column label状态 width100 aligncenter template #default{ row } el-tag :typestatusMap[row.status]?.type ?? info sizesmall effectplain {{ statusMap[row.status]?.label ?? row.status }} /el-tag /template /el-table-column这里我习惯加sizesmall和effectplain。前者是避免默认尺寸的标签把表格行高撑得太高后者是为了降低标签颜色的饱和度和视觉冲击力。一张表格里如果全是高饱和的纯色块用户扫视时会觉得很吵plain风格既保留了状态语义又更耐看。还有个容易被忽略的点表格列宽度。标签文案长短差异很大时要给列留足够宽度同时文案过长可以配合show-overflow-tooltip处理。不要把宽度卡得太死否则“已驳回申诉”这类文案会被截断成“已驳回…”。4.2 表单回显里把多选值渲染成标签编辑页面拿到的数据通常是数组比如用户角色 ID、文章分组 ID。直接渲染字符串数组不好看通常需要把 ID 映射成名称再用标签展示。我会先把可选列表转成 Mapconst groupMap new Map(groups.map((group) [group.id, group.name])) function getGroupName(id) { return groupMap.get(id) ?? id }模板里遍历数组生成标签每个标签可关闭关闭后同步移除对应 iddiv classgroup-tags el-tag v-forid in form.groupIds :keyid closable closeremoveGroup(id) {{ getGroupName(id) }} /el-tag /div用 Map 而不是普通对象的原因是Map 查找 key 时更安全普通对象会受原型链影响万一 key 是toString之类的字符串就可能出差错。像这种 ID 到名称的映射Map 是更可靠的数据结构。如果你的表单本身就是el-select multiple也可以直接给下拉选择加collapse-tags让选中的选项折叠成标签形式。但自定义展示需求多时还是用 el-tag 自己渲染更灵活不受下拉组件内部布局限制。4.3 筛选面板顶部的“已选条件”标签栏筛选页是 el-tag 可操作形态的典型场景。用户设置了地区、状态、时间范围后页面上方生成一排已选条件标签点击关闭即取消该条件并刷新列表。实现这类需求我不建议单独维护一个“标签数组”因为很容易出现“标签显示有但筛选表单实际已清空”的不同步问题。更好的方式是让标签列表通过计算属性派生自真实的筛选条件const activeFilters computed(() { const list [] if (filters.region) { list.push({ key: region, label: 地区${filters.region} }) } if (filters.status) { list.push({ key: status, label: 状态${statusMap[filters.status]?.label ?? filters.status}, }) } return list }) function handleRemoveFilter(filterItem) { filters[filterItem.key] fetchList() }这样关闭标签只是把对应筛选字段置空标签列表会自动重新计算。数据源只有一份从根上避免了不同步问题。如果把标签数组单独存一个ref每次筛选表单变化都要手动同步漏一次就出 bug。4.4 弹窗里的批量打标签操作批量操作也是常见场景表格里勾选多行数据点击“批量打标签”按钮弹窗里展示一组可选标签用户点击后每个选中的行都会被打上对应标签。这个场景里 el-tag 更多是展示层真正要处理的核心是把选中的行 ID 和标签集合做一次关联。操作完成后把新标签追加到对应行的 tags 数组里。需要注意去重同一个商品被重复打上同一个标签时不要生成两个一模一样的 el-tag。可以在追加前用some判断一下是否已经存在。弹窗关闭后记得刷新表格数据否则用户看到的数据和接口不一致会产生困惑。5. 样式定制与踩坑复盘小组件也需要注意的细节5.1 自定义颜色时别忽略文字可读性Element Plus 的 el-tag 提供了color属性可以直接设置背景色。但这里有个隐藏坑设置color之后标签的文字颜色默认是白色。如果你的背景色是浅黄色、浅蓝色这类浅色系白字根本看不清。最稳妥的定制方式不是单独传color而是用 CSS 变量覆盖。给标签加一个自定义类名然后通过类选择器统一设置背景色、文字色、边框色.el-tag--custom { --el-tag-bg-color: #fff3e0; --el-tag-border-color: #ffd591; --el-tag-text-color: #ed6a0c; --el-tag-hover-bg-color: #ffe7ba; }使用 CSS 变量有额外的好处hover 时背景色可以单独控制不会像直接传color那样鼠标移上去突然跳成另一个奇怪的颜色。如果项目里有多个自定义色标签维护一套 CSS 变量结构比到处写内联样式清晰得多。5.2 覆盖组件内部样式时的作用域处理在 Vue 单文件组件里写style scoped时默认样式不会穿透到子组件内部。想调整 el-tag 内部的关闭按钮、间距或字体必须用:deep()style scoped .tag-wrapper :deep(.el-tag) { font-weight: 500; } .tag-wrapper :deep(.el-tag__close) { font-size: 12px; } /style这里建议把:deep()的范围限制在一个带类的容器下避免全局覆盖影响所有页面的 el-tag。不到万不得已不要直接修改.el-tag__close这类内部类名因为组件库升级时内部类名可能会变升级之后覆盖失效排查起来很浪费时间。5.3 我实际踩过的几个标签相关坑把常见问题集中列一下每一条都是真实遇到过并排查过的用数组下标当 key 导致删除中间标签后后续标签的动画和内容错位改成唯一 id 后解决关闭按钮冒泡触发外层 click导致“删除标签直接跳转详情页”用close.stop解决中文输入法回车误添加半个拼音标签通过compositionstart/compositionend标志位解决弹窗打开时接口数据还没返回form.groupIds为空导致标签先渲染成空集合等数据返回后再打开弹窗渲染标签后端返回的tags字段可能是null直接用会报错统一用computed(() props.tags ?? [])做兜底服务端传回大写枚举值前端映射表用小写 key 时匹配不上加一行String(value).toLowerCase()解决。这些坑单独看都不难但它们往往组合出现一个页面里同时有动态标签、关闭事件、状态映射和接口回显几个问题叠加起来排查难度就不是 11 而是指数增长了。5.4 标签数量很多时的展示策略如果一个数据项下挂了二三十个标签全部平铺在页面上会非常占高度。我常用的处理方式是“截断 展开”const MAX_VISIBLE 5 const visibleTags computed(() expanded.value ? tags.value : tags.value.slice(0, MAX_VISIBLE) )超出部分用一个“N”文本按钮展示点击后展开完整标签集合。这个方案比引入虚拟滚动更划算因为标签本身非常轻量几十个 el-tag 的渲染成本很低瓶颈只在于页面空间。只有当标签数量可能达到几百上千时才需要考虑懒加载或虚拟滚动普通后台项目完全没必要为了省这点渲染时间引入额外复杂度。在维护某后台项目时我最后把动态标签、状态映射、关闭事件、展开折叠这一整套逻辑收拢封装成了一个 TagGroup 组件传入items和映射配置关闭和点击行为统一由组件内部处理。如果你也在多个页面里用到标签集合建议尽早做类似的封装——el-tag 本身很小但用法的复杂度一上来散落各地反而不如集中管理省心。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑