博客地址: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
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。