# 利用element组件库实现sku规格配置及展示

话不多说,先上一波示例。

规格配置(SKU)
+ 配置新的规格项目
暂无数据

本示例中可配置是否上传规格图片,预设规格选项(规格名、规格值),新增规格选项(规格名、规格值),图片裁剪,批量配置,表单校验

<template>
  <item-box>
    <template v-slot:title>
      规格配置(SKU)
    </template>
    <template v-slot:content>
      <el-form :model="skuForm" :rules="skuFormRules" ref="skuForm" label-width="140px">
        <el-form-item label="测试编辑功能:" prop="testEdit">
          <el-switch v-model="skuForm.testEdit" @change="handleEditChange" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
        </el-form-item>
        <el-form-item label="商品规格:" prop="skuItem">
          <sku-item ref="skuItem" :editSkuInfo="skuItemData.skuInfo" :config="skuItemConfig" :skuList="skuList" @updateSkuInfo="updateSkuInfo" @deleteSkuValue="deleteSkuValue" @deleteSkuName="deleteSkuName" @addSkuValue="addSkuValue" @addSkuName="addSkuName"></sku-item>
        </el-form-item>
        <el-form-item label="上传规格图片:" prop="isUploadImg">
          <el-switch v-model="skuForm.isUploadImg" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
        </el-form-item>
        <el-form-item label="规格明细:" prop="skuTable">
          <sku-table ref="skuTableRef" :list="skuItemData.skuTableList" :uploadSkuImg="skuForm.isUploadImg" :otherTableHeader="otherTableHeader" :skuList="descartesData"></sku-table>
        </el-form-item>
        <el-form-item label="" prop="">
          <div class="btn-box" style="text-align: right; margin-top: 40px;">
            <el-button  type="primary" @click="testSubmit">测试提交表单校验</el-button>
          </div>
        </el-form-item>
      </el-form>
    </template>
  </item-box>
</template>

