步骤条组件【Vue3】

199 阅读4分钟

一:最终实现效果

最终效果:

1.当前步骤的节点图标大小、文案颜色、与其他步骤不一样

2.步骤条的两端节点需要与下方布局对齐、节点图标相对于文案居中对齐。

3.链接线始终居中链接。

image.png

二 实现代码:

Steps父组件:

<template>
<div class="steps">
    <slot></slot>
</div>
</template>
<script setup lang="ts">
// 自定义步骤条 - 从1开始计算
const props = defineProps({
  current: {
    type: Number,
    default: 0
  },
  // 图标大小
  size: {
    type: Number,
    default: 24
  },
  //最大图标大小
  maxSize:{
    type: Number,
    default: 24
  },
  align: {
      type: String,
      default: 'center'
  }
})
const active = computed(() => {
  return props.current
})
//图标大小
const size = computed(() => {
  return props.size
})
//最大图标大小
const maxSize = computed(() => {
  return props.maxSize
})
const align = computed(() => {
  return props.align
})
const index = ref(0);
const stepIndexList:any = ref([]);
const getStepIndex = () => {
  const temp = index.value;
  // 将当前索引存入数组(避免重复存储,可根据需求调整)
  stepIndexList.value.push(temp);
  // 计数器自增(为下一个子组件准备)
  index.value++;
  // 返回当前索引给调用的子组件
  return temp;
}
//注入
provide("active", active);
provide("maxSize", maxSize);
provide("size", size);
provide("getStepIndex", getStepIndex);
provide("align", align);
provide("stepIndexList", stepIndexList);
</script>
<style lang="less" scoped>
.steps {
    display: flex;
    width: 100%;
    justify-content: space-between;
}
</style>

Step子组件:

<template>
  <div class="step-content-box">
    <div
      class="step-head"
      :class="{
        'step-active-center-head': align === 'center'
      }"
      :style="{
        '--step-icon-size': `${size / 2}px`,
        '--step-icon-max-size': `${maxSize / 2}px`
      }"
    >
      <div
        :style="{
          height: `${maxSize}px`,
          width: `${active === currentIndex ? maxSize : size}px`
        }"
        class="step-icon-box"
        :class="{
          'no-last-step-icon-box': !isLastIndex 
        }"
      >
        <div
          class="step-icon"
          :class="{
            'finised-icon': active > currentIndex,
            'active-icon': active === currentIndex
          }"
          :style="{
            height: `${active === currentIndex ? maxSize : size}px`,
            width: `${active === currentIndex ? maxSize : size}px`
          }"
        >
          {{ active > currentIndex ? '✓' : currentIndex + 1 }}
        </div>
      </div>
      <!-- //不使用flex : 1 ,就不要这样写 -->
      <!-- <div
        v-if="!isLastIndex"
        class="step-line"
        :class="{ 'step-finish-line': active > currentIndex }"
      >
        <div class="step-description">
          {{ description }}
        </div>
      </div> -->
    </div>
    <div class="step-content">
      <div
        class="step-title"
        :class="{
          'step-finish-title': active > currentIndex,
          'step-active-title': active === currentIndex,
          'center-title': align === 'center'
        }"
      >
        {{ title }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
const props = defineProps({
  title: {
    type: String,
    default: ''
  },
  description: {
    type: String,
    default: ''
  },
  icon: {
    type: String,
    default: ''
  }
})
const isLastIndex = computed(() => {
  return (
    stepIndexList.value[stepIndexList.value.length - 1] ===
    currentIndex.value
  )
})
const currentIndex = ref(0)
const active: any = inject('active')
const stepIndexList: any = inject('stepIndexList')
const getStepIndex: any = inject('getStepIndex')
const align = inject('align')
const size: any = inject('size')
const maxSize: any = inject('maxSize')
onMounted(() => {
  currentIndex.value = getStepIndex()
})
</script>

<style scoped lang="less">
.step-content-box {
  // flex: 1; // --- 使用这个属性会导致step组件的title不能紧贴两边-可启用看效果;
  position: relative;
  .step-head {
    display: flex;
    align-items: center;
    width: 100%;
    .step-line {
      width: 100%;
      height: 1px;
      background-color: gray;
      position: relative;
      .step-description {
        color: gray;
        font-size: 12px;
        margin-top: 8px;
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
        top: -30px;
        max-width: 150px;
      }
    }
    .step-icon-box {
      display: flex;
      align-items: center;
      position: relative;
      background-color: white;//盖住超过的线条长度
    }
    // 使用两端布局的线样式 - 与step-line互相冲突 - 两者取其中一个
    .no-last-step-icon-box::after {
      width: 220px !important;
      content: '';
      position: absolute;
      top: 50%;
      right: -220px;
      height: 1px;
      background-color: #ebedf0;
      z-index: -999;
    }
    .step-finish-line {
      background-color: green;
    }
    .step-finish-icon {
      color: green;
    }
    .step-icon {
      border: 1px solid gray;
      color: gray;
      height: 24px;
      width: 24px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 50%;
    }
    .finised-icon {
      color: green;
      border-color: green;
    }
    .active-icon {
      color: blueviolet;
      border-color: blueviolet;
      height: 30px;
      width: 30px;
    }
  }
  .step-active-center-head {
    transform: translateX(calc(50% - var(--step-icon-max-size)));
  }
  .step-content {
    margin-top: 8px;
    position: relative;
    .step-title {
      font-size: 14px;
      color: gray;
    }
    .center-title {
      text-align: center;
    }
    .step-active-title {
      color: blueviolet;
    }
    .step-finish-title {
      color: green;
    }
  }
}
</style>

组件中使用:

image.png

三:封装过程中需要注意的点

1.因为在当前节点的图标大小与其他步骤的图标大小不一致的同时,要保证步骤线基于每一个图标都居中衔接的话,除了传入图标的size大小外,还要传入图标的max-size(其他节点的图标盒子高度要跟max-size保持一致,然后再垂直居中),这样才能保持下方文案在同一水平线以及链接线可以达到图标不一致大小也居中衔接的效果(以最大尺寸为基准。)

image.png

image.png

2.step组件怎么知道自己是第几个步骤条呢?

2.1 在Steps组件中初始化一个变量以及一个函数,插槽元素依次渲染,就能知道自己是属于第几步的步骤元素。

image.png

2.2 Step组件每次初始化时就调用一次方法,

image.png

3.基于定位的链接线长度过长怎么才能保证不穿透下一个步骤元素呢?

先说解决方案**:给步骤元素添加背景色!!

但是为什么加了背景色,就可以盖住穿透的定位元素线,哪怕定位元素的z-index为9999,笔者现在也没弄清楚,有大佬可以在评论区告知一下。**

image.png

4.因为UI要求,步骤条的文案要基于两端对齐,所以每一个step没有采取flex:1去平分空间,想看效果可放开注释代码即可。

四:参考文章

参考文章链接 : juejin.cn/post/745936…