前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >商品多种规格属性的选择(sku 算法)

商品多种规格属性的选择(sku 算法)

原创
作者头像
Krry
修改2020-07-09 17:29:26
6.5K0
修改2020-07-09 17:29:26
举报
文章被收录于专栏:KrryblogKrryblog

博客地址:https://ainyi.com/90

商品多种规格属性的选择,如下图

WechatIMG146.png
WechatIMG146.png

上面的选项代表 sku

官方说法:sku 是库存保有单位;

如上图中每一个单规格选项,例如==珍珠白==、==12GB+512GB==、==不分期==就是一个规格(sku)。商品和 sku 属于一对多的关系,也就是我们可以选择多个sku来确定到某个具体的商品

现在的问题是:每选中一个规格,其他依赖此规格的是否有存货(是否可勾选)

下面将解决这个问题。先用图来描述商品和 sku 的关系

画图描述

用代码实现 sku 算法之前,先用图来描述更为清晰

数据结构与算法 我们学过图。图分为:

  • 有向图和无向图
  • 有权图和无权图

而这种场景中,用户选择规格的时候,是没有先后顺序的,假设我们现在把每种规格看作是无向图的一个顶点的话,我们可以根据这些单项规格的组合规格,就可以画出一个像上图一样的无向图

WechatIMG149.png
WechatIMG149.png

有了图,那如何用代码描述图的结构呢,这就用到==邻接矩阵==的概念

邻接矩阵

线性代数里的知识,邻接矩阵,在代码中,表示它的方法是用一个 n x n 的二维数组来抽象描述邻接矩阵

把上面这个无向图用邻接矩阵(二维数组)表示出来就是:

WechatIMG150.png
WechatIMG150.png

如果两个顶点互通(有连线),那么它们对应下标的值则为 1,否则为 0

假设现在我们有如下规格列表:

代码语言:txt
复制
specList: [
  { title: '颜色', list: ['红色', '紫色'] },
  { title: '套餐', list: ['套餐一', '套餐二'] },
  { title: '内存', list: ['64G', '128G', '256G'] }
]

可供选择的规格组合有:

代码语言:txt
复制
specCombinationList: [
  { id: '1', specs: ['紫色', '套餐一', '64G'] },
  { id: '2', specs: ['紫色', '套餐一', '128G'] },
  { id: '3', specs: ['紫色', '套餐二', '128G'] },
  { id: '4', specs: ['红色', '套餐二', '256G'] }
]

根据 specList 知道:

有==颜色==、==套餐==、==内存==三种规格类别

分别有 红色、紫色、套餐一、套餐二、64G、128G、256G 这些单项规格。每个单项规格作为一个顶点,所以就有如下顶点:

WechatIMG153.png
WechatIMG153.png

可以根据 specCombinationList 的数据画出如下的无向图:

WechatIMG155.png
WechatIMG155.png

现在根据 specCombinationList 我们来模拟一下用户的选择:

代码语言:txt
复制
specCombinationList: [
  { id: '1', specs: ['紫色', '套餐一', '64G'] },
  { id: '2', specs: ['紫色', '套餐一', '128G'] },
  { id: '3', specs: ['紫色', '套餐二', '128G'] },
  { id: '4', specs: ['红色', '套餐二', '256G'] }
]

假设用户先选择了紫色、根据 specCombinationList,我们发现==套餐一==、==套餐二==、==64G==、==128G==是可选的,这个时候我们发现一个问题:显然==跟紫色同级的红色==其实也是可选的。所以这个图其实我们还没有画完。所以相同类型的规格其实是应该连接起来的:

WechatIMG156.png
WechatIMG156.png

无向图画好后,现在我们将它映射到邻接矩阵上面

WechatIMG157.png
WechatIMG157.png

我们继续在邻接矩阵上模拟用户选择的情况:

  • 用户进入页面,所有存在有 1 的情况均可选
  • 当用户选择了某个顶点后,当前顶点所有可选项均被找出(即是当前顶点所在列值为 1 的顶点)
    WechatIMG159.png
    WechatIMG159.png
  • 选取多个顶点时,可选项是各个顶点邻接点的==交集==:(即是选中顶点所在列的==交集==)
    WechatIMG158.png
    WechatIMG158.png

交集后的为 1 的点为可选

到这里,用邻接矩阵描述的方法已经非常清楚了。接下来用代码来实现

代码实现

由上面的描述已经很清楚了,稍加思考应该就知道怎么用代码来实现

我这里使用==Vue==来实现,思路如下:

  1. 根据规格列表(specList)创建邻接矩阵(数组)
  2. 根据可选规格组合(specCombinationList)填写顶点的值
  3. 获得所有可选顶点,然后根据可选顶点填写同级顶点的值

