✅P71-72_属性分组-前端组件抽取-父子组件交互
名词介绍
SPU
SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品集合就可以称为一个SPU。
例如:Apple iPhone 13 就是一个SPU
SKU
SKU(Stock Keeping Unit):SKU一般指最小存货单位、最小售卖单元。 最小存货单位(SKU),即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。 现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号
例如:Apple iPhone 13 粉色 256G 可以确定商品的价格和库存的集合,我们称为SKU
一个spu包括了多个sku,sku决定最终价格
销售属性
每个分类下的商品共享规格参数和销售属性,只是有些商品不一定要用这个分类下全部的属性。
说明:
- 属性是以三级分类组织起来的
- 规格参数中有些是可以提供检索的
- 规格参数也是基本属性,他们具有自己的分组
- 属性的分组也是由三级分类组织起来的
- 属性名确定的,但是值每一个商品不同来决定的
规格参数
例如:手机分类下的产品都会有一个规格与包装属性,也就是我们常说的规格参数
规格参数由 **属性组 **和 **属性 **组成
左侧第一栏即属性组,中间一栏即属性,右侧一栏即属性的值。
【属性分组-规格参数-销售属性-三级分类】关联关系
手机是一级分类,它下面又有属性组,每个属性组又有各自的属性
SPU-SKU-属性表
像网络、像素一般是固定不可选的所以是SPU属性
而内存、容量、颜色等可选的就为SKU销售属性
前端组件抽取&父子组件交互
创建属性分组菜单
系统菜单脚本
将sys_menus.sql
在gulimall_admin
数据库中执行,创建所有菜单
USE `gulimall_admin`;
/*Table structure for table `sys_menu` */
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`name` varchar(50) DEFAULT NULL COMMENT '菜单名称',
`url` varchar(200) DEFAULT NULL COMMENT '菜单URL',
`perms` varchar(500) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
`type` int(11) DEFAULT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
`icon` varchar(50) DEFAULT NULL COMMENT '菜单图标',
`order_num` int(11) DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=76 DEFAULT CHARSET=utf8mb4 COMMENT='菜单管理';
/*Data for the table `sys_menu` */
insert into `sys_menu`(`menu_id`,`parent_id`,`name`,`url`,`perms`,`type`,`icon`,`order_num`) values (1,0,'系统管理',NULL,NULL,0,'system',0),(2,1,'管理员列表','sys/user',NULL,1,'admin',1),(3,1,'角色管理','sys/role',NULL,1,'role',2),(4,1,'菜单管理','sys/menu',NULL,1,'menu',3),(5,1,'SQL监控','http://localhost:8080/renren-fast/druid/sql.html',NULL,1,'sql',4),(6,1,'定时任务','job/schedule',NULL,1,'job',5),(7,6,'查看',NULL,'sys:schedule:list,sys:schedule:info',2,NULL,0),(8,6,'新增',NULL,'sys:schedule:save',2,NULL,0),(9,6,'修改',NULL,'sys:schedule:update',2,NULL,0),(10,6,'删除',NULL,'sys:schedule:delete',2,NULL,0),(11,6,'暂停',NULL,'sys:schedule:pause',2,NULL,0),(12,6,'恢复',NULL,'sys:schedule:resume',2,NULL,0),(13,6,'立即执行',NULL,'sys:schedule:run',2,NULL,0),(14,6,'日志列表',NULL,'sys:schedule:log',2,NULL,0),(15,2,'查看',NULL,'sys:user:list,sys:user:info',2,NULL,0),(16,2,'新增',NULL,'sys:user:save,sys:role:select',2,NULL,0),(17,2,'修改',NULL,'sys:user:update,sys:role:select',2,NULL,0),(18,2,'删除',NULL,'sys:user:delete',2,NULL,0),(19,3,'查看',NULL,'sys:role:list,sys:role:info',2,NULL,0),(20,3,'新增',NULL,'sys:role:save,sys:menu:list',2,NULL,0),(21,3,'修改',NULL,'sys:role:update,sys:menu:list',2,NULL,0),(22,3,'删除',NULL,'sys:role:delete',2,NULL,0),(23,4,'查看',NULL,'sys:menu:list,sys:menu:info',2,NULL,0),(24,4,'新增',NULL,'sys:menu:save,sys:menu:select',2,NULL,0),(25,4,'修改',NULL,'sys:menu:update,sys:menu:select',2,NULL,0),(26,4,'删除',NULL,'sys:menu:delete',2,NULL,0),(27,1,'参数管理','sys/config','sys:config:list,sys:config:info,sys:config:save,sys:config:update,sys:config:delete',1,'config',6),(29,1,'系统日志','sys/log','sys:log:list',1,'log',7),(30,1,'文件上传','oss/oss','sys:oss:all',1,'oss',6),(31,0,'商品系统','','',0,'editor',0),(32,31,'分类维护','product/category','',1,'menu',0),(34,31,'品牌管理','product/brand','',1,'editor',0),(37,31,'平台属性','','',0,'system',0),(38,37,'属性分组','product/attrgroup','',1,'tubiao',0),(39,37,'规格参数','product/baseattr','',1,'log',0),(40,37,'销售属性','product/saleattr','',1,'zonghe',0),(41,31,'商品维护','product/spu','',0,'zonghe',0),(42,0,'优惠营销','','',0,'mudedi',0),(43,0,'库存系统','','',0,'shouye',0),(44,0,'订单系统','','',0,'config',0),(45,0,'用户系统','','',0,'admin',0),(46,0,'内容管理','','',0,'sousuo',0),(47,42,'优惠券管理','coupon/coupon','',1,'zhedie',0),(48,42,'发放记录','coupon/history','',1,'sql',0),(49,42,'专题活动','coupon/subject','',1,'tixing',0),(50,42,'秒杀活动','coupon/seckill','',1,'daohang',0),(51,42,'积分维护','coupon/bounds','',1,'geren',0),(52,42,'满减折扣','coupon/full','',1,'shoucang',0),(53,43,'仓库维护','ware/wareinfo','',1,'shouye',0),(54,43,'库存工作单','ware/task','',1,'log',0),(55,43,'商品库存','ware/sku','',1,'jiesuo',0),(56,44,'订单查询','order/order','',1,'zhedie',0),(57,44,'退货单处理','order/return','',1,'shanchu',0),(58,44,'等级规则','order/settings','',1,'system',0),(59,44,'支付流水查询','order/payment','',1,'job',0),(60,44,'退款流水查询','order/refund','',1,'mudedi',0),(61,45,'会员列表','member/member','',1,'geren',0),(62,45,'会员等级','member/level','',1,'tubiao',0),(63,45,'积分变化','member/growth','',1,'bianji',0),(64,45,'统计信息','member/statistics','',1,'sql',0),(65,46,'首页推荐','content/index','',1,'shouye',0),(66,46,'分类热门','content/category','',1,'zhedie',0),(67,46,'评论管理','content/comments','',1,'pinglun',0),(68,41,'spu管理','product/spu','',1,'config',0),(69,41,'发布商品','product/spuadd','',1,'bianji',0),(70,43,'采购单维护','','',0,'tubiao',0),(71,70,'采购需求','ware/purchaseitem','',1,'editor',0),(72,70,'采购单','ware/purchase','',1,'menu',0),(73,41,'商品管理','product/manager','',1,'zonghe',0),(74,42,'会员价格','coupon/memberprice','',1,'admin',0),(75,42,'每日秒杀','coupon/seckillsession','',1,'job',0);
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
预期效果
下面为具体实现。
抽取三级分类组件-category.vue
不仅仅属性组管理需要用到三级分类菜单,还有规格参数、销售属性等都要用到。因此,将三级分类菜单抽取成一个组件
在src\views\modules\common
下创建三级分类公用组件category.vue
注意: created() { this.getMenuList(); },
一定要在created函数中调用获取页面初始化所需的数据方法,否则页面始终显示无数据,千万要加上!
<template>
<div>
<el-tree :data="menu" :props="defaultProps" node-key="catId" ref="menuTree">
</el-tree>
</div>
</template>
<script>
export default {
data() {
//这里存放数据
return {
menu: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
//方法集合
methods: {
getMenuList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get',
}).then(({ data }) => { // 1.使用解构即{},将data对象解构出来
console.log("获取三级分类数据:", data);
// 2.将data中的data赋值给menu
this.menu = data.data;
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenuList();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {
},
beforeCreate() { }, //生命周期 - 创建之前
beforeMount() { }, //生命周期 - 挂载之前
beforeUpdate() { }, //生命周期 - 更新之前
updated() { }, //生命周期 - 更新之后
beforeDestroy() { }, //生命周期 - 销毁之前
destroyed() { }, //生命周期 - 销毁完成
activated() { }, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped></style>
属性分组 attrgroup.vue
创建attrgroup.vue
在src\views\modules\product
文件夹下创建 attrgroup.vue
,首行输入vue
,回车,构建基本模块
我们想要实现的效果是左边是三级分类显示,右边是属性组表格显示 。因此,我们需要使用布局组件实现左右布局
参考:
- gutter属性:用于设置每列之间的间隙
- 每一行最多有24列
将三级分类菜单设置为6列,将表格设置为18列
<template>
<div>
<el-row :gutter="20">
<el-col :span="6">
三级分类
</el-col>
<el-col :span="18">
属性组表格
</el-col>
</el-row>
</div>
</template>
导入三级分类组件
<template>
<div>
<el-row :gutter="20">
<el-col :span="6">
<category></category>
</el-col>
<el-col :span="18">
属性组表格
</el-col>
</el-row>
</div>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
import Category from '../common/category.vue';
export default {
//import引入的组件需要注入到对象中才能使用
components: { Category },
props: {},
data() {
//这里存放数据
return {
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {
},
beforeCreate() { }, //生命周期 - 创建之前
beforeMount() { }, //生命周期 - 挂载之前
beforeUpdate() { }, //生命周期 - 更新之前
updated() { }, //生命周期 - 更新之后
beforeDestroy() { }, //生命周期 - 销毁之前
destroyed() { }, //生命周期 - 销毁完成
activated() { }, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped></style>
页面效果
属性组表格实现
把前端生成的attrgroup-add-or-update.vue
复制到src\views\modules\product
下
attrgroup.vue
引入公共组件AttrgroupAddOrUpdate
:import AttrgroupAddOrUpdate from './attrgroup-add-or-update.vue';
attrgroup.vue
剩余内容参照代码生成的attrgroup.vue
进行复制,完整内容如下:
<template>
<el-row :gutter="20">
<el-col :span="6">
<category></category>
</el-col>
<el-col :span="18">
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button v-if="isAuth('product:attrgroup:save')" type="primary"
@click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:attrgroup:delete')" type="danger" @click="deleteHandle()"
:disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle"
style="width: 100%;">
<el-table-column type="selection" header-align="center" align="center" width="50">
</el-table-column>
<el-table-column prop="attrGroupId" header-align="center" align="center" label="分组id">
</el-table-column>
<el-table-column prop="attrGroupName" header-align="center" align="center" label="组名">
</el-table-column>
<el-table-column prop="sort" header-align="center" align="center" label="排序">
</el-table-column>
<el-table-column prop="descript" header-align="center" align="center" label="描述">
</el-table-column>
<el-table-column prop="icon" header-align="center" align="center" label="组图标">
</el-table-column>
<el-table-column prop="catelogId" header-align="center" align="center" label="所属分类id">
</el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
<template slot-scope="scope">
<el-button type="text" size="small"
@click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.attrGroupId)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
:current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</el-col>
</el-row>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
import Category from '../common/category.vue';
import AddOrUpdate from './attrgroup-add-or-update.vue';
export default {
//import引入的组件需要注入到对象中才能使用
components: { Category, AddOrUpdate },
props: {},
data() {
//这里存放数据
return {
dataForm: {
key: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
};
},
//方法集合
methods: {
// 获取数据列表
getDataList() {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/attrgroup/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'key': this.dataForm.key
})
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 0
}
this.dataListLoading = false
})
},
// 每页数
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
// 当前页
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
// 多选
selectionChangeHandle(val) {
this.dataListSelections = val
},
// 新增 / 修改
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
// 删除
deleteHandle(id) {
var ids = id ? [id] : this.dataListSelections.map(item => {
return item.attrGroupId
})
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/attrgroup/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
})
})
}
}
}
</script>
<style scoped></style>
页面效果
父子组件传递数据
要实现的效果:我们点击页面左侧三级分类叶子结点,页面右侧表格就能显示关联叶子结点的属性组
实现逻辑:通过使用父子组件来实现此功能,父子组件通过事件进行交互,事件会携带数据。这里子组件就是common下的category.vue,父组件就是attrgroups.vue
参考:Tree树形组件->Events->node-click
子组件发送事件
src\views\modules\common\category.vue
<el-tree
:data="menu"
:props="defaultProps"
node-key="catId"
ref="menuTree"
@node-click="nodeClick"> //节点被点击时的回调
</el-tree>
methods: {
//共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。
nodeClick(data, Node, component) {
//this.$emit 是 Vue.js 中的一个方法,它可以用于子组件向父组件传递事件.
//'node-click'为事件名,后几个参数为要传送的data
this.$emit('node-click', data, Node, component);
console.log("子组件categroy向父组件传递参数为:", data,Node,component);
},
}
父组件接收事件
src\views\modules\product\attrgroup.vue
<el-col :span="6">
<category @node-click="treeNodeClick"></category>
</el-col>
//方法集合
methods: {
//子组件传给父组件的参数
treeNodeClick(data, node, component) {
console.log("父组件接收到子组件传递过来的数据为:", data, node, component);
}
}
查看事件传递的数据
获取分类属性组
接口文档
GET /product/attrgroup/list/{catelogId}
请求参数
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为'//检索关键字
}
响应数据
{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"attrGroupId": 0, //分组id
"attrGroupName": "string", //分组名
"catelogId": 0, //所属分类
"descript": "string", //描述
"icon": "string", //图标
"sort": 0 //排序
"catelogPath": [2,45,225] //分类完整路径
}]
}
}
代码实现
com.gyz.cfmall.product.controller.AttrGroupController#list
@Autowired
private AttrGroupService attrGroupService;
/**
* 获取分类属性分组
*/
@RequestMapping("/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params, @PathVariable("catelogId") Long catelogId) {
PageUtils page = attrGroupService.queryPage(params, catelogId);
return R.ok().put("page", page);
}
com.gyz.cfmall.product.service.impl.AttrGroupServiceImpl#queryPage(java.util.Map<java.lang.String,java.lang.Object>, java.lang.Long)
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
//select * from pms_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
String key = (String) params.get("key");
//构造查询条件
QueryWrapper<AttrGroupEntity> queryWrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(key)) {
queryWrapper.and(obj -> {
obj.eq("attr_group_id", key).or().like("attr_group_name", key);
});
}
//如果传过来的三级分类id为0,就查询所有数据
if (catelogId == 0) {
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), queryWrapper);
return new PageUtils(page);
} else {
queryWrapper.eq("catelog_id", catelogId);
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), queryWrapper);
return new PageUtils(page);
}
}
测试
前后端联调
src\views\modules\product\attrgroup.vue
修改前端获取分类属性组的请求地址:
// 获取数据列表
getDataList() {
this.dataListLoading = true
this.$http({
//注意用飘号才能将catId结构出来
url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),
//省略其它代码.....
})
},
设置catId的默认值:
data() {
//这里存放数据
return {
catId: 0,
//省略其它代码.....
};
},
获取catId值:必须是三级分类,才能显示属性
//方法集合
methods: {
//子组件传给父组件的参数
treeNodeClick(data, node, component) {
console.log("父组件接收到子组件传递过来的数据为:", data, node, component);
if (node.level == 3) {
this.catId = data.catId;
this.getDataList();
}
}
}
组件创建完成就要触发getDataList函数
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getDataList();
},
测试:Mock两条数据
查看页面效果