商品多规格多属性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;