【一】从零实现商城多规格sku
发布于 4 年前 作者 zhuyong 2893 次浏览 来自 分享

前言

在商城里产品的spu、sku展示是很重要的一部分,常见的商城一般没有sku的概念,会把一个多规格的sku拆分成多个spu从而让用户选择。这样是最简单的做法,但是需求是真的跟不上,一般boss都会要求做多规格的sku选择。下面分享一个多规格sku实现的思路以及过程。

效果图


数据分析

要实现sku首先要知道是什么数据组合成的sku列表,下面大概说说我自己sku数据格式。

{
	"code": "1[@0-0](/user/0-0)#1-0#2-0",
	"specs": [
		{
			"id": "0-0",
			"key": "颜色",
			"value": "白色"
		},
		{
			"id": "1-0",
			"key": "图案",
			"value": "圆点"
		},
		{
			"id": "2-0",
			"key": "尺码",
			"value": "XXL"
		}
	]
}

code 表示一个sku,在当前sku_list数据中是唯一存在的,后续都得通过 code 来查找sku数据。
specs 表示sku的规格信息,id 也表示是当前specs里唯一的值,如果用关系型数据库,可能数据库设计的时候,这个id会是子表的id主键,通过id去关联查询对应的数据,我这里0-0 则用预先定义好的规格key的下标来表示,其实跟关系型数据库的主键id一样。只要是唯一不会冲突即可,语义化之后等同于 颜色: 白色,仔细观察其实是有规矩可行的,id会跟code对应起来。

视图规格列表

上面是定义的接口数据,并不能直接在视图上渲染成多规格的样式,因为还需要将多个sku的数据进行转换才能得到视图所见的sku列表。

如何转换数据

接口数据遍历如下:

黑色 圆点 XXL
白色 条纹 S
红色 卡通 L

视图渲染所需数据如下:

黑色 白色 红色
圆点 条纹 卡通
XXL S L

对照两组数据,其实我们将数据一进行了旋转,从而得到了数据二,用数学名词表示即是 矩阵转置,具体是怎样可以百度百科,点我查看。只要搜搜 js数组矩阵转置 等关键词则可以找到相关的代码。

转置计算

const rows = [
  { name: '颜色', values: ['黑色', '白色', '红色', '粉色', '紫色'] },
  { name: '图案', values: ['圆点', '条纹', '卡通'] },
  { name: '尺码', values: ['XXL', 'XL', 'L', 'M', 'S'] },
]
const skus = [
  '1[@0-0](/user/0-0)#1-0#2-0',
  '1@0-1#1-0#2-0',
  '1@0-2#1-0#2-0',
  '1@0-3#1-0#2-0',
  '1@0-4#1-0#2-0',
  '1[@0-0](/user/0-0)#1-1#2-0',
  '1@0-1#1-1#2-1',
  '1@0-2#1-1#2-2',
  '1@0-3#1-1#2-3',
  '1@0-4#1-1#2-0',
  '1[@0-0](/user/0-0)#1-2#2-0',
  '1@0-1#1-2#2-0',
  '1@0-2#1-2#2-0',
  '1@0-3#1-2#2-0',
  '1@0-4#1-2#2-2',
  '1[@0-0](/user/0-0)#1-0#2-0',
  '1@0-1#1-0#2-1',
  '1@0-2#1-0#2-1',
  '1@0-3#1-2#2-4',
  '1@0-2#1-1#2-4',
  '1@0-3#1-0#2-4',
  '1@0-4#1-0#2-3',
  '1@0-4#1-2#2-0',
]

const sku_list = skus.map((v) => {
  const codes = v.split('@')[1].split('#')
  const specs = codes.map((c) => {
    const key = c.split('-')[0]
    const value = c.split('-')[1]
    return {
      id: c,
      key: rows[key].name,
      value: rows[key].values[value],
    }
  })
  return { code: v, specs }
})

const EStatus = {
  PENDING : 'pending',
  DISABLED : 'disabled',
  SELECTED : 'selected',
}

const specs = sku_list.map(v => v.specs)

const _isRepeat = (list, c, cell) => {
    return list[c].cells.some((v) => v.id === cell.id)
}

const _transpose = (specs) => {
    const result = []
    for (let c = 0; c < specs[0].length; c++) {
      result[c] = { key: '', cells: [] }
      for (let i = 0; i < specs.length; i++) {
        // 去重
        const cell = specs[i][c]
        if (!_isRepeat(result, c, cell)) {
          result[c].key = cell.key
          result[c].cells.push({
            id: cell.id,
            status: EStatus.PENDING,
            value: cell.value,
          })
        }
      }
    }
    return result
}

