vue电商项目sku 规格 详细步骤
Posted 奥特曼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue电商项目sku 规格 详细步骤相关的知识,希望对你有一定的参考价值。
一、概念及需求
-
SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
-
SKU(Stock Keeping Unit)库存量单位,即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。
区别:
SPU:属性值 特性相同的就是SPU 例如 iphone 12pro就是一个SPU
SKU:代表该商品可选规格的任意组合,他是库存单位的唯一标识 例如挑选手机时的 规格、容量、颜色 组合起来就是SKU iphone12pro 紫色 +256
需求:在平常买东西时可能当前商品的SKU规格库存没有 例如下面左图中 20cm的锅没有库存,在看右图中点击中国后30cm的尺寸也没有库存,怎么判断有没有库存呢? 这就需要每点击一个规格属性 都要去判断其他有没有库存
二、实现
(1)创建组件
goods-sku.vue
<template>
<div class="goods-sku">
<dl>
<dt>颜色</dt>
<dd>
<img class="selected" src="https://yanxuan-item.nosdn.127.net/d77c1f9347d06565a05e606bd4f949e0.png" alt="">
<img class="disabled" src="https://yanxuan-item.nosdn.127.net/d77c1f9347d06565a05e606bd4f949e0.png" alt="">
</dd>
</dl>
<dl>
<dt>尺寸</dt>
<dd>
<span class="disabled">10英寸</span>
<span class="selected">20英寸</span>
<span>30英寸</span>
</dd>
</dl>
<dl>
<dt>版本</dt>
<dd>
<span>美版</span>
<span>港版</span>
</dd>
</dl>
</div>
</template>
<script>
export default {
name: 'GoodsSku'
}
</script>
<style scoped lang="less">
.sku-state-mixin () {
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
&.selected {
border-color: @xtxColor;
}
&.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
}
.goods-sku {
padding-left: 10px;
padding-top: 20px;
dl {
display: flex;
padding-bottom: 20px;
align-items: center;
dt {
width: 50px;
color: #999;
}
dd {
flex: 1;
color: #666;
> img {
width: 50px;
height: 50px;
.sku-state-mixin ();
}
> span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
.sku-state-mixin ();
}
}
}
}
</style>
注册组件
index.vue
<div v-if="goodsData.id"> //当有数据的时候再去执行
<GoodsSku />
</div>
+ import GoodsSku from './components/goods-sku'
name: 'XtxGoodsPage',
+ components: { GoodsRelevant, GoodsImage, GoodsSales, GoodsName, GoodsSku },
setup () {
目前效果
(2)规格数据渲染
goosData 是通过商品的id获取的 把他传给子组件GoodsSku
<GoodsSku :goodsData="goodsData"/>
返回的数据 goodsData
子组件接收
<script>
export default {
name: 'GoodsSku',
props: {
goodsData: {
type: Object,
default: () => ({ specs: [], skus: [] })// 防止第一次打开页面没数据报错
}
}
}
</script>
数据渲染
<template>
<div class="goods-sku">
<dl v-for="item in goodsData.specs" :key="item.id">
<dt> {{item.name}} </dt>
<dd>
<template v-for="val in item.values" :key="val.name">
<img v-if="val.picture" class="selected" :src="val.picture" alt="val.picture" :title="val.picture">
<span v-else> {{val.name}} </span>
</template>
</dd>
</dl>
</div>
</template>
目前效果
(3)切换逻辑
点击时:
它是已选中:则从已选中-->改成-->未选中
它是未选中:则把它的兄弟改成未选中,把它自己改成选中
<template v-for="val in item.values" :key="val.name">
<img @click="clickSpecs(item,val)" v-if="val.picture" :class="{selected:val.selected}" :src="val.picture" alt="val.picture" :title="val.picture">
<span @click="clickSpecs(item,val)" :class="{selected:val.selected}" v-else> {{val.name}} </span>
</template>
把当前项和 所有项传下去 进行排他思想
setup () {
// 选中排他 item所有项 val当前项
const clickSpecs = (item, val) => {
// 如果当前选中
if (val.selected) {
// 就把当前取消选中
val.selected = false
} else {
// 否则把每一项都取消选中
item.values.forEach(item => {
item.selected = false
})
// 把自己选中
val.selected = true
}
}
return { clickSpecs }
}
(4)禁用效果
通过skus中的数据,可以来计算当前情况下,某个规格是否可选。
-
在组件初始化的时候去判断每个规格是否点击
-
在选中某个规格值之后,去判断其他规格值是否可选
当SKU规格库存为0的时候应该 禁用此规格的点击
(5)禁用效果 采用方式 路径字典
根据当前商品的skus数据生成路径查询字典对象,方便控制后期判断属性是否可用。
计算集合的子集的方法
src/vender/power-set.js
/**
* Find power-set of a set using BITWISE approach.
*
* @param {*[]} originalSet
* @return {*[][]}
*/
export default function bwPowerSet(originalSet) {
const subSets = [];
// We will have 2^n possible combinations (where n is a length of original set).
// It is because for every element of original set we will decide whether to include
// it or not (2 options for each set element).
const numberOfCombinations = 2 ** originalSet.length;
// Each number in binary representation in a range from 0 to 2^n does exactly what we need:
// it shows by its bits (0 or 1) whether to include related element from the set or not.
// For example, for the set {1, 2, 3} the binary number of 0b010 would mean that we need to
// include only "2" to the current set.
for (let combinationIndex = 0; combinationIndex < numberOfCombinations; combinationIndex += 1) {
const subSet = [];
for (let setElementIndex = 0; setElementIndex < originalSet.length; setElementIndex += 1) {
// Decide whether we need to include current element into the subset or not.
if (combinationIndex & (1 << setElementIndex)) {
subSet.push(originalSet[setElementIndex]);
}
}
// Add current subset to the list of all subsets.
subSets.push(subSet);
}
return subSets;
}
js算法库 https://github.com/trekhleb/javascript-algorithms
封装方法getPowerSet
goods-sku.vue
// 计算合集子集的方法
import getPowerSet from '@/vender/power-set'
// 定义要分隔的字符串
const spliter = '*'
// 根据sku数据得到的字典对象
const getPathMap = (skus) => {
// 用来保存的路径字典
const pathMap = {}
// 遍历每一个sku规格
skus.forEach(sku => {
// 1.过滤出有效的sku 因为库存会出有0的情况
if (sku.inventory) {
// 2. 拼接起来得到属性值数组例如 ['黑色','中国','10cm']
const specs = sku.specs.map(spec => spec.valueName)
// 3.得到sku的子集 [[] ["蓝色"] ["中国"]["蓝色", "中国"] ["10cm"]...]
const powerSet = getPowerSet(specs)
// 将子集循环
powerSet.forEach(set => {
const key = set.join(spliter)
if (pathMap[key]) {
// 已经有key往数组追加
pathMap[key].push(sku.id)
} else {
// 没有key设置一个数组
pathMap[key] = [sku.id]
}
})
}
})
return pathMap
}
setup (props) {
// 省略其他...
const pathMap = getPathMap(props.goods.skus)
console.log(pathMap)
}
路径字典代表所有出现的可能性
(6)初始禁用状态
例如路径字典上的没有20cm 那么视图上 20cm的规格应该不能选中
添加 动态添加class类 每一项中的disabled为ture就触发disabled类
<template v-for="val in item.values" :key="val.name">
<img v-if="val.picture"
:class="{selected:val.selected,disabled:val.disabled}"
@click="clickSpecs(item,val)"
:src="val.picture" :title="val.name">
<span v-else
:class="{selected:val.selected,disabled:val.disabled}"
@click="clickSpecs(item,val)">{{val.name}}</span>
</template>
// 更新按钮的禁用状态
const updateDisabledStatus = (pathMap, specs) => {
specs.forEach((spec, i) => {
spec.values.forEach(val => {
// 当前的状态 = 去路径字典里里找name 如果找到了就为说明存在true 取个反
val.disabled = !pathMap[val.name]
})
})
}
setup (props) {
const pathMap = getPathMap(props.goodsData.skus)
// 组件初始化的更新禁用状态 参数路径地图,初始数据
+ updateDisabledStatus(pathMap,props.goodsData.specs)
const clickSpecs = (item, val) => {
// 目前加上类还是可以点击 如果是禁用状态 不作为
+ if (val.disabled) return false
// 1. 选中与取消选中逻辑
if (val.selected) {
val.selected = false
} else {
item.values.forEach(bv => { bv.selected = false })
val.selected = true
}
}
return { clickSpecs }
}
(7)点击时禁用状态
首先要获取当前用户的选择,然后
对于每一个按钮:
-
如果它已经选中了。就忽略。
-
如果它没有选中,假设它已经选中,与当前已经选中的条件组合在一起,然后去路径字典中查找,如果找到,就表示它可选,找不到就说明它的disabled为true
补充一个函数 每点击一次 就拿到拼接的数组 getSelectedArr的结果是:[undefined, ‘中国’, undefined]
// 当前选中规格集合 [undefined, "中国", undefined]
const getSelectedArr = (specs) => {
return specs.map((spec) => {
const selectedVal = spec.values.find((val) => val.selected)
// 找到了要里面的name 没有要 undefined
return selectedVal ? selectedVal.name : undefined
})
}
const clickSpecs = (item, val) => {
if (val.disabled) return false
// 如果当前选中
if (val.selected) {
// 就把当前取消选中
val.selected = false
} else {
// 否则把每一项都取消选中
item.values.forEach((item) => {
item.selected = false
})
// 把自己选中
val.selected = true
}
// 每次点击都更新禁用状态
+ updateDisabledStatus(pathMap, props.goodsData.specs)
}
更新updateDisabledStatus函数
// 更新按钮的禁用状态
const updateDisabledStatus = (pathMap, specs) => {
// 1. 当前用户的选择状态[undefined, "中国", undefined]
const _selectedArr = getSelectedArr(specs)
console.log(_selectedArr)
specs.forEach((spec, i) => {
const selectedArr = [..._selectedArr]
// 对每一个按钮
spec.values.forEach((val) => {
// 2. 已经选中的按钮不需要判断
if (val.selected) return false
// 3. 假设他能选,更新选中之后的条件
selectedArr[i] = val.name
console.log(selectedArr[i])
// 4.过滤掉undefined得到key
const key = selectedArr.filter(v => v).join(spliter)
// 当前的状态 = 去路径字典里里找name 如果找到了就为说明存在true 取个反
val.disabled = !pathMap[key]
})
})
}
(8)父传子-传入默认选中的sku
传入id时 子组件高亮并选中父组件传过来对应的规格
父组件
<GoodsSku :goodsData="goodsData" skuId="1369155865461919746" ></GoodsSku>
子组件
props: {
skuId: {
type: String,
default: ''
}
},
定义initSelectedStatus函数
// 根据skuId还原用户选中的规格
const initSelectedStatus = (goodsData, skuId) => {
// 1. 找到选中的具体的规格 sku对象
const sku = goodsData.skus.find(sku => sku.id === skuId)
// 2. 设置对应的按钮的selected为true
if (sku) {
const selectArr = sku.specs.map(it => it.valueName)
console.log('找到选中的具体的规格', selectArr, sku)
// ["黑色", "中国", "10cm"]
goodsData.specs.forEach((spec, idx) => {
spec.values.forEach(value => {
value.selected = (value.name === selectArr[idx])
})
})
}
}
setup (props, { emit }) {
// 根据传入的skuId默认选中规格按钮
+ initSelectedStatus(props.goods, props.skuId)
// 组件初始化的时候更新禁用状态
(9)数据通讯-子传父-传出sku信息
选中完整的规格信息(在规格列表中,所有的规格都选了,没有是undefined的情况)时,向父组件传递有效数据。具体结构如下
{
skuId: sku的id,
price: sku的价格(注意:每个sku价格可能是不同的)
oldPrice: sku的原价格
inventory: sku的库存,
specsText: 商品的说明,例如:'颜色:黑色 产地:中国 尺度:20cm'//
}
如果选中不完整,也要向父组件传递数据,方便父组件进一步操作。这里约定传{}空对象。
const tryEmit = () => {
// 触发change事件将sku数据传递出去
const selectedArr = getSelectedArr(props.goodsData.specs).filter(v => v)
// 当选中的length 和 一共的lengs相等就调用下面 否则传空对象过去
if (selectedArr.length === props.goodsData.specs.length) {
const skuIds = pathMap[selectedArr.join(spliter)]
const sku = props.goodsData.skus.find(sku => sku.id === skuIds[0])
// 传递
emit('change', {
skuId: sku.id,
price: sku.price,
oldPrice: sku.oldPrice,
inventory: sku.inventory,
specsText: sku.specs.reduce((p, n) => `${p} ${n.name}:${n.valueName}`, '').replace(' ', '')
})
} else {
emit('change', {})
}
}
点击时调用
const clickSpecs = (item, val) => {
// ...
updateDisabledStatus(pathMap, props.goodsData.specs)
// 抛出事件
+ tryEmit()
}
父组件接收
<GoodsSku :goods="goods" @change="skuChange"/>
setup () {
const goods = useGoods()
// sku改变时候触发
const skuChange = (sku) => {
if (sku.skuId) {
goods.value.price = sku.price
goods.value.oldPrice = sku.oldPrice
goods.value.inventory = sku.inventory
}
}
return { goods, changeSku }
}
总代码
父组件
<GoodsSku :goodsData="goodsData" skuId="1369155865461919746" @change="skuChange" ></GoodsSku>
setup () {
const goodsData = ref({})
findGoods(route.params.id).then((data) => {
goodsData.value = data.result
})
const skuChange = (sku) => {
console.log(sku)
if (sku.skuId) {
goodsData.value.price = sku.price
goodsData.value.oldPrice = sku.oldPrice
goodsData.value.inventory = sku.inventory
}
}
return { goodsData, skuChange }
}
sku组件
<template>
<div class="goods-sku">
<dl v-for="item in goodsData.specs" :key="item.id">
<dt>{{ item.name }}</dt>
<dd>
<template v-for="val in item.values" :key="val.name">
<img
@click="clickSpecs(item, val)"
v-if="val.picture"
:class="{ selected: val.selected, disabled: val.disabled }"
:src="val.picture"
alt="val.picture"
:title="val.picture"
/>
<span
@click="clickSpecs(item, val)"
:class="{ selected: val.selected, disabled: val.disabled }"
v-else
>
{{ val.name }}
</span>
</template>
</dd>
</dl>
</div>
</template>
<script>
// 计算合集子集的方法
import getPowerSet from '@/vender/power-set'
// 定义要分隔的字符串
const spliter = '*'
// 根据sku数据得到的字典对象
const getPathMap = (skus) => {
// 用来保存的路径字典
const pathMap = {}
// 遍历每一个sku规格
skus.forEach((sku) => {
// 1.过滤出有效的sku 因为库存会出有0的情况
if (sku.inventory) {
// 2. 拼接起来得到属性值数组例如 ['黑色','中国','10cm']
const specs = sku.specs.map((spec) => spec.valueName)
// 3.得到sku的子集 [[] ["蓝色"] ["中国"]["蓝色", "中国"] ["10cm"]...]
const powerSet = getPowerSet(specs)
// 将子集循环
powerSet.forEach((set) => {
const key = set.join(spliter)
if (pathMap[key]) {
// 已经有key往数组追加
pathMap[key].push(sku.id)
} else {
// 没有key设置一个数组
pathMap[key] = [sku.id]
}
})
}
})
return pathMap
}
// 更新按钮的禁用状态
const updateDisabledStatus = (pathMap, specs) => {
// 1. 当前用户的选择状态[undefined, "中国", undefined]
const _selectedArr = getSelectedArr(specs)
console.log(_selectedArr)
specs.forEach((spec, i) => {
const selectedArr = [..._selectedArr]
// 对每一个按钮
spec.values.forEach((val) => {
// 2. 已经选中的按钮不需要判断
if (val.selected) return false
// 3. 假设他能选,更新选中之后的条件
selectedArr[i] = val.name
// 4.过滤掉undefined得到key
const key = selectedArr.filter(v => v).join(spliter)
// 当前的状态 = 去路径字典里里找name 如果找到了就为说明存在true 取个反
val.disabled = !pathMap[key]
})
})
}
// 当前选中规格集合 [undefined, "中国", undefined]
const getSelectedArr = (specs) => {
return specs.map((spec) => {
const selectedVal = spec.values.find((val) => val.selected)
// 找到了要里面的name 没有要 undefined
return selectedVal ? selectedVal.name : undefined
})
}
// 根据id 还原选中规格
const initSelectedStatus = (goodsData, skuId) => {
// 1. 找到选中的具体的规格 sku对象
const sku = goodsData.skus.find(sku => sku.id === skuId)
if (sku) {
const selectArr = sku.specs.map(spec => spec.valueName) // ["黑色", "中国", "10cm"]
goodsData.specs.forEach((spec, idx) => {
spec.values.forEach(value => {
// 给每一项都给当前的状态给value.selected
value.selected = (value.name === selectArr[idx])
})
})
}
}
export default {
name: 'GoodsSku',
props: {
goodsData: {
type: Object,
default: () => ({ specs: [], skus: [] }) // 防止第一次打开页面没数据报错
},
skuId: {
type: String,
default: ''
}
},
setup (props, { emit }) {
// 选中排他 item所有项 val当前项
const clickSpecs = (item, val) => {
if (val.disabled) return false
// 如果当前选中
if (val.selected) {
// 就把当前取消选中
val.selected = false
} else {
// 否则把每一项都取消选中
item.values.forEach((item) => {
item.selected = false
})
// 把自己选中
val.selected = true
}
// 每次点击都更新禁用状态
updateDisabledStatus(pathMap, props.goodsData.specs)
// 抛出事件
tryEmit()
}
const tryEmit = () => {
// 触发change事件将sku数据传递出去
const selectedArr = getSelectedArr(props.goodsData.specs).filter(v => v)
if (selectedArr.length === props.goodsData.specs.length) {
const skuIds = pathMap[selectedArr.join(spliter)]
const sku = props.goodsData.skus.find(sku => sku.id === skuIds[0])
// 传递
emit('change', {
skuId: sku.id,
price: sku.price,
oldPrice: sku.oldPrice,
inventory: sku.inventory,
specsText: sku.specs.reduce((p, n) => `${p} ${n.name}:${n.valueName}`, '').replace(' ', '')
})
} else {
emit('change', {})
}
}
// 路径地图
const pathMap = getPathMap(props.goodsData.skus)
// 组件初始化的更新禁用状态 参数路径地图,初始数据
updateDisabledStatus(pathMap, props.goodsData.specs)
initSelectedStatus(props.goodsData, props.skuId)
return { clickSpecs }
}
}
</script>
<style scoped lang="less">
.sku-state-mixin () {
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
&.selected {
border-color: @xtxColor;
}
&.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
}
.goods-sku {
padding-left: 10px;
padding-top: 20px;
dl {
display: flex;
padding-bottom: 20px;
align-items: center;
dt {
width: 50px;
color: #999;
}
dd {
flex: 1;
color: #666;
> img {
width: 50px;
height: 50px;
.sku-state-mixin ();
}
> span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
.sku-state-mixin ();
}
}
}
}
</style>
以上是关于vue电商项目sku 规格 详细步骤的主要内容,如果未能解决你的问题,请参考以下文章