商品多规格组合功能

376 阅读4分钟

注:本功能使用 vue 框架实现

功能点说明

  • 未选择规格时,须要将不能选择的规格禁用

  • 选择规格后须要重新计算能选的规格与不能选的规格,将不能选的规格禁用

  • 尽量不要进行太多的循环与 if 判断,让语义更清晰

思路分析

skuList 与 stockList 的数据格式

skuList 的数据格式如下:

"skuList": [
  {
    "name": "颜色",
    "sort": 1,
    "children": [
      {
        "name": "黑色",
        "id": 1,
        "sort": 1
      },
      {
        "name": "红色",
        "id": 3,
        "sort": 2
      },
      {
        "name": "粉色",
        "id": 4,
        "sort": 3
      }
    ]
  },
  {
    "name": "尺码",
    "sort": 2,
    "children": [
      {
        "name": "35",
        "id": 5,
        "sort": 1
      },
      {
        "name": "36",
        "id": 6,
        "sort": 2
      },
      {
        "name": "34",
        "id": 7,
        "sort": 3
      }
    ]
  },
  {
    "name": "材质",
    "sort": 4,
    "children": [
      {
        "name": "帆布鞋",
        "id": 11,
        "sort": 1
      },
      {
        "name": "凉鞋",
        "id": 12,
        "sort": 2
      },
      {
        "name": "运动鞋",
        "id": 13,
        "sort": 3
      }
    ]
  }
]

stockList 的数据格式如下:

"stockList": [
  {
    "skuId": "1_7_12",
    "stock": 888,
    "price": 15535400,
    "combination": "黑色;34;凉鞋"
  },
  {
    "skuId": "1_5_12",
    "stock": 99,
    "price": 100,
    "combination": "黑色;35;凉鞋"
  },
  {
    "skuId": "4_6_13",
    "stock": 150,
    "price": 10011,
    "combination": "粉色;36;运动鞋"
  },
  {
    "skuId": "3_5_12",
    "stock": 9999,
    "price": 100,
    "combination": "红色;35;凉鞋"
  },
  {
    "skuId": "4_5_13",
    "stock": 100000,
    "price": 10088,
    "combination": "粉色;35;运动鞋"
  }
]

stockList 的处理

生成 idMap

遍历现有的可选规格列表 stockList,根据可选的规格组合生成 idMap,也就是将每个 id 可选的其余节点筛选出来

// 规格组合的id字符串
handleCombination() {
  const idMap = {
    // id1: [id4, id2, id3],
  };

  this.stockList.forEach((item) => {
    const skuIdList = item.skuId.split("_");
    // 生成idMap
    skuIdList.forEach((skuId) => {
      if (idMap[skuId]) {
        idMap[skuId].push(...skuIdList);
      } else if (idMap[skuId] === undefined) {
        idMap[skuId] = [...skuIdList];
      }
    });
  });

  // value去重
  for (const id in idMap) {
    idMap[id] = [...new Set(idMap[id])];
  }
  this.idMap = idMap;
},

生成 matrix

遍历 sideList 两次,为 matrix 二维数组中的元素进行赋值,第一次遍历得到 rowIndex、rowItem,第二次遍历得到 colindex、colItem

// 根据combineAllId生成matrix二维数组
handleMatrix() {
  this.sideList.forEach((rowItem, rowIndex) => {
    this.sideList.forEach((colItem, colindex) => {
      this.matrix[rowIndex] = this.matrix[rowIndex] || [];
      if (rowItem.id === colItem.id) {
         // 相同的规格是不连通的
        this.matrix[rowIndex][colindex] = 0;
        return;
      }
      const rowIdList = this.idMap[rowItem.id];
      if (!(this.idMap[rowItem.id] && this.idMap[colItem.id])) {
        // 某个规格与其他规格没有交集
        this.matrix[rowIndex][colindex] = 0;
      } else if (this.broIdMap[rowItem.id].includes("" + colItem.id)) {
        // 同级属性下的规格 彼此之间是相互可选的
        this.matrix[rowIndex][colindex] = 1;
      } else if (
        this.idMap[rowItem.id]?.includes("" + colItem.id) ||
        this.idMap[colItem.id]?.includes("" + rowItem.id)
      ) {
        // 规格之间是连通的
        this.matrix[rowIndex][colindex] = 1;
      } else {
        // 规格之间是不连通的
        this.matrix[rowIndex][colindex] = 0;
      }
    });
  });
  console.log("this.matrix", this.matrix);
}

