vue sku后台配置

3,352 阅读6分钟

最终效果gif

本想录制一个视频的,但是好像不支持

文章的最后会给出一个完整的例子

也可以点击这里查看: jsrun.net/AAIKp

相关的数据结构

input输入框部分

点击 颜色, 尺寸, 重量 会生成 sku_arr数据结构

sku_arr: [
    { attr:"颜色", valueList:["黑","白"] },
    { attr:"尺寸", valueList:["大","中"] }
]

table表格头的数据

table_column: ["颜色","图片","尺寸","销售价格","市场价格","库存"]

table表格内的数据

这里直接以属性的名字作为对象的 key

table_data: [
	{ "颜色": "黑", "尺寸": "大", "销售价格": "", "市场价格": "", "库存": "", "图片": "" },
	{ "颜色": "黑", "尺寸": "中", "销售价格": "", "市场价格": "", "库存": "", "图片": "" },
	{ "颜色": "白", "尺寸": "大", "销售价格": "", "市场价格": "", "库存": "", "图片": "" },
	{ "颜色": "白", "尺寸": "中", "销售价格": "", "市场价格": "", "库存": "", "图片": "" },
]

需要做的事情

点击添加规格时生成 sku_arr, 就是input输入框那里

这个比较简单, 直接向数组 push 即可

  this.form.sku_arr.push({ attr: attr_name, valueList: ['黑',  '白'] }) 

值得一提的是, 在v-for="(item, index) in arr"循环 input 的时候, v-model要以 arr[index] 的方式赋值, 不能直接用 item 赋值

同时生成 table表头

表格头中除了属性,还有图片,价格,库存。

而且图片一直在第二列,行合并的规则与第一列的规则相同(下文会讲到)

用数组的 map 函数取到属性的名字,再进行数组合并

// 生成表头的方法
async generateTableColumn() {
    this.table_column = this.form.sku_arr.map(x => x.attr).concat( ['销售价格', '市场价格', '库存'])
    /*
    不写 `$nextTick`会有bug, 没想明白为啥, 大概是vue懒得更新dom吧
    bug复现方式: 删除`$nextTick`后,点击颜色,再点击尺寸,再删除颜色,观察el-table
    */
    await this.$nextTick()
    if (this.form.sku_arr.length != 0) this.table_column.splice(1, 0, '图片')
},

这里使用了 await this.$nextTick() 使用原因:连续点击两个属性后生成表格,这是我删除第一个属性(即删除第一列),删除后重新执行 generateTableColumn() 生成表头数据, 发现图片列没有按照我的预期插入到第二列,所以我在vue异步更新之后操作图片列

同时生成 表格内的数据

这里涉及到计算 笛卡尔积, (我感觉就是把所有排列的情况组合出来)

例如上面的数据

sku_arr: [
    { attr:"颜色", valueList:["黑","白"] },
    { attr:"尺寸", valueList:["大","中"] }
]
  • 需要得到 黑-大, 黑-中, 白-大, 白-中 四项,即表格内的四行数据

  • 如果加了一个 重量1kg2kg , 那应该得到 黑-大-1kg, 黑-大-2kg, 黑-中-1kg,黑-中-5kg,白-大-1kg,白-大-5kg, 白-中-1kg, 白-中-2kg 八项,即表格内的八行数据

笛卡尔积的计算(核心)

此实现方法较为混乱,废弃。 参见 回溯算法的实现 -- 2023年3月24日 09:54:00 补充

/* 重新实现笛卡尔积 传入的数组 '为空', '长度为1', '长度大于1' 三种情况 分别处理

入参数组格式: [
              { attr:"颜色", valueList:["黑","白"] },
              { attr:"尺寸", valueList:["大","中"] }
          ] 
    
    返回的数组格式:[
    {"颜色":"黑","尺寸":"大"},
    {"颜色":"黑","尺寸":"中"},
    {"颜色":"白","尺寸":"大"},
    {"颜色":"白","尺寸":"中"}
    ]
    */
