头图

简介

前文步骤组件状态管理主要从生命周期的角度介绍了步骤组件实例化时组件之间的状态管理,本文将介绍步骤组件具体实现,耐心读完,相信会对您有所帮助。

更多组件剖析详见 👉 📚 Element 2 源码剖析组件总览

步骤组件 step.vue

步骤条主要功能实现都在该组件中。

HTML

模板渲染成一个类名el-step的div元素,元素包含两部分内容

  1. 用于图标、轴线的渲染。
  2. 用于标题、描述的渲染。
// packages\steps\src\step.vue
<template>
  <div class="el-step">
    <!-- 图标 & 轴线 -->
    <div class="el-step__head" >
      <div class="el-step__line" >
        // line
      </div> 
      <div class="el-step__icon">
        // icon
      </div>
    </div>
    <!-- 标题 & 描述 -->
    <div class="el-step__main">
      <div class="el-step__title">
        // title
      </div> 
      <div class="el-step__description">
        // description
      </div>
    </div>
  </div>
</template>

下图是不同状态步骤的展示效果:

image.png

步骤元素通过内联样式和动态类名渲染不同配置下组件样式。

<div
  class="el-step"
  :style="style"
  :class="[
    !isSimple && `is-${$parent.direction}`,
    isSimple && 'is-simple',
    isLast && !space && !isCenter && 'is-flex',
    isCenter && !isVertical && !isSimple && 'is-center'
    ]">
  // ...
</div>

根元素自定义类名

文档中提到当设置 simple 可应用简洁风格,该条件下 align-center 、 direction 、 space 都将失效。

接下将一一分析为什么设置会失效。

属性 direction 用于设置显示方向,生成类名 is-verticalis-horizontal。根据计算属性isSimple判断设置简洁风格时,只会生成类名 is-simple

!isSimple && `is-${$parent.direction}`,  // 生成类名 is-vertical/is-horizontal
isSimple && 'is-simple', // 生成类名 is-simple

// computed 是否简洁风格
isSimple() {
  return this.$parent.simple;
}, 

顶层组件中也会根据属性simpledirection生成根元素类名el-steps--simpleel-steps--verticalel-steps--horizontal

// packages\steps\src\steps.vue
<div
  class="el-steps"
  :class="[
      !simple && 'el-steps--' + direction,
      simple && 'el-steps--simple'
    ]">
    <slot></slot>
</div>

如果未设置间距 space和居中对齐 alignCenter,步骤条末元素会生成类名 is-flex用于自适应宽度。 如果设置了简洁风格,计算属性space中属性space设置无效。

isLast && !space && !isCenter && 'is-flex', // 生成类名is-flex

// computed
// 是否步骤条末元素
isLast() {
  const parent = this.$parent;
  return parent.steps[parent.steps.length - 1] === this;
},
// 间隔设置
space() {
  const { isSimple, $parent: { space } } = this;
  return isSimple ? '' : space ;
},
// 标题描述是否居中对齐
isCenter() {
  return this.$parent.alignCenter;
},

非简洁模式和竖直方向下,设置居中对齐 alignCenter才会生效,生成类名is-center

isCenter && !isVertical && !isSimple && 'is-center' // 生成类名 is-center


// computed 是否竖直方向
isVertical() {
  return this.$parent.direction === 'vertical';
}, 

根元素内联样式

顶层组件 steps 的根元素采用 flex布局。

.el-steps { 
  display: flex;
}  
.el-steps--vertical {
  height: 100%; 
  flex-flow: column;
} 

计算属性style根据属性 space 设置flex-basis,指定了 flex 元素在主轴方向上的初始大小,也就是内容盒(content-box)的尺寸。相当于设置了步骤元素的 widthheight

当一个元素同时被设置了 flex-basis (除值为 auto 外) 和 width (或者在 flex-direction: column 情况下设置了height) , flex-basis 具有更高的优先级。

传入的属性 space值类型为 number时生成格式 {20}px。不设置根据步骤个数自动计算百分比实现自适应间距。

