连肝两天—电商项目多规格sku计算本地库存及价格

1,082 阅读4分钟

前言

前段时间,我们的商城项目设计了一个规格配置,随之而来的就是根据规格来计算商品的库存数,以及限制用户选择无库存的规格,这样极大程度的优化了用户体验,连肝两天我终于将后台系统的商品规格设计及小程序的规格展示,本篇文章主要介绍小程序端的设计,主要设计思想如下:

  • 初始化,先设定一个默认的规格参数,然后计算其它的选项的库存数计算
  • 点击规格触发,遍历计算未选中的规格,计算其是否有库存

后端传给我的数据结构

以下两组数据都是productDetail下面的数组

商品规格数据(xqsGoodsSpecVos)

[    {        "id": 1,        "specName": "口味",        "specVoValues": [            {                "id": 53,                "optionName": "五仁味"            },            {                "id": 54,                "optionName": "黑芝麻味"            },            {                "id": 55,                "optionName": "椒盐味"            },            {                "id": 63,                "optionName": "黑芝麻味+五仁味"            },            {                "id": 66,                "optionName": "黑芝麻+椒盐+五仁"            }        ]
    },
    {
        "id": 5,
        "specName": "重量",
        "specVoValues": [
            {
                "id": 56,
                "optionName": "100g"
            },
            {
                "id": 57,
                "optionName": "200g"
            },
            {
                "id": 58,
                "optionName": "300g"
            }
        ]
    }
]

商品规格价格数据(xqsGoodsSkus)

[
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 448,
        "goodsId": 210,
        "skuId": null,
        "store": 1,
        "price": 25,
        "supplyPrice": 10,
        "historyPrice": 30,
        "specId": "53-56",
        "specName": "五仁味-100g",
        "isDefault": 1
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 449,
        "goodsId": 210,
        "skuId": null,
        "store": 0,
        "price": 30,
        "supplyPrice": 20,
        "historyPrice": 40,
        "specId": "53-57",
        "specName": "五仁味-200g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 450,
        "goodsId": 210,
        "skuId": null,
        "store": 1,
        "price": 40,
        "supplyPrice": 30,
        "historyPrice": 50,
        "specId": "53-58",
        "specName": "五仁味-300g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 451,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 20,
        "supplyPrice": 10,
        "historyPrice": 30,
        "specId": "54-56",
        "specName": "黑芝麻味-100g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 452,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 30,
        "supplyPrice": 20,
        "historyPrice": 40,
        "specId": "54-57",
        "specName": "黑芝麻味-200g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 453,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 40,
        "supplyPrice": 30,
        "historyPrice": 50,
        "specId": "54-58",
        "specName": "黑芝麻味-300g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 454,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 20,
        "supplyPrice": 10,
        "historyPrice": 30,
        "specId": "55-56",
        "specName": "椒盐味-100g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 455,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 30,
        "supplyPrice": 20,
        "historyPrice": 40,
        "specId": "55-57",
        "specName": "椒盐味-200g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 456,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 40,
        "supplyPrice": 30,
        "historyPrice": 50,
        "specId": "55-58",
        "specName": "椒盐味-300g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 457,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 40,
        "supplyPrice": 20,
        "historyPrice": 60,
        "specId": "56-63",
        "specName": "黑芝麻味+五仁味-100g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 458,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 60,
        "supplyPrice": 40,
        "historyPrice": 80,
        "specId": "57-63",
        "specName": "黑芝麻味+五仁味-200g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 459,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 80,
        "supplyPrice": 60,
        "historyPrice": 100,
        "specId": "58-63",
        "specName": "黑芝麻味+五仁味-300g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 460,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 60,
        "supplyPrice": 30,
        "historyPrice": 90,
        "specId": "56-66",
        "specName": "黑芝麻+椒盐+五仁-100g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 461,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 90,
        "supplyPrice": 60,
        "historyPrice": 120,
        "specId": "57-66",
        "specName": "黑芝麻+椒盐+五仁-200g",
        "isDefault": 0
    },
    {
        "gmtModified": null,
        "gmtCreate": null,
        "id": 462,
        "goodsId": 210,
        "skuId": null,
        "store": 1000,
        "price": 232,
        "supplyPrice": 121,
        "historyPrice": 427,
        "specId": "58-66",
        "specName": "黑芝麻+椒盐+五仁-300g",
        "isDefault": 0
    }
]

规格弹窗页面的实现

