专栏首页Krryblog商品多种规格属性的选择(sku 算法)
原创

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

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

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

WechatIMG146.png

上面的选项代表 sku

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

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

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

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

画图描述

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

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

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

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

WechatIMG149.png

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

邻接矩阵

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

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

WechatIMG150.png

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

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

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'] }
]

根据 specList 知道:

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

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

WechatIMG153.png

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

WechatIMG155.png

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

specCombinationList: [
  { id: '1', specs: ['紫色', '套餐一', '64G'] },
  { id: '2', specs: ['紫色', '套餐一', '128G'] },
  { id: '3', specs: ['紫色', '套餐二', '128G'] },
  { id: '4', specs: ['红色', '套餐二', '256G'] }
]

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

WechatIMG156.png

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

WechatIMG157.png

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

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

交集后的为 1 的点为可选

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

代码实现

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

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

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

sku 数据

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

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==

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

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==

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)
    })
  }
}

页面渲染

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

<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

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

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

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 数据量庞大的分页穿梭框实现

    但是第二个分页的 demo 没有,在上一家公司匆匆解决后,没有写入自己的 GitHub,有点可惜...

    Krry
  • 关于 vue 不能 watch 数组变化 和 对象变化的解决方案

    再如使用 splice(0, 2, 3) 从数组下标 0 删除两个元素,并在下标 0 插入一个元素 3:

    Krry
  • 防抖与节流 & 若每个请求必须发送,如何平滑地获取最后一个接口返回的数据

    日常浏览网页中,在进行窗口的 resize、scroll 或者重复点击某按钮发送请求,此时事件处理函数或者接口调用的频率若无限制,则会加重浏览器的负担,界面可能...

    Krry
  • 数据结构与JS也可以成为CP(十)Graph图

    1)深度优先搜索算法比较简单:访问一个没有访问过的顶点,将它标记为已访问,再递归地去访问在初始顶点的邻接表中其他没有访问过的顶点。

    萌兔IT
  • Mybatis之运行原理

    Confuguration封装了多有配置文件的详细信息,把配置文件的信息解析并保存在Configuration对象中,返回包含了Confuguration的De...

    小土豆Yuki
  • 移动端 局部dom实现滚动

    https://github.com/surmon-china/vue-awesome-swiper/issues/423

    念念不忘
  • WordPress博客网站下雪特效

    沈唁
  • 实现一个同步的RenderApplication

    逍遥剑客
  • 确认过眼神,你是喜欢Stream的人

    用户2145235
  • 学习 Phaser.js HTML5游戏开发-DAY3

    3. 构建基本的子弹对象,fire 方法用来初始化子弹实例,update方法用来绘制子弹轨迹

    tonglei0429

扫码关注云+社区

领取腾讯云代金券