index.vue 8.06 KB
<template>
  <div class="zui-steps" :class="stepsClassRender">
    <template v-for="(step, index) of steps">
      <div class="step-wrapper" :class="[$_getStepStatusClass(index)]" :key="stepKey(index)">
        <div v-if="$scopedSlots.icon" class="icon-wrapper">
          <slot name="icon" :index="index" :current-index="currentLength" :step="step"></slot>
        </div>
        <div v-else class="icon-wrapper">
          <template v-if="index < currentLength">
            <slot v-if="$scopedSlots.reached || $slots.reached" name="reached" :index="index"></slot>
            <div v-else class="step-node-default">
              <div class="step-node-default-icon" style="width: 8px; height: 8px; border-radius: 50%;"></div>
            </div>
          </template>
          <template v-else-if="index == currentLength">
            <slot v-if="$scopedSlots.current || $slots.current" name="current" :index="index"></slot>
            <zui-icon v-else name="checkcircle"></zui-icon>
          </template>
          <template v-else>
            <slot v-if="$scopedSlots.unreached || $slots.unreached" name="unreached" :index="index"></slot>
            <div v-else class="step-node-default">
              <div class="step-node-default-icon" style="width: 8px; height: 8px; border-radius: 50%;"></div>
            </div>
          </template>
        </div>
        <div class="text-wrapper">
          <slot v-if="$scopedSlots.content" name="content" :index="index" :step="step"></slot>
          <template v-else>
            <div class="name">{{ step.name }}</div>
            <div class="desc" v-if="step.text">{{ step.text }}</div>
          </template>
        </div>
      </div>
      <div class="bar" :class="barClassRender" :style="$_getStepSizeForStyle(index)" :key="barKey(index)">
        <i class="bar-inner" v-if="progress[index]" :style="$_barInnerStyle(index)"></i>
      </div>
    </template>
  </div>
</template>

