You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mca-project-mall/01-课件资料/01-基础课件/04-业务开发-基础业务-分类管理.md

1251 lines
42 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 业务开发-基础业务-分类管理
启动renren-fast如果出现如下错误
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/1d48e1d389314ed6a23aa3906a70d635.png)
> -Djps.track.ap.dependencies=false
添加相关配置即可
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/4dfbd643eb504e5f9b003548bc4e9ee3.png)
## 分类管理
### 1.后端分类接口
JDK8特性https://blog.csdn.net/qq_38526573/category_11113126.html
在后端服务中我们需要查询出所有的三级分类信息并将这些信息组合为有父子关系的数据所以首先我们需要在对应的entity中添加关联字段 `childrens`
```java
/**
* 当前类别所拥有的所有的子类
*/
@TableField(exist = false)
private List<CategoryEntity> childrens;
```
然后我们在service中完成对应的数据处理的逻辑,具体实现逻辑参考注释
```java
/**
* 查询所有的类别数据,然后将数据封装为树形结构,便于前端使用
*
* @param params
* @return
*/
@Override
public List<CategoryEntity> queryPageWithTree(Map<String, Object> params) {
// 1.查询所有的商品分类信息
List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
// 2.将商品分类信息拆解为树形结构【父子关系】
// 第一步遍历出所有的大类 parent_cid = 0
List<CategoryEntity> list = categoryEntities.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0)
.map(categoryEntity -> {
// 根据大类找到多有的小类 递归的方式实现
categoryEntity.setChildrens(getCategoryChildrens(categoryEntity,categoryEntities));
return categoryEntity;
}).sorted((entity1, entity2) -> {
return (entity1.getSort() == null ? 0 : entity1.getSort()) - (entity2.getSort() == null ? 0 : entity2.getSort());
}).collect(Collectors.toList());
// 第二步根据大类找到对应的所有的小类
return list;
}
/**
* 查找该大类下的所有的小类 递归查找
* @param categoryEntity 某个大类
* @param categoryEntities 所有的类别数据
* @return
*/
private List<CategoryEntity> getCategoryChildrens(CategoryEntity categoryEntity
, List<CategoryEntity> categoryEntities) {
List<CategoryEntity> collect = categoryEntities.stream().filter(entity -> {
// 根据大类找到他的直属的小类
return entity.getParentCid() == categoryEntity.getCatId();
}).map(entity -> {
// 根据这个小类递归找到对应的小小类
entity.setChildrens(getCategoryChildrens(entity, categoryEntities));
return entity;
}).sorted((entity1, entity2) -> {
return (entity1.getSort() == null ? 0 : entity1.getSort()) - (entity2.getSort() == null ? 0 : entity2.getSort());
}).collect(Collectors.toList());
return collect;
}
```
CategoryService中同步定义对应的接口方法
```java
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryEntity> queryPageWithTree(Map<String, Object> params);
}
```
然后在CategoryController中新增对应处理的方法
```java
@GetMapping("/listTree")
public R listTree(@RequestParam Map<String, Object> params){
List<CategoryEntity> list = categoryService.queryPageWithTree(params);
return R.ok().put("data", list);
}
```
启动服务访问测试
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/a9a7ea6c73e045fbbda8b46da7c2a728.png)
### 2. 前端服务串联
#### 2.1 新增菜单
首先我们新增一个 `商品系统`的目录
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/a840495293af4a4d9c63ace48c98c0cf.png)
按照上图的操作完成即可
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/57c20806d3a847b2a41c7bd726ff7756.png)
然后在商品系统下添加 `类别管理`的菜单
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/fb3ef682fe894b1d86108c8789f7cb89.png)
对应的三级分类的页面 product/catagory--> src/views/modules/product/category.vue
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/71ff00256fb045ef9bc8d1b0fecaed81.png)
#### 2.2 类别数据
ElementUI官网https://element.eleme.cn/#/zh-CN
第一步展示静态的数据直接从ElementUI官网拷贝Tree相关案例代码
```html
<template>
<div>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</div>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
data: [{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1'
}]
}]
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1'
}]
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1'
}]
}]
}, {
label: '一级 3',
children: [{
label: '二级 3-1',
children: [{
label: '三级 3-1-1'
}]
}, {
label: '二级 3-2',
children: [{
label: '三级 3-2-1'
}]
}]
}],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
}
}
};
</script>
<style>
</style>
```
页面效果
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/c09d8df688bd4cc7829d91449339128c.png)
第二步:动态获取后台服务的数据
```html
<template>
<div>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</div>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
data: [],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
methods: {
getCategory(){
this.$http({
url: this.$http.adornUrl('/product/category/listTree'),
method: 'get'
}).then(({data}) => {
console.log("成功获取的类别数据:",data.data)
this.data = data.data
})
},
handleNodeClick(data) {
console.log(data);
}
},created(){
this.getCategory();
}
};
</script>
<style>
</style>
```
访问三级分类数据并没有得到我们期望的结果。出现了404错误:`http://localhost:8080/renren-fast/product/category/listTree`
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/eaf443630b344d62bee2860640570e11.png)
针对这个错误提示我们需要通过网关服务来实现统一的路由处理
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/5ff7e802a4fc49c2962955ccb8419e35.png)
修改了前端统一的后端服务地址为路由服务后
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/01ca56cfcff3424b9a7e7592062fab39.png)
访问后这个验证码出不来了
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/96ef047456104c9ea03db7d04053669c.png)
验证码出不来的原因是请求的地址:`http://localhost:8070/captcha.jpg?uuid=a496be9e-d916-4f3e-813d-d396c13a8b87` 跑到网关服务获取验证码了这里网关服务就应该要将这个请求路由到renren-fast服务中。
首先renren-fast服务没有在注册中心中注册网关发现不了先注册renren-fast服务
在renren-fast中依赖commons
```xml
<dependency>
<groupId>com.msb.mall</groupId>
<artifactId>mall-commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
```
添加注册中心和配置中心相关的信息
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/601413e8170f4c83b303890692fbd88b.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/02701c98317545e8a1988010447d6aa0.png)
然后放开注册中心
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/d3ead1be16684b1198e999cb941d6373.png)
最后启动服务提示如下错误
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/0da303100335438d938ca152efe534eb.png)
原因是因为SpringBoot我们把版本升级到了2.4.12那么validation默认被移除了我们需要收到的添加依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.4.12</version>
</dependency>
```
启动服务,注册成功
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/f3c731e5c2114afea7ea7b241e1a20e4.png)
解决验证码图片不显示的问题我们需要在网关服务中添加对renren-fast服务访问的路由
```yml
# 注册中心的信息
spring:
application:
name: mall-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.56.100:8848
gateway:
routes:
- id: route1
uri: http://www.baidu.com
predicates:
- Query=url,baidu
- id: route2
uri: http://www.jd.com
predicates:
- Query=url,jd
- id: app_route
uri: lb://renren-fast
predicates:
- Path=/app/**
filters:
- RewritePath=/app/(?<segment>/?.*), /renren-fast/$\{segment}
# localhost:8070/app/captcha.jpg -->
# localhost:8080/app/captcha.jpg localhost:8080/renren-fast/captcha.jpg
# 指定注册中心的服务端口
server:
port: 8070
```
然后测试访问验证码出现了503的错误
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/884cfbd6f2a24eb28f44fbb88161959a.png)
出现503错误的原因是Gateway网关服务中会根据loadbanlance负载均衡路由到renren-fast但是缺少了对应的依赖在Gateway服务中添加即可
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
```
#### 2.3 跨域问题
同源策略
> 由于浏览器的同源策略,即属于不同域的页面之间不能相互访问各自的页面内容
> `注`:同源策略,单说来就是同协议,同域名,同端口
```xml
URL 说明 是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js 同一域名下 允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js 同一域名下不同文件夹 允许
http://www.a.com:8000/a.js
http://www.a.com/b.js 同一域名,不同端口 不允许
http://www.a.com/a.js
https://www.a.com/b.js 同一域名,不同协议 不允许
http://www.a.com/a.js
http://70.32.92.74/b.js 域名和域名对应ip 不允许
http://www.a.com/a.js
http://script.a.com/b.js 主域相同,子域不同 不允许
http://www.a.com/a.js
http://a.com/b.js 同一域名,不同二级域名(同上) 不允许cookie这种情况下也不允许访问
http://www.cnblogs.com/a.js
http://www.a.com/b.js 不同域名 不允许
```
跨域网站介绍https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/ed7a4937cb7c4bff8d3391994762c18f.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/8cb50f6ea7274bb888f345e0d9701dc1.png)
对于跨域问题的解决我们统一在Gateway中设定。注意删除掉在renren-fast中配置的跨域问题
```java
package com.msb.mall.gateway.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.CorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class MallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
// 配置跨域的信息
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
// SpringBoot升级到2.4.0 之后需要使用该配置
configuration.addAllowedOriginPattern("*");
configuration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",configuration);
return new CorsWebFilter(source);
}
}
```
然后登录操作即可
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/5c541e1f2aa040fe95b10c18e9464020.png)
#### 2.4 查看类别数据
首先需要在Gateway中配置商品服务的路由信息同时要注意配置规则的先后顺序
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/6b5e0ce0412c43ef9406bcc12a5be8ce.png)
然后服务端响应的数据的字段要在Vue文件中显示的对应才能正确的显示
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/c878cb4097c74531b11ce0183891eb20.png)
访问测试
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/d7c8a3b11dae4bec83ced283ecff415b.png)
### 3 删除类别
1> 先完成类型页面的基础处理
添加相关的 `添加` `删除`按钮和复选框按钮以及相关的代码逻辑
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/1a7a8a99a3914b93b9df158c0295fc62.png)
对应的Vue代码
```html
<template>
<div>
<el-tree :data="data" :props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="data.catLevel <= 2" type="text" size="mini" @click="() => append(data)">
添加
</el-button>
<el-button v-if="data.childrens.length == 0" type="text" size="mini" @click="() => remove(node, data)">
删除
</el-button>
</span>
</span>
</el-tree>
</div>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
data: [],
defaultProps: {
children: "childrens",
label: "name",
},
};
},
methods: {
getCategory() {
this.$http({
url: this.$http.adornUrl("/product/category/listTree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取的类别数据:", data.data);
this.data = data.data;
});
},append(data) {
console.log("添加",data)
},
remove(node, data) {
console.log("删除",data,node)
}
},
created() {
this.getCategory();
},
};
</script>
<style>
</style>
```
2> 实现后台服务逻辑删除的操作
在实际开发中针对数据删除这块我们一般都会采用逻辑删除的方法来操作。在本项目中我们可以通过mybatis-Puls中提供的逻辑删除方式来实现
mybatis-plus:https://mp.baomidou.com/guide/
首先配置全局的逻辑删除逻辑
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/403a7263cc934aa28d84fe75475bd623.png)
然后在entity的字段中显示的标明
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/95f292c5780a423286472638a19a6448.png)
然后我们就可以在service中继续使用delete相关的方法来操作了
```java
/**
* 逻辑批量删除操作
* @param ids
*/
@Override
public void removeCategoryByIds(List<Long> ids) {
// TODO 1.检查类别数据是否在其他业务中使用
// 2.批量逻辑删除操作
baseMapper.deleteBatchIds(ids);
}
```
然后通过postman测试
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/19605e51b8dc44909b878a6fb2a1b3aa.png)
然后在数据库中反应的效果
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/fea4812cd2f54445b858e1fcb8c1d993.png)
3> 串联前后端的逻辑
要实现该效果首先需要通过ajax异步的来提交请求到后端服务
```javascript
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
});
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys=[node.parent.data.catId]
} else {
this.$message.error(data.msg);
}
```
然后删除需要必要的提示信息
```javascript
this.$confirm(`是否确认删除【${data.name}】记录?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
// 传递的数据
let ids = [data.catId];
// 把删除的请求提交到后台服务
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
});
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys=[node.parent.data.catId]
} else {
this.$message.error(data.msg);
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
console.log("删除", data, node);
```
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/ee3feda8d14645a7a078858d1d3d5cbf.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/b6771cadc21748139d462ef80901fbf4.png)
最后删除成功数据后Tree数据刷新及对应的Tree的默认展示父节点信息
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/32c340907a3845b0b5ba8d8e2f0aa772.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/1734048d725d4dc9a195d365d3e20f60.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/2764953ded1648bfbf320442fa83c041.png)
到此,三级分类数据的删除操作搞定
### 4.新增类别
后台的添加逻辑已经有了,我们只需要在前端项目中完成新增的相关页面逻辑就可以了
对话框页面
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/e65831992bf9461ebaefdcc3b624579d.png)
点击后弹出页面
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/e4f4c6240136421a8ef98bbdb11862b7.png)
修改新增类别相关的数据
```javascript
append(data) {
this.dialogVisible = true; // 打开弹出窗口
// console.log("添加", data);
this.categoryForm.parentCid = data.catId;// 添加的类别对应的父菜单
this.categoryForm.catLevel = data.catLevel + 1; // 设置添加类别的层级
this.categoryForm.showStatus = 1; // 菜单的显示状态 1 显示 0 被删除
this.categoryForm.sort = 0; // 排序 默认给 0
}
```
完成表单数据提交的ajax请求
```javascript
addDialog(){
// 添加三级分类的类别信息
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.categoryForm, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "数据添加成功",
type: "success",
});
this.dialogVisible = false; // 关闭弹出窗口
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys = [this.categoryForm.parentCid];
} else {
this.$message.error(data.msg);
}
});
}
```
页面效果
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/d2bb484be9044feda4545d930a4a3cab.png)
添加信息隐藏了一个Bug如果在二级分类下面添加三级分类然后添加的三级分类不能显示。原因在Java后端中的Long类型数据的比较。只需要修改相关代码即可
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/9c5be3474cbf487cb48355aa6a58bdda.png)
原因
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/baf2b3d53691403aa6224dd3b4c4007c.png)
### 5.修改类别
同样的后台逻辑已经实现,我们只需要完善前端业务即可
添加修改按钮
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/8a22d836ad704a3399f32f01238bb0d5.png)
点击更新的按钮弹出对话框
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/43c653a8ffaa43109b8ec4f3d0960a5f.png)
然后修改数据后的更新提交
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/6266a59c40b04c0cb4dd13e34afd82eb.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/96c290b512b94da0ac7bf651a90a545d.png)
### 6. 拖拽功能
首先实现拖拽效果
1> 放开拖拽的功能
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/798f3d33cf054fb880387c398187e70d.png)
也就是我们只需要将 draggable设置为TRUE即可
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/fae3c04d533a40819365c4bacfb436a9.png)
然后就是要控制拖拽的位置是否可行因为我们不能破坏三级分类的规则我们不能拖拽后整个分类变成了4级5级分类等控制的条件是在ElementUI中给你我们提供了一个allow-drop属性该属性接收一个方法方法返回true表示可放置返回false表示不可放置我们要做的就是在这个方法中添加我们的放置逻辑
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/4deca037819e430aac2a95e6576c12d6.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/83950d0da8c64906b2a4c527a90db982.png)
```javascript
// draggingNode 要拖拽的节点
// dropNode 目标节点
// type 参数有三种情况:'prev'、'inner' 和 'next'
allowDrop(draggingNode, dropNode, type){
// 判断拖拽的节点是否可以在该位置放置
console.log("--->",draggingNode, dropNode, type)
// 1.获取当前被拖拽的节点的最大的level
this.countNodeLevel(draggingNode);
let deep = Math.abs(this.maxLevel - draggingNode.level ) + 1;
if(type == 'inner'){
return deep + dropNode.level <= 3;
}
return deep + dropNode.parent.level <= 3;
},
countNodeLevel(node){
// 找到所有子节点最大的level
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]);
}
}
}
```
然后实现拖拽后台数据更新,我们需要在后台添加批量更新的方法:
```java
/**
* 批量修改
*/
@RequestMapping("/updateBatch")
//@RequiresPermissions("product:category:update")
public R updateBatch(@RequestBody CategoryEntity[] category){
//categoryService.updateById(category);
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}
```
前端的数据调整需要更新的数据保存在了this.updateNodes中。注意push方法的使用。添加对应的提交操作的方法
```javascript
editSort(parentId){
// 获取最新的数据回写
this.$http({
url: this.$http.adornUrl(`/product/category/updateBatch`),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "拖拽操作成功",
type: "success",
});
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys = [parentId];
} else {
this.$message.error(data.msg);
}
});
}
```
针对拖拽相关的开关设置还是非常有必要的。通过ElementUI中的Switch就可以搞定。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/1a45a39368c5421db8be41cf973dda24.png)
现在是每次拖拽都需要调用后端的接口来处理,如果拖拽的比较频繁那么对系统的性能肯定会有影响,这时我们可以通过批量提交的方式来实现。先添加批量添加的按钮。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/c68dee11818a46eebbfb62b76a164bf3.png)
然后拖拽确定后点击“批量保存”按钮来实现后端数据处理,点击触发相关的方法:
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/1c6fe39c94354f50a9ac67c252210df2.png)
```javascript
,editSort(){
// 获取最新的数据回写
this.$http({
url: this.$http.adornUrl(`/product/category/updateBatch`),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "拖拽操作成功",
type: "success",
});
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys = this.pCid;
// 提交更新的数据后,重新相关的数据
this.updateNodes=[];
this.maxLevel=0;
console.log("parentCid:",this.pCid)
this.parentCid=[];
} else {
this.$message.error(data.msg);
}
});
}
```
这样整个更新分类的操作就完成了完整的vue代码如下
```html
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽">
</el-switch>
<el-button v-if="draggable" @click="editSort">批量保存</el-button>
<el-tree
:data="data"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
@node-drop="handleDrop"
:default-expanded-keys="expandKeys"
:draggable="draggable"
:allow-drop="allowDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="data.catLevel <= 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="data.childrens.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span>
</el-tree>
<!-- 添加 弹出框 -->
<el-dialog
:title="dialogType?'新增':'更新'"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="categoryForm">
<el-form-item label="类别名称" >
<el-input v-model="categoryForm.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标" >
<el-input v-model="categoryForm.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位" >
<el-input v-model="categoryForm.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="submitForm"
>确 定</el-button
>
</span>
</el-dialog>
</div>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
pCid:[],
draggable:false, // 拖拽功能默认是关闭的
updateNodes:[], // 拖拽节点后,需要更新的节点的节点信息
maxLevel:0,
dialogType:false, // true 添加 false 更新
dialogVisible: false,
categoryForm:{name:null,icon:null,productUnit:null,showStatus:1,sort:0,catId:null,catLevel:1},
data: [],
expandKeys: [],
defaultProps: {
children: "childrens",
label: "name",
},
};
},
methods: {
getCategory() {
this.$http({
url: this.$http.adornUrl("/product/category/listTree"),
method: "get",
}).then(({ data }) => {
this.data = data.data;
});
},
append(data) {
this.dialogVisible = true; // 打开弹出窗口
// console.log("添加", data);
this.categoryForm.parentCid = data.catId;// 添加的类别对应的父菜单
this.categoryForm.catLevel = data.catLevel + 1; // 设置添加类别的层级
this.categoryForm.showStatus = 1; // 菜单的显示状态 1 显示 0 被删除
this.categoryForm.sort = 0; // 排序 默认给 0
// 重置更新的数据
this.categoryForm.catId = null;
this.categoryForm.name = "";
this.categoryForm.icon = "";
this.categoryForm.productUnit = "";
// 更新状态
this.dialogType = true;
},
// draggingNode 要拖拽的节点
// dropNode 目标节点
// type 参数有三种情况:'prev'、'inner' 和 'next'
handleDrop(draggingNode, dropNode, type,event){
// 1. 拖拽节点的父节点需要修改
let parentId = 0;
// 找到拖拽节点对应的所有的兄弟节点
let siblings = null;
if(type == 'inner'){
parentId = dropNode.data.catId;
siblings = dropNode.childNodes;
}else{
parentId = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
}
// 2. 拖拽后节点所在的新的兄弟节点间的排序问题
for(let i = 0 ; i < siblings.length ; i ++){
if(siblings[i].data.catId == draggingNode.data.catId){
// 获取的就是拖拽的那个节点那么我们需要更新parent_cid
// 3. 拖拽后的节点及其节点的的 catLevel 更新问题
let catLevel = draggingNode.level;
if(siblings[i].level != catLevel){
// 拖拽后节点的层级发生了变化,
catLevel = siblings[i].level;
// 递归的方式来更新子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({catId:siblings[i].data.catId,sort:i,parentCid:parentId,catLevel:catLevel})
}else{
this.updateNodes.push({catId:siblings[i].data.catId,sort:i})
}
}
console.log("--->",this.updateNodes)
this.pCid.push(parentId);
// 将需要更新的数据提交到后端服务处理
//this.editSort(parentId);
// 在一次拖拽操作完成后重置相关的数据
//this.updateNodes=[];
//this.maxLevel=0;
},updateChildNodeLevel(node){
if(node.childNodes != null && node.childNodes.length > 0){
for(let i = 0 ; i < node.childNodes.length ; i ++){
var childNode = node.childNodes[i].data;
this.updateNodes.push({catId:node.childNodes.catId,catLevel:node.childNodes[i].level});
// 如果还有子节点,同步的更新处理
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
// draggingNode 要拖拽的节点
// dropNode 目标节点
// type 参数有三种情况:'prev'、'inner' 和 'next'
allowDrop(draggingNode, dropNode, type){
// 判断拖拽的节点是否可以在该位置放置
// 1.获取当前被拖拽的节点的最大的level
this.countNodeLevel(draggingNode);
let deep = Math.abs(this.maxLevel - draggingNode.level ) + 1;
if(type == 'inner'){
return deep + dropNode.level <= 3;
}
return deep + dropNode.parent.level <= 3;
},
countNodeLevel(node){
// 在获取子节点前给maxLevel赋值
this.maxLevel = node.data.catLevel;
// 找到所有子节点最大的level
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]);
}
}
},
addDialog(){
// 添加三级分类的类别信息
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.categoryForm, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "数据添加成功",
type: "success",
});
this.dialogVisible = false; // 关闭弹出窗口
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys = [this.categoryForm.parentCid];
} else {
this.$message.error(data.msg);
}
});
},editDialog(){
// 更新三级分类的类别信息
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(this.categoryForm, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "数据更新成功",
type: "success",
});
this.dialogVisible = false; // 关闭弹出窗口
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys = [this.categoryForm.parentCid];
} else {
this.$message.error(data.msg);
}
});
},editSort(){
// 获取最新的数据回写
this.$http({
url: this.$http.adornUrl(`/product/category/updateBatch`),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "拖拽操作成功",
type: "success",
});
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys = this.pCid;
// 提交更新的数据后,重新相关的数据
this.updateNodes=[];
this.maxLevel=0;
console.log("parentCid:",this.pCid)
this.parentCid=[];
} else {
this.$message.error(data.msg);
}
});
},edit(data){
this.dialogType = false;
// 获取最新的数据回写
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "post",
data: this.$http.adornData(this.categoryForm, false),
}).then(({ data }) => {
console.log("获取的数据",data)
// 表单数据回写
this.categoryForm.name = data.category.name;
this.categoryForm.productUnit = data.category.productUnit;
this.categoryForm.icon = data.category.icon;
this.categoryForm.catLevel = data.category.catLevel;
this.categoryForm.parentCid = data.category.parentCid;
// 填充更新数据的id
this.categoryForm.catId = data.category.catId;
this.categoryForm.showStatus = 1;
this.categoryForm.sort = 0;
// 更新类别信息的方法
this.dialogVisible=true; // 打开更新的窗口
});
},submitForm(){
console.log("dialogType",this.dialogType)
// 判断当前的操作是更新还是删除
if(this.dialogType){
// 添加操作
this.addDialog();
}else{
// 更新操作
this.editDialog();
}
},
remove(node, data) {
this.$confirm(`是否确认删除【${data.name}】记录?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
// 传递的数据
let ids = [data.catId];
// 把删除的请求提交到后台服务
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
});
// 重新加载所有的菜单数据
this.getCategory();
// 设置默认展开的父节点信息
this.expandKeys = [node.parent.data.catId];
} else {
this.$message.error(data.msg);
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
console.log("删除", data, node);
},
},
created() {
this.getCategory();
console.log("data数据:",this.data)
},
};
</script>
<style>
</style>
```
### 7.批量删除
因为类别比较多,在操作的时候也可以能面临批量删除的操作。我们在案例中来实现,首先创建批量删除的按钮,同时绑定操作的方法。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/49c552148da744719e2f5fc21a881a37.png)
然后就是触发删除的方法后我们需要找到所有的被选中的节点。结合ElementUI官方的信息来获取。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/689b0b23a33c40c38aa3fe6e9bb6cbb6.png)
然后批量删除的方法
```javascript
batchDelete() {
let catIds = [];
// 批量的删除类别
let checkedNodes = this.$refs.tree.getCheckedNodes(false, false);
// 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 }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
});
// 重新加载所有的菜单数据
this.getCategory();
} else {
this.$message.error(data.msg);
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
```
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1637300624000/5ea6e1ce71634e97b3082a7936a24420.png)