Skip to content

Steps 步骤条

概述

Steps 步骤条是一个用于引导用户按照流程完成任务的分步导航组件。它通过可视化的方式展示任务进度,帮助用户了解当前所处的步骤位置以及整个流程的完成情况。

主要特性

  • 流程可视化:清晰展示任务的各个步骤和当前进度
  • 状态管理:支持多种步骤状态(等待、进行中、完成、错误等)
  • 布局灵活:支持水平和垂直两种布局方向
  • 样式丰富:提供普通和简洁两种风格
  • 高度定制:支持自定义图标、标题和描述
  • 响应式设计:适配不同屏幕尺寸

适用场景

  • 表单向导:多步骤表单填写流程
  • 订单流程:购物车、确认订单、支付、完成等步骤
  • 注册流程:账号信息、验证邮箱、完善资料等
  • 安装向导:软件或系统的安装配置流程
  • 审批流程:文档审批、工作流程等
  • 教程指导:新手引导、操作教程等

学习目标

基础知识

  • 掌握 Steps 的基本概念和使用场景
  • 学会基础步骤条的创建和配置
  • 了解步骤状态的管理方法
  • 掌握水平和垂直布局的使用

进阶技能

  • 学会自定义步骤图标和样式
  • 掌握简洁风格的应用场景
  • 了解步骤条的交互设计
  • 学会响应式步骤条的实现

实战应用

  • 能够设计完整的多步骤流程
  • 掌握步骤条与表单的集成
  • 了解步骤条的性能优化
  • 学会步骤条的可访问性优化

基础用法

基础用法

简单的步骤条。

设置 active 属性,接受一个 Number,表明步骤的 index,从 0 开始。需要定宽的步骤条时,设置 space 属性即可,它接受 Number,单位为 px,如果不设置,则为自适应。设置 finish-status 属性可以改变已经完成的步骤的状态。

vue
<template>
  <el-steps :active="active" finish-status="success">
    <el-step title="Step 1" />
    <el-step title="Step 2" />
    <el-step title="Step 3" />
  </el-steps>
  <el-button style="margin-top: 12px" @click="next">Next step</el-button>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const active = ref(0)
const next = () => {
  if (active.value++ > 2) active.value = 0
}
</script>

含状态的步骤条

每一步骤显示出该步骤的状态。

也可以使用 title 具名插槽,可以用 slot 的方式来取代属性的设置。

vue
<template>
  <el-steps :space="200" :active="1" finish-status="success">
    <el-step title="Done" />
    <el-step title="Processing" />
    <el-step title="Step 3" />
  </el-steps>
</template>

居中的步骤条

标题和描述可以居中。

vue
<template>
  <el-steps :active="2" align-center>
    <el-step title="Step 1" description="Some description" />
    <el-step title="Step 2" description="Some description" />
    <el-step title="Step 3" description="Some description" />
    <el-step title="Step 4" description="Some description" />
  </el-steps>
</template>

带描述的步骤条

每一步都有描述。

vue
<template>
  <el-steps :active="1">
    <el-step title="Step 1" description="Some description" />
    <el-step title="Step 2" description="Some description" />
    <el-step title="Step 3" description="Some description" />
  </el-steps>
</template>

带图标的步骤条

可以在步骤栏中使用各种自定义图标。

通过 icon 属性来设置图标,图标的类型可以参考 Icon 组件的文档,除此以外,还能通过具名 slot 来使用自定义的图标。

vue
<template>
  <el-steps :active="1">
    <el-step title="Step 1" :icon="Edit" />
    <el-step title="Step 2" :icon="Upload" />
    <el-step title="Step 3" :icon="Picture" />
  </el-steps>
</template>

<script lang="ts" setup>
import { Edit, Picture, Upload } from '@element-plus/icons-vue'
</script>

垂直的步骤条

垂直方向的步骤条。

只需要在 el-steps 元素中设置 direction 属性为 vertical 即可。

vue
<template>
  <div style="height: 300px">
    <el-steps direction="vertical" :active="1">
      <el-step title="Step 1" />
      <el-step title="Step 2" />
      <el-step title="Step 3" />
    </el-steps>
  </div>
</template>

简洁风格的步骤条

设置 simple 可应用简洁风格,该条件下 align-center / description / direction / space 都将失效。