sku 数据

先把规格数据写入,创建==specList==、==specCombinationList==;数据一般从接口获取

代码语言:txt
复制
export type CommoditySpecsType = {
  title: string;
  list: Array<string>;
}

export type SpecCategoryType = {
  id: string;
  specs: Array<string>;
}

export type SpecStateType = {
  specList: Array<CommoditySpecsType>;
  specCombinationList: Array<SpecCategoryType>;
}

export const initialState: SpecStateType = {
  specList: [
    { title: '颜色', list: ['红色', '紫色', '白色', '黑色'] },
    { title: '套餐', list: ['套餐一', '套餐二', '套餐三', '套餐四'] },
    { title: '内存', list: ['64G', '128G', '256G'] }
  ],
  specCombinationList: [
    { id: '1', specs: ['紫色', '套餐一', '64G'] },
    { id: '2', specs: ['紫色', '套餐一', '128G'] },
    { id: '3', specs: ['紫色', '套餐二', '128G'] },
    { id: '4', specs: ['黑色', '套餐三', '256G'] }
  ]
}

创建邻接矩阵

首先,我们需要提供一个类来创建邻接矩阵。一个邻接矩阵,首先需要传入一个顶点数组:==vertex==,需要一个用来装邻接矩阵的数组:==adjoinArray==

刚刚我们上面说到了,这个类还必须提供计算==并集==和==交集==的方法:

代码语言:txt
复制
export type AdjoinType = Array<string>;

export default class AdjoinMatrix {
  vertex: AdjoinType; // 顶点数组(包含所有规格)
  quantity: number; // 矩阵长度
  adjoinArray: Array<number>; // 矩阵数组

  constructor(vertx: AdjoinType) {
    this.vertex = vertx
    this.quantity = this.vertex.length
    this.adjoinArray = []
    this.init()
  }

  // 初始化数组
  init() {
    // 邻接矩阵初始化
    this.adjoinArray = Array(this.quantity * this.quantity).fill(0)
  }

  /*
   * @param id string
   * @param sides Array<string>
   *  传入一个顶点,和当前顶点可达的顶点数组,将对应位置置为1
   */
  setAdjoinVertexs(id: string, sides: AdjoinType) {
    const pIndex = this.vertex.indexOf(id)
    sides.forEach(item => {
      const index = this.vertex.indexOf(item)
      // 从邻接矩阵上看,
      // pIndex 是传入的顶点 index;
      // quantity 是邻接矩阵中行的 length;
      // index 是传入的顶点下的可组合的顶点元素下标
      // 那么 [pIndex * this.quantity + index] 就是可组合的 sku,置为 1
      this.adjoinArray[pIndex * this.quantity + index] = 1
    })
  }

  /*
   * @param id string
   * 传入顶点的值,获取该顶点的列
   */
  getVertexCol(id: string) {
    const index = this.vertex.indexOf(id)
    const col: Array<number> = []
    this.vertex.forEach((item, pIndex) => {
      col.push(this.adjoinArray[index + this.quantity * pIndex])
    })
    return col
  }

  /*
   * @param params Array<string>
   * 传入一个顶点数组,求出该数组所有顶点的列的合
   */
  getColSum(params: AdjoinType) {
    // 所有顶点的列,[[], [], ...]
    const paramsVertex = params.map(id => this.getVertexCol(id))
    const paramsVertexSum: Array<number> = []
    // 下面这个 forEach 和 map 能够取出每个顶点的列的同一个 index 下的值(也就是每个顶点列的同一行数据)
    // 得到顶点列的同一行数据后,通过 reduce 进行相加。数字大于等于总列数,说明是可选的
    this.vertex.forEach((item, index) => {
      const rowtotal = paramsVertex
        .map(value => value[index])
        .reduce((total, current) => {
          total += current || 0
          return total
        }, 0)
      paramsVertexSum.push(rowtotal)
    })
    return paramsVertexSum
  }

  /*
   *  @param params Array<string>
   * 传入一个顶点数组,求出并集
   */
  getCollection(params: AdjoinType) {
    const paramsColSum = this.getColSum(params)
    const collections: AdjoinType = []
    paramsColSum.forEach((item, index) => {
      if (item && this.vertex[index]) {
        collections.push(this.vertex[index])
      }
    })
    return collections
  }

  /*
   *  @param params Array<string>
   * 传入一个顶点数组,求出交集
   */
  getUnions(params: AdjoinType) {
    const paramsColSum = this.getColSum(params)
    const unions: AdjoinType = []
    paramsColSum.forEach((item, index) => {
      // 数字大于等于总列数,说明是可选的
      if (item >= params.length && this.vertex[index]) {
        unions.push(this.vertex[index])
      }
    })
    return unions
  }
}

