使用邻接矩阵实现商品SKU表单联动

什么是邻接矩阵


不了解邻接矩阵,请查看邻接矩阵

邻接矩阵是一个用来描绘顶点与边关系的数据结构。它的本质是一个二维数组,适合用来处理最小数据单元之间的关联关系。邻接矩阵有两种模式:无向图以及有向图。无向图主要的特点是不表示方向点与点之间可以双向流通,有向图则包含方向两点间可单向亦可双向。他们主要应用在迷宫、简单地图、级联表单等等图形化场景


先看看我们要实现效果


交互分析

  • 当用户选择青芒色的规格时,所有青芒色相关的可选项均亮起
  • 同规格可选项也为亮起状态,例如,选择青芒色时,所有颜色选项为亮起可选状态
  • 当用户选择图案类型时,在颜色以及图案的公共作用下,部分规格亮起
  • 最后用户选择尺码类型时,在颜色、图案以及尺码的公共作用下,产品规格确定更新
  • 规格选择无顺序要求

如果让后端下发一个递归的树结构1:会产生非常多的数据囤余。2:导致传输的数据量级变大。3:计算放在服务器加大服务器开销。

实现思路

1、获取后端数据

这里以七月老师提供的商品数据为例,由于数据太长,我们只列出部分数据。

const product = {
    "id":2,
    ....
    "sku_list":[
        {
            "id":2,
            "price":77.76,
            "discount_price":null,
            "title":"金属灰·七龙珠",
            ....
            "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
        },
        ....
    ],
    "spu_img_list":[
        {
            "id":165,
            ....
        }
    ],
    "spu_detail_img_list":[
        {
            "id":24,
            ....
        }
    ],
    "sketch_spec_id":1,
    "default_sku_id":2
}

观察效果图,我们还需要一个规格属性列表,数据结构如下:

[{
  key_id: 1,
  key: "颜色"
  list:[{
        value_id: 45,
        value: "金灰色",
        select: false,
        disable: true
    },
   ...
  ]

},
...
]

我们直接从后端数据提取数据就可以了,具体方法如下:

Page({
  data: {
    product: {},
    specsS: [],
    data: [],
    optionSpecs: [],
    commoditySpecs: [],
    shopAdjoin: null,
    selected: [],
    pIndex: 0
  },

  onLoad: function () {
    // 后端获取原始数据
    const product = { ... }
    // 商品sku列表
    const data = product.sku_list
    // 商品默认sku
    const pIndex = product.default_sku_id
    const productSku = data[pIndex]
    // 初始化数组
    const specsS = Array.from({length: productSku.specs.length})
    const commoditySpecs = Array.from({length: productSku.specs.length})
    const selected = []
    // 提取规格属性列表
    data.map((item) => {
      item.specs.map(({key_id, key, value_id, value}, index) => {
        let list = {
          value_id: value_id,
          value: value,
          select: false,
          disable: true
        }
        if (!commoditySpecs[index]) {
          selected.push(key)
          commoditySpecs[index] = {
            key_id: key_id,
            key: key,
            list: [list]
          }
        }
        if (this.container(commoditySpecs[index].list, list) === -1) commoditySpecs[index].list.push(list)
      })
    })
    this.setData({
      commoditySpecs,
      ...
    })
  },
  // 判断一个数组中是否某个元素
  container(vert, id) {
    let tem = -1
    vert.forEach((item, index) => {
      if (item.value_id === id.value_id) tem = index
    })
    return tem
  }
  ...
})

在前端显示

<wxs src="../../wxs/price.wxs" module="p"></wxs>
<view class="container">
  <image class="product-img" src="{{product.img}}"></image>
  <view class="product-sku">
    ....
    <view class="select">
      <block wx:for="{{commoditySpecs}}" wx:for-item="item" wx:key="unique">
        <view class="select-col">
          <view class="line"></view>
          <view class="select-text">
            <text>{{item.key}}</text>
          </view>

          <view class="select-l-tag">
            <block wx:for="{{item.list}}" wx:key="unique" wx:for-index="idx" wx:for-item="list">
              <view style="padding-right: 22rpx">
                <l-tag
                        size="large"
                        type="touch"
                        plain font-color="#333"
                        bind:lintap="selectTap"
                        name="{{index}}"
                        cell="{{list}}"
                        select="{{list.select}}"
                        disable="{{list.disable}}"
                        l-class="{{list.disable ? '' : 'unselect-tag'}}"
                        l-select-class="select-tag">{{list.value}}</l-tag>
              </view>
            </block>
          </view>
        </view>
      </block>
    </view>
    ....
</view>

效果如下:

这样所需要的数据和数据结构就有了,接下来我们通过矩阵来将这数据转化,来改变前端显示。

2、转化成邻接矩阵

先看一下我们转换成的矩阵是什么样的:我们这个商品有三种属性,每个属性有三个选项,一共有九个选项,那么矩阵实际是一个9x9的数组。由于这个商品有四个单品,我们需要将这九个选项选出四组选择,每列代表选中这个选项后有多少可选项组合。

