前言
前段时间,我们的商城项目设计了一个规格配置,随之而来的就是根据规格来计算商品的库存数,以及限制用户选择无库存的规格,这样极大程度的优化了用户体验,连肝两天我终于将后台系统的商品规格设计及小程序的规格展示,本篇文章主要介绍小程序端的设计,主要设计思想如下:
- 初始化,先设定一个默认的规格参数,然后计算其它的选项的库存数计算
- 点击规格触发,遍历计算未选中的规格,计算其是否有库存
后端传给我的数据结构
以下两组数据都是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即可