使用 matrix 二维数组存放规格与规格之间的关系,0 表示两规格之间是不连通的,不可选择,1 表示可两规格之间是连接的,可选择

const matrix = [
  [0, 1, 1, 1, 0, 1, 0, 1, 0],
  [1, 0, 1, 1, 0, 0, 0, 1, 0],
  [1, 1, 0, 1, 1, 0, 0, 0, 1],
  [1, 1, 1, 0, 1, 1, 0, 1, 1],
  [0, 0, 1, 1, 0, 1, 0, 0, 1],
  [1, 0, 0, 1, 1, 0, 0, 1, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0],
  [1, 1, 0, 1, 0, 1, 0, 0, 1],
  [0, 0, 1, 1, 1, 0, 0, 1, 0],
];

skuList 的处理

填充 selectedId 数组

用一个数组保存用户勾选的规格的 id

// 填充selectedId数组
handleSelectedId() {
  this.selectedId = Array(this.skuList.length).fill("");
  console.log("this.selectedId", this.selectedId);
}

生成 sideList

使用 sideList 数组存放所有的规格,最终格式如下:

const sideList = [
  { name: '黑色', id: 1, sort: 1 },
  { name: '红色', id: 3, sort: 2 },
  { name: '粉色', id: 4, sort: 3 },
  { name: '35', id: 5, sort: 1 },
  { name: '36', id: 6, sort: 2 },
  { name: '34', id: 7, sort: 3 },
  { name: '帆布鞋', id: 11, sort: 1 },
  { name: '凉鞋', id: 12, sort: 2 },
  { name: '运动鞋', id: 13, sort: 3 },
];
// 矩阵的每个边 数组
handleSideList() {
  // broIdMap = { id2:id1, id3 }
  const broIdMap = {};
  let broIdList = [];
  this.skuList.forEach((itemx) => {
    broIdList = [];
    itemx.children.forEach((itemy) => {
      this.sideList.push(itemy);
      // 生成broIdList
      broIdList.push("" + itemy.id);
    });
    // 生成broIdMap
    broIdList.forEach((id) => {
      broIdMap[id] = broIdList;
    });
  });
  this.broIdMap = broIdMap;
  console.log("this.sideList", this.sideList);
  console.log("this.broIdMap", this.broIdMap);
}

使用 broIdMap 保存同级属性的 id

const broIdMap = {
  1: ['1', '3', '4'],
  3: ['1', '3', '4'],
  4: ['1', '3', '4'],
  5: ['5', '6', '7'],
  6: ['5', '6', '7'],
  7: ['5', '6', '7'],
  11: ['11', '12', '13'],
  12: ['11', '12', '13'],
  13: ['11', '12', '13'],
};

遍历 sideList 数组,生成 specMap 对象,key 为 id,value 为索引,该对象用于存放每个规格在 sideList 数组中的索引 用户选择规格时可以直接通过 id 拿到索引值,通过索引拿到对应的数组

const specMap = { 1: 0, 3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 11: 6, 12: 7, 13: 8 };
// 根据sideList生成specMap, key 为 id,value 为索引
handleSpecMap() {
  this.sideList.forEach((item, index) => {
    this.specMap[item.id] = index;
    // this.$set(this.specMap, "" + item.id, index);
  });
  console.log("this.specMap", this.specMap);
}

