抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Hello world!

分布式基础概念

微服务

基于业务边界进行服务微化拆分,各个服务器独立部署运行。

集群&分布式&节点

集群是个物理形态,分布式是个工作方式。

分布式是指将不同的业务分布在不同的地方,集群是指将几台服务器集中在一起实现同一业务。

分布式中的每个节点(集群中的一个服务器),都可以做集群,然而集群不一定就是分布式的。

远程调用

在分布式系统中,各个服务可能处于不同主机,但是服务之间不可避免的需要相互调用,即远程调用。

在spring-cloud中使用HTTP+JSON的方式完成远程调用。

负载均衡

在分布式系统中,A服务需要调用B服务,且B服务存在于多台机器中,A服务调用任意一个服务器均可完成功能。但是为了使得每一个服务器不要太忙或者太闲,可以使用负载均衡的调用每一个服务器。

常见的负载均衡算法如下:

  1. 轮询:按顺序,知道最后一个,然后循环。
  2. 最小连接:有限选择连接数最少(压力最小)的服务器。
  3. 散列:根据请求源的IP的散列(HASH)来选择要转发的服务器。

服务注册、发现&注册中心

服务上线后在注册中心注册,从而使得其他服务器或者人能够感知其存在与状态

配置中心

每一个服务器都含有大量的配置,并且每个服务可能部署在多台机器上,所以经常需要变更配置,就可以使用配置中心来集中管理微服务的配置信息。

服务熔断&服务降级

在微服务架构中,微服务之间通过网络进行通讯,当一个服务不可用的时候可能造成雪崩效应。为防止这样的情况,必须要有容错机制来保护服务。

服务熔断:设置服务的超时时间,当被调用的服务经常失败到达某个阈值,我们可以开启断路保护机制,使得后来的请求不再去调用这个服务,而是本地直接返回默认的数据。

服务降级:在运维期间,当系统处于高峰期,系统资源紧张,可以让非核心业务降级运行,即某些服务不处理或者简单处理。

API网关

抽象了微服务中都需要的公共功能,同时提供了客户端负载均衡、服务熔断、统一认证、限流流控、日志统计等功能。

微服务架构图

谷粒商城-微服务架构图

微服务划分图

image-20211009140429986

Linux虚拟机环境搭建

之前的项目已经搭建的差不多了

相关命令

win:

  1. ipconfig
  2. ping 192.168.128.129

linux:

  1. ip addr
  2. ping 10.67.99.2

docker:

  1. 我的挂载文件地址都在mydata中
  2. docker images 当前所有镜像
  3. docker ps 当前正在运行的容器
  4. docker start xxx 启动容器
  5. docker exec -it mysql /bin/bash
  6. docker exec -it redis redis-cli
  7. whereis [容器名] 查看当前进入的容器文件的地址
  8. docker update redis –restart=always 自动重启容器
  9. exit

mysql:

  1. mysql -u root -p 访问mysql

redis:

  1. keys *

项目前后端基础搭建

后面回来得我表示,从一开始直接复制一样的版本号!!!

基础服务模块

注意每个服务模块,先导入web和openfeign

image-20211009155653337

数据库

每个服务都需要独立的数据库

注意这里sql文件不要直接导入,可能导出的时候格式设置和现在不符合(创建的数据库字符编码为utf8mb4),直接导入会中文乱码,用记事本打开后再粘贴运行。

人人开源模板

人人开源 (gitee.com)

renren-fast(后端)renren-fast-vue(前端)renren-generator(生成器)

renren-fast

  1. renren-fast clone后放到父模块中,记得在父模块的pom文件中写入model。
  2. 运行db文件夹中的对应sql
  3. 修改resources下的配置文件
    1. application-dev.yml中的druid数据源信息

renren-fast-vue

  1. clone后用vscode打开
  2. npm install(注意先把package.json中sass的版本改为4.14以上,因为npm的版本为14,两者版本有所对应,详见👉Vue解决报错9_人人开源renren-fast-vue执行npm install报错解决(sass的版本太低而node的版本太高导致)_xiaosi的博客-CSDN博客,奇怪的是package-lock.json中的版本为4.14.1,然而package中却是4.13.1)
  3. npm run dev,妈耶run了一下试试,就开了一个服务,内存已经85%了
  4. 可以在.eslintignore中添加*.js*.vue用于忽略语法检查

