Шаговый модуль оформления заказа Vue.js

Я сделал этот небольшой степпер проверки с помощью Vue (v 2.xx):

var app = new Vue({
  el: "#cart",
  data: {
    stepCounter: 1,
    steps: [{
        step: 1,
        completed: false,
        text: "Cart"
      },
      {
        step: 2,
        completed: false,
        text: "Shipping"
      },
      {
        step: 3,
        completed: false,
        text: "Payment"
      },
      {
        step: 4,
        completed: false,
        text: "Confirmation"
      }
    ]
  },
  methods: {
    doPrev: function() {
      if (this.stepCounter > 1) {
        this.stepCounter--;
        this.doCompleted();
      }
    },
    doNext: function() {
      if (this.stepCounter <= this.steps.length) {
        this.stepCounter++;
        this.doCompleted();
      }
    },
    doCompleted: function() {
      this.steps.forEach(item => {
        item.completed = item.step < this.stepCounter;
      });
    }
  }
});
* {
  margin: 0;
  padding: 0;
  font-family: "Poppins", sans-serif;
}

.progressbar {
  display: flex;
  list-style-type: none;
  counter-reset: steps;
  padding-top: 50px;
  justify-content: space-between;
}

.progressbar li {
  font-size: 13px;
  text-align: center;
  position: relative;
  flex-grow: 1;
  flex-basis: 0;
  color: rgba(0, 0, 0, 0.5);
  font-weight: 600;
}

.progressbar li.completed {
  color: #ccc;
}

.progressbar li.active {
  color: #4caf50;
}

.progressbar li::after {
  counter-increment: steps;
  content: counter(steps, decimal);
  display: block;
  width: 30px;
  height: 30px;
  line-height: 30px;
  border: 2px solid rgba(0, 0, 0, 0.5);
  background: #fff;
  border-radius: 50%;
  position: absolute;
  left: 50%;
  margin-left: -15px;
  margin-top: -60px;
}

.progressbar li.active::after,
.progressbar li.completed::after {
  background: #4caf50;
  border-color: rgba(0, 0, 0, 0.15);
  color: #fff;
}

.progressbar li.completed::after {
  content: '2713';
}

.progressbar li::before {
  content: "";
  position: absolute;
  top: -26px;
  left: -50%;
  width: 100%;
  height: 2px;
  background: rgba(0, 0, 0, 0.5);
  z-index: -1;
}

.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
  background: #4caf50;
}

.progressbar li:first-child::before {
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />

<div id="cart" class="mt-2">
  <div class="container">
    <ul class="progressbar">
      <li v-for="(step, index) in steps" v-bind:class="{ active: index + 1 === stepCounter, completed: step.completed === true }">{{step.text}}</li>
    </ul>
  </div>

  <div class="container px-4 mt-5 text-center">
    <div class="d-flex justify-content-between">
      <button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter === 1}" @click="doPrev()">Prev</button>
      <button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter > steps.length}" @click="doNext()">Next</button>
    </div>
  </div>
</div>

Вопросов

  1. Есть ли место для «сокращения» кода?
  2. Что-нибудь противоречивое на логическом уровне?

2 ответа
2

С использованием step поскольку свойство внутри каждого шага является избыточным и может привести к ошибкам, если значение неверно. Подумайте, что бы произошло, если бы у вас было это:

data: {
  stepCounter: 1,
  steps: [{
      step: 5,
      completed: false,
      text: "Cart"
    },
    {
      step: 3,
      completed: false,
      text: "Shipping"
    },
    {
      step: 7,
      completed: false,
      text: "Payment"
    },
    {
      step: 1,
      completed: false,
      text: "Confirmation"
    }
  ]
},

Чтобы решить эту проблему, удалите step из данных полностью.

data: {
  stepCounter: 1,
  steps: [{
      completed: false,
      text: "Cart"
    },
    {
      completed: false,
      text: "Shipping"
    },
    {
      completed: false,
      text: "Payment"
    },
    {
      completed: false,
      text: "Confirmation"
    }
  ]
},

Подобная ошибка может произойти, если completed свойство введено неверно. Вместо этого используйте текущий stepCounter свойство, чтобы определить, когда отмечать шаг как выполненный.


