前端商品多规格选择问题 SKU 算法实现优化2.0

在阅读本文之前,请先阅读笔者上一篇文章:前端商品多规格选择问题 SKU 算法实现

一、找bug

上一篇文章最后提到实现的算法存在bug,是哪个地方出现问题了呢?

当数据源如下所示时:

export const properties = [
  {
    id: "1",
    name: "容量",
    attributes: [
      { value: "1L", isActive: false, isDisabled: false },
      { value: "4L", isActive: false, isDisabled: false },
    ],
  },
  {
    id: "2",
    name: "颜色",
    attributes: [
      { value: "红色", isActive: false, isDisabled: false },
      { value: "黑色", isActive: false, isDisabled: false },
    ],
  },
  {
    id: "3",
    name: "优惠套餐",
    attributes: [
      { value: "套餐一", isActive: false, isDisabled: false },
      { value: "套餐二", isActive: false, isDisabled: false },
    ],
  },
];

export const skuList = [
  { id: "10", attributes: ["1L", "红色", "套餐一"] },
  { id: "20", attributes: ["1L", "黑色", "套餐二"] },
  { id: "30", attributes: ["4L", "红色", "套餐一"] },
  { id: "40", attributes: ["4L", "红色", "套餐二"] },
];

实现效果如下所示:
在这里插入图片描述
你发现了吗,明明 skuList 中没有 ["1L", "红色", "套餐二"] 的组合,但选择了 1L红色 之后却可以选择 套餐二 呢?这就是上一篇文章中实现存在的bug。
为什么会这样?
仔细看看 skuList

  { id: "10", attributes: ["1L", "红色", "套餐一"] },
  { id: "20", attributes: ["1L", "黑色", "套餐二"] },
  { id: "30", attributes: ["4L", "红色", "套餐一"] },
  { id: "40", attributes: ["4L", "红色", "套餐二"] },

可以看到 1L套餐二 有路径,红色套餐二 有路径,导致 1L + 红色套餐二 有路径。

二、修复过程详解

由上所示,在两个顶点有路径时在邻接矩阵中赋值为1不够明确,那么可以赋值成多少呢?还记得每个 sku 有唯一的 id 标识,因此我们将邻接矩阵中赋值成当前 attribute 所支持的 sku 的集合。
JavaScript 中,用 Set 表示集合,主要用法如下:

// 创建 Set
const letters = new Set();

// 向 Set 添加一些值
letters.add("a");
letters.add("b");
letters.add("c");

集合元素互斥,如果添加相等的元素,则只会保存第一个元素。

Set 对象的方法和属性

new Set()创建新的 Set 对象。
add()向 Set 添加新元素。
clear()从 Set 中删除所有元素。
delete()删除由其值指定的元素。
entries()返回 Set 对象中值的数组。
has()如果值存在则返回 true。
forEach()为每个元素调用回调。
keys()返回 Set 对象中值的数组。
values()与 keys() 相同。
size返回元素计数。

可点击👇了解更多
JavaScript Set 对象

以下详细说明上一篇文章中提出的3个求解步骤中需要修改的部分。

1.初始化顶点集和空邻接矩阵

这部分并没有修改,与原相同,代码实现如下所示:

    // 构造初始空邻接矩阵存储无向图
    initEmptyAdjMatrix() {
      this.properties.forEach((prop) => {
        prop.attributes.forEach((attr) => {
          this.vertexList.push(attr.value);
        });
      });
      for (let i = 0; i < this.vertexList.length; i++) {
        this.matrix[i] = new Array(this.vertexList.length).fill(0);
      }
    },

2.邻接矩阵赋值

setAdjMatrixValue 调用子函数 associateAttributes 时需要增加传参 skuId ,对于 properties 赋值的情况没有 skuId ,统一传一个能够确认与 skuList 中的每一个 skuId 不相同的值即可,这里传1

    // 根据 skuList 和 properties 设置邻接矩阵的值
    setAdjMatrixValue() {
      this.skuList.forEach((sku) => {
        this.associateAttributes(sku.attributes, sku.id);
      });
      this.properties.forEach((prop) => {
        this.associateAttributes(prop.attributes, '1');
      });
    },

在子函数 associateAttributes 中,赋值时需要修改逻辑,判断 this.matrix[index1][index2] 是否有值,若有值,则使用 add 方法在集合中增加当前传入的 skuId,否则赋值为新创建的 Set 对象,并在集合中增加当前传入的 skuId

    // 将 attributes 属性组中的属性在无向图中联系起来
    associateAttributes(attributes, skuId) {
      attributes.forEach((attr1) => {
        attributes.forEach((attr2) => {
          // 因 properties 与 skuList 数据结构不一致,需作处理
          if (attr1 !== attr2 || attr1.value !== attr2.value) {
            if (attr1.value && attr2.value) {
              attr1 = attr1.value;
              attr2 = attr2.value;
            }
            const index1 = this.vertexList.indexOf(attr1);
            const index2 = this.vertexList.indexOf(attr2);
            if (index1 > -1 && index2 > -1) {
              if(this.matrix[index1][index2]) {
                this.matrix[index1][index2].add(skuId);
              }
              else {
                this.matrix[index1][index2] = new Set([skuId]);
              }
            }
          }
        });
      });
    },