水平方向时,步骤末元素设置属性max-width值,其余设置属性margin-right值(属性stepOffset值没有相关计算或赋值,始终为 0)。

//  computed 根元素样式
style: function() {
  const style = {};
  const parent = this.$parent;
  const len = parent.steps.length;

  const space = (typeof this.space === 'number'
    ? this.space + 'px' // 值为 number,生成 {20}px
    : this.space
      ? this.space
      : 100 / (len - (this.isCenter ? 0 : 1)) + '%'); // 未指定,则自适应间距

  style.flexBasis = space;
  if (this.isVertical) return style;
  if (this.isLast) {
    style.maxWidth = 100 / this.stepsCount + '%';
  } else {
    style.marginRight = -this.$parent.stepOffset + 'px';
  }
  return style;
}

属性 space 可设置值可以参考以下内容:

/* 指定<'width'> */
flex-basis: 10em;
flex-basis: 3px;
flex-basis: auto;

/* 固有的尺寸关键词 */
flex-basis: fill;
flex-basis: max-content;
flex-basis: min-content;
flex-basis: fit-content;

/* 在 flex item 内容上的自动尺寸 */
flex-basis: content;

/* 全局数值 */
flex-basis: inherit;
flex-basis: initial;
flex-basis: unset;

下面通过运行实例对比分析下,各属性设置的渲染效果。

<!-- space 未设置 -->
<el-steps :active="2" finish-status="success">
  <el-step title="步骤 1" description="这是一段很长很长很长的描述性文字"></el-step>
  <el-step title="步骤 2" description="这是一段很长很长很长的描述性文字"></el-step>
  <el-step title="步骤 3" description="这段就没那么长了"></el-step>
  <el-step title="步骤 4" description="这段就没那么长了"></el-step>
</el-steps>
<!-- space 未设置  align-center为true 居中对齐-->
<el-steps :active="2" finish-status="success" align-center>
  // ...
</el-steps>
<!-- space 100 即 100px -->
<el-steps :active="2" finish-status="success" :space="100">
  // ...
</el-steps>

步骤元素内容个数相同情况下,不同设置的效果如下:
image.png

各步骤元素DOM结构如下(此处不考虑内部内容样式区别),除了第一个示例,其余每个步骤都是等分。
image.png

那问题来了,第一个示例中 space 未设置,组件会自适应宽度操作,那么末步骤元素发生了什么导致其宽度跟其他步骤元素不一致?

第一个示例DOM结构渲染如下,虽然都设置了flex-basis: 33.3333%;,但是末元素的未生效。

<div class="el-steps el-steps--horizontal">
  <div class="el-step is-horizontal" style="flex-basis: 33.3333%; margin-right: 0px"></div>
  <div class="el-step is-horizontal" style="flex-basis: 33.3333%; margin-right: 0px"></div>
  <div class="el-step is-horizontal" style="flex-basis: 33.3333%; margin-right: 0px"></div> 
  <div class="el-step is-horizontal is-flex" style="flex-basis: 33.3333%; max-width: 25%"></div>
</div>

因为末元素添加了样式类名is-flex,覆盖了flex-basis,等同于 flex: noneflex: 0 0 auto。元素会根据自身宽高来设置尺寸。它是完全非弹性的:既不会缩短,也不会伸长来适应 flex 容器。

.is-flex {
  flex-basis: auto !important; 
  flex-shrink: 0; 
  flex-grow: 0;
}

上文中提到非简洁模式下,只有未设置间距 space和居中对齐 alignCenter时,步骤条末元素会生成类名 is-flex。这也是第二、三示例末元素跟其他元素宽度一致原因。

isLast && !space && !isCenter && 'is-flex', // 生成类名is-flex

竖直方向样式逻辑与水平一致(alignCenter设置无效),此处不再赘述。

图标 & 轴线

根据计算属性currentStatus生成当前步骤状态对应的主题样式 is-[wait/process/finish/error/success]