比如:我们这个商品如果先选中金属灰,那么其它颜色也是可选的,接着单品中带有颜色金属灰的只有一个套餐【颜色:金属灰、图案:七龙珠、尺码:小号 S】,那么选中金属灰,所有可选项就有金属灰、青芒色、橘黄色、七龙珠、小号S。数组中就是这样(1:代表可选项):

       金属灰 青芒色 橘黄色 七龙珠 灌篮高手 圣斗士 小号S 中号M 大号L
金属灰    1     1    1      1      0      0    1     0    0

青芒色    1     1    1      0      1      1    0     1    1

橘黄色    1     1    1      1      0      0    1     0    0

七龙珠    1     0    1      1      1      1    1     0    0

灌篮高手  0     1    0      1      1      1    0     1    0

圣斗士    0     1    0      1      1      1    0     0    1

小号S     1     0    1      1      0      0    1     1    1

中号M     0     1    0      0      1      0    1     1    1

大号L     0     1    0      0      0      1    1     1    1

如果我们接着选择图案:七龙珠,那么金属灰和七龙珠的交集也就是两列之和且数值不小于所选集合数量[2,1,2,2,1,1,2,0,0]-->[1,0,1,1,0,0,1,0,0],可选项为[金属灰、橘黄色、七龙珠、小号S]

最后选择尺码:小号S,那么三列的交集就是[1,0,1,1,0,0,1,0,0],可选项为[金属灰、橘黄色、七龙珠、小号S]

我们程序怎么设计得到这个数组的呢?

首先将一个商品的单品配置写入这个数组,得到的数据如下:

       金属灰 青芒色 橘黄色 七龙珠 灌篮高手 圣斗士 小号S 中号M 大号L
金属灰    1     0    0      1      0      0    1     0    0

青芒色    0     1    0      0      1      1    0     1    1

橘黄色    0     0    1      1      0      0    1     0    0

七龙珠    1     0    1      1      0      0    1     0    0

灌篮高手  0     1    0      0      1      0    0     1    0

圣斗士    0     1    0      0      0      1    0     0    1

小号S     1     0    1      1      0      0    1     0    0

中号M     0     1    0      0      1      0    0     1    0

大号L     0     1    0      0      0      1    0     0    1

这个数组只会显示现有单品配置的可选项,接下来我们要将只选一个配置时,同类配置设置成可选状态。也就是将每类的所有可选项加入其中,即颜色【金属灰 青芒色 橘黄色】、图案【七龙珠 灌篮高手 圣斗士】、尺码【小号S 中号M 大号L】与原来数组取并集,就能得到我们需要的数组,代码如下

/**
 * @Author: steinKuo
 * @Date:   2020-01-03 7:31 上午
 */
class Adjoin {
  constructor(vertex) {
    this.vertex = vertex;
    console.log('init', vertex)
    this.quantity = vertex.length;
    this.init();
  }

  init() {
    this.adjoinArray = Array.from({length: this.quantity * this.quantity});
  }

  container(vert, id) {
    let tem = -1
    vert.forEach((item, index) => {
      if (item.value_id === id.value_id) tem = index
    })
    return tem
  }

  getVertexRow(id) {
    const index = this.container(this.vertex, id)

    const col = [];
    this.vertex.forEach((item, pIndex) => {
      col.push(this.adjoinArray[index + this.quantity * pIndex]);
    });
    return col;
  }

  getAdjoinVertexs(id) {
    return this.getVertexRow(id).map((item, index) => (item ? this.vertex[index] : '')).filter(Boolean);
  }

  setAdjoinVertexs(id, sides) {
    const pIndex = this.container(this.vertex, id)
    sides.forEach((item) => {
      const index = this.container(this.vertex, item)
      this.adjoinArray[pIndex * this.quantity + index] = 1;
    });
  }

  getRowTotal(params) {
    params = params.map(id => this.getVertexRow(id));
    const adjoinNames = [];
    this.vertex.forEach((item, index) => {
      const rowtotal = params.map(value => value[index]).reduce((total, current) => {
        total += current || 0;
        return total;
      }, 0);
      adjoinNames.push(rowtotal);
    });
    return adjoinNames;
  }

  // 交集
  getUnions(params) {
    const row = this.getRowTotal(params);
    row.map((item, index) => item >= params.length && this.vertex[index]).filter(Boolean);
    return row.map((item, index) => item >= params.length && this.vertex[index]).filter(Boolean);
  }

  // 并集
  getCollection(params) {
    params = this.getRowTotal(params);
    return params.map((item, index) => item && this.vertex[index]).filter(Boolean);
  }
}

class ShopAdjoin extends Adjoin {
  constructor(commoditySpecs, data) {
    super(commoditySpecs.reduce((total, current) => [
      ...total,
      ...current.list,
    ], []));
    this.commoditySpecs = commoditySpecs;
    this.data = data;
    // 单规格矩阵创建
    this.initCommodity();
    // 同类顶点创建
    this.initSimilar();
  }

  initCommodity() {
    this.data.forEach((item) => {
      this.applyCommodity(item.specs);
    });
  }

