可自定义设置以下属性:
步骤数组(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:
<script setup lang="ts">
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<Props>(), {
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)
}
}
</script>
<template>
<div
class="m-steps"
:class="{
'steps-small': size === 'small',
'steps-vertical': vertical,
'steps-label-bottom': !vertical && (labelPlacement === 'bottom' || dotted),
'steps-dotted': dotted
}"
:style="`width: ${totalWidth};`"
>
<div
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"
>
<div tabindex="0" class="steps-info-wrap" @click="onChange(index + 1)">
<div class="steps-tail"></div>
<div class="steps-icon">
<template v-if="!dotted">
<span v-if="currentStep <= index + 1" class="steps-num">{
{ index + 1 }}</span>
<svg v-else class="icon-svg" viewBox="64 64 896 896" data-icon="check" aria-hidden="true" focusable="false">
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
></path>
</svg>
</template>
<template v-else>
<span class="steps-dot"></span>
</template>
</div>
<div class="m-steps-content">
<div class="steps-title">{
{ step.title }}</div>
<div v-if="step.description" class="steps-description">{
{ step.description }}</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.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;
}
}
}
}
</style>
②在要使用的页面引入:
<script setup lang="ts">
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
})
</script>
<template>
<div>
<h1>{
{ $route.name }} {
{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">基本使用</h2>
<Steps :steps="steps" :current="current" @change="onChange" />
<h2 class="mt30 mb10">标签放置位置</h2>
<Flex vertical>
<Radio :options="placeOptions" v-model:value="place" button button-style="solid" />
<Steps :steps="steps" :label-placement="place" :current="current" />
</Flex>
<h2 class="mt30 mb10">迷你版</h2>
<Flex vertical>
<Radio :options="sizeOptions" v-model:value="size" button button-style="solid" />
<Steps :steps="minSteps" :size="size" :current="current" />
</Flex>
<h2 class="mt30 mb10">垂直步骤条</h2>
<Space :gap="120">
<Steps :steps="steps" vertical :current="current" />
<Steps :steps="steps" vertical size="small" :current="current" />
</Space>
<h2 class="mt30 mb10">点状步骤条</h2>
<Space vertical>
<Steps :steps="steps" dotted v-model:current="current" />
<Steps :steps="steps" vertical dotted v-model:current="current" />
</Space>
<h2 class="mt30 mb10">可点击</h2>
<h3 class="mb10">设置 v-model:current 后即可点击</h3>
<Space>
<Button @click="onPrev">Prev</Button>
<Button @click="onNext">Next</Button>
</Space>
<br />
<br />
<Steps :steps="steps" v-model:current="current" />
<br />
<Steps :steps="steps" vertical v-model:current="current" />
<h2 class="mt30 mb10">步骤条配置器</h2>
<Row :gutter="24">
<Col :span="6">
<Space gap="small" vertical>
size:
<Radio :options="sizeOptions" v-model:value="state.size" button button-style="solid" />
</Space>
</Col>
<Col :span="6">
<Space gap="small" vertical>
vertical:
<Switch v-model="state.vertical" />
</Space>
</Col>
<Col :span="6">
<Space gap="small" vertical>
labelPlacement:
<Radio :options="placeOptions" v-model:value="state.labelPlacement" button button-style="solid" />
</Space>
</Col>
<Col :span="6">
<Space gap="small" vertical>
dotted:
<Switch v-model="state.dotted" />
</Space>
</Col>
</Row>
<Steps
class="mt30"
:steps="steps"
:size="state.size"
:vertical="state.vertical"
:label-placement="state.labelPlacement"
:dotted="state.dotted"
v-model:current="current"
/>
</div>
</template>