基于Vue的步骤条组件技术解析与实现
一、组件概述
本文将深入解析一个基于Vue 3的水平步骤条组件实现方案,该组件支持动态渲染步骤项、状态标识(进行中/已完成/未开始)、自定义图标及标题,并提供响应式布局能力。组件设计兼顾可复用性与扩展性,适用于表单流程引导、审批进度展示等场景。
二、核心功能特性
-
动态渲染与状态管理
-
通过
stepOptions
数组动态生成步骤项,每项包含flowId
(唯一标识)和flowName
(显示名称)。 -
支持两种状态标识:
- 进行中:当前激活步骤(
currentStep
) - 已完成:已完成的步骤(
nodeStepIndex
) - 未开始:未触发的步骤。
- 进行中:当前激活步骤(
-
-
交互设计
- 点击步骤项可切换激活状态,触发
setHandleStep
事件并传递当前步骤信息。 - 提供只读模式提供只读模式(
onlyView
),禁用点击但保留状态标识。
- 点击步骤项可切换激活状态,触发
-
样式定制
- 支持主题色切换(默认蓝色系),通过修改SCSS变量
--active-color
可快速适配不同设计需求。 - 引导线自适应布局,利用
flex
和伪元素::after
实现动态宽度。
- 支持主题色切换(默认蓝色系),通过修改SCSS变量
三、代码实现解析
1. 模板结构(<template>
)
ini
<template>
<div class="step-container">
<div
class="content"
v-for="(item, index) in stepOptions"
:key="item.flowId"
@click="handleStepClick(item, index)"
>
<div
class="step-item"
:class="{
activeBlue: isActive(index),
}"
>
<div class="step-circle">
<span v-if="index + 1 >= nodeStepIndex" class="step-number">{{
index + 1
}}</span>
<span v-if="index + 1 < nodeStepIndex">
<img
class="w-[1.125rem] h-[1.125rem]"
:src="getAssetUrl(currentStep == index + 1)"
/>
</span>
</div>
<div v-if="index < stepOptions.length - 1" class="step-line"></div>
<div class="step-title">{{ item.flowName }}</div>
</div>
</div>
</div>
</template>
-
关键点:
- 使用
v-for
动态生成步骤项,key
绑定唯一标识flowId
。 - 通过
:class
动态添加activeBlue
类,控制激活状态的样式。
- 使用
2. 脚本逻辑(<script setup>
)
javascript
import { ref, watch } from 'vue'
const props = defineProps({
stepOptions: Array,
handleStep: Number,
nodeStepIndex: Number
})
const currentStep = ref(props.handleStep)
// 状态判断函数
const isActive = (index) => {
return currentStep.value === index + 1 || index + 1 === props.nodeStepIndex
}
// 图标路径生成
const getAssetUrl = (isCurrent) => {
return new URL(`/src/assets/images/${isCurrent ? 'white_correct' : 'blue_correct'}.png`, import.meta.url).href
}
// 监听外部传入的步骤变化
watch(() => props.handleStep, (val) => {
currentStep.value = val
})
-
核心逻辑:
currentStep
响应式跟踪当前激活步骤,与父组件通过props
和emit
双向绑定。getAssetUrl
函数根据步骤状态返回对应图标路径,支持动态加载。
3. 样式设计(<style scoped>
)
css
.step-container {
display: flex;
align-items: center;
justify-content: center;
}
.step-item {
position: relative;
margin-right: 127px; // 127/16rem
width: 100%;
}
.step-circle {
width: 48px; // 48/16rem
height: 48px;
border-radius: 50%;
border: 2px solid #1181fd;
display: flex;
justify-content: center;
align-items: center;
background: rgba(17, 129, 253, 0.1);
&:hover {
background: rgba(17, 129, 253, 0.24);
}
}
.activeBlue {
.step-circle {
background: #1181fd;
border-color: #1181fd;
.step-number {
color: #fff;
}
}
.step-title {
color: #1181fd !important;
}
}
-
布局技巧:
- 使用
flex
实现水平排列,通过margin-right
控制步骤间距。 - 引导线利用伪元素
::after
和flex: 1
实现自适应宽度。
- 使用
4. 完整源码
xml
<template>
<div class="step-container">
<div
class="content"
v-for="(item, index) in stepOptions"
:key="item.flowId"
@click="handleStepClick(item, index)"
>
<div
class="step-item"
:class="{
activeBlue: isActive(index),
}"
>
<div class="step-circle">
<span v-if="index + 1 >= nodeStepIndex" class="step-number">{{
index + 1
}}</span>
<span v-if="index + 1 < nodeStepIndex">
<img
class="w-[1.125rem] h-[1.125rem]"
:src="getAssetUrl(currentStep == index + 1)"
/>
</span>
</div>
<div v-if="index < stepOptions.length - 1" class="step-line"></div>
<div class="step-title">{{ item.flowName }}</div>
</div>
</div>
</div>
</template>
<script setup name="StepBar">
import { ref, watch } from 'vue'
const props = defineProps({
stepOptions: {
type: Array,
default: () => [],
example: [
{ flowId: 1, flowName: '电量填报' },
{ flowId: 2, flowName: '电费填报' },
{ flowId: 3, flowName: '审核' },
],
},
handleStep: {
// 前端操作的节点
type: Number,
default: 1,
},
nodeStepIndex: {
// 工单当前的节点
type: Number,
default: 1,
},
})
const currentStep = ref(props.handleStep)
const emit = defineEmits(['setHandleStep'])
/**
* 处理步骤点击事件
* @param {Object} item - 当前步骤数据
* @param {Number} index - 步骤索引
*/
const handleStepClick = (item, index) => {
currentStep.value = index + 1
emit('setHandleStep', currentStep.value, item)
}
/**
* 判断当前步骤是否激活
* @param {Number} index - 步骤索引
* @returns {Boolean}
*/
const isActive = (index) => {
return currentStep.value === index + 1 || index + 1 === props.nodeStepIndex
}
/**
* 获取步骤图标路径
* @param {Number} index - 步骤索引
* @returns {String}
*/
const getAssetUrl = (isCurrent) => {
return new URL(
`/src/assets/images/${isCurrent ? 'white_correct' : 'blue_correct'}.png`,
import.meta.url,
).href
}
watch(
() => props.handleStep,
(val) => {
currentStep.value = val
},
)
</script>
<style lang="scss" scoped>
/* 步骤条样式 */
.step-container {
display: flex;
align-items: center;
justify-content: center;
.divider-line {
border-top: 2px solid #dde2ed;
position: relative;
top: -64px;
left: 64px;
width: 128px;
margin: 0;
}
.content {
&:last-child {
.divider-line {
display: none;
}
}
}
.step-item {
position: relative;
margin-right: 127px /* 127/16 */;
width: 100%;
}
.step-circle {
width: 48px /* 48/16 */;
height: 48px /* 48/16 */;
border-radius: 50%;
border: 2px solid #1181fd;
display: flex;
justify-content: center;
align-items: center;
background: rgba(17, 129, 253, 0.1);
margin: 0 auto;
&:hover {
background: rgba(17, 129, 253, 0.24);
}
}
.step-circle:hover ~ .step-title {
color: #1181fd;
}
.step-number {
font-family: 'SourceHanSansCN-Medium';
font-weight: 500;
font-size: 20px /* 20/16 */;
color: #1181fd;
}
.step-title {
font-family: 'SourceHanSansCN-Regular';
font-weight: 400;
font-size: 16px /* 16/16 */;
color: #9ca2af;
margin-top: 16px /* 16/16 */;
text-align: center;
position: absolute;
left: 0;
right: 0;
}
.step-line {
width: calc(100% - 90px);
height: 2px /* 2/16 */;
background-color: #dde2ed;
position: absolute;
top: 18px;
right: -36px;
}
.onlyViewStep {
background: rgba(17, 129, 253, 0.1);
border-color: #1181fd;
.step-number {
color: #1181fd;
}
&:hover {
background: rgba(17, 129, 253, 0.24);
.step-title {
color: #1181fd;
}
}
}
.activeBlue .step-circle {
border-color: #1181fd;
background: #1181fd;
.step-number {
color: #fff;
}
}
.activeBlue .step-title {
color: #1181fd !important;
}
.backActive .step-circle {
background: rgba(17, 129, 253, 0.24) !important;
}
.onlyView {
border-color: #1181fd;
.step-circle {
border: 2px solid #1181fd;
border-color: #1181fd !important;
}
.step-number {
color: #1181fd;
}
&:hover {
.step-title {
color: #1181fd;
}
}
}
.onlyViewActive {
.step-circle {
background: #1181fd;
}
.step-title {
color: #1181fd !important;
}
.step-number {
color: #fff;
}
}
}
</style>
四、应用场景示例
ini
<template>
<StepBar
:stepOptions="steps"
:handleStep="currentStep"
:nodeStepIndex="completedStep"
@setHandleStep="handleStepChange"
/>
</template>
<script setup>
import StepBar from '@/components/StepBar.vue'
const steps = [
{ flowId: 1, flowName: '电量填报' },
{ flowId: 2, flowName: '电费计算' },
{ flowId: 3, flowName: '审核' }
]
const currentStep = ref(1)
const completedStep = ref(1)
const handleStepChange = (newStep) => {
currentStep.value = newStep
completedStep.value = Math.min(newStep, 3) // 假设最多3步
}
</script>
五、总结
本文通过拆解一个Vue步骤条组件的实现细节,展示了如何结合动态渲染、状态管理和样式设计构建可复用组件。实际开发中可进一步扩展功能,如支持国际化、集成表单验证等。
该文章仅用于学习记录,不涉及商业传播。