const fences = _transpose(specs)
console.log('数组转置')
console.log(JSON.stringify(fences))

复制代码运行查看结果

[{"key":"颜色","cells":[{"id":"0-0","status":"pending","value":"黑色"},{"id":"0-1","status":"pending","value":"白色"},{"id":"0-2","status":"pending","value":"红色"},{"id":"0-3","status":"pending","value":"粉色"},{"id":"0-4","status":"pending","value":"紫色"}]},{"key":"图案","cells":[{"id":"1-0","status":"pending","value":"圆点"},{"id":"1-1","status":"pending","value":"条纹"},{"id":"1-2","status":"pending","value":"卡通"}]},{"key":"尺码","cells":[{"id":"2-0","status":"pending","value":"XXL"},{"id":"2-1","status":"pending","value":"XL"},{"id":"2-2","status":"pending","value":"L"},{"id":"2-3","status":"pending","value":"M"},{"id":"2-4","status":"pending","value":"S"}]}]

渲染视图

<view class="demo">
	<view
		wx:for="{{ skus }}"
		wx:key="item"
		mark:y="{{ index }}"
		class="rows"
	>
		<view class="key">{{ item.key }}</view>
		<view class="columns">
			<view
				wx:for="{{ item.cells }}"
				wx:key="item"
				mark:x="{{ index }}"
				mark:status="{{ item.status }}"
				class="cell {{ item.status }}"
				bind:tap="change"
			>{{ item.value }}</view>
		</view>
	</view>
</view>

如何获取可视规格

当我们将所有的sku规格进行数据转换之后,还需要将sku的所有组合计算出来,通过拆分 code 可以得到sku组合的信息,通过 组合 算法得到所有的可视规格,即视图所有可以点的规格路径,具体百度百科了解,点我

组合计算

const codes = sku_list.map(v => v.code)
const _combination = (arr, symbol = '#') => {
    let result = []
    let s = []
    for (let i = 0; i < arr.length; i++) {
      s.push(arr[i])
      for (let j = 0; j < result.length; j++) {
        s.push(result[j] + symbol + arr[i])
      }
      result = [...s]
    }
    return result
}

const paths = []
codes.map(v => {
    paths.push(..._combination(v.split('@')[1].split('#')))
})
console.log('数组组合')
console.log(JSON.stringify(paths))

运算结果

["0-0","1-0","0-0#1-0","2-0","0-0#2-0","1-0#2-0","0-0#1-0#2-0","0-1","1-0","0-1#1-0","2-0","0-1#2-0","1-0#2-0","0-1#1-0#2-0","0-2","1-0","0-2#1-0","2-0","0-2#2-0","1-0#2-0","0-2#1-0#2-0","0-3","1-0","0-3#1-0","2-0","0-3#2-0","1-0#2-0","0-3#1-0#2-0","0-4","1-0","0-4#1-0","2-0","0-4#2-0","1-0#2-0","0-4#1-0#2-0","0-0","1-1","0-0#1-1","2-0","0-0#2-0","1-1#2-0","0-0#1-1#2-0","0-1","1-1","0-1#1-1","2-1","0-1#2-1","1-1#2-1","0-1#1-1#2-1","0-2","1-1","0-2#1-1","2-2","0-2#2-2","1-1#2-2","0-2#1-1#2-2","0-3","1-1","0-3#1-1","2-3","0-3#2-3","1-1#2-3","0-3#1-1#2-3","0-4","1-1","0-4#1-1","2-0","0-4#2-0","1-1#2-0","0-4#1-1#2-0","0-0","1-2","0-0#1-2","2-0","0-0#2-0","1-2#2-0","0-0#1-2#2-0","0-1","1-2","0-1#1-2","2-0","0-1#2-0","1-2#2-0","0-1#1-2#2-0","0-2","1-2","0-2#1-2","2-0","0-2#2-0","1-2#2-0","0-2#1-2#2-0","0-3","1-2","0-3#1-2","2-0","0-3#2-0","1-2#2-0","0-3#1-2#2-0","0-4","1-2","0-4#1-2","2-2","0-4#2-2","1-2#2-2","0-4#1-2#2-2","0-0","1-0","0-0#1-0","2-0","0-0#2-0","1-0#2-0","0-0#1-0#2-0","0-1","1-0","0-1#1-0","2-1","0-1#2-1","1-0#2-1","0-1#1-0#2-1","0-2","1-0","0-2#1-0","2-1","0-2#2-1","1-0#2-1","0-2#1-0#2-1","0-3","1-2","0-3#1-2","2-4","0-3#2-4","1-2#2-4","0-3#1-2#2-4","0-2","1-1","0-2#1-1","2-4","0-2#2-4","1-1#2-4","0-2#1-1#2-4","0-3","1-0","0-3#1-0","2-4","0-3#2-4","1-0#2-4","0-3#1-0#2-4","0-4","1-0","0-4#1-0","2-3","0-4#2-3","1-0#2-3","0-4#1-0#2-3","0-4","1-2","0-4#1-2","2-0","0-4#2-0","1-2#2-0","0-4#1-2#2-0"]

