可自定义设置以下属性:
步骤数组(steps),类型:Array<{title?: string, description?: string}>,默认 []
步骤条总宽度(width),类型:number | string,单位 px,默认 'auto'
步骤条大小(size),类型:'default' | 'small',默认 'default'
是否使用垂直步骤条(vertical),当 vertical: true 时,labelPlacement 自动设为 right,类型:boolean,默认 false
标签放置位置(labelPlacement),默认放图标右侧,可选 bottom 放图标下方,类型:'right' | 'bottom',默认 'right'
是否使用点状步骤条(dotted),当 dotted: true 且 vertical: false 时,labelPlacement 将自动设为 bottom,类型:boolean,默认 false
当前选中的步骤,设置 v-model 后,Steps 变为可点击状态(v-model:current),类型:number,默认 1,从 1 开始计数
效果如下图:在线预览
①创建步骤条组件Steps.vue:
import { computed } from 'vue'
interface Step {
title?: string // 标题
description?: string // 描述
}
interface Props {
steps?: Step[] // 步骤数组
width?: number | string // 步骤条总宽度,单位 px
size?: 'default' | 'small' // 步骤条大小
vertical?: boolean // 是否使用垂直步骤条,当 vertical: true 时,labelPlacement 自动设为 right
labelPlacement?: 'right' | 'bottom' // 标签放置位置,默认放图标右侧,可选 bottom 放图标下方
dotted?: boolean // 是否使用点状步骤条,当 dotted: true 且 vertical: false 时,labelPlacement 将自动设为 bottom
current?: number // (v-model) 当前选中的步骤,设置 v-model 后,Steps 变为可点击状态。从 1 开始计数
}
const props = withDefaults(defineProps
steps: () => [],
width: 'auto',
size: 'default',
vertical: false,
labelPlacement: 'right',
dotted: false,
current: 1
})
const totalWidth = computed(() => {
if (typeof props.width === 'number') {
return props.width + 'px'
} else {
return props.width
}
})
const totalSteps = computed(() => {
// 步骤总数
return props.steps.length
})
const currentStep = computed(() => {
if (props.current < 1) {
return 1
} else if (props.current > totalSteps.value + 1) {
return totalSteps.value + 1
} else {
return props.current
}
})
// 若当前选中步骤超过总步骤数,则默认选择步骤1
const emits = defineEmits(['update:current', 'change'])
function onChange(index: number) {
// 点击切换选择步骤
if (currentStep.value !== index) {
emits('update:current', index)
emits('change', index)
}
}
class="m-steps"
:class="{
'steps-small': size === 'small',
'steps-vertical': vertical,
'steps-label-bottom': !vertical && (labelPlacement === 'bottom' || dotted),
'steps-dotted': dotted
}"
:style="`width: ${totalWidth};`"
>
class="m-steps-item"
:class="{
'steps-finish': currentStep > index + 1,
'steps-process': currentStep === index + 1,
'steps-wait': currentStep < index + 1
}"
v-for="(step, index) in steps"
:key="index"
>
{ step.title }}
{ step.description }}
.m-steps {
display: flex;
gap: 16px;
transition: all 0.3s;
&:not(.steps-label-bottom) {
.m-steps-item .steps-info-wrap {
.steps-tail {
display: none;
}
}
}
.m-steps-item {
position: relative;
overflow: hidden;
flex: 1; // 弹性盒模型对象的子元素都有相同的长度,且忽略它们内部的内容
vertical-align: top;
&:last-child {
flex: none;
.steps-info-wrap {
.m-steps-content .steps-title {
padding-right: 0;
&::after {
display: none;
}
}
.steps-tail {
display: none;
}
}
}
.steps-info-wrap {
display: inline-block;
vertical-align: top;
outline: none;
.steps-tail {
height: 9px;
position: absolute;
top: 12px;
inset-inline-start: 0;
width: 100%;
transition: all 0.3s;
&::after {
display: inline-block;
vertical-align: top;
width: 100%;
height: 1px;
background-color: rgba(5, 5, 5, 0.06);
border-radius: 1px;
transition: background-color 0.3s;
content: '';
}
}
.steps-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 8px;
width: 32px;
height: 32px;
border-radius: 50%;
text-align: center;
background-color: rgba(0, 0, 0, 0.06);
border: 1px solid transparent;
transition: all 0.3s;
.steps-num {
display: inline-block;
font-size: 16px;
line-height: 1;
color: rgba(0, 0, 0, 0.65);
transition: all 0.3s;
}
.icon-svg {
display: inline-block;
fill: @themeColor;
width: 16px;
height: 16px;
transition: all 0.3s;
}
.steps-dot {
width: 100%;
height: 100%;
border-radius: 50%;
transition: all 0.3s;
}
}
.m-steps-content {
display: inline-block;
vertical-align: top;
transition: all 0.3s;
.steps-title {
position: relative;
display: inline-block;
color: rgba(0, 0, 0, 0.45);
font-size: 16px;
line-height: 32px;
transition: all 0.3s;
padding-right: 16px;
&::after {
background: #e8e8e8;
position: absolute;
top: 16px;
left: 100%;
display: block;
width: 3000px;
height: 1px;
content: '';
cursor: auto;
transition: all 0.3s;
}
}
.steps-description {
max-width: 140px;
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
line-height: 22px;
word-break: break-all;
transition: all 0.3s;
}
}
}
}
.steps-finish {
.steps-info-wrap {
cursor: pointer;
.steps-tail {
&::after {
background-color: @themeColor;
}
}
.steps-icon {
background-color: #e6f4ff;
border-color: #e6f4ff;
.steps-dot {
background: @themeColor;
}
}
.m-steps-content {
.steps-title {
color: rgba(0, 0, 0, 0.88);
&::after {
background-color: @themeColor;
}
}
.steps-description {
color: rgba(0, 0, 0, 0.45);
}
}
&:hover {
.steps-icon {
border-color: @themeColor;
}
.m-steps-content {
.steps-title {
color: @themeColor;
}
.steps-description {
color: @themeColor;
}
}
}
}
}
.steps-process {
.steps-info-wrap {
.steps-icon {
background-color: @themeColor;
border: 1px solid rgba(0, 0, 0, 0.25);
border-color: @themeColor;
.steps-num {
color: #fff;
}
.steps-dot {
background: @themeColor;
}
}
.m-steps-content {
.steps-title {
color: rgba(0, 0, 0, 0.88);
}
.steps-description {
color: rgba(0, 0, 0, 0.88);
}
}
}
}
.steps-wait {
.steps-info-wrap {
cursor: pointer;
&:hover {
.steps-icon {
border-color: @themeColor;
.steps-num {
color: @themeColor;
}
}
.m-steps-content {
.steps-title {
color: @themeColor;
}
.steps-description {
color: @themeColor;
}
}
}
.steps-icon {
.steps-dot {
background: rgba(0, 0, 0, 0.25);
}
}
}
}
}
.steps-small {
gap: 12px;
.m-steps-item {
.steps-info-wrap {
.steps-icon {
width: 24px;
height: 24px;
.steps-num {
font-size: 12px;
}
.icon-svg {
width: 12px;
height: 12px;
}
}
.m-steps-content {
.steps-title {
font-size: 14px;
line-height: 24px;
padding-right: 12px;
&::after {
top: 12px;
}
}
}
}
}
}
.steps-label-bottom {
gap: 0;
.m-steps-item {
overflow: visible;
.steps-info-wrap {
.steps-tail {
margin-inline-start: 56px;
padding: 4px 24px;
}
.steps-icon {
margin-inline-start: 40px;
}
.m-steps-content {
display: block;
width: 112px;
margin-top: 12px;
text-align: center;
.steps-title {
padding-right: 0;
&::after {
display: none;
}
}
}
}
}
}
.steps-dotted {
.m-steps-item {
overflow: visible;
.steps-info-wrap {
.steps-tail {
height: 3px;
top: 2.5px;
width: 100%;
margin-top: 0;
margin-bottom: 0;
margin-inline: 70px 0;
padding: 0;
&::after {
width: calc(100% - 24px);
height: 3px;
margin-inline-start: 12px;
}
}
.steps-icon {
position: absolute;
width: 8px;
height: 8px;
margin-inline-start: 66px;
padding-inline-end: 0;
line-height: 8px;
background: transparent;
border: 0;
vertical-align: top;
}
.m-steps-content {
width: 140px;
margin-top: 20px;
.steps-title {
line-height: 1.5714285714285714;
}
}
}
}
.steps-process {
.steps-info-wrap .steps-icon {
top: -1px;
width: 10px;
height: 10px;
line-height: 10px;
margin-inline-start: 65px;
}
}
}
.steps-vertical {
display: inline-flex;
flex-direction: column;
gap: 0;
.m-steps-item {
flex: 1 0 auto;
overflow: visible;
&:last-child {
flex: 1 0 auto;
}
&:not(:last-child) {
.steps-info-wrap {
.steps-tail {
display: block;
}
.m-steps-content {
.steps-title {
&::after {
display: none;
}
}
}
}
}
.steps-info-wrap {
.steps-tail {
top: 0;
inset-inline-start: 15px;
width: 1px;
height: 100%;
padding: 38px 0 6px;
&::after {
width: 1px;
height: 100%;
}
}
.steps-icon {
float: left;
margin-right: 16px;
}
.m-steps-content {
display: block;
min-height: 48px;
overflow: hidden;
.steps-title {
line-height: 32px;
}
.steps-description {
padding-bottom: 12px;
}
}
}
}
}
.steps-small.steps-vertical {
.m-steps-item {
.steps-info-wrap {
.steps-tail {
inset-inline-start: 11px;
padding: 30px 0 6px;
}
.m-steps-content {
.steps-title {
line-height: 24px;
}
}
}
}
}
.steps-vertical.steps-dotted {
.m-steps-item {
.steps-info-wrap {
.steps-tail {
top: 12px;
inset-inline-start: 0;
margin: 0;
padding: 16px 0 8px;
&::after {
margin-inline-start: 3.5px;
}
}
.steps-icon {
position: static;
margin-top: 12px;
margin-inline-start: 0;
background: none;
}
.m-steps-content {
width: inherit;
margin: 0;
}
}
}
.steps-process {
.steps-info-wrap {
.steps-icon {
position: relative;
margin-top: 11px;
top: 0;
inset-inline-start: -1px;
}
}
}
}
.steps-small.steps-vertical.steps-dotted {
.m-steps-item {
.steps-info-wrap {
.steps-tail {
top: 8px;
}
.steps-icon {
margin-top: 8px;
}
}
}
.steps-process {
.steps-info-wrap {
.steps-icon {
margin-top: 7px;
}
}
}
}
②在要使用的页面引入:
import Steps from './Steps.vue'
import { ref, watchEffect, reactive } from 'vue'
const steps = ref([
{
title: 'Step 1',
description: 'description 1'
},
{
title: 'Step 2',
description: 'description 2'
},
{
title: 'Step 3',
description: 'description 3'
},
{
title: 'Step 4',
description: 'description 4'
},
{
title: 'Step 5',
description: 'description 5'
}
])
const minSteps = ref([
{
title: 'Step 1'
},
{
title: 'Step 2'
},
{
title: 'Step 3'
},
{
title: 'Step 4'
},
{
title: 'Step 5'
}
])
const current = ref(3)
watchEffect(() => {
console.log('current:', current.value)
})
const sizeOptions = [
{
label: 'default',
value: 'default'
},
{
label: 'small',
value: 'small'
}
]
const size = ref('small')
const placeOptions = [
{
label: 'right',
value: 'right'
},
{
label: 'bottom',
value: 'bottom'
}
]
const place = ref('bottom')
function onChange(index: number) {
// 父组件获取切换后的选中步骤
console.log('change:', index)
}
function onPrev() {
if (current.value > 1) {
current.value--
}
}
function onNext() {
if (steps.value.length >= current.value) {
current.value++
}
}
const state = reactive({
size: 'default',
vertical: false,
labelPlacement: 'right',
dotted: false,
current: 3
})
{
{ $route.name }} {
{ $route.meta.title }}
基本使用
标签放置位置
迷你版
垂直步骤条
点状步骤条
可点击
设置 v-model:current 后即可点击
步骤条配置器
size:
vertical:
labelPlacement:
dotted:
class="mt30" :steps="steps" :size="state.size" :vertical="state.vertical" :label-placement="state.labelPlacement" :dotted="state.dotted" v-model:current="current" />