✅P71-72_属性分组-前端组件抽取-父子组件交互

gong_yz大约 15 分钟谷粒商城

名词介绍

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决定最终价格

销售属性

每个分类下的商品共享规格参数和销售属性,只是有些商品不一定要用这个分类下全部的属性。

说明:

  1. 属性是以三级分类组织起来的
  2. 规格参数中有些是可以提供检索的
  3. 规格参数也是基本属性,他们具有自己的分组
  4. 属性的分组也是由三级分类组织起来的
  5. 属性名确定的,但是值每一个商品不同来决定的

规格参数

例如:手机分类下的产品都会有一个规格与包装属性,也就是我们常说的规格参数

规格参数由 **属性组 **和 **属性 **组成

左侧第一栏即属性组,中间一栏即属性,右侧一栏即属性的值。

【属性分组-规格参数-销售属性-三级分类】关联关系

手机是一级分类,它下面又有属性组,每个属性组又有各自的属性

SPU-SKU-属性表

像网络、像素一般是固定不可选的所以是SPU属性

而内存、容量、颜色等可选的就为SKU销售属性


前端组件抽取&父子组件交互

创建属性分组菜单

系统菜单脚本

sys_menus.sqlgulimall_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引入公共组件AttrgroupAddOrUpdateimport 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两条数据

查看页面效果