vue
<template>
  <el-steps :space="200" :active="1" simple>
    <el-step title="Step 1" :icon="Edit" />
    <el-step title="Step 2" :icon="UploadFilled" />
    <el-step title="Step 3" :icon="Picture" />
  </el-steps>
  <el-steps :active="1" finish-status="success" simple style="margin-top: 20px">
    <el-step title="Step 1" />
    <el-step title="Step 2" />
    <el-step title="Step 3" />
  </el-steps>
</template>

<script lang="ts" setup>
import { Edit, Picture, UploadFilled } from '@element-plus/icons-vue'
</script>

插槽的使用

在 Vue 3.0 项目中使用插槽的方式。

vue
<template>
  <el-steps :active="2" direction="vertical" align-center>
    <el-step title="步骤1" description="描述">
      <template v-slot:description>
        <span>这是测试1</span>
      </template>
    </el-step>
    <el-step title="步骤2" description="描述">
      <template v-slot:description>
        <span>这是测试2</span>
      </template>
    </el-step>
  </el-steps>
</template>

实际应用示例

订单提交流程

电商网站的订单提交步骤条:

vue
<template>
  <div class="order-process">
    <el-steps :active="currentStep" finish-status="success" align-center>
      <el-step 
        title="购物车" 
        description="确认商品信息"
        :icon="ShoppingCart"
      />
      <el-step 
        title="确认订单" 
        description="填写收货信息"
        :icon="Document"
      />
      <el-step 
        title="支付" 
        description="选择支付方式"
        :icon="CreditCard"
      />
      <el-step 
        title="完成" 
        description="订单提交成功"
        :icon="Check"
      />
    </el-steps>
    
    <!-- 步骤内容 -->
    <div class="step-content">
      <div v-if="currentStep === 0" class="cart-step">
        <h3>购物车</h3>
        <div class="cart-items">
          <div v-for="item in cartItems" :key="item.id" class="cart-item">
            <el-image :src="item.image" class="item-image" />
            <div class="item-info">
              <h4>{{ item.name }}</h4>
              <p>价格:¥{{ item.price }}</p>
              <el-input-number v-model="item.quantity" :min="1" size="small" />
            </div>
          </div>
        </div>
        <div class="step-actions">
          <el-button type="primary" @click="nextStep">确认商品</el-button>
        </div>
      </div>
      
      <div v-if="currentStep === 1" class="order-step">
        <h3>确认订单</h3>
        <el-form :model="orderForm" label-width="100px">
          <el-form-item label="收货人">
            <el-input v-model="orderForm.receiver" placeholder="请输入收货人姓名" />
          </el-form-item>
          <el-form-item label="联系电话">
            <el-input v-model="orderForm.phone" placeholder="请输入联系电话" />
          </el-form-item>
          <el-form-item label="收货地址">
            <el-input 
              v-model="orderForm.address" 
              type="textarea" 
              placeholder="请输入详细地址"
            />
          </el-form-item>
        </el-form>
        <div class="step-actions">
          <el-button @click="prevStep">返回</el-button>
          <el-button type="primary" @click="nextStep">确认订单</el-button>
        </div>
      </div>
      
      <div v-if="currentStep === 2" class="payment-step">
        <h3>选择支付方式</h3>
        <el-radio-group v-model="paymentMethod" class="payment-methods">
          <el-radio label="alipay" class="payment-option">
            <div class="payment-info">
              <span class="payment-name">支付宝</span>
              <span class="payment-desc">推荐使用支付宝快捷支付</span>
            </div>
          </el-radio>
          <el-radio label="wechat" class="payment-option">
            <div class="payment-info">
              <span class="payment-name">微信支付</span>
              <span class="payment-desc">使用微信扫码支付</span>
            </div>
          </el-radio>
          <el-radio label="bank" class="payment-option">
            <div class="payment-info">
              <span class="payment-name">银行卡</span>
              <span class="payment-desc">支持各大银行储蓄卡及信用卡</span>
            </div>
          </el-radio>
        </el-radio-group>
        <div class="order-summary">
          <p>订单总额:<span class="total-price">¥{{ totalPrice }}</span></p>
        </div>
        <div class="step-actions">
          <el-button @click="prevStep">返回</el-button>
          <el-button type="primary" @click="submitOrder" :loading="submitting">
            立即支付
          </el-button>
        </div>
      </div>
      
      <div v-if="currentStep === 3" class="success-step">
        <div class="success-content">
          <el-icon class="success-icon" color="#67c23a" :size="60">
            <Check />
          </el-icon>
          <h3>订单提交成功!</h3>
          <p>订单号:{{ orderNumber }}</p>
          <p>我们将尽快为您处理订单</p>
        </div>
        <div class="step-actions">
          <el-button @click="goToOrders">查看订单</el-button>
          <el-button type="primary" @click="continueShopping">继续购物</el-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ShoppingCart, Document, CreditCard, Check } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'