renren-generator

  1. clone,导入到后端项目中,记得在父模块的pom文件中写入model。
  2. 配置application.yml中的数据源配置(注意数据库名的编写)
  3. 配置generator.properties中的生成数据配置(注意模块名、表前缀即可)
  4. 运行application打开页面点击生成代码(如果一直在读取说明配置信息有误)
  5. 将压缩包中的main文件放入对应模块src文件夹
  6. 去renren-fast模块中的common文件夹中复制报错所需要的类

common模块

承接上文,反正就是修修补补

controller中的@RequiresPermissions注释可以通过修改generator的controller template(所以说为什么不能一开始就把模板修改了)

当前项目结构

image-20211009192652587

image-20211009192720046

整合mybatis-plus

依赖

由于common模块中已经导入了mybatis-plus依赖,所以需要使用只需要导入common模块即可

同时需要在common中导入mysql驱动

配置

配置datasource、mapper地址、mybatis-plus等,最好使用yml格式

application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# mysql
spring:
datasource:
#type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.128.129:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# mapper
mybatis-plus:
# mapper.xml地址
mapper-locations: classpath:/mapper/**/*.xml
# 主键自增
global-config:
db-config:
id-type: auto

#服务器(注意不能配置成一样的)
server:
port: 7000

classpath*表示不仅扫描本模块下的类路径,还包括引入的其他依赖的类路径

GulimallProductApplication
1
2
3
4
5
6
7
@MapperScan("com.atguigu.gulimall.product.dao")	// mapper包扫描
@SpringBootApplication
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}

单元测试✅

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringRunner.class)	// 记得加,而且要引入Junit和springboot-starter-test依赖
@SpringBootTest
class GulimallProductApplicationTests {
@Autowired
BrandService brandService;
@Test
void contextLoads() {
BrandEntity be = new BrandEntity();
be.setName("huawei");
brandService.save(be);
}
}

前端调用api接口请求地址配置

注意去static->config->index.js中配置服务地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 开发环境
*/
; (function () {
window.SITE_CONFIG = {};

// api接口请求地址(88为网关配置的端口号,api为自定义规则),注意最后不要有/
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

// cdn地址 = 域名 + 版本号
window.SITE_CONFIG['domain'] = './'; // 域名
window.SITE_CONFIG['version'] = ''; // 版本号(年月日时分)
window.SITE_CONFIG['cdnUrl'] = window.SITE_CONFIG.domain + window.SITE_CONFIG.version;
})();

网关配置

写在application.yml中,千万注意缩进

解决renren-fase验证码

因为修改了端口号,但是renren-fast登录时候的验证码是在她本来的端口8080才能获取,所以需要让网关发现renren-fast

改变如localhost:88/api/captcha -> localhost:8080/renren-fast/captcha

  1. 先给renren-fast把nacos配好(注册中心和配置中心的都可以配)
  2. 编写网关配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
