Java教程

Sku预研作业

本文主要是介绍Sku预研作业,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

效果图如下:

https://img1.sycdn.imooc.com/6733090000015f7701940178.jpg


完成sku的规格选择,首先要获取数据,然后一个个去对应,排列出商品的规格名、规格值。

但仔细观察了API,发现没有获取某个spu具有哪些规格名、规格值的API,只有总的获取spu详细数据的API。

spu详细数据的格式如下:

{

    "id":2,

    ....

    "sku_list":[

        {

            "id":2,

            "price":77.76,

            "discount_price":null,

            "online":true,

            "img":"",

            "title":"金属灰·七龙珠",

            "spu_id":2,

            "category_id":17,

            "root_category_id":3,

            "specs":[

                {

                    "key_id":1,

                    "key":"颜色",

                    "value_id":45,

                    "value":"金属灰"

                },

                {

                    "key_id":3,

                    "key":"图案",

                    "value_id":9,

                    "value":"七龙珠"

                },

                {

                    "key_id":4,

                    "key":"尺码",

                    "value_id":14,

                    "value":"小号 S"

                }

            ],

            "code":"2$1-45#3-9#4-14",

            "stock":5

        },

        {

            "id":3,

            "price":66,

            "discount_price":59,

            "online":true,

            "img":"",

            "title":"青芒色·灌篮高手",

            "spu_id":2,

            "category_id":17,

            "root_category_id":3,

            "specs":[

                {

                    "key_id":1,

                    "key":"颜色",

                    "value_id":42,

                    "value":"青芒色"

                },

                {

                    "key_id":3,

                    "key":"图案",

                    "value_id":10,

                    "value":"灌篮高手"

                },

                {

                    "key_id":4,

                    "key":"尺码",

                    "value_id":15,

                    "value":"中号 M"

                }

            ],

            "code":"2$1-42#3-10#4-15",

            "stock":999

        },

        ....

    ],

    .....

    "sketch_spec_id":1,

    "default_sku_id":2

}

调用接口获取spu的详细数据:

class Spu {

 static async getSpuDetail(id) {

return Http.request({

url: `spu/id/${id}/detail`

})

 }

}


必须通过这种格式的详细数据,获取对应spu的规格名、规格值。


规格名的获取思路是,既然每个sku的规格名都一样,那取第一个sku,获取其中的规格名,将其放在spec_key_list中;

规格值的获取思路是,遍历sku_list,将每个sku的规格值都放在spec_val_list中,由于多个sku会有相同的规格值,需要进行去重。


为了取数方便,规格值存放在二维数组中,spec_val_list[i]表示spec_key_list[i]的规格名对应有哪些规格值。

主要的代码如下(直接写在新建detail页面的onLoad函数中):

const spuDetail = await Spu.getSpuDetail(spuId)

const sku_list = spuDetail.sku_list

let spec_key_list = []

let spec_val_list = []

// 取其中一个sku的规格,它的规格名就是整个spu的规格名

// 在取规格名的同时,为规格值的数组开空间

for(let spec of sku_list[0].specs) {

  spec_key_list.push(spec.key)

  spec_val_list.push([])

}


// sku_list[i]表示单个sku

for(let i = 0; i < sku_list.length; i ++) {

  // 遍历每个sku的规格

  for(let j = 0; j < sku_list[i].specs.length; j ++) {

    let spec = sku_list[i].specs[j]

    // specs[j]和spec_val_list[j]对应,是一个规格名下的规格值

    if(!spec_val_list[j].includes(spec.value)) {

      spec_val_list[j].push(spec.value)

    }

  }

}


this.setData({

  spec_key_list,

  spec_val_list,

  sku_list

})

页面显示就简单了,直接用wx:for循环即可:

<view class="container">

  <block wx:for="{{spec_key_list}}" wx:for-index="kIndex">

  <text>{{item}}</text>

<view class="inner-container">

  <block wx:for="{{spec_val_list[kIndex]}}" wx:for-index="vIndex">

    <l-button>{{item}}</l-button>

  </block>

</view>

  </block>

</view>

这里为了后续点击规格值的处理方便,将规格名的索引设定为kIndex,规格值索引设定为vIndex。

此外运用了LinUI的button,其中的plain属性能很好的模拟出已选中(为false)、未选中(为true)的状态,disabled属性模拟出禁用(为true)状态。


那接下来的问题就是如何根据用户的点击,改变规格值按钮的状态了。