Чтобы сделать компонент многоразовым, я бы рекомендовал использовать некоторые реквизит вместо только данные. Предлагаемый реквизит будет stepCounter и текст для каждого шага.

Я бы предпочел, чтобы ваш компонент можно было использовать следующим образом:

<Stepper :steps="['step one', 'step two', 'step three']" currentStep="2" />

    Есть ли место для «сокращения» кода?

    Сокращения

    Сокращения может упростить разметку. Обработчики кликов уже используют сокращение @click вместо v-on:click. Привязки также можно упростить. Например — вместо:

    v-bind:class="..."
    

    в v-bind можно не указывать:

    :class="..."
    

    Осторожно!

    1

    Вчера был ответ на ваше сообщение в приложении TODO, в котором предлагалось использовать вычисленные свойства. В текущей архитектуре использование вычисляемого свойства кажется невозможным, но метод наблюдателя можно было бы нанять. Вместо настройки такого метода, как doCompleted() которые необходимо вызывать вручную, можно использовать метод наблюдателя для корректировки значений всякий раз, когда это значение изменяется.

    watch: {
      stepCounter: function(newValue, oldValue) {
        this.steps.forEach(item => {
          item.completed = item.step < this.stepCounter;
        });
      }
    },
    

    Обратите внимание, что newValue может использоваться вместо this.stepCounter.

    Хотя он будет занимать то же количество строк, что и текущий код, он снимает нагрузку с методов. doPrev а также doNext необходимости вызывать метод doCompleted. См. Демонстрацию ниже, чтобы проиллюстрировать это.

    const makeStep = (text, index) => {return {text, step: ++index, completed: false}};
    const steps = ['Cart', 'Shipping', 'Payment', 'Confirmation'].map(makeStep);
    var app = new Vue({
      el: "#cart",
      data: {
        stepCounter: 1,
        steps
      },
      watch: {
        stepCounter: function(newValue, oldValue) {
          this.steps.forEach(item => {
            item.completed = item.step <  this.stepCounter;
          });
        }
      },
      methods: {
        doPrev: function() {
          if (this.stepCounter > 1) {
            this.stepCounter--;
          }
        },
        doNext: function() {
          if (this.stepCounter <= this.steps.length) {
            this.stepCounter++;
          }
        }
      }
    });
    * {
      margin: 0;
      padding: 0;
      font-family: "Poppins", sans-serif;
    }
    
    .progressbar {
      display: flex;
      list-style-type: none;
      counter-reset: steps;
      padding-top: 50px;
      justify-content: space-between;
    }
    
    .progressbar li {
      font-size: 13px;
      text-align: center;
      position: relative;
      flex-grow: 1;
      flex-basis: 0;
      color: rgba(0, 0, 0, 0.5);
      font-weight: 600;
    }
    
    .progressbar li.completed {
      color: #ccc;
    }
    
    .progressbar li.active {
      color: #4caf50;
    }
    
    .progressbar li::after {
      counter-increment: steps;
      content: counter(steps, decimal);
      display: block;
      width: 30px;
      height: 30px;
      line-height: 30px;
      border: 2px solid rgba(0, 0, 0, 0.5);
      background: #fff;
      border-radius: 50%;
      position: absolute;
      left: 50%;
      margin: -60px 0 0 -15px;
    }
    
    .progressbar li.active::after,
    .progressbar li.completed::after {
      background: #4caf50;
      border-color: rgba(0, 0, 0, 0.15);
      color: #fff;
    }
    
    .progressbar li.completed::after {
      content: '2713';
    }
    
    .progressbar li::before {
      content: "";
      position: absolute;
      top: -26px;
      left: -50%;
      width: 100%;
      height: 2px;
      background: rgba(0, 0, 0, 0.5);
      z-index: -1;
    }
    
    .progressbar li.active::before,
    .progressbar li.completed::before,
    .progressbar li.active+li::before {
      background: #4caf50;
    }
    
    .progressbar li:first-child::before {
      display: none;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />
    
    <div id="cart" class="mt-2">
      <div class="container">
        <ul class="progressbar">
          <li v-for="(step, index) in steps" :class="{ active: index + 1 === stepCounter, completed: step.completed }">{{step.text}}</li>
        </ul>
      </div>
    
      <div class="container px-4 mt-5 text-center">
        <div class="d-flex justify-content-between">
          <button type="button" class="btn btn-sm btn-success" :class="{ disabled : stepCounter === 1}" @click="doPrev()">Prev</button>
          <button type="button" class="btn btn-sm btn-success" :class="{ disabled : stepCounter > steps.length}" @click="doNext()">Next</button>
        </div>
      </div>
    </div>

    Если структура данных может быть изменена таким образом, чтобы завершенный свойство не требуется, тогда в разметке можно использовать условие step.step < stepCounter чтобы определить, когда completed класс добавляется к каждому элементу списка. См. Фрагмент ниже, чтобы продемонстрировать это.

    const makeStep = (text, index) => {return {text, step: ++index}};
    const steps = ['Cart', 'Shipping', 'Payment', 'Confirmation'].map(makeStep);
    const app = new Vue({
      el: "#cart",
      data: {
        stepCounter: 1,
        steps
      },
      methods: {
        doPrev: function() {
          if (this.stepCounter > 1) {
            this.stepCounter--;
          }
        },
        doNext: function() {
          if (this.stepCounter <= this.steps.length) {
            this.stepCounter++;
          }
        }
      }
    });
    * {
      margin: 0;
      padding: 0;
      font-family: "Poppins", sans-serif;
    }
    
    .progressbar {
      display: flex;
      list-style-type: none;
      counter-reset: steps;
      padding-top: 50px;
      justify-content: space-between;
    }
    
    .progressbar li {
      font-size: 13px;
      text-align: center;
      position: relative;
      flex-grow: 1;
      flex-basis: 0;
      color: rgba(0, 0, 0, 0.5);
      font-weight: 600;
    }
    
    .progressbar li.completed {
      color: #ccc;
    }
    
    .progressbar li.active {
      color: #4caf50;
    }
    
    .progressbar li::after {
      counter-increment: steps;
      content: counter(steps, decimal);
      display: block;
      width: 30px;
      height: 30px;
      line-height: 30px;
      border: 2px solid rgba(0, 0, 0, 0.5);
      background: #fff;
      border-radius: 50%;
      position: absolute;
      left: 50%;
      margin: -60px 0 0 -15px;
    }
    
    .progressbar li.active::after,
    .progressbar li.completed::after {
      background: #4caf50;
      border-color: rgba(0, 0, 0, 0.15);
      color: #fff;
    }
    
    .progressbar li.completed::after {
      content: '2713';
    }
    
    .progressbar li::before {
      content: "";
      position: absolute;
      top: -26px;
      left: -50%;
      width: 100%;
      height: 2px;
      background: rgba(0, 0, 0, 0.5);
      z-index: -1;
    }
    
    .progressbar li.active::before,
    .progressbar li.completed::before,
    .progressbar li.active+li::before {
      background: #4caf50;
    }
    
    .progressbar li:first-child::before {
      display: none;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />
    
    <div id="cart" class="mt-2">
      <div class="container">
        <ul class="progressbar">
          <li v-for="(step, index) in steps" v-bind:class="{ active: index + 1 === stepCounter, completed: step.step < stepCounter }">{{step.text}}</li>
        </ul>
      </div>
    
      <div class="container px-4 mt-5 text-center">
        <div class="d-flex justify-content-between">
          <button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter === 1}" @click="doPrev()">Prev</button>
          <button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter > steps.length}" @click="doNext()">Next</button>
        </div>
      </div>
    </div>

    CSS: правила маржи

    Набор правил .progressbar li::after { содержит эти правила:

    margin-left: -15px;
    margin-top: -60px;
    

    Предполагая, что для правого и нижнего края нет унаследованных от других элементов полей, их можно объединить в одно правило:

    margin: -60px 0 0 -15px;
    

    • Nitpick: не могли бы вы margin: -60px 0 0 -15px; переопределить некоторое правое / нижнее поле, определенное ранее в цепочке, в то время как два отдельных определения этого не делают?

      — Гунтрам Блом

    • Хорошая уловка — я подумал и должен был упомянуть об этом. Я соответствующим образом обновил этот раздел. Спасибо!

      — БУДЕТ ONᴇᴌᴀ

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *