阅读 424

用vuejs2.0实现淘宝规格弹窗

闲聊

最近做了一个电商项目,用了vue-cli搭建了一个vuejs SPA应用。电商嘛,都会涉及到商品选择这个模块,刚好之前没有实现过这个模块的经验,这次写了一下和大家分享一下。

开始

html

<div class="popBox" id="app">
    <div class="sku-list" v-if="specs.length">
      <div class="sku-item" v-for="(spec, index) in specs" :key="index">
        <label>{{ spec.name }}</label>
        <div class="spces-item">
          <button
            v-for="(entry, keys) in spec.entries"
            :key="keys.id"
            v-if="entry.isSelected"
            :disabled="!isSpecEntryAvailable(spec.name, entry.id)"
            :class="{ active: isSpecEntrySelected(spec.name, entry.id) }"
            @click="entry.isSelected && isSpecEntryAvailable(spec.name, entry.id) ? selectSpec(spec.name, entry.id) : ''">
            {{ entry.value }}
          </button>
        </div>
      </div>
      <div class="quantity">
        <div class="btn">
          <button :disabled="!canDec()" @click="decQuantity()" class="btn-minus"></button>
        </div>
        <input type="tel" name="quantity" ref="input" :value="quantity" />
        <div class="btn">
          <button :disabled="!canInc()" @click="incQuantity()" class="btn-plus"></button>
        </div>
      </div>
    </div>
</div>
复制代码

javascript

// sku列表
const SKU_LIST = '[{"id":1,"price":12,"specificationValues":[{"id":1,"value":"原味"},{"id":4,"value":"red"}],"stock":3},{"id":2,"price":33,"specificationValues":[{"id":2,"value":"棕色小熊"},{"id":5,"value":"green"}],"stock":0},{"id":3,"price":33,"specificationValues":[{"id":2,"value":"绿色狐狸(杯盖同红色款"},{"id":4,"value":"red"}],"stock":4}]';

// 规格列表
const SPEC_LIST = '[{"name":"口味","entries":[{"id":1,"value":"原味","isSelected":true},{"id":2,"value":"绿色狐狸(杯盖同红色款)","isSelected":true},{"id":3,"value":"棕色小熊","isSelected":false}]},{"name":"颜色","entries":[{"id":4,"value":"red","isSelected":true},{"id":5,"value":"green","isSelected":true}]}]';

  new Vue({
    el: '#app',
    data() {
      return {
        sku: JSON.parse(SKU_LIST),
        specs: JSON.parse(SPEC_LIST),
        selectedSpecs: [],
        quantity: 1,
      };
    },

    methods: {
      getStockNum() {
        return this.skuList.reduce((acc, value) => acc + value.stock, 0);
      },
      addDefaultSelectedSpecs() {
        this.quantity = Number(Cookies.get('cartNum')) || 1;
        if (!this.selectedSpec) return;
        this.selectedSpec.forEach((spec) => {
          if (this.isSpecEntryAvailable(spec.specName, spec.entryId)) {
            this.selectSpec(spec.specName, spec.entryId);
          }
        });
      },
      selectSpec(specName, entryId) {
        const index = this.selectedSpecs.findIndex(spec => spec.specName === specName);
        if (index === -1) {
          this.selectedSpecs.push({ specName, entryId });
        } else {
          const [spec] = this.selectedSpecs.splice(index, 1);
          if (spec.entryId !== entryId) {
            this.selectedSpecs.push({ specName, entryId });
          }
        }
        this.productPrice = (this.skuPrice() / 100).toFixed(2);
        this.quantity = Math.min(this.quantity, this.skuStock());
      },
      isSpecEntrySelected(specName, entryId) {
        const match = spec => spec.specName === specName && spec.entryId === entryId;
        return this.selectedSpecs.find(match);
      },
      isSpecEntryAvailable(specName, entryId) {
        const combination = this.selectedSpecs.filter(spec => spec.specName !== specName);
        combination.push({ specName, entryId });
        return this.sku.filter(sku => this.isMatchSku(sku, combination) && sku.stock > 0).length > 0;
      },
      isSkuSelected() {
        return this.selectedSpecs.length === this.specs.length;
      },
      isMatchSku(sku, specs) {
        let skuSpecEntryIds = [];
        let specEntryIds = [];

        if (!sku.specificationValues || !specs) {
          skuSpecEntryIds.push(sku.id);
        } else {
          skuSpecEntryIds = sku.specificationValues.map(value => value.id);
          specEntryIds = specs.map(spec => spec.entryId);
        }
        return specEntryIds.length === this.intersection(specEntryIds, skuSpecEntryIds).length;
      },
      intersection(specs, skus) {
        return skus.filter(sku => specs.indexOf(sku) > -1);
      },
      canInc() {
        return this.isSkuSelected() && this.quantity < this.skuStock();
      },
      canDec() {
        return this.isSkuSelected() && this.quantity > 1;
      },
      incQuantity() {
        const quantity = Number(this.quantity);
        if (this.canInc()) {
          this.quantity = quantity + 1;
        }
      },
      decQuantity() {
        const quantity = Number(this.quantity);
        if (this.canDec()) {
          this.quantity = quantity - 1;
        }
      },
      skuStock() {
        const sku = this.findSelectedSku();
        return sku ? sku.stock : this.totalStock;
      },
      skuPrice() {
        const sku = this.findSelectedSku();
        return sku ? sku.price : this.price;
      },
      findSelectedSku() {
        if (!this.isSkuSelected()) {
          return null;
        }
        return this.sku.find(sku => this.isMatchSku(sku, this.selectedSpecs));
      },
    },

    computed: {
      totalStock() {
        return this.sku.reduce((acc, val) => {
          return acc + Number(val.stock);
        }, 0);
      }
    }
  });
复制代码