这里我主要用到的是vant-weap相关组件协助开发

   <!-- 规格弹窗 -->
    <van-popup
      round
      class="spec-popup"
      closeable
      show="{{specPopup}}"
      bind:close="showSpecPopup"
      close-icon="clear"
      position="bottom"
      custom-style="min-height:50%;max-height:80%"
    >
      <view>
        <!-- 商品信息 -->
        <van-card title="{{productDetail.goodsTitle}}">
          <view slot="thumb">
              <van-image
                use-error-slot
                use-loading-slot
                width="88px"
                height="88px"
                src="{{productDetail.images[0]}}"
                lazy-loader="true"
              >
                <text slot="error">XXX</text>
                <text slot="loading">XXX</text>
              </van-image>
            </view>
          <view slot="price" style="color:#ed2856;font-size:24rpx">
            ¥
            <text style="font-size:36rpx;">{{ productDetail.price }}</text>
          </view>
          <view
            style="color:#7b7f84"
            slot="tags"
          >{{ productDetail.specName ? productDetail.specName : '' }}</view>
        </van-card>
        <!-- 规格信息选择 -->
        <block
          wx:for="{{productDetail.xqsGoodsSpecVos}}"
          wx:key="index"
          wx:for-index="specIndex"
          wx:for-item="spec"
        >
          <view class="spec-name">{{ spec.specName }}</view>
          <view class="option">
            <block
              wx:for="{{spec.specVoValues}}"
              wx:for-item="option"
              wx:key="index"
              wx:for-index="optionIndex"
            >
              <!-- 三种状态值,选中、未选中、无货,TODO-有些选择还选不了,想办法处理 -->
              <van-button
                class="select-btn"
                plain
                data-spec-index="{{specIndex}}"
                data-option-index="{{optionIndex}}"
                bindtap="selectOptionName"
                size="mini"
                color="#ed2856"
                wx:if="{{option.visible  && option.store>0 }}"
              >{{ option.optionName }}</van-button>
              <van-button
                plain
                class="un-select-btn"
                size="mini"
                color="#424549"
                data-spec-index="{{specIndex}}"
                data-option-index="{{optionIndex}}"
                bindtap="selectOptionName"
                wx:if="{{!option.visible && option.store>0}}"
              >{{ option.optionName }}</van-button>
              <van-button
                color="#CBCED1"
                disabled
                class="disable-btn"
                size="mini"
                plain
                wx:if="{{option.store == 0}}"
              >{{ option.optionName }}</van-button>
            </block>
          </view>
        </block>
      </view>
    </van-popup>

具体实现UI如下

初始化相关操作

初始化我观察到规格价格中有库存为0的数据,这部分数据我们是不供选择的

过滤库存为0的规格价格数据

this.productDetail.xqsGoodsSkus = this.productDetail.xqsGoodsSkus.filter(item => {
     return item.store > 0;
});

初始化规格参数的一个选中状态

这步操作主要是为了后面我的一个选中与非选中的状态,记录当前选中规格的索引,而不需要每次点击时都去遍历数组将为选中规格的visible改为false

  let store = 0;
  this.selectSpecArr = this.productDetail.xqsGoodsSkus[0].specId.split('-');
  store = this.productDetail.xqsGoodsSkus[0].store;    
  for (let i = 0; i < this.productDetail.xqsGoodsSpecVos.length; i++) {
    for (let j = 0; j < this.productDetail.xqsGoodsSpecVos[i].specVoValues.length; j++) {
      for (let k = 0; k < this.selectSpecArr.length; k++) {
        if (this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].id == this.selectSpecArr[k]) {
          this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].visible = true;
          // 给this.productDetail.xqsGoodsSpecVos[i] 记录当前选择的j索引,后面点击规格方便给上次的规格的visible设为false(未选中状态)
          this.productDetail.xqsGoodsSpecVos[i].curSelectIndex = j;
          break;
        }
      }
    }
  }
  }

计算库存

计算库存其实主要理清,其实我们就是模仿除了选中的规格ID组合的一次预选,此时同时去计算是否有库存,然后去决定规格按钮的一个状态

// 计算库存
for (let i = 0; i < this.productDetail.xqsGoodsSpecVos.length; i++) {
  for (let j = 0; j < this.productDetail.xqsGoodsSpecVos[i].specVoValues.length; j++) {
    if (!this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].visible) {
      // 未选中的进行遍历,判定是否有库存(相当于提前实现该点击事件)
      let tempIndexArr = Object.assign([], this.selectSpecArr);
      this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].store = 0;
      for (let k = 0; k < tempIndexArr.length; k++) {
        if (tempIndexArr[k] == this.productDetail.xqsGoodsSpecVos[i].specVoValues[this.productDetail.xqsGoodsSpecVos[i].curSelectIndex].id) {
          tempIndexArr[k] = this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].id;
          break;
        }
      }
      tempIndexArr.sort((a, b) => { return Number(a) - Number(b) });
      // 和商品价格数据检验,是否有库存
      let tempSelectIndex = tempIndexArr.join('-');
      for (let m = 0; m < this.productDetail.xqsGoodsSkus.length; m++) {
        // 初始化时,所有规格都已经选上,且之前入库时specId做过排序,此时直接转成字符串比较即可
        if (this.productDetail.xqsGoodsSkus[m].specId == tempSelectIndex) {
          this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].store = this.productDetail.xqsGoodsSkus[m].store;
          break;
        }
      }
    } else {
      this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].store = store;
    }
  }
}

点击规格按钮方法

以下方法都在selectOptionName

selectOptionName(e) {
}

实现选中状态的切换