有了这个类,接下来可以创建一个专门用于生成商品多规格选择的类,它继承于==AdjoinMatrix==

创建多规格选择邻接矩阵

我们这个多规格选择的邻接矩阵,需要提供一个查询可选顶点的方法:==getSpecscOptions==

代码语言:txt
复制
import AdjoinMatrix, { AdjoinType } from './adjoin-martix'

import { SpecCategoryType, CommoditySpecsType } from './dataList'

export default class SpecAdjoinMatrix extends AdjoinMatrix {
  specList: Array<CommoditySpecsType>;
  specCombinationList: Array<SpecCategoryType>;

  constructor(
    specList: Array<CommoditySpecsType>,
    specCombinationList: Array<SpecCategoryType>
  ) {
    super(
      specList.reduce(
        (total: AdjoinType, current) => [...total, ...current.list],
        []
      )
    )
    this.specList = specList
    this.specCombinationList = specCombinationList
    // 根据可选规格列表矩阵创建
    this.initSpec()
    // 同级顶点创建
    this.initSameLevel()
  }

  /**
   * 根据可选规格组合填写邻接矩阵的值
   */
  initSpec() {
    this.specCombinationList.forEach(item => {
      this.fillInSpec(item.specs)
    })
  }

  // 填写同级点
  initSameLevel() {
    // 获得初始所有可选项
    const specsOption = this.getCollection(this.vertex)
    this.specList.forEach(item => {
      const params: AdjoinType = []
      // 获取同级别顶点
      item.list.forEach(value => {
        if (specsOption.includes(value)) params.push(value)
      })
      // 同级点位创建
      this.fillInSpec(params)
    })
  }

  /*
   * @params
   * 传入顶点数组,查询出可选规格
   */
  getSpecscOptions(params: AdjoinType) {
    let specOptionCanchoose: AdjoinType = []
    if (params.some(Boolean)) {
      // 获取可选项(交集)
      specOptionCanchoose = this.getUnions(params.filter(Boolean))
    } else {
      // 所有可选项
      specOptionCanchoose = this.getCollection(this.vertex)
    }
    return specOptionCanchoose
  }

  /*
   * @params
   * 填写邻接矩阵的值
   */
  fillInSpec(params: AdjoinType) {
    params.forEach(param => {
      this.setAdjoinVertexs(param, params)
    })
  }
}

页面渲染

最后一步了,可以在页面中直接使用

代码语言:txt
复制
<template>
  <div class="container">
    <div v-for="({ title, list }, index) in initialState.specList" :key="index">
      <p class="title">{{ title }}</p>
      <div class="specBox">
        <button v-for="(ele, listIndex) in list"
          :key="listIndex"
          :disabled="!optionSpecs.includes(ele)"
          :class="{ specAction: specsS.includes(ele) }"
          @click="handleClick(ele, index)"
        >
          {{ ele }}
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import { initialState } from './config/dataList'
import SpecAdjoinMatrix from './config/spec-adjoin-martix'
export default {
  data() {
    return {
      initialState: initialState,
      specsS: [],
      optionSpecs: [],
      specAdjoinMatrix: null
    }
  },
  created() {
    this.initData()
  },
  mounted() {},
  computed: {},
  methods: {
    initData() {
      const { specList, specCombinationList } = this.initialState
      this.specsS = Array(specList.length).fill('')
      // 创建一个规格矩阵
      this.specAdjoinMatrix = new SpecAdjoinMatrix(specList, specCombinationList)
      // 获得可选项表
      this.optionSpecs = this.specAdjoinMatrix.getSpecscOptions(this.specsS)
    },
    handleClick(text, index) {
      const bool = this.optionSpecs.includes(text) // 当前规格是否可选
      // 排除可选规格里面没有的规格
      if (this.specsS[index] !== text && !bool) return
      // 根据text判断是否已经被选中了
      this.specsS[index] = this.specsS[index] === text ? '' : text
      this.optionSpecs = this.specAdjoinMatrix.getSpecscOptions(this.specsS)
    }
  }
}
</script>

<style scoped lang="scss">
<!-- css 代码 -->
</style>

到这里就完成了

展示

WechatIMG164.png
WechatIMG164.png

Git: https://github.com/Krryxa/krry-shop-sku

博客地址:https://ainyi.com/90

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 画图描述
      • 邻接矩阵
      • 代码实现
        • sku 数据
          • 创建邻接矩阵
            • 创建多规格选择邻接矩阵
              • 页面渲染
              • 展示
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档