<script>
import ItemBox from "./components/item-box.vue"
import SkuItem from "./components/sku-item.vue"
import SkuTable from "./components/sku-table.vue"
export default {
  name: "el-table-sku",
  components: {
    ItemBox,
    SkuItem,
    SkuTable
  },
  data() {
    return {
      skuItemConfig: {
        canDelSkuName: true, //是否可以删除规格名
        canDelSkuValue: true, //是否可以删除规格值
        canAddSkuName: true, //是否可以添加规格名
        canAddSkuValue: true //是否可以添加规格值
      },
      skuList: [
        {
          specNameId: "100",
          specName: "颜色",
          valueList: [
            {
              specNameId: "100",
              specName: "颜色",
              specValue: "天空蓝",
              specValueId: "101"
            },
            {
              specNameId: "100",
              specName: "颜色",
              specValue: "草莓红",
              specValueId: "102"
            },
            {
              specNameId: "100",
              specName: "颜色",
              specValue: "深夜黑",
              specValueId: "103"
            }
          ]
        },
        {
          specNameId: "200",
          specName: "内存",
          valueList: [
            {
              specNameId: "200",
              specName: "内存",
              specValue: "128G",
              specValueId: "201"
            },
            {
              specNameId: "200",
              specName: "内存",
              specValue: "256G",
              specValueId: "202"
            },
            {
              specNameId: "200",
              specName: "内存",
              specValue: "512G",
              specValueId: "203"
            }
          ]
        }
      ], //可选规格数据列表
      addSkuImg: false,
      productSkuInfo: [],
      skuForm: {
        testEdit: false, //测试编辑
        skuItem: null, //规格
        isUploadImg: false, //是否上传图片
        skuTable: null //规格表格
      },
      skuFormRules: {},

      descartesData: [], //根据规格返回的笛卡尔数组
      otherTableHeader: {
        price: "价格"
      },

      skuItemData: { //编辑时的数据
        skuInfo: [], //规格值数据
        skuTableList: [] //表格数据
      }
    }
  },
  methods: {
    handleEditChange(val) {
      if (val) {
        this.skuForm.isUploadImg = true
        this.skuItemData = { //编辑时的数据
          skuInfo: [
            {
              specNameId: "200",
              specName: "内存",
              valueList: [
                {
                  specNameId: "200",
                  specName: "内存",
                  specValue: "256G",
                  specValueId: "202"
                },
                {
                  specNameId: "200",
                  specName: "内存",
                  specValue: "128G",
                  specValueId: "201"
                }
              ]
            },
            {
              specName: "颜色",
              specNameId: "100",
              valueList: [
                {
                  specNameId: "100",
                  specName: "颜色",
                  specValue: "深夜黑",
                  specValueId: "103"
                },
                {
                  specName: "颜色",
                  specNameId: "100",
                  specValue: "天空蓝",
                  specValueId: "101"
                }
              ]
            }

          ],
          skuTableList: [
            {
              price: "1",
              skuImg: require("../../public/imgs/css/1.png"),
              syx100: "深夜黑",
              syx200: "256G"
            },
            {
              price: "2",
              skuImg: require("../../public/imgs/css/2.png"),
              syx100: "天空蓝",
              syx200: "256G"
            },
            {
              price: "3",
              skuImg: require("../../public/imgs/css/3.png"),
              syx100: "深夜黑",
              syx200: "128G"
            },
            {
              price: "4",
              skuImg: require("../../public/imgs/css/4.png"),
              syx100: "天空蓝",
              syx200: "128G"
            }
          ]
        }
      } else {
        this.skuItemData = {
          skuInfo: [], //规格值数据
          skuTableList: [] //表格数据
        }
      }
    },
    /**
     * @description: 删除规格值项
     * @param {*} data
     * @return {*}
     * @author: syx
     */
    deleteSkuValue(data) {
      const arr = []
      for (let i = 0; i < this.skuList.length; i++) {
        const item = this.skuList[i]
        //找到规格名
        if (item.specNameId === data.specNameId) {
          item.valueList = item.valueList.filter(valueItem => {
            return valueItem.specValueId !== data.specValueId
          })
        }
        arr.push(item)
      }
      this.skuList = arr

      //清除已选规格中是此规格值的数据
      this.$refs.skuItem.filterSkuValue(data)
    },
    /**
     * @description: 删除规格名
     * @param {*} data 规格名对象
     * @return {*}
     * @author: syx
     */
    deleteSkuName(data) {
      //删除该规格项目
      this.skuList = this.skuList.filter(skuItem => {
        return skuItem.specNameId !== data.specNameId
      })

      //清除已选规格中是此规格名的数据
      this.$refs.skuItem.filterSkuName(data)
    },
    /**
     * @description: 新增规格值
     * @param {*} item = {skuNameItem 规格名对象, specValue 规格值, index 索引}
     * @return {*}
     * @author: syx
     */
    addSkuValue(item) {
      this.$message.success("新增规格值成功")
      const data = {
        specName: item.skuNameItem.specName,
        specNameId: item.skuNameItem.specNameId,
        specValue: item.specValue,
        specValueId: "" + Math.floor((Math.random() * 100000000))
      }

      const specNameItem = this.skuList.find(itemSku => {
        return itemSku.specNameId === item.skuNameItem.specNameId
      })
      specNameItem.valueList.push(data)
      this.$refs.skuItem.setSkuValueItem(item.skuNameItem.valueList, item.index, data)
    },
    /**
     * @description: 添加规格项
     * @param {*} item = { specName规格名, index操作的规格名的索引}
     * @return {*}
     * @author: syx
     */
    addSkuName(item) {
      this.$message.success("新增规格名成功")
      const data = {
        valueList: [],
        specName: item.specName,
        specNameId: "" + Math.floor((Math.random() * 100000000))
      }
      this.skuList.push(data)
      this.$refs.skuItem.setSkuNameItem(item.index, data)
    },
    /**
     * @description: 更改规格事件
     * @param {*} val
     * @return {*}
     * @author: syx
     */
    updateSkuInfo(val) {
      //获取笛卡尔积的数据列表
      let dikaerParams = val.map(item => {
        return item.valueList
      })
      this.descartesData = this.descartes(dikaerParams)
    },

    /**
     * @description: 计算笛卡尔积的 方法
     * @param {*} arr
     * @return {*}
     * @author: syx
     */
    descartes(arr) {
      let array = arr.filter(item => {
        return item.length > 0
      })
      if (!array || !array.length) {
        return []
      }
      if (array.length === 1) {
        return array[0].map(item => {
          return [item]
        })
      }
      let result = [].reduce.call(array, function(col, set) {
        var res = [];
        col.forEach(function(c) {
          set.forEach(function(s) {
            var t = [].concat(Array.isArray(c) ? c : [c]);
            t.push(s);
            res.push(t);
          })
        });
        return res;
      })
      return result
    },
    /**
     * @description: 测试表单提交
     * @param {*}
     * @return {*}
     * @author: syx
     */    
    testSubmit() {
      if (!this.$refs.skuTableRef.tableData.length) {
        return this.$message.warning("请先配置规格信息")
      }
      if (this.$refs.skuTableRef.validateForm()) {
        this.$message.success("表单校验通过")
      }
    }
  }
}
</script>

