博客地址:https://ainyi.com/90
商品多种规格属性的选择,如下图
上面的选项代表 sku
官方说法:sku 是库存保有单位;
如上图中每一个单规格选项,例如==珍珠白==、==12GB+512GB==、==不分期==就是一个规格(sku)。商品和 sku 属于一对多的关系,也就是我们可以选择多个sku来确定到某个具体的商品
现在的问题是:每选中一个规格,其他依赖此规格的是否有存货(是否可勾选)
下面将解决这个问题。先用图来描述商品和 sku 的关系
用代码实现 sku 算法之前,先用图来描述更为清晰
数据结构与算法 我们学过图。图分为:
而这种场景中,用户选择规格的时候,是没有先后顺序的,假设我们现在把每种规格看作是无向图的一个顶点的话,我们可以根据这些单项规格的组合规格,就可以画出一个像上图一样的无向图
有了图,那如何用代码描述图的结构呢,这就用到==邻接矩阵==的概念
线性代数里的知识,邻接矩阵,在代码中,表示它的方法是用一个 n x n 的二维数组来抽象描述邻接矩阵
把上面这个无向图用邻接矩阵(二维数组)表示出来就是:
如果两个顶点互通(有连线),那么它们对应下标的值则为 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 这些单项规格。每个单项规格作为一个顶点,所以就有如下顶点:
可以根据 specCombinationList 的数据画出如下的无向图:
现在根据 specCombinationList 我们来模拟一下用户的选择:
specCombinationList: [ { id: '1', specs: ['紫色', '套餐一', '64G'] }, { id: '2', specs: ['紫色', '套餐一', '128G'] }, { id: '3', specs: ['紫色', '套餐二', '128G'] }, { id: '4', specs: ['红色', '套餐二', '256G'] } ]
假设用户先选择了紫色、根据 specCombinationList,我们发现==套餐一==、==套餐二==、==64G==、==128G==是可选的,这个时候我们发现一个问题:显然==跟紫色同级的红色==其实也是可选的。所以这个图其实我们还没有画完。所以相同类型的规格其实是应该连接起来的:
无向图画好后,现在我们将它映射到邻接矩阵上面
我们继续在邻接矩阵上模拟用户选择的情况:
交集后的为 1 的点为可选
到这里,用邻接矩阵描述的方法已经非常清楚了。接下来用代码来实现
由上面的描述已经很清楚了,稍加思考应该就知道怎么用代码来实现
我这里使用==Vue==来实现,思路如下:
先把规格数据写入,创建==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>
到这里就完成了
Git: https://github.com/Krryxa/krry-shop-sku
博客地址:https://ainyi.com/90
原创声明,本文系作者授权云+社区发表,未经许可,不得转载。
如有侵权,请联系 yunjia_community@tencent.com 删除。
我来说两句