const router = useRouter()
const currentStep = ref(0)
const submitting = ref(false)
const paymentMethod = ref('alipay')
const orderNumber = ref('')

const cartItems = reactive([
  {
    id: 1,
    name: 'iPhone 15 Pro',
    price: 7999,
    quantity: 1,
    image: '/images/iphone15.jpg'
  },
  {
    id: 2,
    name: 'AirPods Pro',
    price: 1999,
    quantity: 1,
    image: '/images/airpods.jpg'
  }
])

const orderForm = reactive({
  receiver: '',
  phone: '',
  address: ''
})

const totalPrice = computed(() => {
  return cartItems.reduce((total, item) => total + item.price * item.quantity, 0)
})

const nextStep = () => {
  if (currentStep.value < 3) {
    currentStep.value++
  }
}

const prevStep = () => {
  if (currentStep.value > 0) {
    currentStep.value--
  }
}

const submitOrder = async () => {
  submitting.value = true
  try {
    // 模拟提交订单
    await new Promise(resolve => setTimeout(resolve, 2000))
    orderNumber.value = 'ORD' + Date.now()
    nextStep()
    ElMessage.success('订单提交成功!')
  } catch (error) {
    ElMessage.error('订单提交失败,请重试')
  } finally {
    submitting.value = false
  }
}

const goToOrders = () => {
  router.push('/orders')
}

const continueShopping = () => {
  router.push('/')
}
</script>

