React 版 sku商品规格选择器和sku生成器

案例

莆善

案例网页版:https://mall.pushare.cn

演示地址:https://tccsg.github.io/react-sku/
仓库地址:https://github.com/tccsg/react-sku

通用版仓库地址: https://github.com/tccsg/ct-sku

因为业务的需求不得不对sku进行深入的研究,在还没写代码之前感觉是简单的,一天就搞定了

结果几天过后..



甚至想打人了,在想当初是谁给我的自信。还好最后还是写出来了,差点误了进度。

仓库的版本是针对普通的 react hook结合ant版的
因为我是用React-Native写的App也是用hook写的比较好迁移
小程序方面用的是Taro这个框架,不过是用class方式写的,迁移起来稍微麻烦🤏,不过问题不大。

代码解析(sku选择器)

代码方面重点解析sku选择器。


sku格式
/**
   * 通过skus初始化 各个规格
   */
  const setDrawOptions = () => {
    const skus = data?.skus
    const _spec = data?.spec
    const dataExtraHold = data.skuHold
    let _tags: Spec = {}
    let _maxPrice = data?.minPrice ?? 0
    // 用于初始化默认选项
    if (_spec) {
      _tags = _spec
      setSkuHold(dataExtraHold as number)
    } else {
      const _tempTagsStrArray: any = {} // 临时字符串数组
      let _skuHold = 0 // 用于计算总库存
      skus?.forEach((s) => {
        _skuHold += s.hold
        s?.properties?.forEach((p) => {
          if (!_tags[p.name]) {
            _tags[p.name] = []
            _tempTagsStrArray[p.name] = []
          }

          if (!_tempTagsStrArray[p.name].includes(p.value)) {
            _tempTagsStrArray[p.name].push(p.value)
            _tags[p.name].push({
              value: p.value,
              disable: false,
              select: false
            })
          }
        })
        if (s.price > _maxPrice) {
          _maxPrice = s.price
        }
      })
      setSkuHold(_skuHold)
      TotalSkuHold = _skuHold
    }
    let _canFlag = !data.canFlag ? false : true
    /**  */
    if (skus?.length === 1 && !skus[0].properties?.length && skus[0].hold <= 0) {
      _canFlag = false
    }
    setCanFlag(_canFlag)
    setProdPrice(data?.minPrice ?? 0)
    setMaxPrice(_maxPrice)
    setSpecDisable(_tags)
  }

初始化各个选项用的,把sku的格式转成以下格式

{
  颜色:[
    {
        value: '红色',
        disable: fasle,   // 是否可点击
        select: false // 是否选中
    },
   //.....
  ]
  容量:[
    {
        value: '16G',
        disable: fasle,   // 是否可点击
        select: false // 是否选中
    },
// .....
  ]
}
/** 用于规格都没选中的时候 设置 规格是否可以点击,该路径上如果跟该属性的组合没库存则该属性不能点击 */
  // 可合并在 skuCore中
  const setSpecDisable = (tags: any) => {
    const { skus } = data
    Object.keys(tags).forEach((sk) => {
      tags[sk].forEach((sv: SpecItem) => {

        const currentSpec = `${sk}:${sv.value}`
        // 找到含有该规格的路径下 库存不为0的 sku
        const querySku = skus.find((sku) => {
          const queryProperty = sku.properties.find(sp => `${sp.name}:${sp.value}` === currentSpec)
          return queryProperty && sku.hold
        })
        // 如果找到 对应该属性的路径 sku有不为0 的则可选
        sv.disable = !querySku
      })
    })
    setSpec({ ...tags })
  }

这个是用于在规格都没选中的判断哪些不能点,因为是早起版本留下来的,也不敢动,应该是可以合并到下面的会提到的skuCore中。

 /** 规格选项点击事件 */
  const onPressSpecOption = (k: string, currentSpectValue: any) => {
    let isCancel = false
    setCount(1)
    // 找到在全部属性spec中对应的属性
    const currentSpects = spec[Object.keys(spec).find((sk) => sk === k) || ''] || []
    // 上一个被选中的的属性
    const prevSelectedSpectValue: any = currentSpects.find((cspec) => cspec.select) || {}
    // 设置前一个被选中的值为未选中
    prevSelectedSpectValue.select = false
    // 只有当当前点击的属性值不等于上一个点击的属性值时候设置为选中状态
    if (prevSelectedSpectValue === currentSpectValue) {
      isCancel = true
    } else {
      // 设置当前点击的状态为选中
      currentSpectValue.select = true
    }

    // 全部有选中的规格数组 ##可优化
    const selectedSpec = Object.keys(spec)
      .filter((sk: string) => spec[sk].find((sv) => sv.select))
      .reduce((prev: string[], currentSpecKey) => {
        return [...prev, `${currentSpecKey}:${spec[currentSpecKey].find((__v) => __v.select)?.value}`]
      }, [])
    if (isCancel) {
      // 如果是取消且全部没选中
      if (!selectedSpec.length) {
        // 初始化是否可点
        setSpecDisable(spec)
      }
    }
    // 如果规格中有选中的 则对整个规格就行 库存判断 是否可点
    if (selectedSpec.length) {
      skuCore(selectedSpec, k)
    }

    let price = null
    if (selectedSpec.length) {
      price = getSkuInfoByKey(spec, 'price')
    } else {
      price = data?.minPrice
    }
    const hold = getSkuInfoByKey(spec, 'hold') ?? TotalSkuHold
    setSpec({ ...spec })
    if (price) {
      setProdPrice(price)
    }
    setSkuHold(hold)
    optionsChange && optionsChange(spec)
  }

