一、概念及需求

  • 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中的数据,可以来计算当前情况下,某个规格是否可选。

  1. 在组件初始化的时候去判断每个规格是否点击

  2. 在选中某个规格值之后,去判断其他规格值是否可选

当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

 幂集算法 https://raw.githubusercontent.com/trekhleb/javascript-algorithms/master/src/algorithms/sets/power-set/bwPowerSet.js

封装方法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)点击时禁用状态

首先要获取当前用户的选择,然后

对于每一个按钮:

  1. 如果它已经选中了。就忽略。

  2. 如果它没有选中,假设它已经选中,与当前已经选中的条件组合在一起,然后去路径字典中查找,如果找到,就表示它可选,找不到就说明它的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>

Logo

前往低代码交流专区

更多推荐