一、关于数据库表的设计
-
1、商品属性表
比如一个衣服有颜色、尺码、款式这个叫属性表
-- ------------------------ -- 商品属性表 -- ------------------------ DROP TABLE IF EXISTS `attribute`; CREATE TABLE `attribute` ( `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键id', `name` varchar(50) not null COMMENT '属性名', `status` tinyint(4) DEFAULT 0 COMMENT '状态,0表示正常,1表示禁用', `remark` varchar(100) default null comment '备注', `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', `deleted_at` timestamp(6) NULL DEFAULT NULL COMMENT '软删除时间', UNIQUE KEY `UK_name_deleted_at` (`name`,`deleted_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT = "商品属性表";
-
2、商品属性值表
上面说的颜色,可能是红色、黄色、绿色,尺码可能是S、M、L
-- ------------------------ -- 商品属性值表 -- ------------------------ DROP TABLE IF EXISTS `attribute_value`; CREATE TABLE `attribute_value` ( `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键id', `attribute_id` int(11) not null comment '关联到attribute表主键id', `name` varchar(50) not null COMMENT '属性值', `status` tinyint(4) DEFAULT 0 COMMENT '状态,0表示正常,1表示禁用', `remark` varchar(100) default null comment '备注', `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', `deleted_at` timestamp(6) NULL DEFAULT NULL COMMENT '软删除时间', UNIQUE KEY `UK_attribute_id_name_deleted_at` (`attribute_id`,`name`,`deleted_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT = "商品属性值表";
-
3、
spu
表,或者直接叫商品表也可以的spu
表比如我们说的苹果手机、华为手机、戴尔笔记本这样的叫spu
-- ------------------------ -- 商品spu表 -- ------------------------ DROP TABLE IF EXISTS `spu`; CREATE TABLE `spu` ( `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键id', `name` varchar(50) not null COMMENT '商品名称', `keyword` varchar(50) not null COMMENT '关键词', `introduction` varchar(100) not null COMMENT '简介', `category_id` int(11) not null COMMENT '关联到category表主键id', `brand_id` int(11) default null comment '关联到brand表主键id', `pic_url` varchar(200) default null comment '封面图', `video_url` varchar(200) default null comment '视频地址', `slider_pic_urls` varchar(500) default null COMMENT '商品轮播图地址', `unit` int(11) default null comment '单位,关联到dict表主键id', `spec_type` tinyint(4) default 0 comment '单双规格,0表示单规格,1表示多规格(sku)', `price` decimal(10,2) default '0.00' comment '商品单价', `market_price` decimal(10,2) default '0.00' comment '商品市场价', `discount_price` decimal(10,2) default '0.00' comment '商品折扣价', `vip_price` decimal(10,2) default '0.00' comment '商品vip价', `cost_price` decimal(10,2) default '0.00' comment '商品成本价', `stock` int(11) default 0 comment '库存(如果是多规格求和)', `sort` int(11) DEFAULT 1 COMMENT '排序', `status` tinyint(4) DEFAULT 0 COMMENT '状态,0表示正常,1表示禁用', `is_hot` tinyint(4) DEFAULT 0 COMMENT '是否热销,0表示不是,1表示是', `is_benefit` tinyint(4) DEFAULT 0 COMMENT '是否优惠推荐,0表示不是,1表示是', `is_best` tinyint(4) DEFAULT 0 COMMENT '是否精品,0表示不是,1表示是', `is_new` tinyint(4) DEFAULT 0 COMMENT '是否新品,0表示不是,1表示是', `is_good` tinyint(4) DEFAULT 0 COMMENT '是否优品推荐,0表示不是,1表示是', `give_integral` int(11) DEFAULT 0 COMMENT '赠送积分', `sales_count` int(11) DEFAULT 0 COMMENT '销量', `browse_count` int(11) DEFAULT 0 COMMENT '浏览数', `content` text NOT NULL COMMENT '商品详情', `attribute` varchar(1000) default null COMMENT '销售属性数组,JSON 格式', `remark` varchar(100) default null comment '备注', `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', `deleted_at` timestamp(6) NULL DEFAULT NULL COMMENT '软删除时间', UNIQUE KEY `UK_name_deleted_at` (`name`,`deleted_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT = "商品spu表";
-
4、
sku
表上面说的苹果手机,可能是【黄色-128GB-大陆版】,【白色-256GB-港版】这样的叫
sku
-- ------------------------ -- 商品sku表 -- ------------------------ DROP TABLE IF EXISTS `sku`; CREATE TABLE `sku` ( `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键id', `spu_id` int(11) NOT NULL comment '关联到spu主键id', `attribute_value` varchar(200) default null comment '销售属性值:用英文,拼接', `price` decimal(10,2) default '0.00' comment '商品单价', `market_price` decimal(10,2) default '0.00' comment '商品市场价', `discount_price` decimal(10,2) default '0.00' comment '商品折扣价', `vip_price` decimal(10,2) default '0.00' comment '商品vip价', `cost_price` decimal(10,2) default '0.00' comment '商品成本价', `bar_code` varchar(64) DEFAULT NULL COMMENT 'SKU 的条形码', `pic_url` varchar(200) NOT NULL COMMENT '图片地址', `stock` int DEFAULT NULL COMMENT '库存', `weight` double DEFAULT NULL COMMENT '商品重量,单位:kg 千克', `volume` double DEFAULT NULL COMMENT '商品体积,单位:m^3 平米', `sales_count` int DEFAULT NULL COMMENT '商品销量', `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', `deleted_at` timestamp(6) NULL DEFAULT NULL COMMENT '软删除时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT = "商品sku表";
二、前端选择销售属性
-
1、最后效果图
-
2、上面的数据提前手动插入到数据库中的
- 3、后端提供一个接口先查询全部的商品属性,在根据商品属性的
id
去查询商品属性值,这块可以自己见代码
三、选择商品属性生成SKU
- 1、效果展示
-
2、由销售属性来生成下面的表格的方法
const checkPropertyList = ref([]); /** * 生成sku数据 * @param {*} skuAttribute 选中的商品属性 */ const generateSku = (skuAttribute) => { console.log(JSON.stringify(skuAttribute)); const attrValue = []; // 获取选中的属性 const checkList = []; for (const item of skuAttribute) { attrValue.push(item.attributeItem.filter((it) => item.checkList.includes(it.id))); checkList.push(item.id); // 选中的主键id } checkPropertyList.value = checkList; console.log(attrValue, '???'); if (attrValue.length == 0) { tableData.value = []; return; } // 处理添加一个属性的时候表格置空 if (!attrValue[attrValue.length - 1].length) { return; } // 循环组成sku数据 const skuList = attrValue .reduce((pre, cur) => { let res = []; for (const item of pre) { for (const it of cur) { let t = item.name + ',' + it.name; res.push({ name: t, url: item.url || it.url || '', }); } } return res; }) .map((it) => { const oldData = afterSku.value.find((item) => item.name == it.name); return { ...it, id: oldData ? oldData.id : '', price: oldData ? oldData.price : '', // 单价 stock: oldData ? oldData.stock : '', // 库存 }; }); tableData.value = skuList; };
-
3、在什么时候调用上面这个方法呢?,直接监听销售属性的变化就可以
watch( () => skuAttributes.value, (newValue) => { generateSku(cloneDeep(newValue)); }, { deep: true, } ); // 监听sku表格的变化,并将当前sku进行备份 const afterSku = ref([]); watch( () => tableData.value, (value) => { afterSku.value = cloneDeep(value); }, { deep: true } );
-
4、点击按钮提交数据给后端
const submitHandler = async () => { ElMessage.success('请查看浏览器控制台'); console.log('销售属性:', JSON.stringify(skuAttributes.value)); console.log('表格数据:', JSON.stringify(tableData.value)); const postData = { name: '苹果13', keyword: '苹果手机', introduction: 'laboris sint in', categoryId: 10, brandId: 50, picUrl: 'http://dummyimage.com/400x400', videoUrl: 'http://voanozyj.vg/suuadzgv', sliderPicUrls: 'http://dummyimage.com/400x400', unit: 25, specType: 79, price: 90, marketPrice: 11, discountPrice: 82, vipPrice: 32, costPrice: 87, stock: 19, sort: 33, isHot: 0, isBenefit: 0, isBest: 0, isNew: 0, isGood: 0, giveIntegral: 43, content: '手机详情', remark: 'magna eu laboris', // 过滤掉没有选择的sku属性 attribute: JSON.stringify(skuAttributes.value), skuList: tableData.value.map((item) => { return { ...item, attributeValue: item.name, barCode: item.name, discountPrice: item.price, costPrice: item.price, picUrl: 'http://dummyimage.com/400x400', vipPrice: item.price, volume: 85, marketPrice: 26, weight: 50, }; }), }; console.log(JSON.stringify(postData), '提交数据'); const data = await SkuService.createSpuApi(postData); console.log(data); };
-
5、完整代码如下
<template> <div class="sku"> <el-card shadow="never"> <el-form> <el-form-item label="销售属性"> <el-card shadow="never" v-for="(item, index) of skuAttributes" :key="index" :gutter="20" class="sku-row" > <el-button type="danger" class="delete-row-btn" @click="deleteRowHandler(index)" >删除</el-button > <el-row :gutter="10"> <el-col :span="2">属性名称:</el-col> <el-col :span="8"> <el-select v-model="item.id" placeholder="请选择属性" @change="changeAttributeHandler" > <el-option v-for="item in attributeItem" :key="item.id" :label="item.name" :value="item.id" :disabled="checkPropertyList.includes(item.id)" /> </el-select> </el-col> </el-row> <el-row :gutter="10"> <el-col :span="2"> 属性值: </el-col> <el-col :span="22"> <el-checkbox-group v-model="item.checkList"> <el-checkbox :label="value.id" v-for="(value, i) of item.attributeItem" :key="i" >{{ value.name }}</el-checkbox > </el-checkbox-group> </el-col> </el-row> </el-card> <div> <el-button type="primary" @click="addSkuAttrHandler">增加销售属性</el-button> </div> </el-form-item> </el-form> </el-card> <!-- 下面表格 --> <el-card shadow="never" style="margin-top: 20px"> <el-table :data="tableData" style="width: 100%" border> <el-table-column prop="name" label="销售规格" width="180" /> <el-table-column prop="price" label="单价"> <template #default="scope"> <el-input v-model="scope.row.price" placeholder="单价"></el-input> </template> </el-table-column> <el-table-column prop="stock" label="库存"> <template #default="scope"> <el-input v-model="scope.row.stock" placeholder="库存"></el-input> </template> </el-table-column> </el-table> <el-button type="primary" style="margin-top: 20px" @click="submitHandler">提交数据</el-button> </el-card> </div> </template> <script setup> import { SkuService } from '@/services'; import { ref, onMounted, watch } from 'vue'; import { cloneDeep } from 'lodash'; import { ElMessage } from 'element-plus'; const skuAttributes = ref([]); // 添加属性 const addSkuAttrHandler = () => { skuAttributes.value.push({ id: null, attributeItem: [], checkList: [], }); }; // 删除 const deleteRowHandler = (index) => { skuAttributes.value.splice(index, 1); }; // 1.获取全部的销售属性 const attributeItem = ref([]); const getAllAttrApi = async () => { const data = await SkuService.getAttributeListApi(); attributeItem.value = data.result; }; // 切换的时候 const changeAttributeHandler = async (item) => { let currentItem = skuAttributes.value.find((it) => it.id == item); const currentIndex = skuAttributes.value.findIndex((it) => it.id == item); const data = await SkuService.getAttributeValueByAttributeIdApi(item); currentItem.attributeItem = data.result; currentItem.name = attributeItem.value.find((it) => it.id == item)?.name; // 替换之前的 skuAttributes.value.splice(currentIndex, 1, currentItem); }; // 表格数据 const tableData = ref([]); const submitHandler = async () => { ElMessage.success('请查看浏览器控制台'); console.log('销售属性:', JSON.stringify(skuAttributes.value)); console.log('表格数据:', JSON.stringify(tableData.value)); const postData = { name: '苹果13', keyword: '苹果手机', introduction: 'laboris sint in', categoryId: 10, brandId: 50, picUrl: 'http://dummyimage.com/400x400', videoUrl: 'http://voanozyj.vg/suuadzgv', sliderPicUrls: 'http://dummyimage.com/400x400', unit: 25, specType: 79, price: 90, marketPrice: 11, discountPrice: 82, vipPrice: 32, costPrice: 87, stock: 19, sort: 33, isHot: 0, isBenefit: 0, isBest: 0, isNew: 0, isGood: 0, giveIntegral: 43, content: '手机详情', remark: 'magna eu laboris', // 过滤掉没有选择的sku属性 attribute: JSON.stringify(skuAttributes.value), skuList: tableData.value.map((item) => { return { ...item, attributeValue: item.name, barCode: item.name, discountPrice: item.price, costPrice: item.price, picUrl: 'http://dummyimage.com/400x400', vipPrice: item.price, volume: 85, marketPrice: 26, weight: 50, }; }), }; console.log(JSON.stringify(postData), '提交数据'); const data = await SkuService.createSpuApi(postData); console.log(data); }; watch( () => skuAttributes.value, (newValue) => { generateSku(cloneDeep(newValue)); }, { deep: true, } ); // 监听sku表格的变化,并将当前sku进行备份 const afterSku = ref([]); watch( () => tableData.value, (value) => { afterSku.value = cloneDeep(value); }, { deep: true } ); const checkPropertyList = ref([]); /** * 生成sku数据 * @param {*} skuAttribute 选中的商品属性 */ const generateSku = (skuAttribute) => { console.log(JSON.stringify(skuAttribute)); const attrValue = []; // 获取选中的属性 const checkList = []; for (const item of skuAttribute) { attrValue.push(item.attributeItem.filter((it) => item.checkList.includes(it.id))); checkList.push(item.id); // 选中的主键id } checkPropertyList.value = checkList; console.log(attrValue, '???'); if (attrValue.length == 0) { tableData.value = []; return; } // 处理添加一个属性的时候表格置空 if (!attrValue[attrValue.length - 1].length) { return; } // 循环组成sku数据 const skuList = attrValue .reduce((pre, cur) => { let res = []; for (const item of pre) { for (const it of cur) { let t = item.name + ',' + it.name; res.push({ name: t, url: item.url || it.url || '', }); } } return res; }) .map((it) => { const oldData = afterSku.value.find((item) => item.name == it.name); return { ...it, id: oldData ? oldData.id : '', price: oldData ? oldData.price : '', // 单价 stock: oldData ? oldData.stock : '', // 库存 }; }); tableData.value = skuList; }; onMounted(() => { getAllAttrApi(); }); </script> <style lang="scss" scoped> .sku-row { width: 100%; margin-bottom: 10px; position: relative; .delete-row-btn { position: absolute; right: 10px; top: 10px; cursor: pointer; z-index: 30; } .sku-value-item { margin-bottom: 10px; position: relative; .close-icon { position: absolute; right: 0; top: 0; z-index: 10; cursor: pointer; opacity: 0; } &:hover { .close-icon { opacity: 1; } } } } </style>
四、前端实现sku
选择
-
1、效果图如下
-
2、实现代码见如下
<template> <div class="sku"> <h3>iphone 13</h3> <div v-for="(item, index) of processAttribute" :key="index"> <div class="title" style="margin-bottom: 10px; margin-top: 10px">{{ item.title }}</div> <template v-for="(item1, index1) of item.attributeItem" :key="index1"> <el-tag type="success" :class="[ { active: item1.activity, disabled: item1.disabled, }, ]" style="margin-right: 10px; cursor: pointer" @click="skuClickHandler(index, index1)" >{{ item1.name }}</el-tag > </template> </div> <div>当前选中的库存:{{ stock }}</div> <div >价格范围: <span v-if="minPrice == maxPrice">{{ maxPrice }}</span> <span v-else>{{ minPrice }}-{{ maxPrice }}</span> </div> </div> </template> <script setup> import { onMounted } from 'vue'; import { SkuService } from '@/services'; import { useRoute } from 'vue-router'; const route = useRoute(); const skuAttribute = ref([]); const skuList = ref([]); const processAttribute = ref([]); const processSkuMap = ref({}); const initData = () => { for (const item of skuAttribute.value) { let temp = { id: item.id, title: item.title, }; temp.attributeItem = item.attributeItem .filter((it) => item.checkList.includes(it.id)) // 过滤有的属性 .map((it) => { return { ...it, activity: false, disabled: itemquantity(it.name) <= 0, // 判断当前是否小于0的库存 stock: itemquantity(it.name), }; }); processAttribute.value.push(temp); } // 对 skuList 数据进行加工,并存入 processSkuMap 中 for (const item of skuList.value) { let combArr = arrayCombine(item.attributeValue.split(',')); for (let j = 0; j < combArr.length; j++) { var key = combArr[j].join(','); if (processSkuMap.value[key]) { // 库存累加,价格添加进数组 processSkuMap.value[key].stock += +item.stock; processSkuMap.value[key].prices.push(item.price); } else { processSkuMap.value[key] = { stock: +item.stock, prices: [item.price], }; } } } // 计算下 // skuCheck(); }; // 计算当前sku的库存 const itemquantity = (item) => { let quantity = 0; skuList.value.forEach((element) => { var skuArr = element.attributeValue.split(','); if (skuArr.indexOf(item) != -1) { quantity += +element.stock; } }); return quantity; }; const arrayCombine = (targetArr) => { let resultArr = []; for (var n = 0; n <= targetArr.length; n++) { var flagArrs = getFlagArrs(targetArr.length, n); while (flagArrs.length) { var flagArr = flagArrs.shift(); var combArr = Array(targetArr.length); for (var i = 0; i < targetArr.length; i++) { if (flagArr[i]) { combArr[i] = targetArr[i]; } } resultArr.push(combArr); } } return resultArr; }; const getFlagArrs = (m, n) => { let flagArrs = []; let flagArr = []; let isEnd = false; for (let i = 0; i < m; i++) { flagArr[i] = i < n ? 1 : 0; } flagArrs.push(flagArr.concat()); // 当n不等于0并且m大于n的时候进入 if (n && m > n) { while (!isEnd) { var leftCnt = 0; for (var i = 0; i < m - 1; i++) { if (flagArr[i] == 1 && flagArr[i + 1] == 0) { for (var j = 0; j < i; j++) { flagArr[j] = j < leftCnt ? 1 : 0; } flagArr[i] = 0; flagArr[i + 1] = 1; var aTmp = flagArr.concat(); flagArrs.push(aTmp); if (aTmp.slice(-n).join('').indexOf('0') == -1) { isEnd = true; } break; } flagArr[i] == 1 && leftCnt++; } } } return flagArrs; }; // 点击sku const skuClickHandler = (key1, key2) => { console.log(key1, key2, '点击了', processAttribute.value[key1]); // 如果不是被禁用的时候才执行 if (!processAttribute.value[key1].attributeItem[key2].disabled) { // 选择和取消选中 processAttribute.value[key1].attributeItem.map((item, index) => { item.activity = index == key2 ? !item.activity : false; }); // 检查当前的sku是否有库存 skuCheck(); // 每次点击的时候判断禁用 getStockPrice(); } }; // 当前选中的sku的库存机最小单价最大单价 const stock = ref(0); const minPrice = ref(null); const maxPrice = ref(null); const skuCheck = () => { let sku = []; processAttribute.value.map((attr) => { let name = ''; attr.attributeItem.map((item) => { console.log(item, '111'); if (item.activity) { name = item.name; } }); sku.push(name); }); console.log(sku, '选中的的值', sku.join(',')); stock.value = processSkuMap.value[sku.join(',')].stock; minPrice.value = Math.min.apply(Math, processSkuMap.value[sku.join(',')].prices); maxPrice.value = Math.max.apply(Math, processSkuMap.value[sku.join(',')].prices); }; // 点击的时候判断库存禁用 const getStockPrice = () => { processAttribute.value.map((attr) => { attr.attributeItem.map((item) => { item.disabled = itemquantity(item.name) <= 0; }); }); let count = 0; let i = 0; processAttribute.value.map((attr, index) => { let flag = false; attr.attributeItem.map((item) => { if (item.activity) { flag = true; } }); if (!flag) { count += 1; i = index; } }); // 当只有一组规格没选时 if (count == 1) { processAttribute.value[i].attributeItem.map((item) => { let sku = []; let text = item.name; processAttribute.value.map((attr, index) => { if (index != i) { attr.attributeItem.map((item2) => { if (item2.activity) { sku.push(item2.name); } }); } else { sku.push(text); } }); if (processSkuMap.value[sku.join(',')].stock == 0) { item.disabled = true; } }); } // 当所有规格都有选时 if (count == 0) { processAttribute.value.map((attr, index) => { let i = index; processAttribute.value[index].attributeItem.map((item) => { if (!item.activity) { let sku = []; let text = item.name; processAttribute.value.map((list, index) => { if (index != i) { list.attributeItem.map((item2) => { if (item2.activity) { sku.push(item2.name); } }); } else { sku.push(text); } }); if (processSkuMap.value[sku.join(',')].stock == 0) { item.disabled = true; } } }); }); } }; const initSkuData = async () => { const { result } = await SkuService.getSkuByIdApi(route.query.id); // console.log(data); console.log(result.skuAttribute, '111'); skuAttribute.value = result.skuAttribute; skuList.value = result.skuList; }; onMounted(async () => { await initSkuData(); initData(); }); </script> <style lang="scss" scoped> .sku { .active { background: skyblue; color: #fff; border: none; } .disabled { background: #ddd; } } </style>
五、具体代码见
- 1、[后端代码](shuiping.kuang/sku_demo (gitee.com))
- 2、[前端代码](shuiping.kuang/vue3-sku (gitee.com))