selectedId 用于保存用户勾选的 id,selectedName 用于保存 name,显示在页面让用户知道选择了哪些规格

this.selectedId = ['', 6, 13];

处理用户选择规格后的情况

selectedId 处理

将用户勾选的规格 id 放在 selectedId 数组中

// 选择规格
selectSpec(itemy, index, indey) {
  if (this.selectedId[index] != itemy.id) {
    //不包含当前点击规格则添加
    this.selectedId[index] = itemy.id; //如果没有则把当前规格添加
    this.subIndex[index] = indey; //添加选中样式
    this.selectedName[index] = itemy.name;
  } else {
    // 包含当前点击规格则取消选中
    this.selectedId[index] = "";
    this.subIndex[index] = -1; //去除样式
    this.selectedName[index] = "";
  }
  // this.selectedId = this.handleSelectArr();
  console.log("this.selectedId", this.selectedId);

  // 拼接规格字符串
  this.handleName();
  this.handleSelectedArr();
},

selectedArr 处理

获取勾选的规格在 matrix 对应的数组,selectedArr 也是一个二维数组

matrix[specMap[id]相当于矩阵中的每一行

const selectedArr = [];
this.selectedId.forEach((id, index) => {
  // 若不是""
  if (id) {
    selectedArr.push(matrix[specMap[id]]);
  }
});
  • selectedArr 长度等于 0,则没有勾选
  • selectedArr 长度等于 1,则 interSection 为 selectedArr 第 0 个数组
  • selectedArr 长度大于 1,说明用户至少勾选了 2 个规格,若选择的是多个规格则求两个数组的交集 interSection

选择规格时,通过 specMap,可直接用 id 拿到索引,通过索引拿到对应的数组,这个数组对应着当前可选的其它规格

// 处理勾选的规格对应的数组
handleSelectedArr() {
  // 获取勾选的规格在matrix对应的数组 去除""的情况
  const selectedArr = [];
  const interSection = [];
  this.selectedId.forEach((id, index) => {
    // 若不是""
    if (id) {
      selectedArr.push(this.matrix[this.specMap[id]]);
    }
  });
  // 取消勾选的所有规格后为[]
  this.interSection = selectedArr[0] || [];

  console.log("this.interSection", this.interSection);

  // 选择多规格时 求交集
  if (selectedArr.length <= 1) return;

  console.log("selectedArr", selectedArr);
  let prev = [];
  selectedArr.forEach((itemx, index) => {
    // 第一次循环
    if (index === 0) {
      prev = itemx;
      return;
    }

    itemx.forEach((itemy, indey) => {
      // console.log(prev[indey], itemy);
      if (itemy === prev[indey] && itemy === 1) {
        interSection[indey] = 1;
      } else {
        interSection[indey] = 0;
      }
    });
    // 每次循环后
    prev = itemx;
  });
  this.interSection = interSection;

  console.log("this.interSection", this.interSection);
},

页面格式处理

在页面渲染时通过 isSelectable 函数判断该按钮是否禁用

<span
  v-for="(itemy, indey) in itemx.children"
  :key="indey"
  :class="`${isSelectable(itemy) ? '' : 'noProduct'} ${
    subIndex[index] === indey ? 'act' : ''
  }`"
  @click.stop="itemy.disabled ? $message({msg: '不可点击',
  type: 'warning',
  time: 1500,
  }) : selectSpec(itemy, index, indey)"
  >{{ itemy.name }}</span
>
// 页面初始化时 处理不可选择的规格
isSelectable(itemy) {
  const colItem = this.specMap[itemy.id];
  // 页面初始化时或用户清空选择了的规格后 interSection为[] 因此需要先判断matrix
  if (!this.matrix[colItem].includes(1)) {
    return false;
  }
  if (this.interSection.length > 0) {
    return this.interSection[colItem];
  }
  return true;
},