一、关于数据库表的设计
-
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-cardshadow="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-selectv-model="item.id"placeholder="请选择属性"@change="changeAttributeHandler"><el-optionv-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-tagtype="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;};// 点击skuconst 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))