generateBaseData(arr) {
    if (arr.length === 0) return []
    if (arr.length === 1) {
        let [item_spec] = arr
        return item_spec.valueList.map(x => {
            return {
                [item_spec.attr]: x
            }
        })
    }
    if (arr.length >= 1) {
        return arr.reduce((accumulator, spec_item) => {
            let acc_value_list = Array.isArray(accumulator.valueList) ? accumulator.valueList : accumulator
            let item_value_list = spec_item.valueList
            let result = []
            for (let i in acc_value_list) {
                for (let j in item_value_list) {
                    let temp_data = {}
                    // 如果是对象
                    if (acc_value_list[i].constructor === Object) {
                        temp_data = {
                            ...acc_value_list[i],
                            [spec_item.attr]: item_value_list[j]
                        }

                        // 否则如果是字符串
                    } else {
                        temp_data[accumulator.attr] = acc_value_list[i]
                        temp_data[spec_item.attr] = item_value_list[j]
                    }
                    result.push(temp_data)
                }
            }
            return result
        })
    }
}

最终的表格数据

用上边的笛卡尔积返回的数组进行map循环返回新数组即可

this.table_data = generateBaseData(arr).map(item => ({ ...item, '销售价格': '', '市场价格': '', '库存': '', '图片': '' }))

生成新数据的问题已经说完了,渲染页面就比较简单了,对 el-table-column 进行循环即可,注意一下key的使用 (当然还要判断列的属性对应不同的页面展示)

<el-table :span-method="spanMethod" border>
    <template v-for="item_column in table_column">
        <el-table-column :key="item_column" :prop="item_column" :label="item_column" />
    </template>
</el-table>

删除属性的操作

删除属性是比较简单的 只要删除 sku_arr 的数据后,在重新生成表头表格数据即可

删除属性值的操作

是不是也是删除 sku_arr 的数据后,在重新生成表头表格数据即可呢? 当然是可以的

但是但是但是..... 如果用户已经录入了一些商品信息,这么做会导致商品信息丢失,所以我没有这么做

// 删除属性值 四个参数:'一级数组索引', '二级数组索引', '属性名字', '属性值'
deleteAttrVal(idx, kdx, attr_name, attr_val) {
    this.form.sku_arr[idx].valueList.splice(kdx, 1)

    // 删除table行
    let data_length = this.form.table_data.length
    for (let i = 0; i < data_length; i++) {
        if (this.form.table_data[i][attr_name] == attr_val) {
            this.form.table_data.splice(i, 1)
            i = i - 1
            data_length = data_length - 1
        }
    }
}

方法的后两个参数是, 当前属性名字当前属性值, 利用数组的splice方法直接删除表格数据

编辑属性值的操作

同样,编辑属性值也不应该让用户数据丢失

newAttrValueBlur(curr_attr, newVal) {
    if (newVal === old_attr_value) return

    //  这里根据规格生成的笛卡尔积计算出table中需要改变的行索引 ( 包含新增和修改 )
    let cartesian_arr = this.generateBaseData(this.form.sku_arr)
    let change_idx_arr = [] // 需要被改变的行的索引
    for (let i in cartesian_arr) {
        if (cartesian_arr[i][curr_attr] === newVal) change_idx_arr.push(Number(i))
    }
    console.log('change_idx_arr', change_idx_arr)

    // 新的表格应该有的长度与现有的表格长度比较, 区分新增还是修改
    let length_arr = this.form.sku_arr.map(x => x.valueList.length)
    let new_table_length = length_arr.reduce((acc, curr_item) => acc * curr_item) // 新的表格数据长度 求乘积
    let old_table_length = this.form.table_data.length // 旧的表格数据长度

    // 如果是修改
    if (new_table_length === old_table_length) {
        this.form.table_data.forEach((item, index) => {
            if (change_idx_arr.includes(index)) this.form.table_data[index][curr_attr] = newVal
        })
        return
    }
    // 如果是新增
    if (new_table_length > old_table_length) {
        // 得到当前属性的当前值和其他规格的 sku_arr, 构造新的表格数据
        let other_sku_arr = this.form.sku_arr.map(item => {
            if (item.attr !== curr_attr) return item
            else return { attr: item.attr, valueList: [newVal] }
        })
        // 得到新增的表格数据
        let ready_map = this.generateBaseData(other_sku_arr)
        let new_table_data = this.mergeTableData(ready_map)
        change_idx_arr.forEach((item_idx, index) => {
            this.form.table_data.splice(item_idx, 0, new_table_data[index])
        })
    }
}

