一、通过Java8 Stream API 获取商品三级分类数据
电商项目的分类有多级(一级、二级、三级)。
我们通过数据库来维护三级分类的数据(对应gulimall_pms库中的pms_category表)。
向表中插入数据,pms_catelog.sql
1.1 修改gulimall-product模块
1.1.0 修改CategoryEntity
package com.atguigu.gulimall.gulimallproduct.entity;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import java.io.Serializable;import java.util.Date;import java.util.List;import lombok.Data;/*** 商品三级分类** @author mrlinxi* @email mrzheme@vip.qq.com* @date 2021-12-07 19:17:01*/@Data@TableName("pms_category")public class CategoryEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 分类id*/@TableIdprivate Long catId;/*** 分类名称*/private String name;/*** 父分类id*/private Long parentCid;/*** 层级*/private Integer catLevel;/*** 是否显示[0-不显示,1显示]*/private Integer showStatus;/*** 排序*/private Integer sort;/*** 图标地址*/private String icon;/*** 计量单位*/private String productUnit;/*** 商品数量*/private Integer productCount;@TableField(exist = false) // 这个注解表示改属性不是表里面的private List<CategoryEntity> children;}
1.1.1 修改CategoryController
这里给CategoryController添加一个方法,功能是返回树形结构的所有分类
/*** 查出所有分类以及子分类,以树形结构组装起来*/@RequestMapping("/list/tree")//@RequiresPermissions("gulimallproduct:category:list")public R list(){// categoryService的list方法可以查到所有分类// 但是我们希望以树形结构获得所有分类// List<CategoryEntity> list = categoryService.list();// 我们需要一个listWithTree方法来生成分类的树形结构List<CategoryEntity> entities = categoryService.listWithTree();return R.ok().put("data", entities);}
1.1.2 CategoryService
package com.atguigu.gulimall.gulimallproduct.service;import com.baomidou.mybatisplus.extension.service.IService;import com.atguigu.common.utils.PageUtils;import com.atguigu.gulimall.gulimallproduct.entity.CategoryEntity;import java.util.List;import java.util.Map;/*** 商品三级分类** @author mrlinxi* @email mrzheme@vip.qq.com* @date 2021-12-07 19:17:01*/public interface CategoryService extends IService<CategoryEntity> {PageUtils queryPage(Map<String, Object> params);List<CategoryEntity> listWithTree();}
1.1.3 CategoryServiceImpl
package com.atguigu.gulimall.gulimallproduct.service.impl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;import java.util.Map;import java.util.stream.Collectors;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.atguigu.common.utils.PageUtils;import com.atguigu.common.utils.Query;import com.atguigu.gulimall.gulimallproduct.dao.CategoryDao;import com.atguigu.gulimall.gulimallproduct.entity.CategoryEntity;import com.atguigu.gulimall.gulimallproduct.service.CategoryService;@Service("categoryService")public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {// 因为CategoryServiceImpl继承了ServiceImpl// 所以这里可以直接使用ServiceImpl里面的baseMapper// baseMapper就是一个CategoryDao的实现,通过泛型实现@AutowiredCategoryDao categoryDao;@Overridepublic PageUtils queryPage(Map<String, Object> params) {IPage<CategoryEntity> page = this.page(new Query<CategoryEntity>().getPage(params),new QueryWrapper<CategoryEntity>());return new PageUtils(page);}@Overridepublic List<CategoryEntity> listWithTree() {// 1、查出所有分类// List<CategoryEntity> entities = baseMapper.selectList(null);List<CategoryEntity> entities = categoryDao.selectList(null);// 2、组装成树形父子结构// 2.1 找到所有的一级分类// 这里用到了java8的新特性Stream API 以及lambda表达式// 首先通过对象的.stream()方法获取Stream的实例// 然后通过filter过滤器选取流中的元素// map接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素List<CategoryEntity> level1Menu = entities.stream().filter(categoryEntity -> {return categoryEntity.getParentCid() == 0; // 过滤分类级别,保留1级分类}).map(menu -> {menu.setChildren(getChildren(menu, entities)); // 找到当前分类的子分类return menu;}).sorted((menu1, menu2) -> { // 按sort属性排序return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());}).collect(Collectors.toList()); // 最后将处理完的流转为一个list的形式return level1Menu;}/*** 递归查找所有菜单的子菜单*/private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {List<CategoryEntity> children = all.stream().filter(categoryEntity -> {// 找到all里面父菜单是root的元素return categoryEntity.getParentCid().equals(root.getCatId());}).map(categoryEntity -> {// 找到all里面父菜单是root的元素的子菜单categoryEntity.setChildren(getChildren(categoryEntity, all));return categoryEntity;}).sorted((menu1, menu2) -> {// 按sort属性排序return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());}).collect(Collectors.toList());return children;}}
这里对stream进行排序的时候不能直接使用自然排序,因为我们需要按照sort属性来排序,所以需要自定一个Comparator。
访问http://localhost:10000/gulimallproduct/category/list/tree 测试一下
二、前端路由规范/跨域之三级分类
我们先启动后台的renren-fast模块,再启动前端项目renrne-fast-vue
启动renren-fast的时候可能报错:You aren’t using a compiler supported by lombok, so lombok will not work and has been disabled.
解决方法
2.1 商品系统新增侧边导航目录

添加的这个目录信息会记录在gulimall_admin数据库的sys_menu表中。
2.2 商品系统新增分类维护菜单

同样,分类维护菜单信息也会记录在sys_menu表中。我们在分类维护的菜单中维护商品的三级分类。
2.3 Vue脚手架路由规范
当点击侧边栏目录新增的分类维护时,会跳转到 /product-category,对应新增菜单设置的路由 product/categor。菜单路由中填写的’/‘会变成’-‘
页面:对应前端项目中 src/views/modules/product/category.vue 页面组件(但是目前没有);我们可以看看角色管理(sys-role)页面,其对应的是前端项目中的src\views\modules\sys\role.vue页面组件
所以,我们在src\views\modules下新建一个product文件夹,在里面创建category.vue页面组件,显示三级目录。
通过使用element-ui的Tree 树形控件来实现三级菜单的展示树形控件
三、配置网关和跨域配置
本节会解决一个前端向多个后端服务请求的问题(API网关),此外还有网关服务与前端前后分离的跨域问题。
3.1 前端工程配置API网关作为唯一接口
修改模板的内容,自定义一个getMenu()函数,在创建完成时调用这个函数。
可以发现报404错误,这是因为请求的URL是写死的,我们根据不同的需求,动态的发送不同的请求。我们在vs中搜索http://localhost:8080/renren-fast
,在static\config\indes.js文件中定义了api接口请求地址。为了动态的发送请求,我们将这里改成后端网关的url地址(端口号88)。
改了之后我们刷新页面发现需要重新登录,而且验证码都没有了,因为前端项目将验证码的url发送给了网关,但网关找不到验证码的后端服务(renren-fast)
因此,我们需要将renren-fast注册到注册中心。
3.2 将renren-fast接入网关服务配置
上面renren-fast-vue配置了请求为网关地址,那么原来应该发送到renren-fast的请求发送到了网关,所以这里需要请求通过网关转发到renren-fast。
3.2.1 renren-fast注册进nacos
在renren-fast中添加gulimall-common的依赖(common中包含了nacos作为注册/配置中心的依赖)
<dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version></dependency>
然后修改renrne-fast的application.yml,设置注册到nacos,然后在主启动类上加上@EnableDiscoveryClient注解,重启。
重启报错:(视频里面是报错com.google.gson不存在,但是我的是报错methodnot exist,可能是项目版本问题)网上搜了下,手动添加guava依赖,并将版本换成31.0-jre(30之前都存在漏洞)重启成功!
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.0-jre</version></dependency>
3.2.2 网关增加路由断言转发到不同服务
在gulimall-gateway中添加路由规则,这里我们约定从前端过来的请求在url上均有/api前缀
server:port: 88spring:application:name: gulimall-gatewaycloud:nacos:discovery:server-addr: localhost:8848 #nacos注册中心的地址gateway:routes:#Query A 参数有A就行,Query B,C 参数B的值为C即可 C可以是正则表达式#实现针对于“http://localhost:88/hello?url=baidu”,转发到“https://www.baidu.com/hello”,#针对于“http://localhost:88/hello?url=qq”的请求,转发到“https://www.qq.com/hello”- id: baidu_routeruri: https://www.baidu.com # 要转发的地址predicates:- Query=url,baidu- id: qq_routeruri: https://www.qq.compredicates:- Query=url,qq- id: admin_routeruri: lb://renren-fast # lb://服务名 进行负载均衡转发predicates: # 我们约定,前端项目来的请求均带有api的前缀- Path=/api/**filters:- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment} #路径重写,替换/api前缀,加上/renren-fast

但是在网关配置转发后还是报404,这是因为网关转发的时候会转发成http://localhost:8080/api/captcha.jpg,而我们需要的转发url是http://localhost:8080/renren-fast/captcha.jpg。因此需要用路由中的路径重写`RewritePath`。[springcloudgateway路径重写文档](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-rewritepath-gatewayfilter-factory)
现在可以显示验证码了,但是登陆还是报错:

被CORS策略(Access-Control-Allow-Origin,访问控制允许来源)阻塞,这是由跨域引起的错误。
3.3 网关服务配置跨域
那么什么是跨域呢?跨域指的是浏览器不能执行其他网站的脚本。他是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略:是指协议、域名、端口都要相同,其中有一个不同都会产生跨域
解决方式:
- 使用nginx部署为同一域

- 配置当次允许跨域请求
添加响应头
- Access-Control-Allow-Origin:支持哪些来源的请求跨域
- Access-Control-Allow-Methods:支持哪些方法跨域
- Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含 cookie
- Access-Control-Expose-Headers:跨域请求暴露的字段
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段: Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如 果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
- Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无 须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果 该首部字段的值超过了最大有效时间,将不会生效。
因为每个服务都可能产生跨域,不可能每个服务都进行修改,而我们是通过网关代理给其他服务,所以直接在网关中统一配置跨域即可。
3.3.1 gulimall-gateway配置跨域
我们新建config包,然后新建一个跨域配置类
package com.atguigu.gulimall.gulimallgateway.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.reactive.CorsWebFilter;import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;/*** @author mrlinxi* @create 2022-03-03 14:13*/@Configurationpublic class GulimallCorsConfiguration {@Beanpublic CorsWebFilter corsWebFilter() {// 这个是CorsConfigurationSource的一个实现类// reactive包下的,因为我们使用的响应式编程UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();// 新建一个Cors的配置对象,在这个配置对象中指定跨域配置CorsConfiguration corsConfiguration = new CorsConfiguration();//1、配置跨域corsConfiguration.addAllowedHeader("*"); //允许哪些头进行跨域corsConfiguration.addAllowedMethod("*"); // 允许所有请求方式进行跨域corsConfiguration.addAllowedOrigin("*"); // 允许任意请求来源 进行跨域corsConfiguration.setAllowCredentials(true); // 允许携带cookie进行跨域// /**表示所有路径,我们对所有路径都用corsConfiguration这个跨域配置source.registerCorsConfiguration("/**", corsConfiguration);return new CorsWebFilter(source);}}
如果springboot版本高于2.4的话 corsConfiguration.addAllowedOrigin(““);要替换成corsConfiguration.addAllowedOriginPattern(““);
Option域前请求成功,但是真实的登陆请求存在报错:
Access to XMLHttpRequest at ‘http://localhost:88/api/sys/login’ from origin ‘http://localhost:8001‘ has been blocked by CORS policy: The ‘Access-Control-Allow-Origin’ header contains multiple values ‘http://localhost:8001, http://localhost:8001‘, but only one is allowed.
这是因为renren-fast服务中自己配置了跨域,与gateway重复了,这里会导致请求头添加重复,导致跨域失败。所以需要将其注释掉。删除 src/main/java/io/renren/config/CorsConfig.java 中的配置内容。
3.3.2 测试
3.4 配置网关转发到gulimall-product
我们前面成功登陆了前端页面,在浏览三级目录的时候可以发现报错:
这是因为我们将前端的请求都转到了renrne-fast,而这里需要转到gulimall-product微服务。
注意:转发到renrne-fast的断言是/api/,转发到gulimall-product的断言是/api/gulimallproduct/,在设置路由规则的时候,较精确的断言要放在模糊的前面,网关是根据配置的顺序依次判断,如果模糊断言在精确断言之前,那么精确断言会失效。
server:port: 88spring:application:name: gulimall-gatewaycloud:nacos:discovery:server-addr: localhost:8848 #nacos注册中心的地址gateway:routes:#Query A 参数有A就行,Query B,C 参数B的值为C即可 C可以是正则表达式#实现针对于“http://localhost:88/hello?url=baidu”,转发到“https://www.baidu.com/hello”,#针对于“http://localhost:88/hello?url=qq”的请求,转发到“https://www.qq.com/hello”- id: baidu_routeruri: https://www.baidu.com # 要转发的地址predicates:- Query=url,baidu- id: qq_routeruri: https://www.qq.compredicates:- Query=url,qq# 注意product_route 跟 admin_router的顺序,网关在进行断言时,会根据断言的先后顺序进行操作# 所以精确断言需要写在模糊断言之前- id: product_routeuri: lb://gulimall-product # lb://服务名 进行负载均衡转发predicates: # 对包含有/api/gulimallproduct的url请求进行路径重写- Path=/api/gulimallproduct/**filters:- RewritePath=/api/(?<segment>.*), /$\{segment}- id: admin_routeruri: lb://renren-fast # lb://服务名 进行负载均衡转发predicates: # 我们约定,前端项目来的请求均带有api的前缀- Path=/api/**filters:- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}# http://localhost:88/api/captcha.jpg?uuid=xxxx http://localhost:8080/renren-fast/captcha.jpg?uuid=xxxx
在nacos新建gulimall-product的命名空间,用于存储其配置文件(视频到这里并没有把配置放在nacos中);视频里面将gulimall-product注册到了nacos,这个我在最开始就弄了,所以就不过多介绍了。
四、三级分类——增删改
4.1 删除
我们需要实现三级菜单分类的删除功能,即前端点击删除,发送请求到guliamll-product模块,后端模块进行删除操作。
逆向工程生成的代码可以完成删除,但是在删除前,我们需要判断当前删除的菜单是否在其他地方被引用,所以我们需要修改逆向工程生成的代码。同时我们在删除记录时,并不是使用物理删除(删除数据库中的记录),而是使用逻辑删除(不删除原始记录,只是不显示,数据库表中show_status属性)。这里需要使用到mybatis-plus的逻辑删除。
4.1.1 配置逻辑删除
修改guliamll-product,配置mybatis-plus的逻辑删除:mybatis-plus逻辑删除文档
- 在application.yml文件中加入逻辑删除的配置
- 实体字段上加@TableLogic注解(如果第一步配置了logic-delete-field(3.3.0版本生效),则该步骤可以省略)
注:@TableLogic注解,可以设定
,value表示逻辑不删除时的值,delval表示逻辑删除时候的值,不指定会从配置文件获取,指定会优先使用指定的值。
application.yml
# 配置数据源spring:datasource:username: rootpassword: 10086url: jdbc:mysql://192.168.190.135:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverapplication:name: gulimall-product # 注册到nacos中的服务名cloud:nacos:discovery:server-addr: localhost:8848 #nacos注册中心的地址# 配置mybatis-plusmybatis-plus:mapper-locations: classpath:/mapper/**/*.xml # 配置sql映射目录global-config:db-config:id-type: auto # 配置主键自增logic-delete-field: showStatus # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)# 我们数据库里面未删除是1,已删除是0,所以需要换一下,在@TableLogic指定也可以logic-delete-value: 0 # 逻辑已删除值(默认为 1)logic-not-delete-value: 1 # 逻辑未删除值(默认为 0)server:port: 10000
CategoryEntity
@TableLogic(value = "1", delval = "0")private Integer showStatus;
4.1.2 修改删除逻辑
修改gulimall-product的CategoryController
在delete方法中,新定义一个removeMenuByIds,改方法在删除前会检查是否存在引用。
/*** 删除* @RequestBody: 获取请求体,只有post请求才有请求体* SpringMVC自动将请求体的数据(json),转为对应的对象*/@RequestMapping("/delete")//@RequiresPermissions("gulimallproduct:category:delete")public R delete(@RequestBody Long[] catIds){///categoryService.removeByIds(Arrays.asList(catIds));categoryService.removeMenuByIds(Arrays.asList(catIds));return R.ok();}
在这里添加方法后CategoryService跟CategoryServiceImpl都需要进行相应的修改,CategoryService直接生成一个方法即可。
CategoryServiceImpl实现removeMenuByIds方法
@Overridepublic void removeMenuByIds(List<Long> asList) {//TODO 1、检查当前删除的菜单是否被别的地方引用// 我们一般不使用直接删除的方式,而是使用逻辑删除,即不是删除数据库中的记录,而是不显示,showStatusbaseMapper.deleteBatchIds(asList);}
这里我们暂时还没有完成引用检查,所以用了个TODO,使用TODO注释后,在Idea下面的TODO栏可以看到代办事项:
4.1.3 批量删除(视频顺序是在4.3节之后)
4.2 添加&修改
我们想达到的目的是,点击Append/Edit按钮,弹出一个对话框,在对话框中输入我们要添加/修改的分类目录信息,随后即可添加至目录。(需要用到el的dialog)添加/修改主要是前端的修改,后端代码不需要进行修改。

前端代码:
<template><div><!-- 使用el的树形控件 --><el-tree:data="menus":props="defaultProps":expand-on-click-node="false"show-checkboxnode-key="catId":default-expanded-keys="expandedKey"><span class="custom-tree-node" slot-scope="{ node, data }"><span>{{ node.label }}</span><span><!-- 添加按钮 只有1、2级菜单能添加子菜单--><el-buttonv-if="node.level <= 2"type="text"size="mini"@click="() => append(data)">Append</el-button><!-- 修改按钮 --><el-button type="text" size="mini" @click="() => edit(data)">Edit</el-button><!-- 删除按钮 没有子菜单的菜单才能进行删除--><el-buttonv-if="node.childNodes.length == 0"type="text"size="mini"@click="() => remove(node, data)">Delete</el-button></span></span></el-tree><el-dialogv-bind:title="title":visible.sync="dialogVisible"width="30%":close-on-click-modal="false"><el-form :model="category"><el-form-item label="分类名称"><el-input v-model="category.name" autocomplete="off"></el-input></el-form-item></el-form><el-form :model="category"><el-form-item label="图标"><el-input v-model="category.icon" autocomplete="off"></el-input></el-form-item></el-form><el-form :model="category"><el-form-item label="计量单位"><el-inputv-model="category.prodcutUnit"autocomplete="off"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="submitData">确 定</el-button></span></el-dialog></div></template><script>//这里可以导入其他文件(比如:组件,工具 js,第三方插件 js,json文件,图片文件等等)//例如:import 《组件名称》 from '《组件路径》';export default {//import 引入的组件需要注入到对象中才能使用components: {},props: {},data() {return {title: "", //用于显示修改/添加dialogType: "", //用于区分是修改对话框还是添加对话框 edit,addcategory: {name: "",parentCid: 0,catLevel: 0,showStatus: 1,sort: 0,productUnit: "",icon: "",catId: null,},dialogVisible: false,menus: [],expandedKey: [],defaultProps: {children: "children",label: "name",},};},methods: {// 获取三级菜单getMenus() {this.$http({// url表示我们的请求地址url: this.$http.adornUrl("/gulimallproduct/category/list/tree"),method: "get",}).then(({ data }) => {console.log("成功获取到菜单数据。。。", data.data);this.menus = data.data;});},// 获取要修改的菜单信息edit(data) {console.log("要修改的数据是:", data);this.title = "修改分类";this.dialogType = "edit";this.dialogVisible = true;// 为了防止回显的数据不是最新的(类似于脏读),发送请求获取当前节点最新的数据this.$http({url: this.$http.adornUrl(`/gulimallproduct/category/info/${data.catId}`),method: "get",}).then(({ data }) => {//请求成功console.log("需要回显的数据", data);// this.category.name = data.category.name;// this.category.catId = data.category.catId;// this.category.icon = data.category.icon;// this.category.productUnit = data.category.productUnit;// this.category.parentCid = data.category.parentCid;// this.category.catLevel = data.category.catLevel;// this.category.sort = data.category.sort;// this.category.showStatus = data.category.showStatus;this.category = data.category; // 可以直接用这种方式,自动解构});},// 获取要添加的菜单信息append(data) {//this.category = {};console.log("append", data);this.title = "添加分类";this.dialogType = "add";this.dialogVisible = true;this.category.parentCid = data.catId;this.category.catLevel = data.catLevel * 1 + 1;this.category.name = "";this.category.catId = null;this.category.icon = "";this.category.productUnit = "";this.category.sort = 0;this.category.showStatus = 1;},// submitData,这个方法根据dialogTyep值调用editCategory跟addCategory方法submitData() {if (this.dialogType == "add") {this.addCategory();}if (this.dialogType == "edit") {this.editCategory();}},// 修改三级分类editCategory() {var { catId, name, icon, productUnit } = this.category; // 通过结构表达式获取当前要修改的属性// var data = {catId: catId, name: name, icon: icon, productUnit: productUnit};var data = { catId, name, icon, productUnit }; // 属性名跟变量名相同可以省略console.log("提交的三级分类数据", this.category);this.$http({url: this.$http.adornUrl("/gulimallproduct/category/update"),method: "post",// 这里data其实可以写成this.category,这样的话后台会将全部字段更新,但是我们只需要修改部分字段。// 所以还是推荐改哪几个字段,传哪几个字段data: this.$http.adornData(data, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单修改成功",type: "success",});// 关闭对话框this.dialogVisible = false;// 刷新出新的菜单this.getMenus();// 设置需要默认展开的菜单this.expandedKey = [this.category.parentCid];});},// 添加三级分类addCategory() {console.log("提交的三级分类数据", this.category);this.$http({url: this.$http.adornUrl("/gulimallproduct/category/save"),method: "post",data: this.$http.adornData(this.category, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单添加成功",type: "success",});// 关闭对话框this.dialogVisible = false;// 刷新出新的菜单this.getMenus();// 设置需要默认展开的菜单this.expandedKey = [this.category.parentCid];});},// 移除三级分类remove(node, data) {var ids = [data.catId];this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {this.$http({url: this.$http.adornUrl("/gulimallproduct/category/delete"),method: "post",data: this.$http.adornData(ids, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单删除成功",type: "success",});// 刷新出新的菜单this.getMenus();// 设置需要默认展开的菜单this.expandedKey = [node.parent.data.catId];});}).catch(() => {});console.log("remove", node, data);},},//计算属性 类似于 data 概念computed: {},//监控 data 中的数据变化watch: {},//方法集合// methods: {},//生命周期 - 创建完成(可以访问当前 this 实例)created() {this.getMenus();},//生命周期 - 挂载完成(可以访问 DOM 元素)mounted() {},beforeCreate() {}, //生命周期 - 创建之前beforeMount() {}, //生命周期 - 挂载之前beforeUpdate() {}, //生命周期 - 更新之前updated() {}, //生命周期 - 更新之后beforeDestroy() {}, //生命周期 - 销毁之前destroyed() {}, //生命周期 - 销毁完成activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发};</script><style lang='scss' scoped>//@import url(); 引入公共 css 类</style>
4.3 拖拽商品目录
在拖拽商品时,涉及到若干个商品目录的更新,我们将需要修改的目录信息,以数组的方式发送给后端商品服务。
在gulimall-product的CategoryController中添加一个批量修改的方法updateSort
/*** 批量修改,前端拖拽菜单后更新菜单信息使用此方法* @param category* @return*/@RequestMapping("/update/sort")//@RequiresPermissions("gulimallproduct:category:update")public R updateSort(@RequestBody CategoryEntity[] category){// 直接通过逆向工程生成的批量修改方法修改categoryService.updateBatchById(Arrays.asList(category));return R.ok();}
前端代码
<template><div><el-switchv-model="draggable"active-text="开启拖拽"inactive-text="关闭拖拽"></el-switch><el-button v-if="draggable" @click="batchSave">批量保存</el-button><el-button type="danger" @click="batchDelete">批量删除</el-button><!-- 使用el的树形控件 显示三级目录 --><el-tree:data="menus":props="defaultProps":expand-on-click-node="false"show-checkboxnode-key="catId":default-expanded-keys="expandedKey":draggable="draggable":allow-drop="allowDrop"@node-drop="handleDrop"ref="menuTree"><span class="custom-tree-node" slot-scope="{ node, data }"><span>{{ node.label }}</span><span><!-- 添加按钮 只有1、2级菜单能添加子菜单--><el-buttonv-if="node.level <= 2"type="text"size="mini"@click="() => append(data)">Append</el-button><!-- 修改按钮 --><el-button type="text" size="mini" @click="() => edit(data)">Edit</el-button><!-- 删除按钮 没有子菜单的菜单才能进行删除--><el-buttonv-if="node.childNodes.length == 0"type="text"size="mini"@click="() => remove(node, data)">Delete</el-button></span></span></el-tree><el-dialogv-bind:title="title":visible.sync="dialogVisible"width="30%":close-on-click-modal="false"><el-form :model="category"><el-form-item label="分类名称"><el-input v-model="category.name" autocomplete="off"></el-input></el-form-item></el-form><el-form :model="category"><el-form-item label="图标"><el-input v-model="category.icon" autocomplete="off"></el-input></el-form-item></el-form><el-form :model="category"><el-form-item label="计量单位"><el-inputv-model="category.prodcutUnit"autocomplete="off"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="submitData">确 定</el-button></span></el-dialog></div></template><script>//这里可以导入其他文件(比如:组件,工具 js,第三方插件 js,json文件,图片文件等等)//例如:import 《组件名称》 from '《组件路径》';export default {//import 引入的组件需要注入到对象中才能使用components: {},props: {},data() {return {pCid: [],draggable: false,updateNodes: [],maxLevel: 0, //统计当前节点的最大层级title: "", //用于显示修改/添加dialogType: "", //用于区分是修改对话框还是添加对话框 edit,addcategory: {name: "",parentCid: 0,catLevel: 0,showStatus: 1,sort: 0,productUnit: "",icon: "",catId: null,},dialogVisible: false,menus: [],expandedKey: [],defaultProps: {children: "children",label: "name",},};},methods: {// 获取三级菜单getMenus() {this.$http({// url表示我们的请求地址url: this.$http.adornUrl("/gulimallproduct/category/list/tree"),method: "get",}).then(({ data }) => {console.log("成功获取到菜单数据。。。", data.data);this.menus = data.data;});},// 批量删除,选定菜点全面的框后批量删除batchDelete() {let delCatIds = [];let checkNodes = this.$refs.menuTree.getCheckedNodes();console.log("被选中需要删除的元素:", checkNodes);for (let i = 0; i < checkNodes.length; i++) {delCatIds.push(checkNodes[i].catId);}this.$confirm(`是否批量删除【${delCatIds}】菜单?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {this.$http({url: this.$http.adornUrl("/gulimallproduct/category/delete"),method: "post",data: this.$http.adornData(delCatIds, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单批量删除成功",type: "success",});// 刷新出新的菜单this.getMenus();});}).catch(() => {});},// 等拖拽全部完成以后,点击批量保存按钮调用保存方法// 以免每拖拽一次,数据库更新一次batchSave() {this.$http({url: this.$http.adornUrl("/gulimallproduct/category/update/sort"),method: "post",data: this.$http.adornData(this.updateNodes, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单顺序等修改成功",type: "success",});// 刷新菜单this.getMenus();// 展开拖拽后的菜单this.expandedKey = this.pCid;this.updateNodes = [];this.maxLevel = 0;// this.pCid = 0;});},// 拖拽成功后触发该函数handleDrop(draggingNode, dropNode, dropType) {console.log("handle drop: ", draggingNode, dropNode, dropType);//1、当前节点最新的父节点idlet pCid = 0; // 拖拽完后,当前节点父节点let siblings = null; // 拖拽完后,当前节点兄弟// 当以after和before方式拖拽,其父节点就是dropNode的父节点// 其兄弟节点就是dropNode的父节点的childNodesif (dropType == "before" || dropType == "after") {pCid =dropNode.parent.data.catId == undefined? 0: dropNode.parent.data.catId; // 当前节点的父节点siblings = dropNode.parent.childNodes;} else {// 这是为inner的情况// 当以inner方式拖拽,其父节点就是dropNode// 其兄弟节点就是dropNode的childNodespCid = dropNode.data.catId;siblings = dropNode.childNodes;}this.pCid.push(pCid);//2、当前拖拽节点的最新顺序(求兄弟节点)//3、当前拖拽节点的最新层级for (let i = 0; i < siblings.length; i++) {// 如果遍历到当前正在拖拽的节点,则需要修改其父节点IDif (siblings[i].data.catId == draggingNode.data.catId) {let catLevel = draggingNode.level;// 如果拖拽节点层级发生变化,那么就需要更新拖拽节点与其子节点的层级if (siblings[i].level != draggingNode.level) {// 修改当前节点层级catLevel = siblings[i].level;// 修改拖拽节点子节点的层级this.updateChildNodeLevel(siblings[i]);}this.updateNodes.push({catId: siblings[i].data.catId,sort: i,parentCid: pCid,catLevel,});} else {this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });}}console.log("updatenodes:", this.updateNodes);},// 更新子节点的层级信息updateChildNodeLevel(node) {if (node.childNodes.length != 0) {for (let i = 0; i < node.childNodes.length; i++) {var cNode = node.childNodes[i].data;this.updateNodes.push({catId: cNode.catId,catLevel: node.childNodes[i].level,});this.updateChildNodeLevel(node.childNodes[i]);}}},// 判断菜单栏是否能够拖拽 因为我们只有三级菜单allowDrop(draggingNode, dropNode, type) {//1、被拖动的当前节点以及所在的父节点总层数不能大于3// 1)判断被拖动的当前节点总层数console.log("allowDrop:", draggingNode, dropNode, type);this.countNodeLevel(draggingNode);// 当前正在拖动节点拥有的深度+父节点所在的深度不大于3即可let deep = Math.abs(this.maxLevel - draggingNode.level + 1);console.log("深度", deep);if (type == "inner") {// console.log(// `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`// );return deep + dropNode.level <= 3;} else {return deep + dropNode.parent.level <= 3;}// return false;},countNodeLevel(node) {// 找到所有子节点,求出最大深度if (node.childNodes != null && node.childNodes.length > 0) {for (let i = 0; i < node.childNodes.length; i++) {if (node.childNodes[i].level > this.maxLevel) {this.maxLevel = node.childNodes[i].level;}this.countNodeLevel(node.childNodes[i]);}}},// 获取要修改的菜单信息edit(data) {console.log("要修改的数据是:", data);this.title = "修改分类";this.dialogType = "edit";this.dialogVisible = true;// 为了防止回显的数据不是最新的(类似于脏读),发送请求获取当前节点最新的数据this.$http({url: this.$http.adornUrl(`/gulimallproduct/category/info/${data.catId}`),method: "get",}).then(({ data }) => {//请求成功console.log("需要回显的数据", data);// this.category.name = data.category.name;// this.category.catId = data.category.catId;// this.category.icon = data.category.icon;// this.category.productUnit = data.category.productUnit;// this.category.parentCid = data.category.parentCid;// this.category.catLevel = data.category.catLevel;// this.category.sort = data.category.sort;// this.category.showStatus = data.category.showStatus;this.category = data.category; // 可以直接用这种方式,自动解构});},// 获取要添加的菜单信息append(data) {//this.category = {};console.log("append", data);this.title = "添加分类";this.dialogType = "add";this.dialogVisible = true;this.category.parentCid = data.catId;this.category.catLevel = data.catLevel * 1 + 1;this.category.name = "";this.category.catId = null;this.category.icon = "";this.category.productUnit = "";this.category.sort = 0;this.category.showStatus = 1;},// submitData,这个方法根据dialogTyep值调用editCategory跟addCategory方法submitData() {if (this.dialogType == "add") {this.addCategory();}if (this.dialogType == "edit") {this.editCategory();}},// 修改三级分类editCategory() {var { catId, name, icon, productUnit } = this.category; // 通过结构表达式获取当前要修改的属性// var data = {catId: catId, name: name, icon: icon, productUnit: productUnit};var data = { catId, name, icon, productUnit }; // 属性名跟变量名相同可以省略 :变量名console.log("提交的三级分类数据", this.category);this.$http({url: this.$http.adornUrl("/gulimallproduct/category/update"),method: "post",// 这里data其实可以写成this.category,这样的话后台会将全部字段更新,但是我们只需要修改部分字段。// 所以还是推荐改哪几个字段,传哪几个字段data: this.$http.adornData(data, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单修改成功",type: "success",});// 关闭对话框this.dialogVisible = false;// 刷新出新的菜单this.getMenus();// 设置需要默认展开的菜单this.expandedKey = [this.category.parentCid];});},// 添加三级分类addCategory() {console.log("提交的三级分类数据", this.category);this.$http({url: this.$http.adornUrl("/gulimallproduct/category/save"),method: "post",data: this.$http.adornData(this.category, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单添加成功",type: "success",});// 关闭对话框this.dialogVisible = false;// 刷新出新的菜单this.getMenus();// 设置需要默认展开的菜单this.expandedKey = [this.category.parentCid];});},// 移除三级分类remove(node, data) {var ids = [data.catId];this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {this.$http({url: this.$http.adornUrl("/gulimallproduct/category/delete"),method: "post",data: this.$http.adornData(ids, false),}).then(({ data }) => {this.$message({showClose: true,message: "菜单删除成功",type: "success",});// 刷新出新的菜单this.getMenus();// 设置需要默认展开的菜单this.expandedKey = [node.parent.data.catId];});}).catch(() => {});console.log("remove", node, data);},},//计算属性 类似于 data 概念computed: {},//监控 data 中的数据变化watch: {},//方法集合// methods: {},//生命周期 - 创建完成(可以访问当前 this 实例)created() {this.getMenus();},//生命周期 - 挂载完成(可以访问 DOM 元素)mounted() {},beforeCreate() {}, //生命周期 - 创建之前beforeMount() {}, //生命周期 - 挂载之前beforeUpdate() {}, //生命周期 - 更新之前updated() {}, //生命周期 - 更新之后beforeDestroy() {}, //生命周期 - 销毁之前destroyed() {}, //生命周期 - 销毁完成activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发};</script><style lang='scss' scoped>//@import url(); 引入公共 css 类</style>