<style>
</style>
显示代码

# 涉及技术点

图片裁剪点击跳转图片裁剪

el-select下拉选项新增删除按钮 点击前往

阻止回车事件弹开的弹框会默认确认的问题点击前往

合并表格列相同项点击前往

动态表单校验问题点击前往

排列组合使用笛卡尔积点击前往

# el-select下拉选项新增删除按钮

代码大致如下

            <el-select v-model="select" clearable ref="skuName" filterable placeholder="请选择规格名">
              <div class="select-list">
                <el-option label="选项1" :value="1"></el-option>
                <i class="del-sku-name el-icon-delete"></i>
              </div>
              <div class="select-list">
                <el-option label="选项2" :value="2"></el-option>
                <i class="del-sku-name el-icon-delete"></i>
              </div>
              <div class="select-list">
                <el-option label="选项3" :value="3"></el-option>
                <i class="del-sku-name el-icon-delete"></i>
              </div>
            </el-select>

首先设置好样式,由于选项列表是在body之外的,所以不能使用scoped 且为了避免影响全局样式,最好设置特殊的唯一的类名

<style lang="less">
.el-scrollbar {
  .select-list {
    width: 100%;
    line-height: 34px;
    position: relative;
    box-sizing: border-box;

    .del-sku-name {
      position: absolute;
      right: 12px;
      top: 50%;
      transform: translateY(-50%);
      cursor: pointer;
    }
  }
}
</style>

由于使用filterable,可对选项进行过滤,此时过滤只会过滤 option ,但是 上面的 i 不会被过滤。会出现下图的情况

全部没有查找到的时候

图片1

查找到部分数据,未隐藏查不到的数据的垃圾桶

图片2

因此最终less代码如下

<style lang="less">
.el-scrollbar {
  &.is-empty {
    display: none;
  }
  .select-list {
    width: 100%;
    line-height: 34px;
    position: relative;
    box-sizing: border-box;

    .del-sku-name {
      position: absolute;
      right: 12px;
      top: 50%;
      transform: translateY(-50%);
      cursor: pointer;
    }
    .el-select-dropdown__item[style="display: none;"] + .del-sku-name{
      display: none;
    }
  }
}
</style>

# 阻止回车事件弹开的弹框会默认确认的问题

当遇到回车键触发的 显示 elment的 confirm弹框时,可能会直接触发确认,导致用户无法点击取消该事件。解决方法如下

this.$confirm(`新增规格值【${specValue}】, 是否继续?`, '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning',
          beforeClose(action, instance, done) {
            if (action == 'confirm') {
              instance.$refs['confirm'].$el.onclick = a()
              function a(e) {
                e = e || window.event;
                if (e.detail != 0) {
                  done()
                }
              }
            } else {
              done()
            }
          }
        }).then(() => {
         this.$message.info("您点击了确定")
        }).catch(() => {
          this.$message.info("您您点击了取消")
        })

# 合并表格列相同项

此功能展示大致如下

图片3

将列相同的值进行合并

关键属性就是 el-table 的span-method 属性

由于此处只对规格值进行合并,因此只需计算规格值需要合并的数据即可。

   <el-table :data="tableData":span-method="objectSpan"></el-table> 
	// 合并行数
    objectSpan({ row, column, rowIndex, columnIndex }) {
      //如果需要上传规格图片  则从 第二列开始 否则从第一列开始   然后进行规格列的长度 未经处理的默认为 rowspan: 1, colspan: 1
      if ((columnIndex < (this.uploadSkuImg ? (Object.keys(this.skuTableHeader).length + 1) : Object.keys(this.skuTableHeader).length)) && (columnIndex >= (this.uploadSkuImg ? 1 : 0))) {
        const _row = this.uploadSkuImg ? this.spanArr[rowIndex][columnIndex - 1] : this.spanArr[rowIndex][columnIndex]
        const _col = _row > 0 ? 1 : 0
        return {
          rowspan: _row,
          colspan: _col
        }
      }
    },
    //计算位置的方法 来得到 spanArr 数列
    getSpanArr(val) {
      const data = JSON.parse(JSON.stringify(val))
      let posObj = {}
      //遍历表格行数
      for (var i = 0; i < data.length; i++) {
        let j = 0
        //存储要合并的数据
        this.spanArr[i] = []
        //遍历规格列数
        Object.keys(this.skuTableHeader).forEach(key => {
          //如果是第一行 先定义个 占用 1格
          if (i === 0) {
            this.spanArr[i][j] = 1
            posObj[key] = 0 //标记位置为 第一行
          } else {
            // 判断当前元素与上一个元素是否相同 如果相同,此行的span设置为占用0格  上一行的占用格数 +1
            if (data[i][key] === data[i - 1][key]) {
              this.spanArr[posObj[key]][j] += 1 //将第一个出现该值的 格子+1个占用格
              this.spanArr[i][j] = 0
            } else {
              this.spanArr[i][j] = 1
              posObj[key] = i //没出现一样的,位置重新标记
            }
          }
          j++
        })
      }
    }