<script>
export default {
  name: 'Steps',
  props: {
    steps: {
      type: Array,
      default: function () {
        /* istanbul ignore next */
        return [];
      },
    },
    current: {
      type: Number,
      default: 0,
      validator(val) {
        return val >= 0;
      },
    },
    direction: {
      type: String,
      default: 'horizontal',
    },
    transition: {
      type: Boolean,
      default: false,
    },
    verticalAdaptive: {
      type: Boolean,
      default: false,
    },
  },
  data: function () {
    return {
      initialed: false,
      progress: [],
      stepsSize: [],
      currentLength: 0,
      duration: 0.3,
      timer: null,
    };
  },
  computed: {
    stepsClassRender: function () {
      return {
        'zui-steps-vertical': this.direction == 'vertical',
        'zui-steps-horizontal': this.direction == 'horizontal',
        'vertical-adaptive': this.direction == 'vertical' && this.verticalAdaptive,
        'no-current': this.currentLength % 1 !== 0,
      };
    },
    barClassRender: function () {
      return [this.direction == 'horizontal' ? 'horizontal-bar' : 'vertical-bar'];
    },
    $_barInnerStyle: function () {
      var progress = this.progress;
      var self = this;
      return function (index) {
        var transform = self.direction == 'horizontal' ? '(' + (progress[index]['len'] - 1) * 100 + '%, 0, 0)' : '(0, ' + (progress[index]['len'] - 1) * 100 + '%, 0)';
        return {
          transform: 'translate3d' + transform,
          transition: 'all ' + progress[index]['time'] + 's linear',
        };
      };
    },
  },
  watch: {
    current: function (val, oldVal) {
      var currentStep = this.$_formatValue(val);
      var newProgress = this.$_sliceProgress(currentStep);
      if (this.transition) {
        var isAdd = currentStep >= oldVal;
        var self = this;
        this.timer && clearTimeout(this.timer);
        this.timer = setTimeout(function () {
          self.$_doTransition(function (newProgress, isAdd, len) {
            if ((isAdd && len > self.currentLength) || (!isAdd && len < self.currentLength)) {
              self.currentLength = len;
            }
          });
        }, 100);
      } else {
        this.progress = newProgress;
        this.currentLength = currentStep;
      }
    },
  },
  created: function () {
    var currentStep = this.$_formatValue(this.current);
    this.currentLength = currentStep;
    this.progress = this.$_sliceProgress(currentStep);
  },
  mounted: function () {
    this.$_initStepSize();
  },
  updated: function () {
    this.$nextTick(function () {
      this.$_initStepSize();
    });
  },
  methods: {
    stepKey: function (index) {
      return 'steps-' + index;
    },
    barKey: function (index) {
      return 'bar-' + index;
    },
    toArray: function (list, start) {
      start = start || 0;
      var i = list.length - start;
      var ret = [];
      while (i--) {
        ret.unshift(list[i + start]);
      }
      return ret;
    },
    // MARK: private methods
    $_initStepSize: function () {
      if (this.direction != 'vertical' || this.verticalAdaptive) {
        return;
      }
      var iconWrappers = this.$el.querySelectorAll('.icon-wrapper');
      var textWrappers = this.$el.querySelectorAll('.text-wrapper');
      var self = this;
      var stepsSize = this.toArray(textWrappers).map(function (wrapper, index) {
        var stepHeight = wrapper.clientHeight;
        var iconHeight = iconWrappers[index].clientHeight;
        if (index == textWrappers.length - 1) {
          // The last step needs to subtract floated height
          stepHeight -= iconHeight;
        } else {
          // Add spacing between steps to prevent distance too close
          // stepHeight += 40
          // 这里减小空白间隙,使其看起来更紧凑
          stepHeight += self.stepHeightFix == 0 ? self.stepHeightFix : self.stepHeightFix || -15;
        }
        return stepHeight > 0 ? stepHeight : 0;
      });
      if (stepsSize.toString() != this.stepsSize.toString()) {
        this.stepsSize = stepsSize;
      }
    },
    $_getStepSizeForStyle: function (index) {
      var size = this.direction == 'vertical' && !this.verticalAdaptive ? this.stepsSize[index] : 0;
      return size
        ? {
            height: size + 'px',
          }
        : null;
    },
    $_getStepStatusClass: function (index) {
      var currentLength = this.currentLength;
      var status = [];
      if (index < currentLength) {
        status.push('reached');
      }
      if (index == Math.floor(currentLength)) {
        status.push('current');
      }
      return status.join(' ');
    },
    $_formatValue: function (val) {
      if (val < 0) {
        return 0;
      } else if (val > this.steps.length - 1) {
        return this.steps.length - 1;
      } else {
        return val;
      }
    },
    $_sliceProgress: function (current) {
      var self = this;
      return this.steps.slice(0, this.steps.length - 1).map(function (step, index) {
        var offset = current - index;
        var progress = self.progress[index];
        var isNewProgress = progress == undefined;
        var len, time;
        if (offset <= 0) {
          len = 0;
        } else if (offset >= 1) {
          len = 1;
        } else {
          len = offset;
        }
        time = (isNewProgress ? len : Math.abs(progress.len - len)) * self.duration;
        return {
          len,
          time,
        };
      });
    },
    $_doTransition: function (progress, isAdd, step) {
      var currentLength = isAdd ? 0 : this.currentLength;
      var self = this;
      var walk = function (index) {
        if ((index < progress.length) & (index > -1) && progress[index]) {
          if (isAdd) {
            currentLength += progress[index].len;
          } else {
            currentLength -= self.progress[index].len - progress[index].len;
          }
          setTimeout(function () {
            index += isAdd ? 1 : -1;
            step(currentLength);
            walk(index);
          }, progress[index].time * 1000);
        }
        self.$set(self.progress, index, progress[index]);
      };
      walk(isAdd ? 0 : progress.length - 1);
    },
  },
};
</script>

<style>
@import './index.css';
</style>