cloud:
gateway:
routes:
- id: admin_route
# lb负载均衡,自动查询服务名为renren-fast的服务器,从而自动获得其ip:port
uri: lb://renren-fast
predicates:
# 匹配所有以api开头的前端请求地址
- Path=/api/**
# 实现转发
filters:
# 注意这里的写法,segment这一部分整体不变,然后别的地方进行替换
# localhost:88/api/xxx -> localhost:8080/renren-fast/xxx
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

还要开启跨域

解决跨域

在网关当中写一个config类统一配置跨域,可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.atguigu.gulimall.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;
@Configuration
public class GulimallCorsConfiguration {
@Bean // 加入到spring容器中直接使用
public CorsWebFilter crosWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true); // 是否允许携带cookie
// path为需要跨域的地址,corsConfiguration为跨域配置
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}

image-20211010154403925

image-20211010154515355

这里说明一下,一个login(methods为OPTION表示预见请求)已经通过了

第二是真实请求,携带了请求数据等

这里测试还是失败的原因是renren-fast自己已经配置过了跨域,两次跨域=没跨,把renren-fast的config文件夹下的CorsConfig注释掉即可

分布式组件

spring-cloud-alibaba

  1. nacos:注册中心配置中心
  2. Sentinel:服务容错(限流、降级、熔断)
  3. Seata:分布式事务解决方案

spring-cloud:

  1. Ribbon:负载均衡
  2. Feign:声明式HTTP客户端(远程服务)
  3. Gateway:API网关
  4. Sleuth:调用链监控

spring-cloud和spring-boot版本对应

https://start.spring.io/actuator/info

版本说明 · alibaba/spring-cloud-alibaba Wiki (github.com)

这个版本一定要对应起来

nacos注册中心

依赖

common模块即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<!-- ******************** spring-cloud ******************** -->
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>

<!-- ******************** spring-cloud版本管理 ******************** -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

配置nacos地址

注意需要使用的服务就需要在application中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# mysql
spring:
datasource:
#type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.128.129:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
#nacos地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 服务名
application:
name: gulimall-coupon

注解开启

1
2
3
4
5
6
7
8
@MapperScan("com.atguigu.gulimall.coupon.dao")    // mapper包扫描
@SpringBootApplication
@EnableDiscoveryClient // nacos
public class GulimallCouponApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallCouponApplication.class, args);
}
}

可视化界面

http://127.0.0.1:8848/nacos

Feign远程服务

依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

服务接口

feign文件夹下写一个接口,在接口中提供声明需要被调用的服务的controller中的对应方法,并且把映射地址改为全地址,并且为接口添加@FeignClient注释,value为需要调用的服务的服务名。

注解开启

在调用服务的启动类上添加注解@EnableFeignClients(basepackages=”接口地址”)

(被调用的服务需要在开启nacos注册发现)

nacos配置中心

依赖

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置文件

在resources文件夹下的bootstrap.properties文件中配置Nacos Config元数据(一定得是bootstrap)

!!!一定要在bootstrap.properties中配置

1
2
3
4
5
6
7
# 当前应用名
spring.application.name=gulimall-coupon
# nacos配置中心的地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# nacos配置中心使用的命名空间(如果是自定义的填写对应id)和组
spring.cloud.nacos.config.namespace=d066b643-6689-4ed1-967d-585be9a3b121
spring.cloud.nacos.config.group=dev

配置中心

在配置中心的配置列表中新建配置,DataID为服务名.properties(如果是要加载application.properties的话),然后可以粘贴上配置内容

注意这里测试了一下,还是把版本都和课件对应上比较好,要不然会有很多问题。

注解开启

在对应controller中@RefreshScope,当配置中心的配置值改变的时候,对应服务中的配置值也会改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RefreshScope   // 开启配置中心动态获取配置
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
@Value("${coupon.user.name}")// 从配置文件中获取值
private String name;
@Value("${coupon.user.age}")
private String age;
/**
* test配置中心
*/
@RequestMapping("/test")
//@RequiresPermissions("coupon:coupon:list")
public R test() {
return R.ok().put("name", name).put("age", age);
}
}

多配置集

image-20211010113405307

1
2
3
4
5
6
7
8
9
10
11
12
13
# nacos配置中心使用的命名空间(如果是自定义的填写对应id)和组
spring.cloud.nacos.config.namespace=d066b643-6689-4ed1-967d-585be9a3b121
spring.cloud.nacos.config.group=dev
# 多配置集
spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true
spring.cloud.nacos.config.ext-config[2].data-id=others.yml
spring.cloud.nacos.config.ext-config[2].group=dev
spring.cloud.nacos.config.ext-config[2].refresh=true

其他

  1. 命名空间:用于配置隔离,可以通过在bootstrap.properties配置文件修改从而修改对应内容
    1. 不同的生产环节下使用不同命名空间隔离
    2. 不同微服务之间隔离
  2. 配置集:所有的配置的集合
  3. 配置集ID:即nacos中的Data ID
  4. 配置分组:即nacos中的group,也可以隔离

Gateway网关

模块

创建Gateway模块,记得引入common,在父模块中添加model

启动类上加如下

1
2
@EnableDiscoveryClient  //nacos
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

配置文件

application.properties

1
2
3
4
5
6
#nacos注册中心
spring.cloud.nacos.discovery.server-addr=127.0.0.1.8848
#应用名
spring.application.name=gulimall-gateway
#服务端口号
server.port=12000