首先需要一个二维数组spec_status来存储每个规格值的状态值,spec_status[i][j]表示第i行第j列的规格值的状态。此处的i就是规格名的索引也就是kIndex,j是规格值的索引也就是vIndex。另外,为了之后能够具体准确地获取到当前规格值,除了状态之外,还需要记录规格值id。

具体代码如下(在获取spec_val_list和spec_key_list代码的基础上添加):

const spec_status = []

...

// 在遍历规格名的同时,为spec_status开空间

// spec_status[i]代表spec_key_list[i]的规格名对应规格值的选中状态和id

for(let spec of sku_list[0].specs) {

  ...

  spec_status.push([])

}


for(let i = 0; i < sku_list.length; i ++) {

  // 遍历每个sku的规格

  for(let j = 0; j < sku_list[i].specs.length; j ++) {

    let spec = sku_list[i].specs[j]

    // 发现首次出现的规格值,存入它的状态和id

    if(!spec_val_list[j].includes(spec.value)) {

      ...

      spec_status[j].push({

        // 默认起初都是未选中

        status: 0,

        value_id: spec.value_id

      })

    }

  }

}


this.setData({

  spec_status,

  ...

})


新建一个spec.wxs,把自定义的状态值绑定到前端显示(对应plain、disabled属性),规定0为未选中状态,1为选中状态,-1为禁用状态,代码如下:

function specStatus(status) {

if(status === -1) {

return {

disabled: true,

plain: false

}

}

var ifPlain = true

if(status === 1) {

ifPlain = false

}

return {

disabled: false,

plain: ifPlain

}

}


module.exports = {

  specStatus: specStatus

}


在wxml中可以引入wxs并调用该函数,需要在每个规格值按钮上监听点击事件,并且需要给监听函数传递kIndex、vIndex:

<wxs class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/wxs/spec.wxs" module="sp"></wxs>

...

<l-button data-kid="{{kIndex}}" data-vid="{{vIndex}}"

          bindtap="clickSpec" class="spec-val" 

          plain="{{sp.specStatus(spec_status[kIndex][vIndex].status).plain}}"

          disabled="{{sp.specStatus(spec_status[kIndex][vIndex].status).disabled}}">

  {{item}}

</l-button>


接下来开始编写点击的监听函数clickSpec,先做最基础的,点击后常规的状态切换,点击未选中/已选中状态切换为已选中/未选中状态。此外如果点击未选中的规格值时,该规格名下若已有其他已选中的规格值,需要将其转换为未选中状态,具体代码如下:

clickSpec(event) {

  const kid = event.currentTarget.dataset.kid

  const vid = event.currentTarget.dataset.vid

  // 获取点击的规格值的当前状态

  const status = this.data.spec_status[kid][vid].status

  // 如果是禁用状态,不做任何处理

  if(status === -1) {

    return

  } else {

    // 其他状态,进行相互切换

    this.data.spec_status[kid][vid].status = 1 - status

    // 如果当前状态(切换前)为未选中状态,将当前规格名的其他规格值(非禁用状态)切换为未选中状态

    if(status === 0) {

      for(let i = 0; i < this.data.spec_status[kid].length; i ++) {

        if(i !== vid && this.data.spec_status[kid][i].status != -1) {

          this.data.spec_status[kid][i].status = 0

        }

      }

    }

    // 检查组合是否存在的函数

    // this.checkComb(kid)

  }

  this.setData({

    spec_status: this.data.spec_status

  })

}


接下来是最关键的一步,也就是根据库中存在的组合,与当前所选的规格值对比,将所有不存在组合的规格值给禁用。这块逻辑比较复杂,打算将其封装成一个独立的函数checkComb,在clickSpec中进行调用。

       先分享一下之前踩了坑的错误思路,之前想的是先获取所有被选中的规格值,再去依次遍历所有未被选中的规格名的规格值。遍历时依次取当前规格值和已选中的规格值组合,再对比存在的所有组合,如果没有这个组合,就禁用。

       但这个思路有个错误的地方在于,哪怕规格名有规格值被选中,依然可以切换选中的规格值。那这个逻辑就无法判断当前已选中规格值的规格名中,有哪些规格值需要禁用,只能判断未选中的规格名中,哪些规格值需要禁用。


整个思路需要调整一下,不应该关注于是否已被选中,反复操作了线上demo后发现,当前点击的规格值所在的规格名,其实永远都不会被切换成禁用状态,禁用的是非本次点击的规格值所在的规格名。

也就是说,关键在于当前点击的规格名到底是什么,用当前点击的被选中的规格值和其他规格名的规格值依次组合(无论此次点击是选中还是未选中,这个逻辑都通用),再配合上已选中的其他规格值,查看是否包含在库中已有的组合中,如果不存在就禁用该规格值。

