商品多规格多属性sku的设置一直都是商城系统里比较好玩的部分,因为涉及多个相关表的操作。原理:
设置属性》设置属性值》多属性组合。
下面是我找到的一个vue功能,人家分享出来也是不完整的,我自己进行了调整和封装成了组件,附上php代码。
开启多规格:
数据库效果:
不开启多规格时:
要优化的地方:一键操作、规格图片上传。
php接收数据(我是基于thinkphp的,自己可以用file_get_ contents("php//input")功能获取):
public function set() { //获取sku数据 $sku_list = input('sku_list', '{}'); $sku = @json_decode($sku_list, true); $goods_id = input('goods_id', 0, 'intval');//todo 默认写了一个,可以自己进行商品的增改操作后获取 if ($sku) { Db::startTrans(); try { //基础的字段 $baseFields = ['cost_price', 'market_price', 'price', 'store_count', 'src']; foreach ($sku as $k => $specItems) { if (empty($specItems['price'])) { throw new \Exception('价格填写不完整'); } if (empty($specItems['cost_price'])) { throw new \Exception('成本价不完整'); } if (empty($specItems['store_count'])) { throw new \Exception('库存不完整'); } //基础结构 $spec_goods_price = [ 'goods_id' => $goods_id,//商品spu的id 'key' => '',//属性值的id拼接 'key_name' => '',//规格名称组合 'cost_price' => $specItems['cost_price'],//成本价 'market_price' => $specItems['market_price'],//市场价 'price' => $specItems['price'],//销售价格 'store_count' => $specItems['store_count'],//库存 'src' => $specItems['src'],//封面图片 ]; $spec_item_ids = []; //单规格 if (count($specItems) <= count($baseFields)) { $specItems['规格'] = '默认'; $specItems['src'] = ''; } foreach ($specItems as $key => $value) { //处理属性和属性值 if (!in_array($key, $baseFields)) { //属性表 $spec_id = Db::name('sku_spec')->where(['name' => $key])->value('id'); if (!$spec_id) { $spec_id = Db::name('sku_spec')->insertGetId([ 'name' => $key, 'addtime' => time() ]); } //属性值表 $spec_item_id = Db::name('sku_spec_value')->where(['spec_id' => $spec_id, 'item' => $value])->value('id'); if (!$spec_item_id) { $spec_item_id = Db::name('sku_spec_value')->insertGetId(['spec_id' => $spec_id, 'item' => $value]); } $spec_item_ids[] = $spec_item_id;//key } } //从小到大排序,方便下单计算 sort($spec_item_ids); $key_names = []; foreach ($spec_item_ids as $id) { $spec_item = Db::name('sku_spec_value')->where(['id' => $id])->find(); $specName = Db::name('sku_spec')->where(['id' => $spec_item['spec_id']])->value('name'); $key_names[] = $specName . ':' . $spec_item['item']; } if (count($spec_item_ids) > 1) { $glueKey = '_'; $glueName = ' '; } else { $glueKey = ''; $glueName = ''; } //拼接字key $spec_goods_price_key = join($glueKey, $spec_item_ids); //拼接规格名称 $spec_goods_price_key_name = join($glueName, $key_names); $spec_goods_price['key'] = $spec_goods_price_key; $spec_goods_price['key_name'] = $spec_goods_price_key_name; //最终的规格属性库存表(sku表) if ($hasId = Db::name('sku_spec_price')->where(['goods_id' => $goods_id, 'key' => $spec_goods_price_key])->value('id')) { Db::name('sku_spec_price')->where(['id' => $hasId])->save($spec_goods_price); } else { $res = Db::name('sku_spec_price')->insertGetId($spec_goods_price); if (!$res) { throw new Exception('添加规格信息失败'); } } } } catch (Exception $e) { Db::rollback(); data_return($e->getMessage(), 500); } Db::commit(); data_return('success', 200, [ 'param' => $sku, ]); } data_return('success', 200); }
前端vue在商品详情页调用组件:
js里面监听sku提交:
引入组件:
import goodsSku from "@/xxxxx/GoodsSku.vue";
components: {goodsSku}, methods: { //监听提交的数据 onSubmitSku(postData) { console.log('skuInfo',postData) }, },
在vue上面写组件:
<template> <div class="sku"> <goods-sku :goods_id="goods_id" @submitSku="onSubmitSku"></goods-sku> </div> </template>
组件代码(style样式太长了,没放,整个组件在附件):
<template> <div class="sku_set"> <el-form :model="ruleForm" :inline="false" ref="ruleForm" label-width="100px" class="demo-ruleForm"> <el-form-item label="是否多规格:" > <el-switch v-model="ruleForm.specType"></el-switch> </el-form-item> <!-- 规格 --> <el-divider></el-divider> <div> <div class="goods-spec" v-if="ruleForm.specType"> <h3>商品规格</h3> <el-link type="primary" @click="addPrivateSpec" class="goods-spec-add">添加规格</el-link> </div> <div v-if="ruleForm.specType" class="goods-container" v-for="(attr, index) in items" :key="index"> <div class="goods-content"> <div class="goods-content-box"> <div class="goods-content-left"> <el-form label-width="80px" style="width:400px"> <el-form-item label="规格名"> <el-input v-model="attr.value" placeholder="请输入规格名"></el-input> </el-form-item> <el-form-item label="规格值"> <el-tag v-for="tag in attr.detail" :key="tag" closable :disable-transitions="false" @close="handleClose(tag, attr)"> {{ tag }} </el-tag> <el-input class="input-new-tag" v-if="attr.inputVisible" v-model="attr.inputValue" :ref="`saveTagInput${index}`" size="small" @keyup.enter.native="handleInputConfirm(attr.inputValue, attr)" @blur="handleInputConfirm(attr.inputValue, attr)" > </el-input> <el-button v-else class="button-new-tag" size="small" @click="showInput(attr, index)">+ 添加</el-button> </el-form-item> </el-form> </div> <div class="goods-content-right"> <el-link type="danger" @click="delPrivateSpec(index)">删除规格</el-link> </div> </div> </div> </div> <p style="margin:24px 0 10px 0">价格 / 库存</p> <el-table ref="multipleTable" :data="tableColumnList.tableBodyList" stripe tooltip-effect="dark" style="width: 100%;margin-top:1%;"> <el-table-column :label="item.propName" :property="item.prop" v-for="item in tableColumnList.tableHeaderList" :key="item.prop" align="center"> <template v-slot="scope"> <span>{{ scope.row[scope.column.property] }}</span> </template> </el-table-column> <el-table-column label="价格(元)"> <template v-slot="scope"> <el-input v-model.number="scope.row.price"></el-input> </template> </el-table-column> <el-table-column label="成本价(元)"> <template v-slot="scope"> <el-input v-model.number="scope.row.cost_price"></el-input> </template> </el-table-column> <el-table-column label="原价(元)"> <template v-slot="scope"> <el-input v-model.number="scope.row.market_price"></el-input> </template> </el-table-column> <el-table-column label="库存"> <template v-slot="scope"> <el-input v-model.number="scope.row.store_count"></el-input> </template> </el-table-column> <el-table-column label="图片"> <template v-slot="scope"> <!--上传图片的组件哦--> <el-image v-if="scope.row.src" :src="scope.row.src"></el-image> <el-tag v-else class="el-icon el-icon-plus">上传</el-tag> </template> </el-table-column> </el-table> </div> <el-button type="primary" @click="submit()" style="width: 20%;margin-left: 30%;margin-top: 20px;">保存</el-button> </el-form> </div> </template> <script> import {sku_get,sku_set} from "@/request/api/sku"; export default { data() { return { ruleForm: { specType: false, }, inputVisible: false, inputValue: '', tableColumnList: { tableHeaderList: [], tableBodyList: [ {} ] }, items: [ //sku属性 { value: '', //规格名 detail: [], //规格值数组 inputVisible: false, inputValue: '' } ], id: "", //无则添加有则编辑 timer: '', //刷新子组件 } }, mounted() { }, props: { //商品 ID goods_id: { type: Number, default: 0, }, }, computed: { // 计算规格 calculateAttribute() { // 初始化 let obj = {} this.items.forEach((item) => { // 判断有没有输入规格名 if (item.value) { //规格名:规格值 //'颜色':'尺寸' obj[item.value] = item.detail } }) return obj } }, watch: { // 监听规格数据 calculateAttribute(newVal) { if (!this.ruleForm.specType) { return; } //this.attribute(newVal); let cloneNewVal = JSON.parse(JSON.stringify(newVal)) let attrName = [] //规格名数组 let attrValue = [] //规格值数组 for (let key in cloneNewVal) { attrName.push(key) attrValue.push(cloneNewVal[key]) } // 表格内容数据(笛卡尔积算法) let finalArr = this.cartesianProductOf(...attrValue) let tableObj = { tableBodyList: [], tableHeaderList: [] } // 表格内容 tableObj.tableBodyList = finalArr.map((item) => { let obj = { cost_price: 0, //成本价 market_price: 0, //原价 price: 0, //现价 store_count: 0, //库存 src: '', //商品规格图片路径 } for (let i = 0; i < item.length; i++) { obj[attrName[i]] = item[i] } return obj }) this.tableColumnList.tableBodyList = tableObj.tableBodyList //表格内容数据 // 表头 let skuTableArr = Object.keys(newVal) tableObj.tableHeaderList = skuTableArr.map((item) => { return { prop: item, propName: item } }) this.tableColumnList.tableHeaderList = tableObj.tableHeaderList // 表头 console.log('new',newVal) }, 'ruleForm.specType': { deep: true, handler: function (newV, oldV) { console.log('xxx',newV) if (!newV) { this.tableColumnList = this.$options.data().tableColumnList; this.items = this.$options.data().items; } } } }, methods: { submit() { console.log('header',this.tableColumnList.tableHeaderList) console.log('body',this.tableColumnList.tableBodyList) let body = this.tableColumnList.tableBodyList; //测试提交到后台set方法 let postData = { goods_id: this.goods_id, sku_list:body }; //同步到父组件 this.$emit('submitSku',postData); //测试提交 postData.sku_list = JSON.stringify(body) sku_set(postData).then((res)=>{ console.log('set',res) }).catch(()=>{ console.log('set err') }) }, removeEmptyChildren(node) { const that = this; node.forEach(item => { if ('children' in item && item.children.length === 0) { delete item.children } else if ('children' in item && item.children.length) { that.removeEmptyChildren(item.children) } }) return node; }, /*****规格*****/ // 添加规格 addPrivateSpec(index) { this.items.push({ value: '', detail: [], inputVisible: false, inputValue: '' }) }, delPrivateSpec(index) { this.items.splice(index, 1) }, handleInputConfirm(val, attr) { if (val) { attr.detail.push(val) } attr.inputVisible = false attr.inputValue = '' }, handleClose(tag, item) { item.detail.splice(item.detail.indexOf(tag), 1) }, showInput(attr, index) { attr.inputVisible = true this.$nextTick((_) => { this.$refs[`saveTagInput${index}`][0].$refs.input.focus() }) }, // 笛卡尔积算法 cartesianProductOf(...args) { return args.reduce( (total, current) => { let ret = [] total.forEach((a) => { current.forEach((b) => { ret.push(a.concat([b])) }) }) return ret }, [[]] ) } }, components: {}, onload: () => { console.log('多规格组件init') } }; </script>
数据库结构:
CREATE TABLE `tp_sku_spec_price` ( `id` int(11) NOT NULL AUTO_INCREMENT, `goods_id` int(11) DEFAULT '0' COMMENT '商品id', `key` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '规格键名', `key_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '规格键名中文', `price` decimal(10,2) DEFAULT NULL COMMENT '价格', `store_count` int(11) unsigned DEFAULT '10' COMMENT '库存数量', `src` varchar(512) DEFAULT NULL COMMENT '商品规格图片路径', `cost_price` decimal(10,2) DEFAULT NULL COMMENT '底价', PRIMARY KEY (`id`) USING BTREE, KEY `key` (`key`) USING BTREE, KEY `goods_id` (`goods_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='商品sku规格库存表'; CREATE TABLE `tp_sku_spec` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '规格表', `name` varchar(55) DEFAULT NULL COMMENT '规格名称', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; CREATE TABLE `tp_sku_spec_value` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '规格项id', `spec_id` int(11) DEFAULT NULL COMMENT '规格id', `item` varchar(54) DEFAULT NULL COMMENT '规格项', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;