网关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: test_route
uri: https://www.baidu.com
predicates:
- Query=url,baidu

- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url,qq

url为转发的地址,predicates断言为判断条件(详见官网),以上的意思为当uri中有一个key value对应url=baidu、url=qq,则进行跳转。

测试地址:http://localhost:88/?url=baidu

接口文档

1、分页请求参数 - 谷粒商城 - 易文档 (easydoc.net)

商品服务 - 三级分类(后台)

product模块,category相关

nacos配置

application.yml
1
2
3
4
5
6
7
8
9
10
11
12
# mysql
spring:
# nacos 注册中心
cloud:
nacos:
discovery:
# nacos地址
server-addr: 127.0.0.1:8848
# 服务名
application:
name: gulimall-product

bootstrap.properties
1
2
3
4
5
6
# 当前应用名
spring.application.name=gulimall-product
# nacos配置中心的地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# nacos配置中心使用的命名空间(如果是自定义的填写对应id)和组
spring.cloud.nacos.config.namespace=415d4381-f509-4bb6-8ad8-c0497e20d904

网关配置

这里坑很多,顺序,还有缩进,还有断言匹配还有转发的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
# 实现转发
filters:
# localhost:88/api/product/xxx -> localhost:10000/product/xxx
- RewritePath=/api/(?<segment>.*),/$\{segment}
# 注意顺序,高精度的在前
- id: admin_route
# lb负载均衡,自动查询服务名为renren-fast的服务器,从而自动获得其ip:port
uri: lb://renren-fast
predicates:
# 匹配所有以api开头的前端请求地址
- Path=/api/**
# 实现转发
filters:
# 注意这里的写法,segment这一部分整体不变,然后别的地方进行替换
# localhost:88/api/xxx -> localhost:8080/renren-fast/xxx
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

递归树形结构查询所有分类(后端)

CategoryEntity

1
2
3
4
5
6
7
8
9
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
/**
* 子分类
*/
@TableField(exist = false) // 表示该属性在数据表中不存在
private List<CategoryEntity> children;
}

CategoryController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 分页查询所有分类,并且以树形结构组合
*/
@RequestMapping("/list/tree")
public R listWithTree() {

List<CategoryEntity> list = categoryService.listWithTree();

return R.ok().put("data", list);
}
}

CategoryServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
/**
* 分页查询所有分类,并且以树形结构组合
*
* @return
*/
@Override
public List<CategoryEntity> listWithTree() {
// 1、查出所有分类
List<CategoryEntity> categories = baseMapper.selectList(null);
// 2、组装树形结构
// 流式、过滤、map映射、排序、递归、lambda
List<CategoryEntity> level1Menus = categories.stream().filter((categoryEntity) -> {
return categoryEntity.getParentCid() == 0;
}).map(categoryEntity -> {
categoryEntity.setChildren(getChildrens(categoryEntity, categories));
return categoryEntity;
}).sorted((ce1, ce2) -> {
return (ce1.getSort() == null ? 0 : ce1.getSort()) - (ce2.getSort() == null ? 0 : ce2.getSort());
}).collect(Collectors.toList());
return level1Menus;
}
/**
* 递归查询所有子分类
*
* @param father
* @param entityList
* @return
*/
private List<CategoryEntity> getChildrens(CategoryEntity father, List<CategoryEntity> entityList) {
List<CategoryEntity> childList = entityList.stream().filter((categoryEntity) -> {
// 取出每一个entity,判断其是否符合,符合的留下
return categoryEntity.getParentCid() == father.getCatId();
}).map(categoryEntity -> {
// 取出每一个entity,为其设置children(使用递归)
categoryEntity.setChildren(getChildrens(categoryEntity, entityList));
return categoryEntity;
}).sorted((ce1, ce2) -> {
// 这里防止getSort()返回null,然后根据sort权重排序
return (ce1.getSort() == null ? 0 : ce1.getSort()) - (ce2.getSort() == null ? 0 : ce2.getSort());
}).collect(Collectors.toList());
return childList;
}
}

上来就放了个大招,java8的新特性

显示所有分类(前端)

路由配置

启动renrenfast和renrenfase-vue

