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、上面的数据提前手动插入到数据库中的
?
id
去查询商品属性值,这块可以自己见代码SKU
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>