代码也都有注释这里就不做解释了。

/**
   * 核心代码
   * @param selectedSpec 已选中的数组
   * @param currentSpecName 当前点击的规格的名称
   */
  const skuCore = (selectedSpec: string[], currentSpecName?: string) => {
    const { skus } = data
    Object.keys(spec).forEach((sk: string) => {
      if (sk !== currentSpecName) {
        // 找出该规格中选中的值
        const currentSpecSelectedValue = spec[Object.keys(spec).find((_sk) => sk === _sk) || ''].find((sv) => sv.select)
        spec[sk].forEach((sv: SpecItem) => {
          // 判断当前的规格的值是否是选中的,如果是选中的 就不要判断是否可以点击直接跳过循环
          if (!sv.select) {
            const _ssTemp = [...selectedSpec]
            // 如果当前规格有选中的值
            if (!!currentSpecSelectedValue) {
              const sIndex = _ssTemp.findIndex((_sv) => _sv === `${sk}:${currentSpecSelectedValue.value}`)
              _ssTemp.splice(sIndex, 1)
            }
            _ssTemp.push(`${sk}:${sv.value}`)
            const _tmpPath: SkuItem[] = []
            // 找到包含该路径的全部sku
            skus.forEach((sku: SkuItem) => {
              // 找出skus里面包含目前所选中的规格的路径的数组的数量
              const querSkus = _ssTemp.filter((_sst: string) => {
                const querySpec = sku.properties.some((p) => {
                  return `${p.name}:${p.value}` === _sst
                })
                return querySpec
              })
              const i = querSkus.length
              if (i === _ssTemp.length) {
                _tmpPath.push(sku) // 把包含该路径的sku全部放到一个数组里
              }
            })
            const hasHoldPath = _tmpPath.find((p) => p.hold) // 判断里面是要有个sku不为0 则可点击
            let isNotEmpty = hasHoldPath ? hasHoldPath.hold : 0
            sv.disable = !isNotEmpty
          }
        })
      }
    })
    judgeCanAdd(skus)
  }

这个就是用来判断,当我们点击一个规格值后,除了当前规格的所有值其他规格选中值以外的值都要判断是否可点,这些值要跟当前所选中的规格值组合成一个路径,然后从skus中找到包含当前路径的所有sku,然后通过判断这些sku中是否含有非0的库存则说明这个规格值可以点。

/**
   * 
   * @param _spec 规格属性
   * @param sk 该sku下的 sk这个key的值
   * 
   * 不传sk的话返回所有信息
   */
  const getSkuInfoByKey = (_spec: Spec, sk?: string) => {
    // 已选的规格:[{ name:规格名称, value:已选规格内容 }]
    const selectedSpec: { name: string, value: string }[] = []

    Object.keys(_spec).forEach((k) => {
      const selectedValue = _spec[k].find((sv) => sv.select)
      if (selectedValue) {
        // 这块部分也可以在选择的时候直接处理
        selectedSpec.push({
          name: k,
          value: selectedValue.value
        }) 
      }
    })
    // 在规格没有全选的情况下 不执行查询操作
    if (selectedSpec.length !== Object.keys(_spec).length) {
      return
    }
    const { skus } = data
    const querySku = skus.find((sku) => {
      // 对比两个数组找到 两个都不存在的sku 如果为0 则说明完全匹配就是该sku
      const diffSkus = _.xorWith(selectedSpec, sku.properties, _.isEqual)
      return !diffSkus.length
    })
    if (querySku && querySku[sk ?? '']) {
      return querySku[sk ?? '']
    } else if (querySku) {
      return querySku
    } else {
      return null
    }
  }

用于当全部规格值都选中的时候,找到含这个路径的sku,就可以知道库存和价格了,也可以单独获取价格或者库存等的这些信息。

好了当我知道可以用的时候我是


演示图片
演示图片

演示地址:https://tccsg.github.io/react-sku/
仓库地址:https://github.com/tccsg/react-sku

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容