系统管理 -> 菜单管理 -> 添加目录和菜单(我的妈呀可视化操作这也太棒了吧)

路径中填写/会被替换成-,所以可以根据这个对应src->views->modules下的路径

http://localhost:8001/#/product-category对应文件夹src->views->modules->product->category.vue

vue页面

renren-fast-vue\src\views\modules\product\category.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- 商品管理-分类维护 -->
<template>
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</template>
<script>
export default {
components: {},
data() {
return {
menus: [],
defaultProps: {
children: "children", // 子节点属性
label: "name" //显示的属性
}
};
},
methods: {
// 获取所有分类
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
// 这里data使用了解构
console.log("调用api获得菜单数据" + data.data);
this.menus = data.data;
});
}
},
created() {
this.getMenus();
},
};
</script>

删除分类(后端)

逻辑删除

在application.yml中配置

1
2
3
4
5
6
7
8
9
10
11
# mybatis
mybatis-plus:
# mapper.xml地址
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
# 主键自增
id-type: auto
# 逻辑删除
logic-delete-value: 0
logic-not-delete-value: 1

在对应实体类的字段上添加注解@TableLogic

1
2
3
4
5
6
7
8
9
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
/**
* 逻辑删除
*/
@TableLogic(value = "1", delval = "0")
private Integer showStatus;
}

CategoryController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 删除分类,判断是否被其他引用
*/
@PostMapping("/delete")
//@RequiresPermissions("product:category:delete")
public R deleteWithoutUse(@RequestBody Long[] catIds) {
// 检查当前删除的菜单是否被其他的地方引用
categoryService.removeByIdsWithoutUse(Arrays.asList(catIds));
return R.ok();
}
}

CategoryServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
/**
* 删除分类,判断是否被其他引用
*
* @param asList
*/
@Override
public void removeByIdsWithoutUse(List<Long> asList) {
//TODO 检查是否被其他的东西引用
baseMapper.deleteBatchIds(asList);
}
}

删除分类(前端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!-- 商品管理-分类维护 -->
<template>
<!-- ====================== 分类树形结构显示 开始 ====================== -->
<!--
node-key="catId" 其值为节点数据中的一个字段名
:expand-on-click-node="false" 表示设置展开节点的点击位置,FALSE只有当点击箭头的时候才展开
:default-expanded-keys="[2, 3]" 表示默认展开的节点
-->
<el-tree
node-key="catId"
show-checkbox
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:default-expanded-keys="expandedKey"
>
<!-- ====================== 操作按钮 开始 ====================== -->
<!-- 使用slot添加可操作性按钮 -->
<!-- node为每一个节点对象(不和后端中entity对应,而是element自定义的),data为该节点的数据 -->
<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)"
>
Append
</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>
<!-- ====================== 分类树形结构显示 结束 ====================== -->
</template>