<!-- 图标 & 轴线 -->
<div  class="el-step__head" :class="`is-${currentStatus}`">
  <div  class="el-step__line">
    // 轴线...
  </div> 
  <div class="el-step__icon"> 
    // 图标...
  </div>
</div>

图标

图标元素是类名el-step__icon的div元素,提供了具名icon插槽自定义步骤节点图标。插槽内容默认展示Icon图标或表示节点顺序圆环数字(从1开始)。简洁风格下,只有Icon设置生效。

当组件状态值为 successerror 时,使用系统提供图标。

<div class="el-step__icon" :class="`is-${icon ? 'icon' : 'text'}`">
  <slot v-if="currentStatus !== 'success' && currentStatus !== 'error'" name="icon">
    <!-- 节点图标 -->
    <i v-if="icon" class="el-step__icon-inner" :class="[icon]"></i>
    <!-- 节点序号 -->
    <div class="el-step__icon-inner" v-if="!icon && !isSimple">{{ index + 1 }}</div>
  </slot>
  <i v-else :class="['el-icon-' + (currentStatus === 'success' ? 'check' : 'close')]"
    class="el-step__icon-inner is-status"
  >
  </i>
</div>

数字圆环的样式is-text设置的。

:class="`is-${icon ? 'icon' : 'text'}`"

.el-step__icon.is-text {
  border-radius: 50%;
  border: 2px solid;
  border-color: inherit;
}

不同设置组件效果对比如下:

image.png

设置居中对齐 alignCenter时,图标的居中效果是通过样式控制的。

.el-step.is-center .el-step__head {
  text-align: center; 
}

轴线

轴线元素是类名el-step__line的div元素,使用了绝对布局。

<div class="el-step__line">
  <i class="el-step__line-inner" :style="lineStyle"></i>
</div>

通过偏移量、height、width等属性设置轴线位置、长度以及粗细。

.el-step__line {
  position: absolute;
  border-color: inherit;
  background-color: #c0c4cc;
}
/* 水平方向 */
.el-step.is-horizontal .el-step__line {
  height: 2px;
  top: 11px;
  left: 0;
  right: 0;
}
/* 水平居中 */
.el-step.is-center .el-step__line {
  left: 50%;
  right: -50%;
}
/* 竖直方向 */
.el-step.is-vertical .el-step__line {
  width: 2px;
  top: 0;
  bottom: 0;
  left: 11px;
}

使用了伪类:last-of-type 设置最后一个步骤元素中轴线不显示。

.el-step:last-of-type .el-step__line {
  display: none;
}

居中对齐下的轴线偏移量有些特殊,渲染效果如下。
image.png

实际元素DOM结构如下:
image.png

轴线进度状态效果通过类名el-step__line-inner元素实现,该元素内联样式绑定属性lineStyle

属性lineStyle通过方法calcProgress根据当前步骤的状态计算而来。 方法updateStatus作用在生命周期中详细介绍过。

当前步骤元素不是第一个,此时步骤的状态为已完成,就会更新其上一元素的 lineStyle 值,显示进度效果。

updateStatus(val) { 
  // 存在上一步骤节点  计算进度
  if (prevChild) prevChild.calcProgress(this.internalStatus); 
},

calcProgress(status) {
  let step = 100;
  const style = {}; 
  
  style.transitionDelay = 150 * this.index + 'ms'; // 延迟响应过渡效果
  if (status === this.$parent.processStatus) { 
    step = this.currentStatus !== 'error' ? 0 : 0;
  } else if (status === 'wait') {
    step = 0;
    style.transitionDelay = (-150 * this.index) + 'ms'; // 负时会导致过渡立即开始
  }
  // 简洁风格无效
  style.borderWidth = step && !this.isSimple ? '1px' : 0;
  // 方向不同 赋值不同属性
  this.$parent.direction === 'vertical'
    ? style.height = step + '%'
    : style.width = step + '%';

  this.lineStyle = style;
}

标题 & 描述

标题是类名el-step__title的div元素,提供了具名title插槽用于自定义标题,后备插槽内容为属性 title值。

