Vue Element UI el-steps组件样式深度定制与工程化实践
2026/9/5 18:34:21 网站建设 项目流程

1. 项目背景与核心痛点

在Vue.js项目中,Element UI作为一套成熟的前端组件库,极大地提升了开发效率。其el-steps步骤条组件常用于展示流程进度,如订单状态、表单填写步骤等,开箱即用的样式简洁明了。然而,在实际业务开发中,我们几乎总会遇到需要定制化UI的场景——产品经理拿着设计稿,要求步骤条的图标、颜色、线条、间距甚至整个交互反馈都要与设计稿保持一致,而Element UI默认的样式往往无法满足这些个性化需求。

这时,很多开发者,尤其是刚接触Vue和Element UI的朋友,会感到无从下手。直接修改node_modules里的源码?这显然是不可维护的。在组件上写行内样式?效果有限且混乱。全局覆盖CSS?又怕影响其他地方的el-steps组件。这个“修改样式”的需求,背后其实是一系列前端工程化问题的缩影:如何精准、优雅且可维护地覆盖第三方UI库的样式。

网络上相关的搜索热词也印证了这一点:“elementui table 固定列高度错乱”、“vue3修改tabs标签页样式”、“修改el-dropdown的样式,不改全局的”,这些都指向了同一个核心诉求——在享受组件库便利的同时,如何突破其样式限制,实现深度定制。本文将围绕el-steps组件,深入剖析几种主流的样式修改方案,从最简单的样式覆盖到复杂的组件封装,并结合我多年的踩坑经验,为你提供一套可直接“抄作业”的解决方案。

2. 理解 el-steps 的 DOM 结构与样式作用域

在动手修改之前,我们必须先搞清楚我们要修改的对象到底是什么。el-steps不是一个简单的<div>,它是由多层嵌套的DOM元素和复杂的CSS类名构成的复合组件。盲目地写样式只会事倍功半。

2.1 组件渲染后的 DOM 结构剖析

假设我们有一个简单的横向步骤条:

<template> <el-steps :active="1" simple> <el-step title="步骤 1" /> <el-step title="步骤 2" /> <el-step title="步骤 3" /> </el-steps> </template>

它最终在浏览器中渲染出的HTML结构大致如下(已简化):

<div class="el-steps el-steps--simple"> <div class="el-step is-horizontal"> <div class="el-step__head"> <div class="el-step__line"> <i class="el-step__line-inner" style="border-radius: 0px; width: 0%;"></i> </div> <div class="el-step__icon is-text"> <div class="el-step__icon-inner">1</div> </div> </div> <div class="el-step__main"> <div class="el-step__title">步骤 1</div> </div> </div> <!-- 第二个 el-step... --> </div>

从这个结构我们可以清晰地看到样式作用的层级:

  1. 最外层容器.el-steps控制整体布局(如simple模式)。
  2. 单个步骤容器.el-step控制单个步骤的布局(水平is-horizontal或垂直is-vertical)。
  3. 步骤头部.el-step__head包含图标和连接线。
  4. 图标区域.el-step__icon及其内部的.el-step__icon-inner,控制数字或图标的样式。
  5. 连接线.el-step__line.el-step__line-inner,控制步骤之间的连线。
  6. 主要内容区.el-step__main包含标题.el-step__title和描述.el-step__description

2.2 CSS 作用域与样式穿透的必要性