<script>
export default {
components: {},
data() {
return {
menus: [], // 菜单列表
defaultProps: {
children: "children", // 子节点属性
label: "name" // 显示的属性
},
expandedKey: [] // 默认展开的节点
};
},
methods: {
// 按钮操作
append(data) {},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`确定删除【${data.name}】菜单吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
// 调用API
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",
duration: 800,
onClose: () => {
// 刷新列表
this.getMenus();
// 展开父节点位置(这里的id对应node-key绑定的属性名)
this.expandedKey = [node.parent.data.catId];
}
});
} else {
this.$message.error(data.msg);
}
});
})
.catch(() => {});
}
},
};
</script>

添加分类(后端)

已有

添加分类(前端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<!-- 商品管理-分类维护 -->
<template>
<div>
<!-- ====================== 分类树形结构显示 开始 ====================== -->
<!--
node-key="catId" 其值为节点数据中的一个字段名
:expand-on-click-node="false" 表示设置展开节点的点击位置,FALSE只有当点击箭头的时候才展开
:default-expanded-keys="[2, 3]" 表示默认展开的节点
-->
<el-tree
node-key="catId"
show-checkbox
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:default-expanded-keys="expandedKey"
>
<!-- ====================== 操作按钮 开始 ====================== -->
<!-- 使用slot添加可操作性按钮 -->
<!-- node为每一个节点对象(不和后端中entity对应,而是element自定义的),data为该节点的数据 -->
<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)"
>
Append
</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
title="添加分类菜单"
:visible.sync="dialogVisible"
width="30%"
center
>
<el-form :model="category">
<el-form-item label="分类名">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory()">确 定</el-button>
</div>
</el-dialog>
<!-- ====================== 添加分类的对话框 结束 ====================== -->
</div>
</template>

<script>
export default {
components: {},
data() {
return {
menus: [], // 菜单列表
defaultProps: {
// 菜单树属性
children: "children", // 子节点属性
label: "name" // 显示的属性
},
expandedKey: [], // 默认展开的节点
dialogVisible: false, // 是否打开添加分类的对话框
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0
} // 菜单对象
};
},
methods: {
// 添加分类菜单
addCategory() {
// 调用api
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();
// 展开父节点位置数组(这里的id对应node-key绑定的属性名)
this.expandedKey = [this.category.parentCid];
})
.catch(result => {
this.$message({
message: "保存失败",
type: "error"
});
});
},
// 打开添加菜单对话框
append(data) {
this.dialogVisible = true;
// 初始化category对象
this.category.name = "";
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
},
}
};
</script>

修改分类(后端)

已有

修改分类(前端)

实现简单地信息修改,包括name,icon,productUnit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<!-- 商品管理-分类维护 -->
<template>
<div>
<!-- ====================== 分类树形结构显示 开始 ====================== -->
<!-- ====================== 操作按钮 开始 ====================== -->
<!-- 使用slot添加可操作性按钮 -->
<!-- node为每一个节点对象(不和后端中entity对应,而是element自定义的),data为该节点的数据 -->
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button type="text" size="mini" @click="openDialogWithInfo(data)">
修改
</el-button>
</span>
</span>
<!-- ====================== 操作按钮 结束 ====================== -->
</el-tree>
<!-- ====================== 分类树形结构显示 结束 ====================== -->
<!-- ====================== 添加分类的对话框 开始 ====================== -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="30%"
>
<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>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addOrUpdate()">确 定</el-button>
</div>
</el-dialog>
<!-- ====================== 添加分类的对话框 结束 ====================== -->
</div>
</template>
<script>
export default {
components: {},
data() {
return {
menus: [], // 菜单列表
defaultProps: {
// 菜单树属性
children: "children", // 子节点属性
label: "name" // 显示的属性
},
expandedKey: [], // 默认展开的节点
dialogVisible: false, // 是否打开添加分类的对话框
dialogType: "", // 对话框用于添加还是修改
dialogTitle: "", // 对话框标题
category: {
name: "",
catId: null,
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
icon: "",
productUnit: ""
} // 菜单对象
};
},
methods: {
// 打开添加菜单对话框
openDialog(data) {
this.dialogVisible = true;
this.dialogType = "add";
this.dialogTitle = "添加分类";
// 初始化category对象
this.category.catId = null;
this.category.name = "";
this.category.parentCid = data.catId;
this.category.icon = "";
this.category.productUnit = "";
this.category.catLevel = data.catLevel * 1 + 1;
},
// 打开修改菜单对话框
openDialogWithInfo(data) {
this.dialogVisible = true;
this.dialogType = "update";
this.dialogTitle = "修改分类";
// 调用接口获取对应id的信息,并初始化category对象
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
this.category.catId = data.data.catId;
// 只修改这几个值
this.category.name = data.data.name;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
// 用于修改完之后展开父菜单
this.category.parentCid = data.data.parentCid;
});
},
// 对话框确定按钮判断
addOrUpdate() {
if (this.dialogType == "update") {
this.updateCategory();
} else if (this.dialogType == "add") {
this.addCategory();
}
},
// 修改分类菜单
updateCategory() {
// 使用解构
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
// 不能直接发送this.category对象,而是重新封装需要修改的对象
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "修改成功",
type: "success"
});
// 关闭对话框
this.dialogVisible = false;
// 刷新列表
this.getMenus();
// 展开父节点位置数组(这里的id对应node-key绑定的属性名)
this.expandedKey = [this.category.parentCid];
});
},
}
};
</script>

拖拽修改分类(后端)

1
2
3
4
5
6
7
8
/**
* 批量修改
*/
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] category) {
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}

拖拽修改分类(前端)

el-tree 开启draggable即可,allow-drop判断

这一part值得重复观看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
<!-- 商品管理-分类维护 -->
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽排序"
inactive-text="关闭拖拽排序"
>
</el-switch>
<el-button v-if="draggable" @click="batchSave">
批量保存
</el-button>
<!-- ====================== 分类树形结构显示 开始 ====================== -->
<!--
node-key="catId" 其值为节点数据中的一个字段名
:expand-on-click-node="false" 表示设置展开节点的点击位置,FALSE只有当点击箭头的时候才展开
:default-expanded-keys="[2, 3]" 表示默认展开的节点
draggable 开启可拖拽的方式
-->
<el-tree
node-key="catId"
show-checkbox
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
<!-- ====================== 操作按钮 开始 ====================== -->
<!-- 使用slot添加可操作性按钮 -->
<!-- node为每一个节点对象(不和后端中entity对应,而是element自定义的),data为该节点的数据 -->
<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="openDialog(data)"
>
添加
</el-button>
<el-button type="text" size="mini" @click="openDialogWithInfo(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="dialogTitle"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="30%"
>
<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>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addOrUpdate()">确 定</el-button>
</div>
</el-dialog>
<!-- ====================== 添加分类的对话框 结束 ====================== -->
</div>
</template>

<script>
export default {
components: {},
data() {
return {
menus: [], // 菜单列表
defaultProps: {
// 菜单树属性
children: "children", // 子节点属性
label: "name" // 显示的属性
},
expandedKey: [], // 默认展开的节点
dialogVisible: false, // 是否打开添加分类的对话框
dialogType: "", // 对话框用于添加还是修改
dialogTitle: "", // 对话框标题
category: {
name: "",
catId: null,
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
icon: "",
productUnit: ""
}, // 菜单对象
pCid: 0,
maxLevel: 0, // 用于记录子节点最深深度
updateNodes: [], //所有需要修改的节点
draggable: false // 可否拖拽
};
},
created() {
this.getMenus();
},
methods: {
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.updateNodes = [];
this.maxLevel = 0;
this.getMenus();
this.expandedKey = [this.pCid];
this.pCid = 0;
})
.catch(() => {
this.$message({
message: "菜单顺序修改失败",
type: "error"
});
});
},
// 修改子节点的层级
updateNodesChildLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
this.updateNodes.push({
catId: node.childNodes[i].data.catId,
catLevel: node.childNodes[i].level
});
this.updateNodesChildLevel(node.childNodes[i]);
}
}
},
// 拖拽成功触发
handleDrop(draggingNode, dropNode, dropType, ev) {
// 获取最新节点的父节点id(真实数据库中的id,而不是node的id)和顺序和层级
let pCid = 0;
let siblings = null;
if (dropType == "inner") {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
} else {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
}
//对兄弟节点排序,获得当前的顺序
for (let i = 0; i < siblings.length; i++) {
// 先判断当前遍历到的节点是不是被拖拽的节点,是的话需要修改他的pCid
if (siblings[i].data.catId == draggingNode.data.catId) {
// 判断层级是否改变
if (siblings[i].level != draggingNode.level) {
// 字节点的层级也要改变
this.updateNodesChildLevel(siblings[i]);
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: siblings[i].level
});
} else {
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid
});
}
} else {
// 需要修改的商品对象,放入updateNodes
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
this.pCid = pCid;
console.log("this.updateNodes:", this.updateNodes);
// 调用api
// 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.updateNodes = [];
// this.maxLevel = 0;
// this.getMenus();
// this.expandedKey = [pCid];
// })
// .catch(() => {
// this.$message({
// message: "菜单顺序修改失败",
// type: "error"
// });
// });
},
// 判断是否被拖拽放置(这里要注意层级和深度)
allowDrop(draggingNode, dropNode, type) {
// 当前被拖动到的节点的最大深度(因为可能拖家带口)+拖动到的节点的层数 <= 3即可
// 求当前子节点最大层级
console.log("draggingNode:", draggingNode);
this.countNodeLevel(draggingNode);
// 注意这里,需要用子节点的最大层级-当前被拖动的节点的层级+1,获得的值就是被拖动整体的最大深度
console.log("maxLevel:" + this.maxLevel);
console.log("draggingNode.level:" + draggingNode.level);
let deep = this.maxLevel - draggingNode.level + 1;
console.log("deep:" + deep);
this.maxLevel = 0;
// 注意这里要判断type: 前后中(因为elementui他的设计就是这样,两个node是指拖拽后有位置关系的两个节点,type是返回的两个node之间的关系,)
if (type == "inner") {
console.log("dropNode.level:" + dropNode.level);
return deep + dropNode.level <= 3;
} else {
console.log("dropNode.parent.level:" + dropNode.parent.level);
return deep + dropNode.parent.level <= 3;
}
},
// 递归查找最深子节点,记录在this.maxLevel中
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;
}
// 注意这里maxLevel放在data中,要不然递归拿不到
this.countNodeLevel(node.childNodes[i]);
}
} else {
// 没有子节点
this.maxLevel = node.level;
}
},
// 获取所有分类
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
console.log(data);
// 这里data使用了解构
this.menus = data.data;
});
},
// 打开添加菜单对话框
openDialog(data) {
this.dialogVisible = true;
this.dialogType = "add";
this.dialogTitle = "添加分类";
// 初始化category对象
this.category.catId = null;
this.category.name = "";
this.category.parentCid = data.catId;
this.category.icon = "";
this.category.productUnit = "";
this.category.catLevel = data.catLevel * 1 + 1;
},
// 打开修改菜单对话框
openDialogWithInfo(data) {
this.dialogVisible = true;
this.dialogType = "update";
this.dialogTitle = "修改分类";
// 调用接口获取对应id的信息,并初始化category对象
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
this.category.catId = data.data.catId;
// 只修改这几个值
this.category.name = data.data.name;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
// 用于修改完之后展开父菜单
this.category.parentCid = data.data.parentCid;
});
},
// 对话框确定按钮判断
addOrUpdate() {
if (this.dialogType == "update") {
this.updateCategory();
} else if (this.dialogType == "add") {
this.addCategory();
}
},
// 修改分类菜单
updateCategory() {
// 使用解构
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
// 不能直接发送this.category对象,而是重新封装需要修改的对象
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "修改成功",
type: "success"
});
// 关闭对话框
this.dialogVisible = false;
// 刷新列表
this.getMenus();
// 展开父节点位置数组(这里的id对应node-key绑定的属性名)
this.expandedKey = [this.category.parentCid];
});
},
// 添加分类菜单
addCategory() {
// 调用api
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();
// 展开父节点位置数组(这里的id对应node-key绑定的属性名)
this.expandedKey = [this.category.parentCid];
})
.catch(result => {
this.$message({
message: "保存失败",
type: "error"
});
});
},
// 删除对应菜单
remove(node, data) {
var ids = [data.catId];
this.$confirm(`确定删除【${data.name}】菜单吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
// 调用API
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对应node-key绑定的属性名)
this.expandedKey = [node.parent.data.catId];
})
.catch(result => {
this.$message({
message: "删除失败",
type: "error"
});
});
})
.catch(() => {});
}
}
};
</script>

TODO : 当前bug,如果层级3的菜单拖拽到层级2后失踪,应该和pcid有关(拖拽至层级1有效)

层级2无子节点拖拽到层级2后失踪(不过看起来后台数据库没问题呀?)

TODO:这个bug被后来的批量保存改正了,反正都批量保存了,也有问题了

批量删除分类(前端)

image-20211016171830386

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
 <div>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<!-- ====================== 分类树形结构显示 开始 ====================== -->
<!--
node-key="catId" 其值为节点数据中的一个字段名
:expand-on-click-node="false" 表示设置展开节点的点击位置,FALSE只有当点击箭头的时候才展开
:default-expanded-keys="[2, 3]" 表示默认展开的节点
draggable 开启可拖拽的方式‘
ref 可以从this.$refs中获取到该对应组件
-->
<el-tree
node-key="catId"
show-checkbox
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
ref="menuTree"
>

<script>
export default {
methods: {
// 批量删除
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(() => {
// 调用API
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(result => {
this.$message({
message: "批量删除失败",
type: "error"
});
});
})
.catch(() => {});
},
}
}
</script>

评论




🧡💛💚💙💜🖤🤍