赋值后,邻接矩阵如下所示:

1L4L红色黑色套餐一套餐二
1L0{1}{10}{20}{10}{20}
4L{1}0{30, 40}0{30}{40}
红色{10}{30, 40}0{1}{10, 30}{40}
黑色{20}0{1}00{20}
套餐一{10}{30}{10, 30}00{1}
套餐二{20}{40}{40}{20}{1}0

3.判断 attribute 是否可选

原逻辑是判断得到的 res 数组中是否每个值均为1,若符合则可选;否则若其中一个值为0,则不可选置灰。
修改后的逻辑更为复杂,感觉这块有可能出bug。如果有小伙伴发现了评论区告诉我~
我的思路是:此时 res 数组中存储的元素可能有两种类型,Number 类型的 0 或者 Object 类型的 Set 对象, Set 对象中存储的可能为 1 或者 skuList 中存在的 skuId1skuIdString 类型。我将结果区分为三种情况,注意三种情况有先后关系,后一种排除前一种存在的可能,即使用 if - else if - else 的控制流:

  • res 数组中存在值为 0 的元素,则返回 false ,表示需要置灰,这种情况与原逻辑相同
  • 排除上一种情况之后,此时 res 数组中存储的是 Set 对象, Set 对象存储的是 1 或者 skuId。若 res 数组中存在包含值为 1Set 对象,则返回 true ,表示可选。
  • 排除以上两种情况之后,此时 res 数组中存储的是 Set 对象, Set 对象中存储的是 skuId。当且仅当每个 Set 对象中包含相同的一个 skuId 时,可选返回 true,否则不可选返回 false

一、三情况比较容易理解,对于第二种情况举例说明:选择了 1L红色 之后,4L 能不能选呢?此时对于 4L 这个 attributeres 数组应为 [{'1'}, {'30', '40'}] ,数组中不包含 0 且包含 1,此时应该是可选的。

代码实现如下所示:

    // 判断当前 attribute 是否可选,返回 true 表示可选,返回 false 表示不可选,选项置灰
    canAttributeSelect(attribute) {
      if (!this.selected || !this.selected.length || attribute.isActive) {
        return true;
      }
      let res = [];
      this.selected.forEach((value) => {
        const index1 = this.vertexList.indexOf(value);
        const index2 = this.vertexList.indexOf(attribute.value);
        res.push(this.matrix[index1][index2]);
      });
      // console.log(attribute.value, '->', res);
      if(res.some((item)=> (item === 0))) {
        return false;
      }
      else if(res.some((item) => (item.has('1')))) {
        return true;
      }
      else {
        const first = res[0];
        const others = res.slice(1);
        return Array.from(first).some((skuId) => (others.every((item) => (item.has(skuId)))));
      }
    },

三、Vue源码

data.js
存储数据 propertiesskuList 初始值

export const properties = [
  {
    id: "1",
    name: "容量",
    attributes: [
      { value: "1L", isActive: false, isDisabled: false },
      { value: "4L", isActive: false, isDisabled: false },
    ],
  },
  {
    id: "2",
    name: "颜色",
    attributes: [
      { value: "红色", isActive: false, isDisabled: false },
      { value: "黑色", isActive: false, isDisabled: false },
    ],
  },
  {
    id: "3",
    name: "优惠套餐",
    attributes: [
      { value: "套餐一", isActive: false, isDisabled: false },
      { value: "套餐二", isActive: false, isDisabled: false },
    ],
  },
];

export const skuList = [
  { id: "10", attributes: ["1L", "红色", "套餐一"] },
  { id: "20", attributes: ["1L", "黑色", "套餐二"] },
  { id: "30", attributes: ["4L", "红色", "套餐一"] },
  { id: "40", attributes: ["4L", "红色", "套餐二"] },
];

// 1L -> 套餐二, 红色 -> 套餐二, 1L+红色 -> 套餐二

SkuSelector.vue
引入 data.js 中的 propertiesskuList 赋初始值,运行时需要注意 data.js 文件路径,可能需要修改:

import { properties, skuList } from '../data';

完整代码如下所示:

<template>
  <div class="root">
    <p>商品多规格选择示例2.0</p>
    <div v-for="(property, propertyIndex) in properties" :key="propertyIndex">
      <p>{{ property.name }}</p>
      <div class="sku-box-area">
        <template v-for="(attribute, attributeIndex) in property.attributes">
          <div
            :key="attributeIndex"
            :class="[
              'sku-box',
              'sku-text',
              attribute.isActive ? 'active' : '',
              attribute.isDisabled ? 'disabled' : '',
            ]"
            @click="handleClickAttribute(propertyIndex, attributeIndex)"
          >
            {{ attribute.value }}
          </div>
        </template>
      </div>
    </div>
  </div>
</template>

<script>
import { properties, skuList } from '../data';

export default {
  name: "SkuSelector2",
  components: {},
  computed: {},
  data() {
    return {
      properties: [], // property 列表
      skuList: [], // sku 列表
      matrix: [], // 邻接矩阵存储无向图
      vertexList: [], // 顶点数组
      selected: [], // 当前已选的 attribute 列表
    };
  },
  mounted() {
    this.properties = properties;
    this.skuList = skuList;

    this.initEmptyAdjMatrix();
    this.setAdjMatrixValue();
  },
  methods: {
    // 当点击某个 attribute 时,如:黑色
    handleClickAttribute(propertyIndex, attributeIndex) {
      const attr = this.properties[propertyIndex].attributes[attributeIndex];
      // 若选项置灰,直接返回,表现为点击无响应
      if (attr.isDisabled) {
        return;
      }

      // 重置每个 attribute 的 isActive 状态
      const isActive = !attr.isActive;
      this.properties[propertyIndex].attributes[attributeIndex].isActive =
        isActive;
      if (isActive) {
        this.properties[propertyIndex].attributes.forEach((attr, index) => {
          if (index !== attributeIndex) {
            attr.isActive = false;
          }
        });
      }

      // 维护当前已选的 attribute 列表
      this.selected = [];
      this.properties.forEach((prop) => {
        prop.attributes.forEach((attr) => {
          if (attr.isActive) {
            this.selected.push(attr.value);
          }
        });
      });

      // 重置每个 attribute 的 isDisabled 状态
      this.properties.forEach((prop) => {
        prop.attributes.forEach((attr) => {
          attr.isDisabled = !this.canAttributeSelect(attr);
        });
      });
    },

    // 构造初始空邻接矩阵存储无向图
    initEmptyAdjMatrix() {
      this.properties.forEach((prop) => {
        prop.attributes.forEach((attr) => {
          this.vertexList.push(attr.value);
        });
      });
      for (let i = 0; i < this.vertexList.length; i++) {
        this.matrix[i] = new Array(this.vertexList.length).fill(0);
      }
    },

    // 根据 skuList 和 properties 设置邻接矩阵的值
    setAdjMatrixValue() {
      this.skuList.forEach((sku) => {
        this.associateAttributes(sku.attributes, sku.id);
      });
      this.properties.forEach((prop) => {
        this.associateAttributes(prop.attributes, '1');
      });
    },

    // 将 attributes 属性组中的属性在无向图中联系起来
    associateAttributes(attributes, skuId) {
      attributes.forEach((attr1) => {
        attributes.forEach((attr2) => {
          // 因 properties 与 skuList 数据结构不一致,需作处理
          if (attr1 !== attr2 || attr1.value !== attr2.value) {
            if (attr1.value && attr2.value) {
              attr1 = attr1.value;
              attr2 = attr2.value;
            }
            const index1 = this.vertexList.indexOf(attr1);
            const index2 = this.vertexList.indexOf(attr2);
            if (index1 > -1 && index2 > -1) {
              if(this.matrix[index1][index2]) {
                this.matrix[index1][index2].add(skuId);
              }
              else {
                this.matrix[index1][index2] = new Set([skuId]);
              }
            }
          }
        });
      });
    },

    // 判断当前 attribute 是否可选,返回 true 表示可选,返回 false 表示不可选,选项置灰
    canAttributeSelect(attribute) {
      if (!this.selected || !this.selected.length || attribute.isActive) {
        return true;
      }
      let res = [];
      this.selected.forEach((value) => {
        const index1 = this.vertexList.indexOf(value);
        const index2 = this.vertexList.indexOf(attribute.value);
        res.push(this.matrix[index1][index2]);
      });
      console.log(attribute.value, '->', res);
      if(res.some((item)=> (item === 0))) {
        return false;
      }
      else if(res.some((item) => (item.has('1')))) {
        return true;
      }
      else {
        const first = res[0];
        const others = res.slice(1);
        return Array.from(first).some((skuId) => (others.every((item) => (item.has(skuId)))));
      }
    },
  },
};
</script>