先计算位置 getSpanArr,比如想要得到如下所示的结果

图片4

想要得到上面的显示效果,需要获得数据 spanArr 为 如下数据

spanArr: [
	[2, 1],
	[0, 1],
	[2, 1],
	[0, 1]
]

最外层的数据的长度表示有几行

第一行的 [2, 1] 表示 第一行的第一格子占用 2个高度的格子,第一行的第二个格子占用1个高度的格子。

第二行的 [0, 1] 表示 第二行的第一格子占用 0个高度的格子(因为已经被第一行给占了,如果非0会导致表格错位),第二行的第二个格子占用1个高度的格子。

以此类推

上述方法就是为了获取 spanArr的方法,然后又由于涉及是否上传规格图片,所以得判断。

# 正常对所有表格列都进行相同项合并的话代码如下

	// 合并行数
    objectSpan({ row, column, rowIndex, columnIndex }) {
        const _row = this.spanArr[rowIndex][columnIndex]
        const _col = _row > 0 ? 1 : 0
        return {
          rowspan: _row,
          colspan: _col
        }
    },
    //计算位置的方法 来得到 spanArr 数列   val 为表格数据
    getSpanArr(val) {
      const data = JSON.parse(JSON.stringify(val))
      let posObj = {}
      //遍历表格行数
      for (var i = 0; i < data.length; i++) {
        let j = 0
        //存储要合并的数据
        this.spanArr[i] = []
        //遍历规格列数
        Object.keys(data[i]).forEach(key => {
          //如果是第一行 先定义个 占用 1格
          if (i === 0) {
            this.spanArr[i][j] = 1
            posObj[key] = 0 //标记位置为 第一行
          } else {
            // 判断当前元素与上一个元素是否相同 如果相同,此行的span设置为占用0格  上一行的占用格数 +1
            if (data[i][key] === data[i - 1][key]) {
              this.spanArr[posObj[key]][j] += 1 //将第一个出现该值的 格子+1个占用格
              this.spanArr[i][j] = 0
            } else {
              this.spanArr[i][j] = 1
              posObj[key] = i //没出现一样的,位置重新标记
            }
          }
          j++
        })
      }
    }

本示例为使用上述代码对表格相邻列的值一样进行合并

<template>
  <el-table :data="tableData" :span-method="objectSpan" style="width: 100%">
    <el-table-column prop="date" label="日期" width="180">
    </el-table-column>
    <el-table-column prop="name" label="姓名" width="180">
    </el-table-column>
    <el-table-column prop="address" label="地址">
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  name: "el-table-span-method",
  data() {
    return {
      spanArr: [],
      tableData: [
        {
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄'
        }, {
          date: '2016-05-02',
          name: '王小难',
          address: '上海市普陀区金沙江路 1519 弄'
        }, {
          date: '2016-05-01',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1519 弄'
        }, {
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1516 弄'
        }
      ],
    }
  },
  created() {
    this.getSpanArr(this.tableData)
  },
  methods: {
    // 合并行数
    objectSpan({ row, column, rowIndex, columnIndex }) {
      const _row = this.spanArr[rowIndex][columnIndex]
      const _col = _row > 0 ? 1 : 0
      return {
        rowspan: _row,
        colspan: _col
      }
    },
    //计算位置的方法 来得到 spanArr 数列   val 为表格数据
    getSpanArr(val) {
      const data = JSON.parse(JSON.stringify(val))
      let posObj = {}
      //遍历表格行数
      for (var i = 0; i < data.length; i++) {
        let j = 0
        //存储要合并的数据
        this.spanArr[i] = []
        //遍历规格列数
        Object.keys(data[i]).forEach(key => {
          //如果是第一行 先定义个 占用 1格
          if (i === 0) {
            this.spanArr[i][j] = 1
            posObj[key] = 0 //标记位置为 第一行
          } else {
            // 判断当前元素与上一个元素是否相同 如果相同,此行的span设置为占用0格  上一行的占用格数 +1
            if (data[i][key] === data[i - 1][key]) {
              this.spanArr[posObj[key]][j] += 1 //将第一个出现该值的 格子+1个占用格
              this.spanArr[i][j] = 0
            } else {
              this.spanArr[i][j] = 1
              posObj[key] = i //没出现一样的,位置重新标记
            }
          }
          j++
        })
      }
    }
  }
}
</script>