在Vue的单文件组件(.vue文件)中,我们通常在<style>标签内写CSS。如果给<style>加上了scoped属性,Vue会为模板中的每个元素添加一个唯一的><template> <div class="custom-steps-wrapper"> <el-steps :active="activeStep" finish-status="success"> <el-step title="方案确认" /> <el-step title="支付定金" /> <el-step title="服务进行" /> <el-step title="完成评价" /> </el-steps> </div> </template> <style scoped> .custom-steps-wrapper { padding: 20px; } /* 使用 :deep() 穿透到组件内部 */ .custom-steps-wrapper :deep(.el-step__head.is-process) { color: #67C23A; /* 将进行中状态的图标和文字改为绿色 */ border-color: #67C23A; } .custom-steps-wrapper :deep(.el-step__title.is-process) { color: #67C23A; /* 进行中状态的标题也改为绿色 */ font-weight: 600; /* 加粗标题 */ } /* 修改图标大小 */ .custom-steps-wrapper :deep(.el-step__icon) { width: 32px !important; /* 有时需要 !important 来覆盖库的高优先级样式 */ height: 32px !important; font-size: 18px; } /* 修改连接线颜色 */ .custom-steps-wrapper :deep(.el-step__line) { background-color: #E4E7ED; /* 未完成线颜色 */ } .custom-steps-wrapper :deep(.el-step__head.is-finish .el-step__line) { background-color: #67C23A; /* 已完成线颜色 */ } </style>

关键点解析

  • :deep()是Vue 3的推荐写法,它会将括号内的选择器作为“深度”选择器处理,编译后不会加上组件的>.custom-steps-wrapper :deep(.el-step.is-simple .is-finish .el-step__icon::after) { content: '✓'; /* 可以在这里修改这个对勾的样式 */ color: #67C23A; font-weight: bold; }

    修改伪元素的样式,同样需要使用深度选择器穿透到对应的类名上。务必通过开发者工具准确定位到生成伪元素的具体CSS规则,然后进行覆盖。

    4. 进阶定制:利用插槽与渲染函数

    如果基础样式覆盖无法满足需求,比如你需要完全替换掉默认的图标(从数字换成自定义图片),或者改变整个步骤条的结构布局,那么就需要用到更强大的工具:插槽(Slots)和渲染函数(Render Function)。

    4.1 使用 icon 插槽自定义图标

    el-step组件提供了一个icon插槽,允许我们完全替换掉默认的图标区域。这是最灵活的自定义图标方式。

    <template> <el-steps :active="1"> <el-step title="注册"> <template #icon> <!-- 完全自定义图标内容 --> <div class="custom-icon"> <img src="@/assets/step-register.svg" alt="注册" /> </div> </template> </el-step> <el-step title="实名认证"> <template #icon> <div class="custom-icon"> <i class="el-icon-user-solid"></i> <!-- 使用Element图标 --> </div> </template> </el-step> <el-step title="完成"> <template #icon> <div class="custom-icon is-finish"> <i class="el-icon-success"></i> </div> </template> </el-step> </el-steps> </template> <style scoped> .custom-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background-color: #f0f9eb; /* 浅绿色背景 */ color: #67C23A; } .custom-icon.is-finish { background-color: #67C23A; color: white; } /* 注意:使用了icon插槽后,原本的 .el-step__icon 相关样式可能不再适用, 需要我们自己为 .custom-icon 编写所有状态(进行中、完成、等待)的样式。 可以通过在父组件动态绑定class来实现状态同步,但这会比较复杂。 一种更简单的方法是,我们依然利用Element的状态类,但将样式作用在我们的自定义图标上 */ :deep(.el-step__head.is-process) .custom-icon { background-color: #409EFF; color: white; } </style>

    实操心得

    • 使用icon插槽后,你就接管了图标的所有渲染逻辑。这意味着Element UI默认根据状态(process, finish, wait)变化的图标颜色和样式将完全失效
    • 你需要自己为自定义图标元素(如上例中的.custom-icon)编写CSS,并想办法让它响应步骤的状态变化。一种方法是像上面那样,依然依赖:deep()选择器,根据父元素(.el-step__head)上的状态类来设置子元素(.custom-icon)的样式。另一种更可控的方法是在数据层判断状态,动态绑定不同的class。

    4.2 使用 render-content 渲染函数进行终极定制

    当你需要自定义的不仅仅是图标,而是整个el-step的内容(包括标题、描述的布局和结构)时,render-content属性是你的终极武器。它接受一个渲染函数(或JSX),可以完全自定义每个步骤节点的渲染内容。

    这个功能非常强大,但复杂度也最高,需要对Vue的渲染函数或JSX有一定了解。

    <template> <el-steps :active="activeStep" :space="200" direction="vertical" :process-status="processStatus" :render-content="renderStepContent" > <!-- 这里的 el-step 可以只提供数据,渲染完全交给 render-content --> <el-step /> <el-step /> <el-step /> </el-steps> </template> <script setup> import { h, ref } from 'vue'; const activeStep = ref(1); const processStatus = ref('process'); // 进行中状态的颜色类型 // 定义渲染函数 const renderStepContent = (index, status, title, description) => { // index: 步骤索引,status: 状态(wait/process/finish/error/success), title/description 来自 el-step 的 prop(本例未传,则为空) // 自定义状态映射和图标 const statusConfig = { wait: { icon: 'el-icon-clock', color: '#C0C4CC', text: '等待' }, process: { icon: 'el-icon-loading', color: '#409EFF', text: '进行中' }, finish: { icon: 'el-icon-success', color: '#67C23A', text: '已完成' }, error: { icon: 'el-icon-error', color: '#F56C6C', text: '错误' } }; const config = statusConfig[status] || statusConfig.wait; // 使用 h() 函数创建虚拟节点 return h('div', { class: 'my-custom-step' }, [ h('div', { class: 'step-head', style: { color: config.color } }, [ h('i', { class: config.icon }), h('span', { class: 'step-index' }, `步骤 ${index + 1}`) ]), h('div', { class: 'step-main' }, [ h('div', { class: 'step-title' }, `自定义标题 ${index + 1}`), h('div', { class: 'step-desc' }, `这里是自定义的描述信息,状态:${config.text}`) ]) ]); }; </script> <style scoped> .my-custom-step { display: flex; align-items: flex-start; margin-bottom: 20px; } .step-head { display: flex; flex-direction: column; align-items: center; margin-right: 12px; flex-shrink: 0; } .step-head i { font-size: 24px; margin-bottom: 4px; } .step-index { font-size: 12px; color: #909399; } .step-main { text-align: left; } .step-title { font-weight: bold; margin-bottom: 4px; } .step-desc { font-size: 12px; color: #606266; } </style>

    为什么选择渲染函数?当你需要:

    • 步骤的UI设计与Element UI默认结构差异巨大。
    • 需要根据复杂的业务逻辑动态决定每一步的显示内容。
    • 希望将步骤条与你的业务组件深度集成。

    在这种情况下,样式覆盖和简单插槽都显得力不从心,而渲染函数提供了最大的自由度。代价是,你需要自己实现所有交互逻辑(如果需要的话)和样式,相当于基于el-steps的“骨架”重新造了一个轮子。

    5. 样式隔离与工程化实践

    当项目变大,多个地方都需要定制el-steps时,如何管理这些样式就成为了一个工程问题。我们既希望样式能精准生效,又不希望它们互相干扰或污染全局。

    5.1 创建可复用的样式模块

    我们可以将针对el-steps的定制样式抽离成一个单独的SCSS或CSS文件,作为一个“样式模块”来管理。

    // styles/modules/_custom-steps.scss // 品牌主题步骤条 .brand-steps { @include step-variant(#67c23a, #f0f9eb); // 主色,背景色 &--compact { :deep(.el-step) { margin-right: 0; } :deep(.el-step__title) { font-size: 14px; } } } // 错误状态强调的步骤条 .error-emphasis-steps { @include step-variant(#f56c6c, #fef0f0); :deep(.el-step__head.is-error) { .el-step__icon { transform: scale(1.1); } } } // 定义一个Mixin来生成步骤条变体,避免重复代码 @mixin step-variant($primary-color, $icon-bg) { :deep(.el-step__head.is-process), :deep(.el-step__head.is-finish) { color: $primary-color; border-color: $primary-color; } :deep(.el-step__title.is-process), :deep(.el-step__title.is-finish) { color: $primary-color; } :deep(.el-step__icon) { background-color: $icon-bg; } :deep(.el-step__line-inner) { background-color: $primary-color; } }

    然后在需要的组件中引入并应用这个类:

    <template> <div class="brand-steps brand-steps--compact"> <el-steps>...</el-steps> </div> </template> <style scoped lang="scss"> @import '@/styles/modules/custom-steps'; // 或者,如果配置了全局样式导入,可以直接使用类名 </style>

    5.2 使用 CSS Modules 或 Scoped CSS 的权衡

    • Scoped CSS:优点是简单直观,样式天然隔离。缺点是深度选择器:deep()的语法稍显繁琐,且在大型项目中,如果多个组件需要同样的定制,代码会重复。
    • CSS Modules:通过<style module>将CSS类名局部化并作为计算属性导出,提供了更严格的隔离和更灵活的JavaScript交互能力。但在覆盖第三方组件样式时,同样需要:global():deep()来穿透,且学习成本稍高。

    我的经验是,对于中后台项目,使用Scoped CSS配合:deep()和上面提到的“样式模块”模式,在简单性和可维护性之间能取得很好的平衡。将通用的、复杂的定制样式抽离成模块,在组件中按需引入和应用父级类名,既能复用又能隔离。

    5.3 处理全局样式与按需引入

    如果你使用的是Element UI的按需引入(通过babel-plugin-componentunplugin-vue-components),那么全局样式文件(如element-ui/lib/theme-chalk/index.css)可能不会完整引入。此时,你覆盖样式时引用的Element UI内部类名(如.el-step__line)必须是存在的。

    确保你的定制样式在Element UI的基础样式之后引入。在Vue CLI或Vite项目中,通常在main.jsApp.vue中引入Element UI的CSS,而你的模块样式在组件中引入,顺序是正确的。如果遇到样式优先级问题,检查一下打包后的CSS顺序。

    6. 常见问题排查与性能优化

    修改第三方组件样式时,总会遇到一些“诡异”的问题。这里总结几个高频坑点和优化建议。

    6.1 样式不生效的排查清单

    1. 检查选择器是否正确:打开浏览器开发者工具(F12),检查你试图修改的元素,看看你写的CSS选择器是否命中了它。检查编译后的选择器是否带上了奇怪的哈希,导致无法匹配。
    2. 检查样式加载顺序:你的自定义样式是否在Element UI的样式之后加载?如果之前,会被覆盖。在Sources或Network面板查看CSS文件加载顺序。
    3. 检查CSS特异性:你的选择器特异性可能低于Element UI原有的规则。尝试:
      • 增加选择器的层级(如多加一个具有唯一性的父类)。
      • 查看Element UI原有规则的 specificity,写出比它更高的选择器(谨慎使用)。
      • 调试阶段,可以临时在样式规则末尾添加!important来确认是否是优先级问题。确认后,应尽量通过增加特异性来解决,避免滥用!important
    4. 检查是否使用了正确的状态类.is-process,.is-finish,.is-wait这些类是动态添加的。确保你的步骤状态(active,status属性)设置正确,并且你的CSS选择器包含了对应的状态类。
    5. 检查Vue的Scoped样式:确认你是否需要对第三方组件内部元素使用:deep()进行样式穿透。

    6.2 样式覆盖的性能考量

    过度使用:deep()或非常复杂的选择器可能会影响浏览器的渲染性能,尤其是在步骤条数量很多(如渲染一个包含上百个步骤的时间轴)时。

    • 尽量使用直接的类名选择器.custom-wrapper :deep(.el-step__icon).custom-wrapper :deep(div > div > .el-step__head .el-step__icon)性能更好。
    • 避免使用通配符*在深度选择器内:deep(*)会导致性能下降。
    • 将不变的样式提升到更高层级:如果多个el-step的某些样式相同,尽量将样式定义在它们的共同父元素上,而不是每个步骤都重复定义。

    6.3 响应式设计的适配

    el-steps在屏幕宽度不足时会自动调整,但在深度定制样式后,这种自适应可能会被破坏。你需要测试在不同屏幕尺寸下的表现。

    .custom-steps-wrapper { :deep(.el-steps) { /* 默认横向 */ } @media (max-width: 768px) { :deep(.el-steps) { flex-direction: column; /* 在小屏幕下改为垂直布局 */ } :deep(.el-step) { margin-bottom: 10px; } /* 可能需要隐藏连接线或调整图标位置 */ :deep(.el-step__line) { display: none; } } }

    媒体查询同样需要写在深度选择器内部,或者写在包裹el-steps的父容器样式里,以确保能影响到组件内部的布局。

    7. 实战案例:实现一个设计稿中的复杂步骤条

    让我们结合一个真实案例,将上述所有技巧串联起来。假设设计稿要求实现一个如下效果的步骤条:

    1. 步骤图标为圆形,内部是数字,已完成和当前步骤有发光边框。
    2. 连接线是虚线。
    3. 标题在图标上方,描述在图标下方。
    4. 在移动端垂直排列,且图标和文字布局变化。

    实现步骤:

    步骤1:分析结构与布局默认的el-steps是图标在左,标题描述在右。要实现标题在上、图标在中、描述在下,默认结构不满足。我们决定放弃icon插槽(因为它只替换图标区域),而采用render-content进行整体结构重绘。

    步骤2:实现渲染函数

    <template> <div class="complex-steps-wrapper"> <el-steps :active="activeStep" direction="horizontal" :space="flexibleSpace" :render-content="renderComplexStep" > <el-step v-for="(step, index) in stepList" :key="index" /> </el-steps> </div> </template> <script setup> import { h, ref, computed } from 'vue'; const activeStep = ref(2); const stepList = ref([ { title: '提交订单', desc: '您的订单已提交' }, { title: '支付货款', desc: '等待支付' }, { title: '商家发货', desc: '预计明天发出' }, { title: '确认收货', desc: '等待您确认' }, ]); // 动态计算间距,用于响应式 const flexibleSpace = computed(() => { return document.documentElement.clientWidth < 768 ? 80 : 200; }); const renderComplexStep = (index, status) => { const step = stepList.value[index]; const isActive = index === activeStep.value; const isFinished = index < activeStep.value; const statusClass = isFinished ? 'is-finished' : (isActive ? 'is-active' : 'is-waiting'); return h('div', { class: ['complex-step', statusClass] }, [ // 标题在上 h('div', { class: 'step-title' }, step.title), // 图标在中 h('div', { class: 'step-icon' }, [ h('div', { class: 'icon-inner' }, index + 1) ]), // 描述在下 h('div', { class: 'step-desc' }, step.desc), // 连接线(除了最后一个步骤) index < stepList.value.length - 1 ? h('div', { class: 'step-connector' }) : null ]); }; </script>

    步骤3:编写配套样式

    <style scoped lang="scss"> .complex-steps-wrapper { padding: 40px 20px; :deep(.el-steps) { // 隐藏Element UI默认渲染的内容 .el-step__head, .el-step__main { display: none; } // 让我们的自定义内容可以正常布局 .el-step { display: flex; flex: 1; position: relative; flex-direction: column; // 改为垂直布局以适应我们的设计 align-items: center; } } .complex-step { display: flex; flex-direction: column; align-items: center; position: relative; flex: 1; min-width: 0; // 防止文本溢出 } .step-title { font-size: 16px; font-weight: bold; margin-bottom: 15px; color: #333; text-align: center; order: 1; } .step-icon { width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 15px; order: 2; position: relative; z-index: 2; background: white; border: 2px solid #dcdfe6; // 默认灰色边框 .icon-inner { font-size: 18px; font-weight: bold; color: #909399; } } .step-desc { font-size: 12px; color: #909399; text-align: center; order: 3; } // 状态样式 .complex-step.is-active { .step-icon { border-color: #409eff; box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2); // 发光边框 .icon-inner { color: #409eff; } } .step-title { color: #409eff; } } .complex-step.is-finished { .step-icon { border-color: #67c23a; background-color: #67c23a; .icon-inner { color: white; } } .step-title { color: #67c23a; } .step-desc { color: #67c23a; } } // 连接线(虚线) .step-connector { position: absolute; top: 25px; // 对齐图标中心 left: calc(50% + 25px); // 从图标中心右侧开始 right: -50%; height: 0; border-top: 2px dashed #dcdfe6; z-index: 1; } .is-finished .step-connector { border-top-style: solid; border-top-color: #67c23a; } // 移动端适配 @media (max-width: 768px) { :deep(.el-steps) { flex-direction: column; .el-step { width: 100%; margin-bottom: 30px; align-items: flex-start; flex-direction: row; // 改为横向排列:图标 | 文字区 } } .complex-step { flex-direction: row; align-items: center; width: 100%; } .step-title { order: 2; margin-bottom: 0; margin-left: 15px; text-align: left; flex: 1; } .step-icon { order: 1; margin-bottom: 0; } .step-desc { display: none; // 移动端隐藏描述以节省空间 } .step-connector { // 移动端连接线变为垂直 top: 50px; left: 25px; // 对齐图标中心 right: auto; bottom: -30px; width: 0; height: auto; border-top: none; border-left: 2px dashed #dcdfe6; } } } </style>

    这个案例综合运用了render-content、深度样式覆盖、状态管理、响应式设计。关键点在于,我们通过:deep(.el-steps) .el-step__head { display: none; }隐藏了Element UI默认渲染的节点,然后完全用自己的DOM结构和样式来绘制步骤条,仅利用了el-steps组件最核心的“状态管理”和“布局容器”功能。这种方式提供了最大的灵活性,但也需要开发者承担更多的实现成本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询