<style>
.root {
  width: 350px;
  padding: 24px;
}
.sku-box-area {
  display: flex;
  flex: 1;
  flex-direction: row;
  flex-wrap: wrap;
}
.sku-box {
  border: 1px solid #cccccc;
  border-radius: 6px;
  margin-right: 12px;
  padding: 8px 10px;
  margin-bottom: 10px;
}
.sku-text {
  font-size: 16px;
  line-height: 16px;
  color: #666666;
}
.active {
  border-color: #ff6600;
  color: #ff6600;
}
.disabled {
  opacity: 0.5;
  border-color: #e0e0e0;
  color: #999999;
}
</style>

四、实现效果

可以看到已修复了bug,选择了 1L红色 之后,套餐二 不可选,同理,选择了 1L套餐二 之后,红色 不可选。
在这里插入图片描述

五、其他

考虑更多边界情况,发现存在如下问题:

  1. 问题:代码实现中 handleClickAttribute(propertyIndex, attributeIndex) 使用到 index,因此依赖于数据源中 properties 数组按序存储,若顺序调换则会出错
    解决:可以在 mounted 对数据源 properties 进行处理,使之按序存放
  2. 问题:构建图时将顶点设置为 attributevalue,若不同的属性有相同的值(虽然这种情况很少发生,但仍需考虑),则会出错
    解决:可以改为设置为 id-value 的形式,如 19701-黑色,比较简便的方法是在 mounted 对数据源 properties 进行处理,其他原有的代码不需要变更
  3. 问题:刚开始的想法是动态维护一个数组 filteredSkuList 表示经过当前的选择之后支持的 sku,用数组的长度是否为 1 来判断是否选择完毕,这样若第一个属性有两个值,第二个属性有一个值,则第一个属性选择之后,不管第二个属性有没有选,数组的长度都将是 1,会出错
    解决:可以通过当前已选择的 property 的个数判断是否与 property 的总数相等,即通过判断 this.selected.lengththis.properties.length 是否相等
  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
以下是一个React组件的代码实现,用于实现前端SKU算法商品规格选择功能: ```jsx import React, { useState, useEffect } from "react"; const SKUSelector = ({ skuList }) => { const [selectedValues, setSelectedValues] = useState({}); const [availableOptions, setAvailableOptions] = useState({}); useEffect(() => { // 初始化可选项列表 let options = {}; skuList.forEach((sku) => { sku.attributes.forEach((attr) => { if (!options[attr.name]) { options[attr.name] = [attr.value]; } else if (!options[attr.name].includes(attr.value)) { options[attr.name].push(attr.value); } }); }); setAvailableOptions(options); }, [skuList]); const handleValueChange = (name, value) => { // 更新已选项列表 setSelectedValues({ ...selectedValues, [name]: value }); // 根据已选项列表筛选可选项列表 let options = { ...availableOptions }; for (let attrName in selectedValues) { if (attrName !== name) { skuList.forEach((sku) => { if ( sku.attributes.find((attr) => attr.name === attrName)?.value !== selectedValues[attrName] ) { options[attrName] = options[attrName].filter( (option) => option !== selectedValues[attrName] ); } }); } } setAvailableOptions(options); }; const getAvailableValues = (name) => { // 获取指定规格属性的可选项列表 return availableOptions[name] || []; }; const getSelectedSKU = () => { // 根据已选项列表获取SKU信息 let selectedSKU = null; skuList.forEach((sku) => { let matches = true; sku.attributes.forEach((attr) => { if (selectedValues[attr.name] !== attr.value) { matches = false; } }); if (matches) { selectedSKU = sku; } }); return selectedSKU; }; return ( <div> {skuList.length > 0 ? ( skuList[0].attributes.map((attr) => ( <div key={attr.name}> <label>{attr.name}:</label> <select value={selectedValues[attr.name] || ""} onChange={(e) => handleValueChange(attr.name, e.target.value)} > <option value="">请选择</option> {getAvailableValues(attr.name).map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> </div> )) ) : ( <div>暂无商品信息</div> )} {getSelectedSKU() ? ( <div> <p>已选规格:{JSON.stringify(selectedValues)}</p> <p>剩余库存:{getSelectedSKU().stock}</p> </div> ) : ( <div>请选择完整规格属性</div> )} </div> ); }; export default SKUSelector; ``` 该组件接受一个SKU列表作为props,每个SKU包含一个属性列表和一个库存数量。在组件中,首先使用useEffect钩子初始化可选项列表,然后使用useState钩子管理已选项列表和可选项列表的状态。 当用户选择某个规格属性值时,组件会根据已选项列表筛选可选项列表,并更新已选项列表。当用户选择所有规格属性值后,组件会根据已选项列表获取相应的SKU信息,并显示剩余库存量。 该组件仅为示例代码,具体实现方式可能因业务需求而异。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值