当属性值的输入框失去焦点的时候,执行保存操作

区分一下 修改旧的创建新的

  • 失去焦点后得到新的 sku_arr,重新计算笛卡尔积
  • 遍历新的笛卡尔积,根据失去焦点时的属性属性值 找到需要被改变的表格数据的索引,即 change_idx_arr (新增或者修改的索引都在这里存着)
  • 新的数组长度已有的表格数据数组长度比较, 相等则为修改,新的大于旧的则为新增

修改逻辑

比较简单,单纯的修改数组元素即可

// 如果是修改
if (new_table_length === old_table_length) {
    this.form.table_data.forEach((item, index) => {
        if (change_idx_arr.includes(index)) this.form.table_data[index][curr_attr] = newVal
    })
    return
}

新增逻辑

新增的时候要用新添加的属性值,和其他属性下的所有值生成新的笛卡尔积, 再遍历 change_idx_arr, 使用 splice 将数据插入到指定位置

// 如果是新增
if (new_table_length > old_table_length) {
    // 得到当前属性的当前值和其他规格的 sku_arr, 构造新的表格数据
    let other_sku_arr = this.form.sku_arr.map(item => {
        if (item.attr !== curr_attr) return item
        else return { attr: item.attr, valueList: [newVal] }
    })
    // 得到新增的表格数据
    let ready_map = this.generateBaseData(other_sku_arr)
    let new_table_data = this.mergeTableData(ready_map)
    change_idx_arr.forEach((item_idx, index) => {
        this.form.table_data.splice(item_idx, 0, new_table_data[index])
    })
}

无限合并行

至此,所有的表格数据相关的操作均已完成,接下来就是 el-table 的行合并操作

<el-table :data="table_data" :span-method="spanMethod" border>

// 合并行
spanMethod({ row, column, rowIndex, columnIndex }) {
    if (columnIndex == 0) {
        let key_0 = column.label
        let first_idx = this.form.table_data.findIndex(x => x[key_0] == row[key_0])
        const calcSameLength = () => this.form.table_data.filter(x => x[key_0] == row[key_0]).length
        first_column_rule = rowIndex == first_idx ? [calcSameLength(), 1] : [0, 0]
        return first_column_rule

        // 第二列的图片与第一列主规格使用相同合并规则 ( 恰好el-table的合并方法是横着走的 )
    } else if (columnIndex == 1) {
        return first_column_rule
        // 其他列
    } else {
        // 表格数据的每一项, 
        const callBacks = (table_item, start_idx = 0) => {
            if (columnIndex < start_idx) return true
            let curr_key = this.table_column[start_idx]
            return table_item[curr_key] === row[curr_key] && callBacks(table_item, ++start_idx)
        }
        let first_idx = this.form.table_data.findIndex(x => callBacks(x))
        const calcSameLength = () => this.form.table_data.filter(x => callBacks(x)).length
        return rowIndex == first_idx ? [calcSameLength(), 1] : [0, 0]
    }
}

这里有两个注意点

  • 由于业务需要,第二列的图片的合并规则要与第一列一样,恰好这个方法在表格中是横向一个单元格一个单元格走的,所以在处理第一列的时候,保存一下第一列的规则,当进行到第二列的时候使用第一列规则,

    不能存到 data

  • 接下来就是无限合并

有个大问题就是后边的合并依赖之前合并,直到第一列 (单凭借文字好像描述的不够清楚)

