商品规格选择sku --- Vue版

2019/05/07 JavaScript 共 5293 字,约 16 分钟

朋友问我像淘宝那样的选择商品的规格,如商品的颜色,尺寸,款式,并且还要根据后端传来的库存,哪些还有,哪些没了。咋一看不复杂,然后后面被一步一步的套牢,出不来了。最终借助 Google 的力量,完成了对这个类型的需求,有个初步的了解,并输出 blog 以记录。

什么是 sku

  • Stock Keeping Unit(库存量单位)
  • 定义有很多,在电商上来看,我的理解是,一件商品的基础属性组成的单品,如一件衣服的基础属性有颜色,款式,尺寸三个属性,那么这三个属性合一就组成了一个单品 sku,不同的颜色、款式、尺寸,就构成了不同的单品。

OK,对于第一次接触的同学来说,让我们从开始了解商品规则和其选择吧~

我是从数据存储到前端展示这样一个流程,开始分享的。(如果对后端感兴趣的话可以看前两段

在数据库中的存储

在搜索中看到不少设计,太过复杂的方式我还没看懂,不过还是看到一个分了几个表建立的,感觉容易理解些。

建表:

  • 商品表:主键是商品 ID、自增,还有商品的 name 字段。
  • 属性表:一件商品都有哪些属性,主键是属性 ID、自增,还要有属性的 name 字段。同时可以和商品表关联
  • 属性值表:一个属性都有哪些取值,主键是属性值 ID、自增,还要有属性值的 name 字段。同时和属性表关联
  • sku 表:由上面 3 个表可以构成 sku 表,主键是 sku_id、自增,可以有 sku_value,sku_sku_code,stock,price 字段。

后端返回数据

返回数据的话,比如针对某个商品返回它的 sku,那么理想的是返回三个字段,一个是属性对象,一个是属性值对象,一个这个商品的 sku 对象。

不过我示例中的返回数据,不是这样,所以要进一步处理成前端需要的格式。

思路部分

  • 首先,将商品的属性和属性值,展示出来,比如颜色下有蓝色、红色,款式下有经典款和流行款,尺寸有 M 和 L。
  • 当选择一个属性后,需要判断和其他属性构成的 sku,库存是否为空,为空的话是要置灰或者提示没有库存了。
  • 选择用对象存储不同的 sku 的库存,性能是否好点。
  • 对已选择的属性,进行其他属性循环填充,如果没库存,就可以置灰其他属性。

处理数据+代码规划

朋友给的数据对象,有三个字段,分别是该商品的 shopSkuVo(sku 对象),shopMainSpecVo(属性对象),shopChildVoList(属性值对象)

  • 那么,首先我要将属性对象和属性值对象,取出来,得到一个规格对象,如:
let mainSpec = [
    {
        id: 1,
        name: '颜色',
        attr: [
            {
                id: 1,
                name: '蓝色',
            },
            {
                id: 2,
                name: '白色',
            },
        ],
    },
    {
        id: 2,
        name: '尺寸',
        attr: [
            {
                id: 3,
                name: 'M',
            },
            {
                id: 4,
                name: 'L',
            },
        ],
    },
];
  • 得到这样的规格字段后,就可以借助 Vue 中的 v-for,进行 dom 渲染了。
  • 考虑到用对象来存储 sku 的数据,这里我根据数据的特色,使用 sku_value 来做对象的 key,对应的值就是这条 sku 数据。如:
let skuObj = {
    '红色/M/经典款': {
        id: 1, // sku_id
        sku_value: '红色/M/经典款',
        stock: 20,
        price: '33.0',
    },
    '红色/M/流行款': {
        id: 4, // sku_id
        sku_value: '红色/M/经典款',
        stock: 330,
        price: '53.0',
    },
};
  • 核心思路,每次用户点击一个属性后,将该属性和其他点击过的属性,以及还没点击的属性,组合起来,检查库存数量,进而判断还没点击的属性,是否置灰显示。

代码实现

后端数据格式展示:(基于篇幅,我只展示了每种的前两条数据)

let data = {
    shopSkuVo: [
        {
            skuId: '38',
            goodsId: null,
            specNames: 'S/红/乞丐款',
            specIds: null,
            skuNo: null,
            skuImg: 'C30000/1/img/20190322/37ff02207d0349489a90a3f7d4ff578d',
            skuPrice: null,
            stock: 15,
            enabled: null,
            goodSkuSpecDetails: null,
            classifyList: null,
        },
        {
            skuId: '39',
            goodsId: null,
            specNames: 'S/红/爆炸款',
            specIds: null,
            skuNo: null,
            skuImg: 'C30000/1/img/20190322/37ff02207d0349489a90a3f7d4ff578d',
            skuPrice: null,
            stock: 14,
            enabled: null,
            goodSkuSpecDetails: null,
            classifyList: null,
        },
    ],
    shopMainSpecVo: [
        {
            skuId: '38',
            mainSpecId: '1',
            mainSpecMerchantId: null,
            mainSpecName: '尺寸',
            mainSpecSort: null,
            mainSpecPid: '0',
            goodSkuSpecDetailsID: '6742da9e002b496e8058c8cebaf292f2',
        },
        {
            skuId: '38',
            mainSpecId: '12',
            mainSpecMerchantId: null,
            mainSpecName: '颜色',
            mainSpecSort: null,
            mainSpecPid: '0',
            goodSkuSpecDetailsID: 'df0f08465d9f43cf8f951ed79743cea5',
        },
    ],
    shopChildVoList: [
        {
            skuId: '38',
            ChildId: '52',
            ChildMerchantId: null,
            ChildSpecName: 'S',
            ChildSpecPid: '1',
            goodSkuSpecDetailsID: '6742da9e002b496e8058c8cebaf292f2',
        },
        {
            skuId: '38',
            ChildId: '13',
            ChildMerchantId: null,
            ChildSpecName: '',
            ChildSpecPid: '12',
            goodSkuSpecDetailsID: 'df0f08465d9f43cf8f951ed79743cea5',
        },
    ],
};

Vue 实例绑定的 template 模板:

<div v-for="(shopMain, rowIndex) in mainSpec" :key="rowIndex">
    <span></span>
    <ul class="item clearfix">
        <li
            v-for="(cItem, colIndex) in shopMain.item"
            @click="clickButton($event, cItem.name, rowIndex, colIndex)"
            :class="[cItem.enabled?'':'disabled',subIndex[rowIndex] == colIndex?'checked':'']"
        >
            
        </li>
    </ul>
</div>

Vue 实例中定义的 data 字段:

data() {
        return {
            shopSkuVo: data.shopSkuVo,   // 本地我将后端返回的数据放到一个js中了,用import引入。
            shopMainSpecVo: data.shopMainSpecVo,
            shopChildVoList: data.shopChildVoList,

            // sku 对象,
            skuObj: {},
            // 规则属性 数组对象
            mainSpec: [],

            // 选择的 属性 数组和其下标,用来进行stock判断和前端样式控制。
            selectItemArr: [],
            subIndex: [],
        }
    },
    ```

处理 sku 数据

  • 对属性对象和属性值对象进行数据处理,得到一个规则对象。对 sku 对象进行数据处理,得到 sku,商品 value 为 key 的 sku 对象。
created() {
        this.shopMainSpecVo = this.deduplication(this.shopMainSpecVo, 'mainSpecId')

        this.shopMainSpecVo.forEach(s => {
            let obj = {}
            obj.name = s.mainSpecName
            obj.item = []
            this.filterShopByKey(s.mainSpecId).forEach(n => {
                obj.item.push({name: n})
            })
            this.mainSpec.push(obj)
        })

        this.skuObj = this.getObjByKey(this.shopSkuVo, 'specNames')

        this.checkItem()
    },
methods: {
  deduplication(arr, key) {
            let obj = {}, result = []

            arr.forEach(a => {
                if (!obj.hasOwnProperty(a[key])) {
                    obj[a[key]] = a
                }
            })

            for (let k in obj) {
                result.push(obj[k])
            }

            return result
        },
  filterShopByKey(key) {
            return Array.from(new Set(this.shopChildVoList.filter(f => f.ChildSpecPid === key).map(m => m.ChildSpecName))).sort()
        },
  getObjByKey(arr, key) {
            let obj = {}
            arr.forEach(a => {
               obj[a[key]] = a
            })

            return obj
        },
  // 属性值按钮点击,控制点击元素高亮、同属性的其他属性值去高亮,并进行检查。
  clickButton(e, item, rowIndex, colIndex) {
            if (this.selectItemArr[rowIndex] !== item) {  // 如果点击当前属性中 未勾选的按钮,则取消该属性别的按钮勾选,且记录下来。
                this.selectItemArr[rowIndex] = item
                this.subIndex[rowIndex] = colIndex
            } else {  // 如果是点击当前属性中 已勾选的按钮,则取消勾选
                this.selectItemArr[rowIndex] = ''
                this.subIndex[rowIndex] = -1
            }

            this.checkItem()
        },
   checkItem() {
     				// 临时 存放 选择属性值的数组。
            let tmpSelectArr = []
      // 将已经勾选的属性,存入临时数组里。
            this.mainSpec.forEach((m, index) => {
                tmpSelectArr[index] = this.selectItemArr[index] ? this.selectItemArr[index] : ''
            })

// 对已勾选属性进行补充,即如果已经选了颜色和尺寸了,那么模拟用户选择款式,目前是暴力循环。在选择完后,调用isEnabled方法,判断补充的属性值是否可以点击。
            this.mainSpec.forEach((m, mIndex) => {
                let last = tmpSelectArr[mIndex]
                m.item.forEach((s, sIndex) => {
                    tmpSelectArr[mIndex] = s.name
                    s.enabled = this.isEnabled(tmpSelectArr)
                })
                tmpSelectArr[mIndex] = last
            })
            this.$forceUpdate() //重绘
        },

   // 判断属性组合之后,添加的属性是否可以点击
   isEnabled(arr) {
            for (let i in arr) {
                if (arr[i] === '') return true  // 如果添加的属性中,有空的,直接返回true。
            }

            return this.skuObj[arr.join('/')].stock === 0 ? false : true
        },
}

样式适配状态

  • template 的样式,其中一个 css 属性非常棒,是 pointer-events: none;,这个属性可以阻止当前元素的事件触发,再搭配 cursor 和 background-color,就可以实现不可点击的交互。
.item {
    display: inline-block;
}

.item li {
    float: left;
    padding: 5px 10px;
    margin-right: 5px;
    background-color: #c9e9ed;
    cursor: pointer;
}

.item li.checked {
    background-color: orange;
}

.item li.disabled {
    background-color: #cccccc;
    cursor: not-allowed;
    pointer-events: none;
}

文档信息

Search

    Table of Contents