1.商品服务-三级分类
1.1三级分类介绍
1.2查询三级分类查询-递归树型结构数据获取
1.2.1导入数据pms_catelog.sql到数据表pms_category
1.2.2一次性查出所有分类及子分类
1.2.2.1修改CategoryController.java
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
1.2.2.2CategoryEntity
新增子分类属性
@TableField(exist = false) //表示数据库表中不存在
private List<CategoryEntity> children;
1.2.2.3实现接口CategoryService.java的listwithTree()
@Override
public List<CategoryEntity> listWithTree() {
//1、查出所有分类。baseMapper来自于继承的ServiceImpl<>类,跟CategoryDao一样用法
List<CategoryEntity> entities = baseMapper.selectList(null);
//2、递归组装多级分类的树形结构。先过滤得到一级分类,再加工递归设置一级分类的子孙分类,再排序,再收集
List<CategoryEntity> level1Menus = entities.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == 0)
.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());
return level1Menus;
}
//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all){
List<CategoryEntity> children = all.stream()
.filter(CategoryEntity -> CategoryEntity.getParentCid().equals(root.getCatId()))
.map(categoryEntity -> {
//1、递归查找子菜单
categoryEntity.setChildren(getChildren(categoryEntity, all));
return categoryEntity;
})
.sorted((menu1, menu2) -> {
//2、菜单排序、判空处理空指针异常
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
})
.collect(Collectors.toList());
return children;
}
1.2.2.4启动product服务测试结果
打开F12访问:localhost:10000/product/category/list/tree
1.3后台页面管理三级分类
1.启动后台管理系统renren-fast
2.启动前端renren-fast-vue;终端输入命令:npm run dev
1.3.1新增目录-商品系统
系统管理中的菜单管理:点击新增按钮
点击确定后刷新页面可见
数据库中可查到
1.3.2新增菜单-分类维护
1.3.3前端展示三级分类
需求:在左侧点击【商品系统-分类维护】,希望在此展示3级分类。可以看到
url是http://localhost:8001/#/product-category
填写的菜单路由是product/category
对应的视图是src/view/modules/product/category.vue (renren-fast-vue文件)
1.3.3.1创建product/category
视图
1.创建
src/views/mudules/product/category.vue
2.在
category.vue中
创建vue模板 (输入vue加回车,可快速生成模板。)3.elementui看如何使用多级目录
Element组件网址
进入Element官网,进入组件,找到Tree树形控件
模仿用法写入vue
<!-- -->
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
return {
data: [],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
//获取后台数据
getMenus(){
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(data=>{
console.log("成功了获取到菜单数据....", data)
})
}
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
//创建完成时,就调用getMenus函数
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {
},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped>
</style>
1.3.3.2测试
localhost:8001/#/product-category
F12刷新页面
发现404请求端口问题:
他是给8080端口发的请求,而我们的商品服务在10000端口。我们以后还会同时发向更多的端口,所以需要配置网关,前端只向网关发送请求,然后由网关自己路由到相应端口。
1.3.3.3查看在哪定义的请求路径
复制 http://localhost:8080/renren-fasthttp://localhost:8080/renren-fast
ctrl + shift + f 全局搜索
1.3.3.4修改请求到网关
请求地址修改为
刷新测试
刷新,发现验证码出不来。
验证码请求路径问题:
分析原因:前端给网关发验证码请求,但是验证码请求在renren-fast服务里,所以要想使验证码好使,需要把renren-fast服务注册到服务中心,并且由网关进行路由
1.3.3.5renren-fast注册到nacos
renren-fast修改pom文件依赖gulimall-common
修改renren-fast配置文件application.yml
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
启动类添加注解 @EnableDiscoveryClient
1.3.3.6配置网关
需求: http://localhost:88/api/xxx 转发--> http://renren-fast:8080/renren-fast/xxx
例如:http://localhost:88/api/captcha.jpg 转发--> http://renren-fast:8080/renren-fast/captcha.jpg
修改gulimall-gateway配置文件application.yml
- id: admin_route
uri: lb://renren-fast #负载均衡
predicates:
-Path=/api/** #断言
##前端项目,/api
## http://localhost:88/api/captcha.jpg
## http://renren-fast:8080/api/captcha.jpg
测试验证发现请求地址为:http://renren-fast:88/api/captcha.jpg
再次修改gulimall-gateway配置文件application.yml,添加下列配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
# 路由id,自定义,只要唯一即可
- id: admin_route
# uri路由的目标地址。lb就是负载均衡,后面跟服务名称。
uri: lb://renren-fast
#断言工厂的Path,请求路径必须符合指定规则
predicates:
- Path=/api/** # 把所有api开头的请求都转发给renren-fast
#局部过滤器。回顾默认过滤器default-filters是与routes同级
filters:
#路径重写。逗号左边是原路径,右边是重写后的路径
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
# 默认规则, 请求过来:http://localhost:88/api/captcha.jpg 转发--> http://renren-fast:8080/renren-fast/captcha.jpg
刷新测试,出现跨域错误
1.4解决跨域问题
1.4.1跨域和同源策略
跨域:指的是浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略
:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域问题
1.4.2跨域流程
预检请求:options
1.4.3解决跨域方法1:使用nginx反向代理为同一域
1.4.4解决跨域方法2:配置当前请求允许跨域
CORS:CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制。
Access-Control-Allow-Origin : 支持哪些来源的请求跨域
Access-Control-Allow-Method : 支持那些方法跨域
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 :表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将失效。
在网关中统一配置
在gulimall-gateway的gulimall.gateway.config包下中新建配置类
//这个包别导错了,有一个很像的。
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class GulimallCorsConfiguration{
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration= new CorsConfiguration();
//1、配置跨域
// 允许跨域的请求头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOriginPattern("*");
//注释的这句会报错。因为当allowCredentials为真时,allowedorigin不能包含特殊值"*",因为不能在"访问-控制-起源“响应头中设置该值。
//corsConfiguration.addAllowedOrigin("*");//这句会报错
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置,两个*号就是可以匹配包含0到多个/的路径
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
启动测试
发现有多个值错误
注释掉renren-fast中的跨域,不然会有一些重复的规则导致跨域失败:
io.renren/config/CorsConfig
重启测试成功
1.5前端树形展示三级分类数据
1.5.1网关配置product路由
需求:
localhost:88/api/product/xx -> localhost:10000/product/xx
网关路由配置
# 精确的路由要放在/api的admin_route上面
- id: product_route
uri: lb://gulimall-product #路由的目标地址
predicates: # 路由断言。也就是判断请求是否符合路由规则的条件。
- Path=/api/product/** # 路径断言。这个是按照路径匹配,只要以/api/product/开头就符合要求
filters: #局部过滤器
- RewritePath=/api/(?<segment>.*),/$\{segment} #重写路径,/api/xx过滤成/xx
1.5.2将product服务注册并配置到nacos
1.新建bootstrap.properties
2.nacos新建命名空间
3.修改application.yml注册到注册中心
4.启动类添加注解@EnableDiscoveryClient开启服务注册发现功能
启动测试数据显示如下
1.5.3修改前端组件category.vue
需求:将显示的数据展示到页面上
想要的数据在data.data里,需要将其解构出来
结构代码修改如下
category.vue代码
<!-- -->
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
return {
menus: [],
defaultProps: {
children: 'children', //子节点
label: 'name' //name属性作为标签的值,展示出来
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus(){
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data})=>{
console.log("成功了获取到菜单数据....", data.data)
this.menus = data.data;
})
}
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {
},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped>
</style>
启动product测试
http://localhost:10000/product/category/list/tree
1.6逻辑删除三级分类
1.6.1分类的新增和删除
需求:
- 在每一个菜单后面添加append, delete
- 点击按钮时,不进行菜单的打开合并,仅点击箭头时展示子分类:expand-on-click-node="false"
- 当没有子菜单时,才可以显示delete按钮;当为一级、二级菜单时,才显示append按钮。使用v-if判断是否显示
- <el-tree>添加多选框show-checkbox
- 设置node-key=""标识每一个节点的不同
<!-- -->
<template>
<el-tree
:data="menus"
show-checkbox
:props="defaultProps"
@node-click="handleNodeClick"
:expand-on-click-node="false"
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{
{ node.label }}</span>
<span>
<el-button
type="text"
v-if="node.level <= 2"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
type="text"
v-if="node.childNodes.length == 0"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
return {
menus: [],
defaultProps: {
children: "children", //子节点
label: "name", //name属性作为标签的值,展示出来
},
};
},
methods: {
handleNodeClick(data) {},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功了获取到菜单数据....", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
},
remove(node, data) {
console.log("remove", node, data);
},
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.6.2后端:逻辑删除
实体类成员变量加上
@TableLogic(value = "0", delval = "1")
注解
CategoryEntity注解逻辑删除:
/**
* 是否显示[0-不显示,1显示]
*本项目使用的是category表的show_status字段,逻辑删除值正好相反
*状态为1表示未删除,状态为0表示删除。
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
修改CategoryController.java
/**
删除
@RequestBody:获取请求体,必须发送POST请求
*SpringMvc自动将请求体的数据(json),转为对应的对象
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//1、检查当前删除的菜单,是否被别的地方引用
categoryService.removeMenuByIds(Arrays.asList(catId));
return R.ok();
}
实现类CategoryServicelmpl.java实现CategoryService.java接口方法(Ctrl+N快速检索)
@Override
public void removeMenuByIds(List<Long>asList){
//TODO 1、检查当前删除的菜单,是否被别的地方引用
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
修改日志级别
logging:
level:
com.xmh.guliamll.product: debug
在测试工具发送POST请求
http://localhost:88/api/product/category/delete
查看控制台打印语句,发现是update操作
1.6.3前端:逻辑删除
需求:
- 编写前端remove方法,实现向后端发送请求
- 点击delete弹出提示框,是否删除这个节点: elementui中MessageBox 弹框中的确认消息添加到删除之前
- 删除成功后有消息提示: elementui中Message 消息提示
- 删除后刷新页面后,分类应该保持之前展开状态: el-tree组件的default-expanded-keys属性,默认展开。 每次删除之后,把删除菜单的父菜单的id值赋给默认展开值即可。
1.6.3.1创建请求代码块快捷命令
文件->首选项->用户片段->新建全局代码片段,文件名vue.code-snippets(snippets译为代码片段,片段)
"http-get请求": {
"prefix": "httpget",
"body":[
"this.\\$http({",
"url: this.\\$http.adornUrl(''),",
"method:'get',",
"params:this.\\$http.adornParams({})",
"}).then(({data})=>{",
"})"
],
"description":"httpGET请求"
},
"http-post请求":{
"prefix":"httppost",
"body":[
"this.\\$http({",
"url:this.\\$http.adornUrl(''),",
"method:'post',",
"data: this.\\$http.adornData(data, false)",
"}).then(({data})=>{ })"
],
"description":"httpPOST请求"
}
//在<el-tree>中设置默认展开属性,绑定给expandedKey
:default-expanded-keys="expandedKey"
//data中添加属性,删除后给它赋值父节点id,令树形控件刷新后展开
expandedKey: [],
//完整的remove方法
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单。删除后给它赋值父节点id,令树形控件刷新后展开
this.expandedKey = [node.parent.data.catId]
});
})
.catch(() => {});
},
1.7新增三级分类
1.7.1前端:新增
需求:
- 点击append新增弹出对话框,输入分类名称
- 确定/取消后,关闭对话框
- 确定后发送post请求,成功后刷新前端展示页面
创建对话框组件el-dialog:
对话框标签el-dialog放在el-tree标签上下都是可以的,主要是visible.sync属性控制对话框的显示和隐藏
<!--对话框组件-->
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<el-form :model="categroy">
<el-form-item label="分类名称">
<el-input v-model="categroy.name" 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="addCategory">确 定</el-button>
</span>
</el-dialog>
//data中新增数据
//按照数据库格式声明的数据。注意category属性用来接收输入框的参数,需要赋值默认属性,包括父id,层级、展示状态为1,排序值是0
categroy: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
//判断是否显示对话框
dialogVisible: false,
//修改append方法,新增addCategory方法
//点击append后,计算category属性,显示对话框
append(data) {
console.log("append", data);
this.dialogVisible = true;
this.categroy.parentCid = data.catId;
this.categroy.catLevel = data.catLevel * 1 + 1;
},
//点击确定后,发送post请求
//成功后显示添加成功,展开刚才的菜单
addCategory() {
console.log("提交的数据", this.categroy);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.categroy, false),
}).then(({ data }) => {
this.$message({
message: "添加成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.categroy.parentCid];
this.dialogVisible = false;
});
1.8修改三级分类
1.8.1后端:修改
后端,修改“回显方法”结果对象的键为"data"
product模块的CategoryController
1.8.2前端:修改
需求:
- 新增Edit按钮:复制之前的append
- updata方法是由id进行更新的,所以
data
中的category
中新增catId
- 增加、修改的同时修改图标和计量单位,所以data的
category
新增inco,productUnit
- 新建
edit
方法,用来绑定Edit按钮。新建editCategory
方法,用来绑定对话框的确定按钮- 复用对话框(新增、修改)
- 新建方法
submitData
,与对话框的确定按钮进行绑定,在方法中判断,如果dialogType==add
调用addCategory(),如果dialogType==edit
调用editCategory()- data数据中新增
title
,绑定对话框的title,用来做提示信息。判断dialogType
的值,动态提示信息- 修改回显必须发请求,而非直接从实参中获取。防止多个人同时操作,对话框中的回显的信息应该是由数据库中读出来的:点击Edit按钮,发送httpget请求。
- 成功之后发送提示消息,展开刚才的菜单
- 编辑之后,再点击添加,发现会回显刚才编辑的信息。所以在
append
方法中重置回显的信息
新增修改共享表单对话框,所以还要修改添加方法,第一步初始化数据
提交修改表单时,不能像新增一样把携带初始化值的category直接提交上去
方法一:局部更新,只解构出表单里的属性封装成对象,再提交(推荐)
方法二:全量更新,回显时,把其他数据库字段也赋值
<!--编辑按钮-->
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<!--可复用的对话框-->
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%">
<el-form :model="categroy">
<el-form-item label="分类名称">
<el-input v-model="categroy.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="categroy.inco" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input
v-model="categroy.productUnit"
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>
//data, 新增了title、dialogType。 categroy中新增了inco、productUnit、catId
data() {
return {
title: "",
dialogType: "",
categroy: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
inco: "",
productUnit: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children", //子节点
label: "name", //name属性作为标签的值,展示出来
},
};
//方法
//绑定对话框的确定按钮,根据dialogType判断调用哪个函数
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
//绑定Edit按钮,设置dialogType、title,从后台读取数据,展示到对话框内
edit(data) {
this.dialogType = "edit";
this.title = "修改菜单";
this.dialogVisible = true;
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
console.log(data);
this.categroy.catId = data.data.catId;
this.categroy.name = data.data.name;
this.categroy.inco = data.data.inco;
this.categroy.productUnit = data.data.productUnit;
});
},
//绑定对话框的确定按钮,向后台发送更新请求,传过去想要修改的字段
editCategory() {
var { catId, name, inco, productUnit } = this.categroy;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, inco, productUnit }, false),
}).then(({ data }) => {
this.$message({
message: "修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.categroy.parentCid];
this.dialogVisible = false;
});
},
//点击append按钮,清空编辑之后的回显数据
append(data) {
this.dialogType = "add";
this.title = "添加菜单";
console.log("append", data);
this.dialogVisible = true;
this.categroy.parentCid = data.catId;
this.categroy.catLevel = data.catLevel * 1 + 1;
this.categroy.name = "",
this.categroy.inco = "",
this.categroy.productUnit = ""
},
1.9修改层级关系,实现拖拽效果
1.9.1前端:拖拽
实现逻辑:
1.在
<el-tree>
中加入属性draggable
表示节点可拖拽2.在<el-tree>中加入属性:allow-drop="allowDrop",拖拽时判定目标节点能否被放置。
3.allowDrop有三个参数draggingNode表示拖拽的节点,dropNode表示拖拽到哪个节点,type表示拖拽的类型’prev’、‘inner’ 和 ‘next’,表示拖拽到目标节点之前、里面、之后
注意:函数实现判断,拖拽后必须保持数型的三层结构。
- 节点的深度 = 最深深度 - 当前深度 + 1
- 当拖拽节点拖拽到目标节点的内部,要满足: 拖拽节点的深度 + 目标节点的深度 <= 3
- 当拖拽节点拖拽的目标节点的两侧,要满足: 拖拽节点的深度 + 目标节点的父节点的深度 <= 3
<!--el-tree中添加属性-->
draggable
:allow-drop="allowDrop"
// data中新增属性,用来记录当前节点的最大深度
maxLevel: 1,
//新增方法
allowDrop(draggingNode, dropNode, type) {
console.log("allowDrag:", draggingNode, dropNode, type);
//节点的最大深度
this.countNodeLevel(draggingNode.data);
console.log("level:", this.maxLevel);
//当前节点的深度
let deep = (this.maxLevel - draggingNode.data.catLevel) + 1;
console.log(deep)
if (type == "inner"){
return (deep + dropNode.level) <= 3;
}else{
return (deep + dropNode.parent.level) <= 3;
}
},
//计算当前节点的最大深度
countNodeLevel(node) {
//找到所有的子节点,求出最大深度
if (node.children != null && node.children.length > 0){
for (let i = 0; i < node.children.length; i++){
if (node.children[i].catLevel > this.maxLevel){
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
拖拽后的数据收集
- 在<el-tree>中加入属性@node-drop="handleDrop",表示拖拽事件结束后触发事件handleDrop,handleDrop共四个参数,draggingNode:被拖拽节点对应的 Node; dropNode:结束拖拽时最后进入的节点; dropType:被拖拽节点的放置位置(before、after、inner);ev:event
- 拖拽可能影响的节点的数据:parentCid、catLevel、sort
- data中新增updateNodes,把所有要修改的节点都传进来。
- 要修改的数据:拖拽节点的parentCid、catLevel、sort
- 要修改的数据:新的兄弟节点的sort (把新的节点收集起来,然后重新排序)
- 要修改的数据:子节点的catLevel
//el-tree中新增属性,绑定handleDrop,表示拖拽完触发
@node-drop="handleDrop"
//data 中新增数据,用来记录需要更新的节点(拖拽的节点(parentCid、catLevel、sort),拖拽后的兄弟节点(sort),拖拽节点的子节点(catLevel))
updateNodes: [],
//新增方法
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新父节点的id
let pCid = 0;
//拖拽后的兄弟节点,分两种情况,一种是拖拽到两侧,一种是拖拽到内部
let sibings = null;
if (dropType == "before" || dropType == "after") {
pCid = dropNode.parent.data.catId == undefined ? 0: dropNode.parent.data.catId;
sibings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
sibings = dropNode.childNodes;
}
//2、当前拖拽节点的最新顺序
//遍历所有的兄弟节点,如果是拖拽节点,传入(catId,sort,parentCid,catLevel),如果是兄弟节点传入(catId,sort)
for (let i = 0; i < sibings.length; i++) {
if (sibings[i].data.catId == draggingNode.data.catId){
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (sibings[i].level != draggingNode.level){
//当前节点的层级发生变化
catLevel = sibings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(sibings[i]);
}
this.updateNodes.push({catId:sibings[i].data.catId, sort: i, parentCid: pCid, catLevel:catLevel});
}else{
this.updateNodes.push({catId:sibings[i].data.catId, sort: i});
}
}
//每次拖拽后把数据清空,否则要修改的节点将会越拖越多
this.updateNodes = [],
this.maxLevel = 1,
}
// 修改拖拽节点的子节点的层级
updateChildNodeLevel(node){
if (node.childNodes.length > 0){
for (let i = 0; i < node.childNodes.length; i++){
//遍历子节点,传入(catId,catLevel)
var cNode = node.childNodes[i].data;
this.updateNodes.push({catId:cNode.catId,catLevel:node.childNodes[i].level});
//处理子节点的子节点
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [pCid];
});
1.9.2批量拖拽功能
- 添加开关,控制拖拽功能是否开启
- 每次拖拽都要和数据库交互,不合理。批量拖拽过后,一次性保存
<!--添加拖拽开关和批量保存按钮-->
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽"
>
</el-switch>
<el-button v-if="draggable" size="small" round @click="batchSave"
>批量保存</el-button
>
//data中新增数据
pCid:[], //批量保存过后要展开的菜单id
draggable: false, //绑定拖拽开关是否打开
//修改了一些方法,修复bug,修改过的方法都贴在下面了
//点击批量保存按钮,发送请求
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = this.pCid;
});
this.updateNodes = [];
},
//
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新父节点的id
let pCid = 0;
let sibings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
sibings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
sibings = dropNode.childNodes;
}
//2、当前拖拽节点的最新顺序
for (let i = 0; i < sibings.length; i++) {
if (sibings[i].data.catId == draggingNode.data.catId) {
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (sibings[i].level != draggingNode.level) {
//当前节点的层级发生变化
catLevel = sibings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(sibings[i]);
}
this.updateNodes.push({
catId: sibings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: sibings[i].data.catId, sort: i });
}
}
this.pCid.push(pCid);
console.log(this.pCid)
//3、当前拖拽节点的最新层级
//console.log("updateNodes", this.updateNodes)
//拖拽之后重新置1
this.maxLevel = 1;
},
// 修改拖拽判断逻辑
allowDrop(draggingNode, dropNode, type) {
console.log("allowDrag:", draggingNode, dropNode, type);
this.maxLevel = draggingNode.level;
//节点的最大深度
this.countNodeLevel(draggingNode);
console.log("maxLevel:", this.maxLevel);
//当前节点的深度
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("level",deep);
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
//计算深度时,用当前数据,而不是数据库中的数据。因为可能还没来得及保存到数据库
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]);
}
}
},
1.9.3后端:拖拽
思路:
- 在后端编写批量修改的方法
update/sort
- 前端发送post请求,把要修改的数据发送过来
- 提示信息,展开拖拽节点的父节点
CategoryController批量修改功能
//批量修改,参数要传数组,不能传list
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] category){
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}
测试批量修改功能
http://localhost:88/api/product/category/update/sort
1.10批量删除分类
1.10.1后端:批量删除
1.10.2前端:批量删除
1.新增删除按钮
<el-button type="danger" size="small" @click="batchDelete" round>批量删除</el-button>
<!--eltree中新增属性,用作组件的唯一标示-->
ref="menuTree"
2.批量删除方法
batchDelete(){
let catIds = [];
let catNames = [];
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
for (let i = 0; i < checkedNodes.length; i++){
catIds.push(checkedNodes[i].catId);
catNames.push(checkedNodes[i].name);
}
this.$confirm(`是否批量删除【${catNames}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
this.$http({
url:this.$http.adornUrl('/product/category/delete'),
method:'post',
data: this.$http.adornData(catIds, false)
}).then(({data})=>{
this.$message({
message: "菜单批量删除成功",
type: "success",
});
this.getMenus();
})
}).catch(()=>{
});
},
1.11前端category.vue最终代码
<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-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>
<el-button
v-if="node.level <=2"
type="text"
size="mini"
@click="() => append(data)"
>新增</el-button>
<el-button type="text" size="mini" @click="edit(data)">编辑</el-button>
<el-button
v-if="node.childNodes.length==0"
type="text"
size="mini"
@click="() => remove(node, data)"
>删除</el-button>
</span>
</span>
</el-tree>
<el-dialog
: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-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" 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"
}
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
batchDelete() {
let catIds = [];
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
console.log("被选中的元素", checkedNodes);
for (let i = 0; i < checkedNodes.length; i++) {
catIds.push(checkedNodes[i].catId);
}
this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false)
}).then(({ data }) => {
this.$message({
message: "菜单批量删除成功",
type: "success"
});
this.getMenus();
});
})
.catch(() => {});
},
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
message: "菜单顺序等修改成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = this.pCid;
this.updateNodes = [];
this.maxLevel = 0;
// this.pCid = 0;
});
},
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
let pCid = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
this.pCid.push(pCid);
//2、当前拖拽节点的最新顺序,
for (let i = 0; i < siblings.length; i++) {
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: catLevel
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//3、当前拖拽节点的最新层级
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);
// this.maxLevel
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;
}
},
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.dialogType = "edit";
this.title = "修改分类";
this.dialogVisible = true;
//发送请求获取当前节点最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
//请求成功
console.log("要回显的数据", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
/**
* parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
*/
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
//提交修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
//提交添加三级分类
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
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("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("remove", node, data);
}
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>