通过递归函数 callBacks 从第一列 start_idx = 0开始,一直往后找,递归终止的条件是 columnIndex < start_idx,

在执行到某个单元格的时候 columnIndex 是固定的, start_idx进行加加,直到大于 columnIndex,代表这个单元格计算完毕,得到合并规则

自此,所有功能介绍完毕

经历了一次又一次的修改,最终总算是让自己稍微满意了一些,

html 示例 可直接运行

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>sku</title>
		<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
		<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
		<script src="https://unpkg.com/element-ui/lib/index.js"></script>
		
		<style type="text/css">
			
			 /* 保存时如果没有验证通过, 要触发浏览器滚动, 这里是设置table表格中的 销售价格 最终渲染的label样式 */
			label[for*='table_data'] {
				visibility: hidden;
				margin-top: -40px;
			}
		</style>
	</head>
	<body>
		<div id="app">
			<el-form ref="form" :rules="rules" :model="form">
				<el-form-item label="添加规格" prop="sku_arr">
					<div style="display: flex;">
						<div>
							<el-button v-for="(item, idx) in default_attr" :key="idx" :disabled="attrBtnDisabled" @click="clickDefaultAttr(item)">{{item}}</el-button>
						</div>

						<el-popover placement="top" width="240" v-model="add_popover_bool" @after-enter="$refs.addValueInput.focus()">
							<div style="display: flex; grid-gap: 10px;">
								<el-input ref="addValueInput" v-model.trim="add_value" @keyup.enter.native="confirm()" />
								<el-button type="primary" @click="confirm()">确定</el-button>
							</div>

							<el-button slot="reference" size="small" type="primary" :disabled="attrBtnDisabled" style="margin-left: 40px;">自定义</el-button>
						</el-popover>
						
						
						<el-button type="primary" @click="onSubmit()" style="margin-left: 100px;">提交</el-button>
					</div>
				</el-form-item>

				<!-- 规格列表 和 表格 -->
				<section style="margin: 0 0 20px 50px;">
					<!-- 展示已经选择的 -->
					<div v-for="(item, index) in form.sku_arr" :key="index" style="margin-top: 10px;">

						<!-- 属性 -->
						<div>
							<el-input v-model.trim="item.attr" placeholder="属性" style="width: 120px;" @focus="attrFocus(item.attr)" @blur="attrBlur(item.attr)"></el-input>
							<el-button type="danger" size="mini" icon="el-icon-delete" circle @click="deleteAttr(index)"></el-button>
						</div>

						<!-- 属性值 -->
						<div style="display: flex; margin-top: 10px;">
							<div v-for="(ktem, kndex) in item.valueList" :key="kndex" style="margin-right: 20px;">
								<el-input size="small" ref="attrValueInput" v-model.trim="item.valueList[kndex]" placeholder="值" style="width: 100px;" @focus="attrValueFocus(item.valueList[kndex])" @blur="newAttrValueBlur(item.attr, item.valueList[kndex])"></el-input>

								<el-button v-if="item.valueList.length > 1" type="danger" size="mini" icon="el-icon-delete" circle @click="deleteSmall(index, kndex, item.attr, item.valueList[kndex])" />
							</div>

							<el-button type="primary" size="mini" :disabled="item.valueList.includes('')" icon="el-icon-plus" @click="addAttributeValue(index)">添加值</el-button>
						</div>
					</div>

					<el-table :data="form.table_data" :span-method="spanMethod" style="margin-top: 20px;" border>
						<template v-for="item_column in table_column">

							<el-table-column v-if="item_column == '图片'" :key="item_column" min-width="150" width="170" align="center" :resizable="false" label="图片">
								<template slot-scope="{ row, $index }">
									<!-- <el-form-item :prop="`table_data.${ $index }.图片`" :rules="rules.sku_img" label-width="0"> -->
									图片组件
									<!-- </el-form-item> -->
								</template>
							</el-table-column>

							<!-- 销售价格 使用表单验证 和 自定义表头 -->
							<el-table-column v-else-if="item_column == '销售价格'" :key="item_column" align="center" :resizable="false">
								<!-- 自定义表头 -->
								<template slot="header">
									<div><span style="color: #ff5353;">*</span>销售价格</div>
								</template>

								<template slot-scope="{ row, $index }">
									<el-form-item :prop="`table_data.${$index}.销售价格`" :rules="rules.sku_sale_price" label-width="0" label=" ">
										<el-input v-model="row[item_column]" :placeholder="item_column" />
									</el-form-item>
								</template>
							</el-table-column>

							<!-- 市场价格 -->
							<el-table-column v-else-if="item_column == '市场价格'" :key="item_column" align="center" :resizable="false" :label="item_column">
								<template slot-scope="{ row }">
									<div style="height: 62px;">
										<el-input v-model="row[item_column]" :placeholder="item_column" />
									</div>
								</template>
							</el-table-column>

							<!-- 库存 -->
							<el-table-column v-else-if="item_column == '库存'" :key="item_column" align="center" :resizable="false" :label="item_column">
								<template slot-scope="{ row, $index }">
									<div style="height: 62px;">
										<el-input v-model="row[item_column]" :placeholder="item_column" />
									</div>
								</template>
							</el-table-column>


							<!-- 其他属性列 -->
							<el-table-column v-else align="center" :resizable="false" :key="item_column" :prop="item_column" :label="item_column" />
						</template>
					</el-table>
				</section>
			</el-form>
		</div>
		<script>
		window.onload = () => {

			let attr_name_value = new Map([ // Map 数据结构, 根据属性名获取对应属性值 返回数组
				['颜色', ['黑', '白', '红']],
				['尺寸', ['大', '中', '小']],
				['重量', ['500g', '1kg', '5kg']]
			])
			let base_column = ['销售价格', '市场价格', '库存'] // 基本的列

			let first_column_rule = [] // 第一列与第二列使用相同的合并规则 (不能存在data中)
			let old_attr = '' // 每次当属性获得焦点时都会获取输入框内的值,保存于此
			let old_attr_value = '' // 每次当属性值获得焦点时都会获取输入框内的值,保存于此

			new Vue({
				el: '#app',
				computed: {
					// 已添加的属性(字符串数组)
					selectedAttr() {
						return this.form.sku_arr.map(x => x.attr)
					},
					// 是否可以添加属性 最多两个属性
					attrBtnDisabled() {
						return false
						return this.form.sku_arr.length >= 2
					}
				},
				data: {
					default_attr: ['颜色', '尺寸', '重量'], // 默认规格
					table_column: base_column, // 表格列
					add_popover_bool: false, // 添加属性的小弹窗
					add_value: '', // 添加属性的
					// 上边的数据是录入sku相关

					// 表单
					form: {
						sku_arr: [],
						table_data: [], // 表格中的数据
					},

					// 验证规则
					rules: {
						// sku 相关验证
						sku_arr: {
							validator: (rule, value, callback) => {
								if (value.length === 0) return callback(new Error('请添加规格'))
								else return callback()
							},
							trigger: 'blur'
						},
						sku_img: [
							{ required: true, message: '图片不能为空', trigger: 'blur' },
							{ type: 'string', message: '请等待图片上传完毕', trigger: 'blur' },
						],
						sku_sale_price: { required: true, message: '价格不能为空', trigger: 'blur' }
					}
				},

				methods: {
					// 点击默认的规格按钮
					clickDefaultAttr(attr_name) {
						if (this.selectedAttr.includes(attr_name)) return
						this.form.sku_arr.push({ attr: attr_name, valueList: [...attr_name_value.get(attr_name)] }) //解决引用类型导致的问题

						this.generateTableColumn()
						this.traverseSku() // 处理SKU, 生成表格

						console.log(this.form.sku_arr)
					},
					// 点击自定义里的确定 添加新的规格
					confirm() {
						if (!this.add_value) return
						this.form.sku_arr.push({ attr: this.add_value, valueList: [''] })

						this.generateTableColumn()
						this.traverseSku()

						this.add_popover_bool = false
						this.add_value = ''
					},
					// 属性获得焦点时 得到旧的值 存一下
					attrFocus(oldVal) {
						old_attr = oldVal
					},
					// 属性失去焦点时
					attrBlur(newVal) {
						console.log('attrBlur')
						// 如果 '新值等于旧值' 或者 '空' 什么也不做
						if (newVal === old_attr || newVal === '') return

						// 生成处理表头数据和表格数据
						this.generateTableColumn()
						this.traverseSku()
					},
					// 删除属性
					deleteAttr(idx) {
						this.form.sku_arr.splice(idx, 1)
						// 生成处理表头数据和表格数据
						this.generateTableColumn()
						this.traverseSku()
					},


					// 添加属性值
					async addAttributeValue(idx) {
						this.form.sku_arr[idx].valueList.push('')
						// 让新增的输入框获得焦点
						await this.$nextTick()
						this.$refs.attrValueInput[this.$refs.attrValueInput.length - 1].focus()
					},
					// 属性值获得焦点时 得到旧的值 在输入框失去焦点的时候, 如果值没有变化, 则什么也不做
					attrValueFocus(oldVal) {
						old_attr_value = oldVal
					},
					// 属性值失去焦点时, 操作表格数据 (新版本 可以实现无限个规格)
					newAttrValueBlur(curr_attr, newVal) {
						if (newVal === old_attr_value) return

						//  这里根据规格生成的笛卡尔积计算出table中需要改变的行索引 ( 包含新增和修改 )
						let cartesian_arr = this.generateBaseData(this.form.sku_arr)
						console.log(cartesian_arr)
						let change_idx_arr = [] // 需要被改变的行的索引
						for (let i in cartesian_arr) {
							if (cartesian_arr[i][curr_attr] === newVal) change_idx_arr.push(Number(i))
						}
						console.log('change_idx_arr', change_idx_arr)

						// 新的表格应该有的长度与现有的表格长度比较, 区分新增还是修改
						let length_arr = this.form.sku_arr.map(x => x.valueList.length)
						let new_table_length = length_arr.reduce((acc, curr_item) => acc * curr_item) // 新的表格数据长度 求乘积
						let old_table_length = this.form.table_data.length // 旧的表格数据长度

						// 如果是修改
						if (new_table_length === old_table_length) {
							this.form.table_data.forEach((item, index) => {
								if (change_idx_arr.includes(index)) this.form.table_data[index][curr_attr] = newVal
							})
							return
						}
						// 如果是新增
						if (new_table_length > old_table_length) {
							// 得到当前属性的当前值和其他规格的 sku_arr, 构造新的表格数据
							let other_sku_arr = this.form.sku_arr.map(item => {
								if (item.attr !== curr_attr) return item
								else return { attr: item.attr, valueList: [newVal] }
							})
							// 得到新增的表格数据
							let ready_map = this.generateBaseData(other_sku_arr)
							let new_table_data = this.mergeTableData(ready_map)
							change_idx_arr.forEach((item_idx, index) => {
								this.form.table_data.splice(item_idx, 0, new_table_data[index])
							})
						}
					},
					// 删除属性值 四个参数:'一级数组索引', '二级索引', '属性名字', '属性值'  后两个参数用来删除行
					deleteSmall(idx, kdx, attr_name, attr_val) {
						this.form.sku_arr[idx].valueList.splice(kdx, 1)

						// 删除table行
						let data_length = this.form.table_data.length
						for (let i = 0; i < data_length; i++) {
							if (this.form.table_data[i][attr_name] == attr_val) {
								this.form.table_data.splice(i, 1)
								i = i - 1
								data_length = data_length - 1
							}
						}
					},

					// 根据 `this.form.sku_arr` 生成表格列, `table_column` 用于 el-table-column 的 v-for
					async generateTableColumn() {
						this.table_column = this.form.sku_arr.map(x => x.attr).concat(base_column)
						/*
							不写 `$nextTick`会有bug, 没想明白为啥, 大概是vue懒得更新dom吧
							bug复现方式: 删除`$nextTick`后,勾选颜色,再勾选尺寸,再取消勾选颜色,观察el-table
						*/
						await this.$nextTick()
						if (this.form.sku_arr.length != 0) this.table_column.splice(1, 0, '图片')
					},

					// 合并行
					spanMethod({ row, column, rowIndex, columnIndex }) {
						if (columnIndex == 0) {
							let key_0 = column.label
							let first_idx = this.form.table_data.findIndex(x => x[key_0] == row[key_0])
							const calcSameLength = () => this.form.table_data.filter(x => x[key_0] == row[key_0]).length
							first_column_rule = rowIndex == first_idx ? [calcSameLength(), 1] : [0, 0]
							return first_column_rule

							// 第二列的图片与第一列主规格使用相同合并规则 ( 恰好el-table的合并方法是横着走的 )
						} else if (columnIndex == 1) {
							return first_column_rule

							// 其他列
						} else {
							// 表格数据的每一项, 
							const callBacks = (table_item, start_idx = 0) => {
								if (columnIndex < start_idx) return true
								let curr_key = this.table_column[start_idx]
								return table_item[curr_key] === row[curr_key] && callBacks(table_item, ++start_idx)
							}
							let first_idx = this.form.table_data.findIndex(x => callBacks(x))
							const calcSameLength = () => this.form.table_data.filter(x => callBacks(x)).length
							return rowIndex == first_idx ? [calcSameLength(), 1] : [0, 0]
						}
					},
					// 合并 sku 与 '图片', '销售价格', '库存', '市场价格' , 返回整个表格数据数组
					mergeTableData(arr) {
						return arr.map(item => ({ ...item, '销售价格': '', '市场价格': '', '库存': '', '图片': '' }))
					},
					// 遍历 `sku_arr` 生成表格数据
					traverseSku() {
						let ready_map = this.generateBaseData(this.form.sku_arr)
						this.form.table_data = this.mergeTableData(ready_map)
					},
					// 重新实现笛卡尔积  入参是: this.form.sku_arr 传入的数组 '为空', '长度为1', '长度大于1' 三种情况 分别处理
					generateBaseData(arr) {
						if (arr.length === 0) return []
						if (arr.length === 1) {
							let [item_spec] = arr
							return item_spec.valueList.map(x => {
								return {
									[item_spec.attr]: x
								}
							})
						}
						if (arr.length >= 1) {
							return arr.reduce((accumulator, spec_item) => {
								let acc_value_list = Array.isArray(accumulator.valueList) ? accumulator.valueList : accumulator
								let item_value_list = spec_item.valueList
								let result = []
								for (let i in acc_value_list) {
									for (let j in item_value_list) {
										let temp_data = {}
										// 如果是对象
										if (acc_value_list[i].constructor === Object) {
											temp_data = {
												...acc_value_list[i],
												[spec_item.attr]: item_value_list[j]
											}

											// 否则如果是字符串
										} else {
											temp_data[accumulator.attr] = acc_value_list[i]
											temp_data[spec_item.attr] = item_value_list[j]
										}
										result.push(temp_data)
									}
								}
								return result
							})
						}
					},

					onSubmit() {
						this.$refs.form.validate(async (valid, object) => {
							if (!valid) {
								// 获取元素距离body顶部的距离
								let getTop = dom => {
									let top = dom.offsetTop
									// while括号里的值是 dom.offsetParent
									while (dom = dom.offsetParent) top = top + dom.offsetTop
									return top
								}
								let [first_prop] = Object.keys(object)
								let top = getTop(document.querySelector(`label[for='${first_prop}']`))
								window.scrollTo({ top: top - 70, behavior: 'smooth' })
								
								return
							}

							this.btn_loading = true

						})
					}
				}
			})
		}
		</script>
	</body>
</html>