点击规格按钮实现选中与未选中的切换,此时同时获取当前选中的规格ID组合

下面将所有规格ID toString() 是为了后面的比较规格做准备

let optionIndex = e.currentTarget.dataset.optionIndex;
let specIndex = e.currentTarget.dataset.specIndex;
let specVos = this.productDetail.xqsGoodsSpecVos[specIndex];
// 全选状态
specVos.specVoValues[optionIndex].visible = !specVos.specVoValues[optionIndex].visible;
if (specVos.curSelectIndex !== null) {
  // 选中时相当于将上次记录的ID做一次替换
  for (let index = 0; index < this.selectSpecArr.length; index++) {
    if (this.selectSpecArr[index] == specVos.specVoValues[specVos.curSelectIndex].id) {
      this.selectSpecArr[index] = specVos.specVoValues[optionIndex].id.toString();
      break;
    }
  }
} else {
  this.selectSpecArr.push(specVos.specVoValues[optionIndex].id.toString());
}
if (specVos.curSelectIndex != null) {
  specVos.specVoValues[specVos.curSelectIndex].visible = false;
}
if (specVos.curSelectIndex !== optionIndex) {
  specVos.curSelectIndex = optionIndex;
} else {
  // selectSpecArr 移除选中的id
  let selectIndex = this.selectSpecArr.indexOf(specVos.specVoValues[optionIndex].id.toString());
  if (selectIndex != -1) {
    this.selectSpecArr.splice(selectIndex, 1)
  }
  specVos.curSelectIndex = null;
}
let store = 0;
this.selectSpecArr.sort((a, b) => { return Number(a) - Number(b) });
let selectIndex = this.selectSpecArr.join('-');

计算价格

这里则是遍历规格数组里面的specId和我们选中的ID组合比较,获取到选中的库存和价格

 // 计算价格
for (let index = 0; index < this.productDetail.xqsGoodsSkus.length; index++) {
  if (this.productDetail.xqsGoodsSkus[index].specId == selectIndex) {
    this.productDetail.price = this.productDetail.xqsGoodsSkus[index].price.toFixed(2);
    this.productDetail.costPrice = this.productDetail.xqsGoodsSkus[index].supplyPrice.toFixed(2);
    store = this.productDetail.xqsGoodsSkus[index].store;
    this.productDetail.specName = this.productDetail.xqsGoodsSkus[index].specName;
    this.productDetail.skuId = this.productDetail.xqsGoodsSkus[index].id;
    break;
  }
}
// 修改总价
this.totalPrice = floatMultiply(
  this.productDetail.price,
  this.productQuantity
);

计算库存

这里和初始化计算的逻辑基本库存一致,但我做了一些小区分:

  • 初始化:规格是全部选上了
  • 点击触发,此时规格不一定是全选上了,此时我用了Set集合来判断,价格规格当中的ID组合是否包含需计算的规格ID组合
  // 计算是否有库存
for (let i = 0; i < this.productDetail.xqsGoodsSpecVos.length; i++) {
  for (let j = 0; j < this.productDetail.xqsGoodsSpecVos[i].specVoValues.length; j++) {
    if (!this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].visible) {
      // 未选中的进行遍历,判定是否有库存(相当于提前实现该点击事件)
      let tempIndexArr = Object.assign([], this.selectSpecArr);
      this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].store = 0;
      if (this.productDetail.xqsGoodsSpecVos[i].curSelectIndex != null) {
        for (let k = 0; k < tempIndexArr.length; k++) {
          if (tempIndexArr[k] == this.productDetail.xqsGoodsSpecVos[i].specVoValues[this.productDetail.xqsGoodsSpecVos[i].curSelectIndex].id) {
            tempIndexArr[k] = this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].id.toString();
            break;
          }
        }
      } else {
        tempIndexArr.push(this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].id.toString())
      }
      tempIndexArr.sort((a, b) => { return Number(a) - Number(b) });
      // 和商品价格数据检验,是否有库存
      // let tempSelectIndex = tempIndexArr.join('-');
      // console.log('计算的规格ID:', tempIndexArr);
      for (let m = 0; m < this.productDetail.xqsGoodsSkus.length; m++) {
        // 改变比较两个数组是否包含关系 this.productDetail.xqsGoodsSkus[m].specId 是否包含 tempSelectIndex
        let idArr = this.productDetail.xqsGoodsSkus[m].specId.split('-');
        // 使用set集合判断 idArr 是否包含计算的规格ID组合
        let tempSet = new Set([...idArr, ...tempIndexArr]);
        if (tempSet.size == idArr.length) {
          this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].store = this.productDetail.xqsGoodsSkus[m].store;
          break;
        }
      }
    } else {
      this.productDetail.xqsGoodsSpecVos[i].specVoValues[j].store = 1;
    }
  }
}
}

总结

其实一开始未接触过规格组合的相关功能,但是还是只要理清具体的思路(代码写的较烂,勿喷),按照正确的逻辑走下去,去调试、debugger即可

效果图如下