听起来有些绕,拿具体的例子来说,假设有以下规格:

颜色:金属灰、青芒色、橘黄色

图案:七龙珠、圣斗士、灌篮高手

尺码:大  号、中  号、小  号


存在的组合有:金属灰-七龙珠-小号、青芒色-灌篮高手-中号、青芒色-圣斗士-大号、橘黄色-七龙珠-小号

假如第一次点击青芒色,图案中能和已选中的青芒色,形成存在组合的有青芒色-灌篮高手、青芒色-圣斗士,因此七龙珠处于禁用状态;尺码中,和青芒色形成存在组合的,有青芒色-大号、青芒色-中号,小号禁用。


第二次点击圣斗士,已选中的组合是青芒色-圣斗士,因为点击图案可能会令颜色规格值的状态发生变化,因此依然需要遍历颜色,此时将青芒色改为待定,选中组合视为只有圣斗士,发现只有青芒色-圣斗士是存在的组合,金属灰、橘黄色禁用;遍历尺码,尺码未被选中,那已选中的组合就是正常的青芒色-圣斗士了,能形成组合的只有青芒色-圣斗士-大号,小号、中号禁用。


最后第三次点击大号,遍历颜色,能和圣斗士-大号(青芒色改为待定)形成组合的只有青芒色-圣斗士-大号,将七龙珠、灌篮高手禁用;遍历图案,能和青芒色-大号(圣斗士改为待定)形成组合的只有青芒色-圣斗士-大号,将小号、中号禁用。


注意,判断某个组合是否存在,是去判断这个组合是否是已存在组合的子集,而不是指两个组合完全一样,需要先写一个判断某个数组是否是另一个数组子集的函数:

_isSubset(subArr, arr) {

  // 去掉undefined元素

  let arrC = []

  for (const item of subArr) {

    if(item) {

      arrC.push(item)

    }

  }

  return arrC.every(i => arr.includes(i))

}


另外,还要一个存有所有sku规格组合的all_comb数组,这是一个二维数组,其中每个元素是存有每个sku规格值id组合的数组。为了便于确定当前规格组合是哪个sku,除了存储规格值组合数组之外,还需要存储该组合对应的skuID。即all_comb[i].id为第i个sku的ID,all_comb[i].specs为第i个sku的规格值id组合数组:

const spuId = options.id

const spuDetail = await Spu.getSpuDetail(spuId)

const sku_list = spuDetail.sku_list

const all_comb = []

...


for(let i = 0; i < sku_list.length; i ++) {

  all_comb.push({

    id: 0,

    specs: []

  })

  // 遍历每个sku的规格

  for(let j = 0; j < sku_list[i].specs.length; j ++) {

    let spec = sku_list[i].specs[j]

    all_comb[i].id = sku_list[i].id

    all_comb[i].specs.push(spec.value_id)

    ...

  }

}


this.setData({

  all_comb,

  ...

})


整体的思路都有了,还剩下一个细节,就是反选的问题,把已选中的规格值取消后,又该如何变化呢?

其实是一样的思路,依然是不用变更此次点击的规格值所在规格名的状态,比如在选中青芒色-圣斗士-大号后,

反选点击青芒色,颜色的规格值状态是不会发生任何改变的,该禁用的还是禁用,还是应该去遍历其他的规格名


青芒色-圣斗士-大号全选中状态举例来说,反选青芒色后,已选中的组合是圣斗士-大号,遍历图案时就只剩大号了(图案和先前一样处于待定状态),能和大号形成组合的只有圣斗士,其余仍处于禁用状态;遍历尺码只剩圣斗士(尺码处于待定状态),能和圣斗士形成组合的也只有大号,其余禁用。


反选圣斗士后,已选中的只有大号,遍历颜色时和大号能形成组合的只有青芒色;遍历尺码时,尺码变成了待定,已选中的组合就是空的,小号、中号、大号都有存在的组合,因此将小号、中号的禁用状态解除。


最后反选大号,被选中的组合为空,所有被禁用的格子都被放开了。


不难发现其实反选和正选的逻辑几乎是完全一样的,只不过正选是将不能形成组合的给禁用反选是将可以形成组合的给解除禁用状态

理清楚这些,写出checkComb的整体代码就没那么困难了:

// 传入当前点击的规格名id

