一、通过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
*/
@TableId
private 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的实现,通过泛型实现
@Autowired
CategoryDao categoryDao;
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);
return new PageUtils(page);
}
@Override
public 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: 88
spring:
application:
name: gulimall-gateway
cloud:
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_router
uri: https://www.baidu.com # 要转发的地址
predicates:
- Query=url,baidu
- id: qq_router
uri: https://www.qq.com
predicates:
- Query=url,qq
- id: admin_router
uri: 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
*/
@Configuration
public class GulimallCorsConfiguration {
@Bean
public 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: 88
spring:
application:
name: gulimall-gateway
cloud:
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_router
uri: https://www.baidu.com # 要转发的地址
predicates:
- Query=url,baidu
- id: qq_router
uri: https://www.qq.com
predicates:
- Query=url,qq
# 注意product_route 跟 admin_router的顺序,网关在进行断言时,会根据断言的先后顺序进行操作
# 所以精确断言需要写在模糊断言之前
- id: product_route
uri: lb://gulimall-product # lb://服务名 进行负载均衡转发
predicates: # 对包含有/api/gulimallproduct的url请求进行路径重写
- Path=/api/gulimallproduct/**
filters:
- RewritePath=/api/(?<segment>.*), /$\{segment}
- id: admin_router
uri: 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: root
password: 10086
url: jdbc:mysql://192.168.190.135:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: gulimall-product # 注册到nacos中的服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos注册中心的地址
# 配置mybatis-plus
mybatis-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方法
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 1、检查当前删除的菜单是否被别的地方引用
// 我们一般不使用直接删除的方式,而是使用逻辑删除,即不是删除数据库中的记录,而是不显示,showStatus
baseMapper.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-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<!-- 添加按钮 只有1、2级菜单能添加子菜单-->
<el-button
v-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-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog
v-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-input
v-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,add
category: {
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-switch
v-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-checkbox
node-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-button
v-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-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog
v-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-input
v-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,add
category: {
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、当前节点最新的父节点id
let pCid = 0; // 拖拽完后,当前节点父节点
let siblings = null; // 拖拽完后,当前节点兄弟
// 当以after和before方式拖拽,其父节点就是dropNode的父节点
// 其兄弟节点就是dropNode的父节点的childNodes
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId; // 当前节点的父节点
siblings = dropNode.parent.childNodes;
} else {
// 这是为inner的情况
// 当以inner方式拖拽,其父节点就是dropNode
// 其兄弟节点就是dropNode的childNodes
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
this.pCid.push(pCid);
//2、当前拖拽节点的最新顺序(求兄弟节点)
//3、当前拖拽节点的最新层级
for (let i = 0; i < siblings.length; i++) {
// 如果遍历到当前正在拖拽的节点,则需要修改其父节点ID
if (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>