描述类名el-step__description的div元素,提供了具名description插槽用于自定义描述性文字,后备插槽内容为属性 description值。简洁风格下,description 设置失效。

它们根据计算属性currentStatus生成当前步骤状态对应的主题样式 is-[wait/process/finish/error/success]

<!-- 标题 & 描述 -->
<div class="el-step__main">
  <div class="el-step__title" ref="title" :class="['is-' + currentStatus]">
    <slot name="title">{{ title }}</slot>
  </div>
  <!-- 简洁风格的箭头 -->
  <div v-if="isSimple" class="el-step__arrow"></div>
  <div  v-else class="el-step__description" :class="['is-' + currentStatus]">
    <slot name="description">{{ description }}</slot>
  </div>
</div>

简洁风格

前面章节中介绍了简洁风格会导致很多设置无效,接下来整体的看下简洁风格的实现效果。

image.png

此时DOM结构渲染如下。

<div class="el-steps el-steps--simple">
  <!-- 步骤 1 -->
  <div class="el-step is-simple">
    <div class="el-step__head">
      <div class="el-step__line">
        <i class="el-step__line-inner"></i>
      </div>
      <div class="el-step__icon is-icon">
        <i class="el-step__icon-inner el-icon-edit"></i>
      </div>
    </div>
    <div class="el-step__main">
      <div class="el-step__title">步骤 1</div>
      <div class="el-step__arrow"></div>
    </div>
  </div>
  <!-- 步骤 2 -->
  <!-- 步骤 3 -->
</div>

此时步骤元素使用flex布局,所以图标、标题、 箭头元素都在一行内展示。

.el-step.is-simple {
  display: flex;
  align-items: center;
}

.el-step.is-simple .el-step__main {
  display: flex;
  align-items: stretch;
}

轴线元素DOM渲染了,但是没有设置宽、高、边框粗细,页面就无法展示。

.el-step.is-horizontal .el-step__line {
  height: 2px; 
}
.el-step.is-vertical .el-step__line {
  width: 2px; 
}

calcProgress(status) {
  // ...
  // 简洁风格无效
  style.borderWidth = step && !this.isSimple ? '1px' : 0; 
}

箭头样式使用伪类:before:after定义。

.el-step.is-simple .el-step__arrow::after,
.el-step.is-simple .el-step__arrow::before {
  content: "";
  display: inline-block;
  position: absolute;
  height: 15px;
  width: 1px;
  background: #c0c4cc;
}
.el-step.is-simple .el-step__arrow::before { 
  transform: rotate(-45deg) translateY(-4px); 
  transform-origin: 0 0;
}
.el-step.is-simple .el-step__arrow::after { 
  transform: rotate(45deg) translateY(4px); 
  transform-origin: 100% 100%;
}
.el-step.is-simple:last-of-type .el-step__arrow {
  display: none;
}

样式实现

组件样式源码 packages\theme-chalk\src\steps.scss 使用混合指令嵌套生成组件样式。

// packages\theme-chalk\src\steps.scss

// 生成 .el-steps
@include b(steps) {
  // ...
  
  // 生成 .el-steps--simple
  @include m(simple) {
    // ...
  }
  // 生成 .el-steps--horizontal
  @include m(horizontal) {
    // ...
  }
  // 生成 .el-steps--vertical
  @include m(vertical) {
    // ...
  }
}

组件样式源码 packages\theme-chalk\src\step.scss 使用混合指令嵌套生成组件样式。

// packages\theme-chalk\src\step.scss