<style>
</style>
显示代码

# 动态表单校验问题

此处配置了最简单的规格设置,只设置了一个价格配置项。价格就涉及到校验填写内容是否正确,就需要做到校验,按以往form添加rule对此处已不再适用,研究许久,可自己判断并设置校验错误信息。

 <el-form ref="form" :model="skuTableForm" size="small">
      <el-table :data="tableData" :key="tablehead.length" :span-method="objectSpan">
        <el-table-column width="160px" align="center"  v-if="showUploadImg" :prop="format.imgField" :label="format.imgName">
          <template slot-scope="scope">
              <el-form-item :error="scope.row['error' + key]">
                <el-input v-model="scope.row[key]" clearable @blur="tableValueBlur(scope.row, key)" @input="tableValueChange(scope.row, key, item)" :placeholder="'请输入' + item"></el-input>
              </el-form-item>
          </template>
        </el-table-column>
      </el-table>
 </el-form>
 /**
     * @description: 触发表单校验
     * @param {*} row
     * @param {*} key
     * @return {*}
     * @author: syx
     */
    tableValueChange(row, key, item) {
      if (!row[key]) {
        row['error' + key] = item + '不能为空'
        return
      }
      if (!(/^\d+(\.\d+)?$/).test(row[key])) {
        row['error' + key] = '请输入正确格式的' + item
        return
      }
      row['error' + key] = ''
    },
    /**
     * @description: 失去焦点事件
     * @param {*} row
     * @param {*} key
     * @return {*}
     * @author: syx
     */
    tableValueBlur(row, key) {
      if (!row['error' + key]) {
        row[key] = Number(row[key]).toFixed(2)
      }
    },

这样就会当输入框输入的时候触发校验并会及时显示错误信息。

WARNING

缺点:validate方法不能触发校验,需要手工触发校验。

# 排列组合使用笛卡尔积

填写规格名,就会排列组合成多种多样的商品,于是需要用到计算笛卡尔积的方法,具体方法如下

    /**
     * @description: 计算笛卡尔积的 方法
     * @param {*} arr
     * @return {*}
     * @author: syx
     */
    descartes(arr) {
      let array = arr.filter(item => {
        return item.length > 0
      })
      if (!array || !array.length) {
        return []
      }
      if (array.length === 1) {
        return array[0].map(item => {
          return [item]
        })
      }
      let result = [].reduce.call(array, function(col, set) {
        var res = [];
        col.forEach(function(c) {
          set.forEach(function(s) {
            var t = [].concat(Array.isArray(c) ? c : [c]);
            t.push(s);
            res.push(t);
          })
        });
        return res;
      })
      return result
    },

通过笛卡尔列表制作表格数据

/**
     * @description: 获取笛卡尔列表数据
     * @param {*}
     * @return {*}
     * @author: syx
     */
    descartesArray() {
      let descartesTable = []
      for (let i = 0; i < this.skuList.length; i++) {
        const item = this.skuList[i]
        const obj = {}
        for (let j = 0; j < item.length; j++) {
          const childItem = item[j]
          //解决id存数字的话  对象会被重新排序
          obj["syx" + childItem[this.format.specNameId]] = childItem[this.format.specValue]
        }
        descartesTable.push(obj)
      }
      return descartesTable
    }

# 总结

一个规格列表,耗掉半管血。difficult,关注微信公众号【爆米花小布】,不用再造轮子啦。如有bug,也可通过公众号进行反馈。 更加严谨的规格代码实现可阅读此篇文章 el-table上下行单元格合并,实现好看的SKU表格 (opens new window)