修改规格状态

当点击规格列表里任意一个时,点击的需要显示激活状态,无规格的需要显示禁用状态。改变自身的状态很容易,要改变其他规格的状态就有点复杂了,需要通过多次循环遍历计算当前点击的可视规格,将不存在可视规格里的规格全部修改成禁用状态,语言组织起来比较难以理解,过程即是通过行号、列号找到对应规格,然后通过组合计算可视规格,通过对比以后就知道该显示的状态是什么了。

修改事件

// index.js
import { sku_list } from '../mocks/demo.mock'
import Sku, { IFence } from './sku'

Page({
  data: {
    sku: {} as Sku,
    skus: [] as IFence[],
  },
  onLoad() {
    const sku = new Sku(sku_list)
    this.data.sku = sku
    this.setData({
      skus: sku.fences,
    })
  },
  change({ mark }) {
    const { sku } = this.data
    sku.change(mark)
    this.setData({
      skus: sku.fences,
    })
  },
})
const selected = []
const change = ({ x, y, status }) => {
	if (status === EStatus.DISABLED) return
	// 改变点击的cell
	_changeCurrentCellStatus(x, y, status)
	// 改变其他cell
	fences.forEach((v, y) => {
		v.cells.forEach((cell, x) => {
			_changeOtherCellStatus(cell, x, y)
		})
	})
}

修改自身状态

const _setCellStatus = (x, y, status) => {
  fences[y].cells[x].status = status
}
const _changeCurrentCellStatus = (x, y, status) => {
	const cell = fences[y].cells[x]
	// 选择
	if (status === EStatus.PENDING) {
			selected[y] = cell
			_setCellStatus(x, y, EStatus.SELECTED)
	}
	// 反选
	else if (status === EStatus.SELECTED) {
		selected[y] = null
		_setCellStatus(x, y, EStatus.PENDING)
	}
}

修改无规格状态

const _changeOtherCellStatus = (cell, x, y) => {
	const path = _generatePath(cell, y)
	if (!path) return
	// 判断是否存在
	if (paths.includes(path)) {
		_setCellStatus(x, y, EStatus.PENDING)
	} else {
		_setCellStatus(x, y, EStatus.DISABLED)
	}
}

const _generatePath = (cell, y) => {
	const path = []
	for (let index = 0; index < fences.length; index++) {
		if (index === y) {
			if (isSelected(y, cell)) {
					return
			}
			path.push(cell.id)
		} else {
			const cell = selected[index]
			if (cell) {
					path.push(selected.id)
			}
		}
	}
	return path.join('#')
}

const isSelected = (index, cell) => {
	const value = selected[index]
	if (!value) {
			return false
	}
	return value.id === cell.id
}

模拟点击规格

change({x: 0, y: 0, status: EStatus.PENDING})
change({x: 0, y: 1, status: EStatus.PENDING})
change({x: 0, y: 2, status: EStatus.PENDING})
console.log('点击规格')
console.log(selected)

已选择sku的信息

[ { id: '0-0', status: 'selected', value: '黑色' },
  { id: '1-0', status: 'selected', value: '圆点' },
  { id: '2-0', status: 'selected', value: 'XXL' } ]

总结

这篇文章主要分享多规格数据的转换,以及通过 code 码来获取所有的可视规格,通过行列号获取当前点击的规格以及当前点击的可视规格,比较绕口。查看在线代码示例,直接运行查看结果,也可查看代码片段直接体验demo。后续将继续分享多规格sku的联动,价格、图片、库存等同步更新。由于代码片段包体积有限制,项目如果报ts错误,执行 npm i 或者 yarn add,将小程序的声明依赖添加就行了。

1 回复

大佬,教我写代码

回到顶部