前言
在商城里产品的spu、sku展示是很重要的一部分,常见的商城一般没有sku的概念,会把一个多规格的sku拆分成多个spu从而让用户选择。这样是最简单的做法,但是需求是真的跟不上,一般boss都会要求做多规格的sku选择。下面分享一个多规格sku实现的思路以及过程。
效果图
数据分析
要实现sku首先要知道是什么数据组合成的sku列表,下面大概说说我自己sku数据格式。
{
"code": "1@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#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#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#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#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
,将小程序的声明依赖添加就行了。