<style scoped>
.order-process {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.step-content {
  margin-top: 40px;
  padding: 30px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.cart-items {
  margin: 20px 0;
}

.cart-item {
  display: flex;
  align-items: center;
  padding: 15px;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  margin-bottom: 10px;
}

.item-image {
  width: 60px;
  height: 60px;
  margin-right: 15px;
}

.item-info {
  flex: 1;
}

.item-info h4 {
  margin: 0 0 5px 0;
  color: #303133;
}

.payment-methods {
  margin: 20px 0;
}

.payment-option {
  display: block;
  width: 100%;
  padding: 15px;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  margin-bottom: 10px;
}

.payment-info {
  display: flex;
  flex-direction: column;
  margin-left: 10px;
}

.payment-name {
  font-weight: 500;
  color: #303133;
}

.payment-desc {
  font-size: 12px;
  color: #909399;
  margin-top: 2px;
}

.order-summary {
  text-align: right;
  margin: 20px 0;
  padding: 15px;
  background: #f5f7fa;
  border-radius: 6px;
}

.total-price {
  font-size: 18px;
  font-weight: 600;
  color: #f56c6c;
}

.success-content {
  text-align: center;
  padding: 40px 0;
}

.success-icon {
  margin-bottom: 20px;
}

.step-actions {
  text-align: center;
  margin-top: 30px;
}

.step-actions .el-button {
  margin: 0 10px;
}
</style>

用户注册向导

多步骤用户注册流程:

vue
<template>
  <div class="register-wizard">
    <el-steps :active="currentStep" direction="vertical" class="register-steps">
      <el-step 
        title="账号信息" 
        description="设置登录账号和密码"
        :icon="User"
        :status="getStepStatus(0)"
      />
      <el-step 
        title="个人信息" 
        description="完善个人基本信息"
        :icon="UserFilled"
        :status="getStepStatus(1)"
      />
      <el-step 
        title="邮箱验证" 
        description="验证邮箱地址"
        :icon="Message"
        :status="getStepStatus(2)"
      />
      <el-step 
        title="完成注册" 
        description="注册成功"
        :icon="Check"
        :status="getStepStatus(3)"
      />
    </el-steps>
    
    <div class="wizard-content">
      <!-- 步骤1:账号信息 -->
      <div v-show="currentStep === 0" class="step-panel">
        <h3>设置账号信息</h3>
        <el-form :model="accountForm" :rules="accountRules" ref="accountFormRef">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="accountForm.username" placeholder="请输入用户名" />
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input 
              v-model="accountForm.password" 
              type="password" 
              placeholder="请输入密码"
              show-password
            />
          </el-form-item>
          <el-form-item label="确认密码" prop="confirmPassword">
            <el-input 
              v-model="accountForm.confirmPassword" 
              type="password" 
              placeholder="请再次输入密码"
              show-password
            />
          </el-form-item>
        </el-form>
        <div class="step-actions">
          <el-button type="primary" @click="validateAndNext('accountFormRef')">
            下一步
          </el-button>
        </div>
      </div>
      
      <!-- 步骤2:个人信息 -->
      <div v-show="currentStep === 1" class="step-panel">
        <h3>完善个人信息</h3>
        <el-form :model="profileForm" :rules="profileRules" ref="profileFormRef">
          <el-form-item label="真实姓名" prop="realName">
            <el-input v-model="profileForm.realName" placeholder="请输入真实姓名" />
          </el-form-item>
          <el-form-item label="手机号" prop="phone">
            <el-input v-model="profileForm.phone" placeholder="请输入手机号" />
          </el-form-item>
          <el-form-item label="邮箱" prop="email">
            <el-input v-model="profileForm.email" placeholder="请输入邮箱地址" />
          </el-form-item>
          <el-form-item label="性别">
            <el-radio-group v-model="profileForm.gender">
              <el-radio label="male">男</el-radio>
              <el-radio label="female">女</el-radio>
            </el-radio-group>
          </el-form-item>
        </el-form>
        <div class="step-actions">
          <el-button @click="prevStep">上一步</el-button>
          <el-button type="primary" @click="validateAndNext('profileFormRef')">
            下一步
          </el-button>
        </div>
      </div>
      
      <!-- 步骤3:邮箱验证 -->
      <div v-show="currentStep === 2" class="step-panel">
        <h3>验证邮箱</h3>
        <p>我们已向 <strong>{{ profileForm.email }}</strong> 发送了验证码</p>
        <el-form :model="verifyForm" ref="verifyFormRef">
          <el-form-item label="验证码" prop="code">
            <div class="verify-input">
              <el-input 
                v-model="verifyForm.code" 
                placeholder="请输入6位验证码"
                maxlength="6"
              />
              <el-button 
                @click="sendVerifyCode" 
                :disabled="countdown > 0"
                class="send-code-btn"
              >
                {{ countdown > 0 ? `${countdown}s后重发` : '发送验证码' }}
              </el-button>
            </div>
          </el-form-item>
        </el-form>
        <div class="step-actions">
          <el-button @click="prevStep">上一步</el-button>
          <el-button type="primary" @click="verifyEmail" :loading="verifying">
            验证邮箱
          </el-button>
        </div>
      </div>
      
      <!-- 步骤4:完成注册 -->
      <div v-show="currentStep === 3" class="step-panel">
        <div class="success-content">
          <el-icon class="success-icon" color="#67c23a" :size="80">
            <Check />
          </el-icon>
          <h3>注册成功!</h3>
          <p>欢迎加入我们,{{ profileForm.realName }}!</p>
          <p>您现在可以使用账号 <strong>{{ accountForm.username }}</strong> 登录系统</p>
        </div>
        <div class="step-actions">
          <el-button type="primary" @click="goToLogin">立即登录</el-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { User, UserFilled, Message, Check } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'

const router = useRouter()
const currentStep = ref(0)
const verifying = ref(false)
const countdown = ref(0)

const accountForm = reactive({
  username: '',
  password: '',
  confirmPassword: ''
})

const profileForm = reactive({
  realName: '',
  phone: '',
  email: '',
  gender: 'male'
})

const verifyForm = reactive({
  code: ''
})

const accountRules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' }
  ],
  confirmPassword: [
    { required: true, message: '请确认密码', trigger: 'blur' },
    {
      validator: (rule, value, callback) => {
        if (value !== accountForm.password) {
          callback(new Error('两次输入密码不一致'))
        } else {
          callback()
        }
      },
      trigger: 'blur'
    }
  ]
}

const profileRules = {
  realName: [
    { required: true, message: '请输入真实姓名', trigger: 'blur' }
  ],
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱地址', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
  ]
}

const getStepStatus = (step) => {
  if (step < currentStep.value) return 'finish'
  if (step === currentStep.value) return 'process'
  return 'wait'
}

const validateAndNext = async (formRef) => {
  const form = formRef === 'accountFormRef' ? accountFormRef.value : profileFormRef.value
  try {
    await form.validate()
    nextStep()
  } catch (error) {
    ElMessage.error('请完善必填信息')
  }
}