// 生成 .el-step
@include b(step) {
  // ...
  
  // 生成 .el-step:last-of-type .el-step__line
  @include pseudo(last-of-type) {
    @include e(line) {
      // ...
    } 
    // 生成 .el-step:last-of-type.is-flex 
    @include when(flex) {
      // ...
    }
    // 生成 .el-step:last-of-type .el-step__description,.el-step:last-of-type .el-step__main
    @include e((main, description)) {
      // ...
    }
  }
  // 生成.el-step__head
  @include e(head) {
    // ...
    
    // 生成 .el-step__head.is-[wait/process/finish/error/success]
    @include when(process) {
      // ...
    }
    // wait/finish/error/success ... 
  }
  // 生成 .el-step__icon 
  @include e(icon) {
    // ...
    
    // 生成 .el-step__icon.is-text
    @include when(text) {
      // ...
    }
    // 生成 .el-step__icon.is-icon
    @include when(icon) {
      // ...
    }
  }
  // 生成 .el-step__icon-inner
  @include e(icon-inner) {
    // ...
    
    // 生成 .el-step__icon-inner[class*="el-icon"]:not(.is-status)
    &[class*=el-icon]:not(.is-status) {
      // ...
    }

    // 生成 .el-step__icon-inner.is-status
    @include when(status) {
      // ...
    }
  }
  // 生成 .el-step__line
  @include e(line) {
    // ...
  }
  // 生成 .el-step__line-inner
  @include e(line-inner) {
    // ...
  }
  // 生成 .el-step__main
  @include e(main) {
    // ...
  }
  // 生成 .el-step__title
  @include e(title) {
    // ...
    
    // 生成  .el-step__title.is-[wait/process/finish/error/success]
    @include when(process) {
      // ...
    } 
    // wait/finish/error/success
  }
  // 生成 .el-step__description
  @include e(description) {
    // ...
    // 生成  .el-step__description.is-[wait/process/finish/error/success]
    @include when(process) {
      // ...
    }
    // wait/finish/error/success
 
  }
  // 生成 .el-step.is-horizontal
  @include when(horizontal) {
    // ...
    
    // 生成 .el-step.is-horizontal .el-step__line
    @include e(line) {
      // ...
    }
  }
  // 生成.el-step.is-vertical
  @include when(vertical) {
    // ...
    
    // 生成.el-step.is-vertical .el-step__head/main/title/line
    @include e(head) { /*...*/ }
    @include e(main) { /*...*/ } 
    @include e(title) { /*...*/ } 
    @include e(line) { /*...*/ }
     
    // 生成.el-step.is-vertical .el-step__icon.is-icon 
    @include e(icon) {
      @include when(icon) {
        // ...
      }
    }
  }
  
  @include when(center) { 
    // 生成.el-step.is-center .el-step__head/description/line
    @include e(head) { /*...*/ }
    @include e(description) { /*...*/ } 
    @include e(line) { /*...*/ }  
  }
  // 生成.el-step.is-simple
  @include when(simple) {
    // ...
    
    // 生成.el-step.is-simple .el-step__head/icon/main/title
    @include e(head) { /*...*/ }
    @include e(icon) { /*...*/ }
    @include e(main) { /*...*/ }
    @include e(title) { /*...*/ }
    
    
    @include e(icon-inner) {
      // 生成 .el-step.is-simple .el-step__icon-inner[class*="el-icon"]:not(.is-status) 
      &[class*=el-icon]:not(.is-status) {
        // ...
      }
      // 生成 .el-step.is-simple .el-step__icon-inner.is-status
      &.is-status {
        // ...
      }
    }
    
    // 生成 .el-step.is-simple:not(:last-of-type) .el-step__title 
    @include pseudo('not(:last-of-type)') {
      @include e(title) {
        // ...
      }
    }
    // 生成 .el-step.is-simple .el-step__arrow
    @include e(arrow) {
      // ...
      
      // 生成 .el-step.is-simple .el-step__arrow::after,.el-step.is-simple .el-step__arrow::after 
      &::before,
      &::after {
        // ...
      }
      // 生成 .el-step.is-simple .el-step__arrow::before  
      &::before {
        // ...
      }
      // 生成 .el-step.is-simple .el-step__arrow::after 
      &::after {
        // ...
      }
    }
    // 生成 .el-step.is-simple:last-of-type .el-step__arrow 
    @include pseudo(last-of-type) {
      @include e(arrow) {
        // ...
      }
    }
  }
} 

📚参考&关联阅读

'CSS/flex',MDN\
'CSS/:last-of-type',MDN

关注专栏

如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!


anduril
16 声望1 粉丝