checkComb(kid) {

  // 获取当前已选中的规格值

  let selectedV = []

  for (let i = 0; i < this.data.spec_status.length; i++) {

    for (let j = 0; j < this.data.spec_status[i].length; j++) {

      if(this.data.spec_status[i][j].status === 1) {

        selectedV[i] = this.data.spec_status[i][j].value_id

      }

    }

  }


  // 哪怕规格值均未选中,也需要继续判断,需要复原被禁用的规格值

  // 遍历规格名,组合起来,与all_comb对比,不存在的组合的规格值禁用

  for (let i = 0; i < this.data.spec_status.length; i++) {

    // 筛选出非本次点击的规格名

    if(i !== kid) {

      for (let j = 0; j < this.data.spec_status[i].length; j++) {

        // 存一份被选中的规格值,用于后续的复原

        const oV = selectedV[i]

        // 哪怕当前规格也被选中了,那也依次替换进行组合

        selectedV[i] = this.data.spec_status[i][j].value_id

        // 默认不是一个组合

        let isComb = false

        for (let comb of this.data.all_comb) {

          // 如果组合是一个子集,那就是一个组合

          if(this._isSubset(selectedV, comb.specs)) {

            isComb = true

            // 如果被禁用了,复原

            if(this.data.spec_status[i][j].status === -1) {

              this.data.spec_status[i][j].status = 0

            }

            break;

          }

        }

        // 如果不是一个组合,那么禁用该规格值

        if(!isComb) {

          this.data.spec_status[i][j].status = -1

        }

        // 复原selectedV数组

        selectedV[i] = oV

      }

    }

  }


  this.setData({

    spec_status: this.data.spec_status

  })

}


最困难最关键的一步已经完成,后续只剩一些显示上的优化了。

添加功能:所有规格名都被选中规格值后,显示当前sku的信息。


之前说过,通过all_comb可以获得当前规格组合的skuId,又能通过sku_list获取到sku中的详细信息,那思路就简单了:一旦判断出规格值全被选中,判断该规格值是all_comb中哪一个组合的子集,获取该组合的skuId,再通过skuId获取对应的sku,并绑定数据。


单独封装一个函数,接收参数为sku的id,将该id对应的sku信息进行数据绑定:

setSku(skuId) {

  for (const sku of this.data.sku_list) {

    if(sku.id === skuId) {

      this.setData({

        sku

      }) }}

}

在checkComb中添加以下内容,进行sku的绑定:

checkComb(kid) {

  // 获取当前已选中的规格值

  let selectedV = []

  // 计算当前已选了多少个规格值

  let specCount = 0

  for (let i = 0; i < this.data.spec_status.length; i++) {

    for (let j = 0; j < this.data.spec_status[i].length; j++) {

      if(this.data.spec_status[i][j].status === 1) {

        selectedV[i] = this.data.spec_status[i][j].value_id

        specCount++

      }

    }

  }

  

  // 如果规格均选中了就可以确定sku了

  if(specCount === this.data.spec_status.length) {

    for (let comb of this.data.all_comb) {

      // 如果规格均选中,又是一个组合,那该组合就是所选的规格

      if(this._isSubset(selectedV, comb.specs)) {

        this.setSku(comb.id)

      }

    }

  }

  ...

}

然后在wxml中绑定即可:

<image class="img" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="{{sku.img}}"></image>

<text class="title">{{sku.title}}</text>

...


最后添加功能:有默认sku的spu,在进入规格选择页面时选定默认的sku。思路也很简单,判断当前spu中是否存在default_sku_id,如果有,使用该default_sku_id进行sku绑定。


有些细节需要注意,选定默认sku时,该sku的规格值按钮状态需要改为已选中,且无法和已选中规格值组合的规格值按钮需处于禁用状态。


更改状态只需在初始化时判断该sku是否为默认的sku,如果是,则将其规格值的状态设定为已选中状态。禁用规格只需依次调用checkComb,模拟点击的过程即可:

for(let i = 0; i < sku_list.length; i ++) {

  for(let j = 0; j < sku_list[i].specs.length; j ++) {

    ...

    if(!spec_val_list[j].includes(spec.value)) {

      let status

      if(sku_list[i].id === spuDetail.default_sku_id) {

        status = 1

      } else {

        status = 0

      }

      spec_status[j].push({

        status,

        value_id: spec.value_id

      })

    }

  }

}

....

// 如果有默认sku,绑定默认sku(需要在绑定sku_list后绑定)

// 这里甚至都不需要手动绑定sku,在checkComb中会自己绑定

if(spuDetail.default_sku_id) {

  for (let i = 0; i < spec_key_list.length; i++) {

    this.checkComb(i)

  }

}


暂时完毕,后续看课程进行对比。

这篇关于Sku预研作业的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!