const nextStep = () => {
  if (currentStep.value < 3) {
    currentStep.value++
  }
}

const prevStep = () => {
  if (currentStep.value > 0) {
    currentStep.value--
  }
}

const sendVerifyCode = () => {
  countdown.value = 60
  const timer = setInterval(() => {
    countdown.value--
    if (countdown.value <= 0) {
      clearInterval(timer)
    }
  }, 1000)
  ElMessage.success('验证码已发送')
}

const verifyEmail = async () => {
  if (!verifyForm.code) {
    ElMessage.error('请输入验证码')
    return
  }
  
  verifying.value = true
  try {
    // 模拟验证
    await new Promise(resolve => setTimeout(resolve, 1500))
    nextStep()
    ElMessage.success('邮箱验证成功!')
  } catch (error) {
    ElMessage.error('验证码错误')
  } finally {
    verifying.value = false
  }
}

const goToLogin = () => {
  router.push('/login')
}
</script>

<style scoped>
.register-wizard {
  display: flex;
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
  gap: 40px;
}

.register-steps {
  width: 300px;
  flex-shrink: 0;
}

.wizard-content {
  flex: 1;
  background: #fff;
  padding: 30px;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.step-panel h3 {
  margin: 0 0 20px 0;
  color: #303133;
}

.verify-input {
  display: flex;
  gap: 10px;
}

.send-code-btn {
  white-space: nowrap;
}

.success-content {
  text-align: center;
  padding: 40px 0;
}

.success-icon {
  margin-bottom: 20px;
}

.step-actions {
  text-align: center;
  margin-top: 30px;
}

.step-actions .el-button {
  margin: 0 10px;
}
</style>

API

Steps Attributes

属性名说明类型可选值默认值
space每个 step 的间距,不填写将自适应间距。支持百分比。number / string
direction显示方向stringvertical/horizontalhorizontal
active设置当前激活步骤number0
process-status设置当前步骤的状态stringwait / process / finish / error / successprocess
finish-status设置结束步骤的状态stringwait / process / finish / error / successfinish
align-center进行居中对齐booleanfalse
simple是否应用简洁风格booleanfalse

Steps Slots

插槽名说明子标签
默认插槽Step

Step Attributes

属性名说明类型可选值默认值
title标题string
description描述文案string
iconStep 组件的自定义图标。也支持 slot 方式写入string / Component
status设置当前步骤的状态,不设置则根据 steps 确定状态stringwait / process / finish / error / success

Step Slots

插槽名说明
icon自定义图标
title自定义标题
description自定义描述文案

最佳实践

  1. 步骤数量:步骤不得少于 2 步,建议不超过 6 步以保持清晰
  2. 状态管理:合理使用 process-statusfinish-status 来表示不同的步骤状态
  3. 响应式设计:在移动端考虑使用垂直布局或简洁风格
  4. 图标选择:选择语义明确的图标来增强用户理解
  5. 描述信息:为复杂流程提供必要的描述信息
  6. 交互反馈:可以通过点击事件实现步骤间的跳转

常见问题

Q: 如何实现步骤条的点击跳转?

A: 可以通过 @click.native 事件监听步骤的点击,然后更新 active 属性值。

Q: Vue 3.0 中如何使用插槽?

A: 在 Vue 3.0 中需要使用 <template v-slot:slotName> 的方式,不能再使用 slot-scope

Q: 简洁风格下哪些属性会失效?

A: 设置 simple 为 true 时,align-centerdescriptiondirectionspace 属性都将失效。

Q: 如何自定义步骤条的样式?

A: 可以通过 CSS 覆盖默认样式,如修改 .el-steps.el-step__head.el-step__title 等类名的样式。

总结

Steps 步骤条是一个功能强大的流程导航组件,具有以下特点:

  • 流程清晰:通过可视化方式展示任务进度和步骤关系
  • 状态丰富:支持多种步骤状态,准确反映当前进度
  • 布局灵活:支持水平和垂直布局,适应不同场景
  • 高度定制:支持自定义图标、标题、描述等内容
  • 交互友好:可配合表单验证实现智能步骤跳转
  • 响应式设计:在不同设备上都能提供良好的用户体验

适用于多步骤流程的场景,如表单向导、订单流程、注册流程等。通过合理的步骤设计和状态管理,可以大大提升用户完成复杂任务的体验。

参考资料

Element Plus Study Guide