  initSimilar() {
    // 获得所有可选项
    const specsOption = this.getCollection(this.vertex);
    this.commoditySpecs.forEach((item) => {
      const params = [];
      item.list.forEach((value) => {
        if (this.container(specsOption, value) > -1) params.push(value);
      });
      // 同级点位创建
      console.log('initSimilar', params)
      this.applyCommodity(params);
    });
  }

  querySpecsOptions(params) {
    // 判断是否存在选项填一个
    if (params.some(Boolean)) {
      // 过滤一下选项
      params = this.getUnions(params.filter(Boolean));
    } else {
      // 兜底选一个
      params = this.getCollection(this.vertex);
    }
    // console.log('querySpecsOptions', params)
    return params;
  }

  // 选出相应规格
  querySpecs(params) {
    let pIndex = -1
    this.data.forEach((items, index) => {
      let tem = 0
      items.specs.forEach((item) => {
        tem = params.includes(item.value) ? tem + 1 : tem
      })
      if (tem === params.length) {
        pIndex = index
      }
    });
    return pIndex
  }

  applyCommodity(params) {
    params.forEach((param) => {
      this.setAdjoinVertexs(param, params);
    });
  }
}

export {
  ShopAdjoin
}

3、联动表单数据

在我们点击的时候,上方提示文字会有请选择xxx,当选完之后,会有已选xxx的文字效果。

主要思路就是查看specsS已选列表中的数据,通过querySpecs()方法,在已设置的数组中进行交并集的筛选。当specsS中的数量跟规格列表的种类数量是不是相同,如果相同那就全部选中了,然后通过这些选中的规格可以确定一个SKU,我们就能拿到这个SKU的库存,价格,图像等信息,这里我们把这个SKU存起来了,添加库显示等可以直接在页面添加相关数据。

交互的具体代码如下:

Page({
  data: {
    product: {},
    specsS: [],
    optionSpecs: [],
    commoditySpecs: [],
    shopAdjoin: null,
    selected: [],
    pIndex: 0
  },

  onLoad: async function () {
    const product = {
    ....
    }
    const data = product.sku_list

    const pIndex = product.default_sku_id
    const productSku = data[pIndex]
    const specsS = Array.from({length: productSku.specs.length})
    const commoditySpecs = Array.from({length: productSku.specs.length})
    const selected = []

    data.map((item) => {
      item.specs.map(({key_id, key, value_id, value}, index) => {
        let list = {
          value_id: value_id,
          value: value,
          select: false,
          disable: true
        }
        if (!commoditySpecs[index]) {
          selected.push(key)
          commoditySpecs[index] = {
            key_id: key_id,
            key: key,
            list: [list]
          }
        }
        if (this.container(commoditySpecs[index].list, list) === -1) commoditySpecs[index].list.push(list)
      })
    })
    const shopAdjoin = new ShopAdjoin(commoditySpecs, data)

    this.setData({
      commoditySpecs,
      product,
      shopAdjoin,
      productSku,
      selected,
      specsS,
      pIndex
    })
  },

  selectTap(event) {
    let lists = this.data.commoditySpecs
    let specsS = this.data.specsS
    let pIndex = this.data.pIndex
    let selected = []
    const name = event.detail.name
    const cell = event.detail.cell

    // 禁用不可选项
    if (!cell.disable) return

    // 已选中选项集合
    if (!specsS[name]) {
      specsS[name] = cell
    } else {
      specsS[name] = specsS[name].value_id === cell.value_id ? null : cell;
    }

    // 选项状态更改
    lists[name].list.map(({value_id, value, select, disable}, index) => {
      // 一行中只能有一个被选中
      if (value_id === cell.value_id) {
        lists[name].list[index].select = !select
      } else {
        lists[name].list[index].select = false
      }
    })

    // 获得可选项表
    const data = this.data.shopAdjoin.querySpecsOptions(specsS)

    lists.map(({key_id, key, list}, index) => {
      // 如果没选中,则加进提示数组
      if (!specsS[index]) selected.push(key)

      list.map(({value_id, value, select, disable}, i) => {
        // 将不在可选项表中的选项禁用
        list[i].disable = this.container(data, list[i]) > -1
      })
    })

    // 显示选择商品
    if (selected.length === 0) {
      selected = specsS.map((item) => item.value)
      pIndex = this.data.shopAdjoin.querySpecs(selected)
    }

    const productSku = this.data.product.sku_list[pIndex]
    this.setData({
      commoditySpecs: lists,
      specsS,
      selected,
      productSku
    })
  },
  container(vert, id) {
    let tem = -1
    vert.forEach((item, index) => {
      if (item.value_id === id.value_id) tem = index
    })
    return tem
  }

})

这样就完成了,如有纰漏,欢迎指出。

最后说明一下这篇文章主要是对慕课网学习中7七月老师布置作业的独立完成和思考,所有思路与代码是参考邻接矩阵后动手完成的,如果有小伙伴想一起加入成长,可以慕课来报7七月老师的从java后端到全栈课程的学习。