谷粒商城第二篇服务功能-商品服务-三级分类

商品服务三级分类工程初始化及查询搭建

在数据库中插入数据
在这里插入图片描述

1.controller类

@RestController
@RequestMapping("product/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    /**
     * 查出所有分类以及子分类,以树形结构组装起来
     */
    @RequestMapping("/list/tree")
    public R list(){
        List<CategoryEntity> entityList = categoryService.listWithTree();

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

2.service类

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Override
    public List<CategoryEntity> listWithTree(){
//       1. 查出所有分类
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
//        2.组装成父子的树形结构
//        查找以及分类
        List<CategoryEntity> level1Menu = categoryEntities.stream().filter((categoryEntity -> {
            return categoryEntity.getParentCid() == 0;
        })).map((menu)->{
            menu.setChildren(getChildrens(menu,categoryEntities));
            return menu;
        }).sorted((menu1,menu2)->{
            return (menu1.getSort()==null?0:menu1.getSort())-(menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());
        return level1Menu;
    }

//    递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
        List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == root.getCatId();
        }).map(categoryEntity -> {
//            1.找到子菜单
            categoryEntity.setChildren(getChildrens(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;
    }
}

3.测试

在这里插入图片描述

4.启动renren前端及后端工程,进入工程创建商品目录

在这里插入图片描述

5.在商品系统下创建分类维护菜单

在这里插入图片描述

6.创建category模块并且测试

在这里插入图片描述
在这里插入图片描述

7.编写代码

category.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.dataListLoading = true
            this.$http({
                url: this.$http.adornUrl('/product/category/list/tree'),
                method: 'get'
            }).then(data=>{
                console.log("成功获取到菜单数据...",data)
            })
        }
    },
    // 监听属性 类似于data概念
    computed: {},
    // 监控data中的数据变化
    watch: {},
    // 生命周期 - 创建完成(可以访问当前this实例)
    created() {
        this.getMenus()
    },
    // 生命周期 - 挂载完成(可以访问DOM元素)
    mounted() {

    },
    beforeCreate() { }, // 生命周期 - 创建之前
    beforeMount() { }, // 生命周期 - 挂载之前
    beforeUpdate() { }, // 生命周期 - 更新之前
    updated() { }, // 生命周期 - 更新之后
    beforeDestroy() { }, // 生命周期 - 销毁之前
    destroyed() { }, // 生命周期 - 销毁完成
    activated() { } // 如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped></style>

访问页面路径端和端口都不对在这里插入图片描述

8.修改为所有请求调用网关

修改index.js 修改为网关地址,现在前端访问后端renrenfast会报错
在这里插入图片描述

9.将renrenfast加入注册中心中

1.在renren-fast后端工程加入gulimall-common,并且修改springboot版本,以及对应cloud版本

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>io.renren</groupId>
	<artifactId>renren-fast</artifactId>
	<version>3.0.0</version>
	<packaging>jar</packaging>
	<description>renren-fast</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.7.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
<!--	<parent>-->
<!--		<groupId>org.springframework.boot</groupId>-->
<!--		<artifactId>spring-boot-starter-parent</artifactId>-->
<!--		<version>2.6.6</version>-->
<!--	</parent>-->

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<mybatisplus.version>3.3.1</mybatisplus.version>
		<mysql.version>8.0.28</mysql.version>
		<mssql.version>4.0</mssql.version>
		<oracle.version>11.2.0.3</oracle.version>
		<druid.version>1.1.13</druid.version>
		<quartz.version>2.3.0</quartz.version>
		<commons.lang.version>2.6</commons.lang.version>
		<commons.fileupload.version>1.2.2</commons.fileupload.version>
		<commons.io.version>2.5</commons.io.version>
		<commons.codec.version>1.10</commons.codec.version>
		<commons.configuration.version>1.10</commons.configuration.version>
		<shiro.version>1.9.0</shiro.version>
		<jwt.version>0.7.0</jwt.version>
		<kaptcha.version>0.0.9</kaptcha.version>
		<qiniu.version>7.2.23</qiniu.version>
		<aliyun.oss.version>2.8.3</aliyun.oss.version>
		<qcloud.cos.version>4.4</qcloud.cos.version>
		<swagger.version>2.7.0</swagger.version>
		<joda.time.version>2.9.9</joda.time.version>
		<gson.version>2.8.5</gson.version>
		<fastjson.version>1.2.79</fastjson.version>
		<hutool.version>4.1.1</hutool.version>
		<lombok.version>1.18.4</lombok.version>

		<!--wagon plugin 配置-->
		<service-path>/work/renren</service-path>
		<pack-name>${project.artifactId}-${project.version}.jar</pack-name>
		<remote-addr>192.168.1.10:22</remote-addr>
		<remote-username>root</remote-username>
		<remote-passwd>123456</remote-passwd>
	</properties>

	<dependencies>
		<dependency>
			<groupId>com.hejiawang.gulimall</groupId>
			<artifactId>gulimall-common</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context-support</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<!--<dependency>-->
			<!--<groupId>org.springframework.boot</groupId>-->
			<!--<artifactId>spring-boot-devtools</artifactId>-->
			<!--<optional>true</optional>-->
		<!--</dependency>-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>${mybatisplus.version}</version>
			<exclusions>
				<exclusion>
					<groupId>com.baomidou</groupId>
					<artifactId>mybatis-plus-generator</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql.version}</version>
		</dependency>
		 <!--oracle驱动-->
		<dependency>
			<groupId>com.oracle</groupId>
			<artifactId>ojdbc6</artifactId>
			<version>${oracle.version}</version>
		</dependency>
		 <!--mssql驱动-->
		<dependency>
			<groupId>com.microsoft.sqlserver</groupId>
			<artifactId>sqljdbc4</artifactId>
			<version>${mssql.version}</version>
		</dependency>
		 <!--postgresql驱动-->
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>${druid.version}</version>
		</dependency>
		<dependency>
			<groupId>org.quartz-scheduler</groupId>
			<artifactId>quartz</artifactId>
			<version>${quartz.version}</version>
			<exclusions>
				<exclusion>
					<groupId>com.mchange</groupId>
					<artifactId>c3p0</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>${commons.lang.version}</version>
		</dependency>
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>${commons.fileupload.version}</version>
		</dependency>
		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>${commons.io.version}</version>
		</dependency>
		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>${commons.codec.version}</version>
		</dependency>
		<dependency>
			<groupId>commons-configuration</groupId>
			<artifactId>commons-configuration</artifactId>
			<version>${commons.configuration.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>${shiro.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>${shiro.version}</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>${jwt.version}</version>
		</dependency>
		<dependency>
			<groupId>com.github.axet</groupId>
			<artifactId>kaptcha</artifactId>
			<version>${kaptcha.version}</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>${swagger.version}</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>${swagger.version}</version>
		</dependency>
		<dependency>
			<groupId>com.qiniu</groupId>
			<artifactId>qiniu-java-sdk</artifactId>
			<version>${qiniu.version}</version>
		</dependency>
		<dependency>
			<groupId>com.aliyun.oss</groupId>
			<artifactId>aliyun-sdk-oss</artifactId>
			<version>${aliyun.oss.version}</version>
		</dependency>
		<dependency>
			<groupId>com.qcloud</groupId>
			<artifactId>cos_api</artifactId>
			<version>${qcloud.cos.version}</version>
			<exclusions>
				<exclusion>
					<groupId>org.slf4j</groupId>
					<artifactId>slf4j-log4j12</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>joda-time</groupId>
			<artifactId>joda-time</artifactId>
			<version>${joda.time.version}</version>
		</dependency>
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>${gson.version}</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>${fastjson.version}</version>
		</dependency>
		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>${hutool.version}</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
		</dependency>
	</dependencies>

	<build>
		<finalName>${project.artifactId}</finalName>
		<extensions>
			<extension>
				<groupId>org.apache.maven.wagon</groupId>
				<artifactId>wagon-ssh</artifactId>
				<version>2.8</version>
			</extension>
		</extensions>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<fork>true</fork>
				</configuration>
			</plugin>
			<!-- 跳过单元测试 -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<configuration>
					<skipTests>true</skipTests>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>wagon-maven-plugin</artifactId>
				<version>1.0</version>
				<configuration>
					<fromFile>target/${pack-name}</fromFile>
					<url><![CDATA[scp://${remote-username}:${remote-passwd}@${remote-addr}${service-path}]]></url>
					<commands>
						<!-- Kill Old Process -->
						<command>kill -9 `ps -ef |grep ${project.artifactId}.jar|grep -v "grep" |awk '{print $2}'`</command>
						<!-- Restart jar package,write result into renren.log -->
						<command><![CDATA[nohup java -jar ${service-path}/${pack-name} --spring.profiles.active=test > ${service-path}/renren.log 2>&1 & ]]></command>
						<command><![CDATA[netstat -nptl]]></command>
						<command><![CDATA[ps -ef | grep java | grep -v grep]]></command>
					</commands>
					<!-- 运行命令 mvn clean package wagon:upload-single wagon:sshexec-->
					<displayCommandOutputs>true</displayCommandOutputs>
				</configuration>
			</plugin>

			<plugin>
				<groupId>com.spotify</groupId>
				<artifactId>docker-maven-plugin</artifactId>
				<version>0.4.14</version>
				<!--<executions>-->
					<!--<execution>-->
						<!--<phase>package</phase>-->
						<!--<goals>-->
							<!--<goal>build</goal>-->
						<!--</goals>-->
					<!--</execution>-->
				<!--</executions>-->
				<configuration>
					<imageName>renren/fast</imageName>
					<dockerDirectory>${project.basedir}</dockerDirectory>
					<resources>
						<resource>
							<targetPath>/</targetPath>
							<directory>${project.build.directory}</directory>
							<include>${project.build.finalName}.jar</include>
						</resource>
					</resources>
				</configuration>
				<!-- 运行命令 mvn clean package docker:build 打包并生成docker镜像 -->
			</plugin>
		</plugins>
	</build>

	<repositories>
		<repository>
			<id>public</id>
			<name>aliyun nexus</name>
			<url>https://maven.aliyun.com/repository/public/</url>
			<releases>
				<enabled>true</enabled>
			</releases>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>public</id>
			<name>aliyun nexus</name>
			<url>https://maven.aliyun.com/repository/public/</url>
			<releases>
				<enabled>true</enabled>
			</releases>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>

</project>

2.启动参数中加上@EnableDiscouveryClient

在这里插入图片描述

3.配置文件加上注册中心配置

在这里插入图片描述

4.启动工程

在这里插入图片描述

10.修改网关工程,配置转发

修改网关配置加上/api即可路由到

spring:
#  datasource:
#    username: root
#    password: 1q1w1e1r
#    url: jdbc:mysql://127.0.0.1:3306/gulimall_sms
#    driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    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
#           前端项目,/api
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**

server:
  port: 88

在这里插入图片描述
前端所有请求加上/api
在这里插入图片描述

11.测试并调整网关

访问主页有报错
在这里插入图片描述
因为网关转发的时候,未带工程前缀
http://localhost:88/api/captcha.jpg ====> http://renren-fast:8080/api/captcha.jpg
修改gateway工程加上重写的工程路径

#           前端项目,/api
#        http://localhost:88/api/captcha.jpg http://renren-fast:8080/api/captcha.jpg
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/?(?<segment>.*), /renren-fast/$\{segment}

在这里插入图片描述
重启gateway再次访问页面即可看到验证码
在这里插入图片描述

12.登录跨域问题

1.原因

由于浏览器为http://localhost:8001/#/login,前端中的js访问资源协议域名端口不一样即会有跨域问题
在这里插入图片描述
登录方法为OPTIONS在这里插入图片描述

2.跨域流程

跨域解释
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
在这里插入图片描述

3.解决办法

方法一:通过nginx将浏览器访问地址和请求地址统一

在这里插入图片描述

方法二:后端配置将预检请求允许跨域

在这里插入图片描述

13.解决登录跨域问题

本机调试采取方法二
在网关工程中添加配置类允许跨域

package com.hejiawang.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.CorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.server.ServerWebExchange;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Configuration
public class GulimallCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
//        1.配置跨域
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
        corsConfiguration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);

    }
}

在这里插入图片描述
将renren-fast后端跨域内容注释掉,使用我们自己配的
在这里插入图片描述
重启服务即可成功登录

14.初始化gulimall-product项目

1.网关配置product对应路由转发

        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/?(?<segment>.*), /$\{segment}

2.在nacos上创建商品服务的命名空间

在这里插入图片描述

3.在nacos product命名空间下创建要管理的配置文件

在这里插入图片描述

4.商品服务客户端增加nacos配置中心相关配置

添加bootstrap文件及配置中心地址

spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=a7d07264-e472-4466-9b28-e14caee4e0d7

在这里插入图片描述
添加注册发现注解
在这里插入图片描述
添加注册中心地址
在这里插入图片描述
启动服务看到product注册上来了在这里插入图片描述

15.尝试访问

直接访问product可以返回数据
在这里插入图片描述
通过网关访问报错 http://localhost:88/api/product/category/list/tree
在这里插入图片描述
因为网关被拦截,优先顺序,将最模糊的路由放在最后重启
在这里插入图片描述
再次访问网关http://localhost:88/api/product/category/list/tree访问成功
在这里插入图片描述

16.前端工程中配置全路径到gateway,并且配置menu请求路径

在这里插入图片描述

<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.dataListLoading = true
            this.$http({
                url: this.$http.adornUrl('/product/category/list/tree'),
                method: 'get'
            }).then(data=>{
                console.log("成功获取到菜单数据...",data)
            })
        }
    },
    // 监听属性 类似于data概念
    computed: {},
    // 监控data中的数据变化
    watch: {},
    // 生命周期 - 创建完成(可以访问当前this实例)
    created() {
        this.getMenus()
    },
    // 生命周期 - 挂载完成(可以访问DOM元素)
    mounted() {

    },
    beforeCreate() { }, // 生命周期 - 创建之前
    beforeMount() { }, // 生命周期 - 挂载之前
    beforeUpdate() { }, // 生命周期 - 更新之前
    updated() { }, // 生命周期 - 更新之后
    beforeDestroy() { }, // 生命周期 - 销毁之前
    destroyed() { }, // 生命周期 - 销毁完成
    activated() { } // 如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped></style>

在这里插入图片描述
请求测试返回数据
在这里插入图片描述

17.修改前端

<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'
            }
        };
    },
    methods: {
        handleNodeClick(data) {
            console.log(data);
        },
        getMenus() {
            this.dataListLoading = true
            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>

即可看到菜单显示出来
在这里插入图片描述

商品服务三级分类逻辑删除后端

1.CategoryController类

    /**
     * 删除
     */
    @RequestMapping("/delete")
    public R delete(@RequestBody Long[] catIds){
//        原始方法先注释
//        categoryService.removeByIds(Arrays.asList(catIds));

//        1、检查当前删除的菜单,是否被其他地方引用到

        categoryService.removeMenuByIds(Arrays.asList(catIds));

        return R.ok();
    }

2.CategoryServiceImpl类

    @Override
    public void removeMenuByIds(List<Long> asList) {
//        TODO: 检查当前删除的菜单是否被引用,
        baseMapper.deleteBatchIds(asList);
    }

3.CategoryEntity类对对应字段加上@TableLogic

package com.hejiawang.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

import lombok.Data;

/**
 * 商品三级分类
 * 
 * @author hejiawang
 * @email hejiawangs@gmail.com
 * @date 2023-06-13 14:42:51
 */
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 分类id
	 */
	@TableId
	private Long catId;
	/**
	 * 分类名称
	 */
	private String name;
	/**
	 * 父分类id
	 */
	private Long parentCid;
	/**
	 * 层级
	 */
	private Integer catLevel;
	/**
	 * 是否显示[0-不显示,1显示]
	 */
	@TableLogic(value = "1",delval = "0")
	private Integer showStatus;
	/**
	 * 排序
	 */
	private Integer sort;
	/**
	 * 图标地址
	 */
	private String icon;
	/**
	 * 计量单位
	 */
	private String productUnit;
	/**
	 * 商品数量
	 */
	private Integer productCount;
	@TableField(exist = false)
	private List<CategoryEntity> children;

}

4.配置文件加上全局配置及

# MapperScan
# sql映射文件位置
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
#使用的地方相反可使用@TableLogic,全局默认配置
      logic-delete-value: 1
      logic-not-delete-value: 0
logging:
  level:
    com.hejiawang.gulimall: debug

5.测试接口

查询初始化数据
在这里插入图片描述
调用接口
在这里插入图片描述
结果
在这里插入图片描述
打印sql过程为update
在这里插入图片描述

6.前端修改

1.新增代码模板片段 vue.code,将模板粘贴进去

在这里插入图片描述

{
    "Print to console": {
        "prefix": "vue",
        "body": [
            "<template>",
            "<div class='$2'>$5</div>",
            "</template>",
            "",
            "<script>",
            "//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)",
            "//例如:import 《组件名称》 from '《组件路径》';",
            "",
            "export default {",
            "//import引入的组件需要注入到对象中才能使用",
            "components: {},",
            "data() {",
            "//这里存放数据",
            "return {",
            "",
            "};",
            "},",
            "//监听属性 类似于data概念",
            "computed: {},",
            "//监控data中的数据变化",
            "watch: {},",
            "//方法集合",
            "methods: {",
            "",
            "},",
            "//生命周期 - 创建完成(可以访问当前this实例)",
            "created() {",
            "",
            "},",
            "//生命周期 - 挂载完成(可以访问DOM元素)",
            "mounted() {",
            "",
            "},",
            "beforeCreate() {}, //生命周期 - 创建之前",
            "beforeMount() {}, //生命周期 - 挂载之前",
            "beforeUpdate() {}, //生命周期 - 更新之前",
            "updated() {}, //生命周期 - 更新之后",
            "beforeDestroy() {}, //生命周期 - 销毁之前",
            "destroyed() {}, //生命周期 - 销毁完成",
            "activated() {} //如果页面有keep-alive缓存功能,这个函数会触发",
            "}",
            "</script>",
            "<style scoped>",
            "$4",
            "</style>"
        ],
        "description": "生成vue模板"
    },
    "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请求"
    }
}
2.修改前端删除后展开父菜单并且刷新前端
<template>
    <el-tree :data="menus" :props="defaultProps" :default-expanded-keys="expandedKey" node-key="catId" show-checkbox :expand-on-click-node="false">
        <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>
// 这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
// 例如:import 《组件名称》 from '《组件路径》';

export default {
    // import引入的组件需要注入到对象中才能使用
    components: {},
    data() {
        return {
            expandedKey: [],
            menus: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            }
        };
    },
    methods: {
        append(data) {
            console.log("append", data);
        },

        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 }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey= [node.parent.data.catId]
                    // if (data && data.code === 0) {
                    //     this.$message({
                    //         message: '操作成功',
                    //         type: 'success',
                    //         duration: 1500,
                    //         onClose: () => {
                    //             this.getDataList()
                    //         }
                    //     })
                    // } else {
                    //     this.$message.error(data.msg)
                    // }
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });

            console.log("remove", node, data);
        },
        getMenus() {
            this.dataListLoading = true
            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>

在这里插入图片描述

商品服务三级分类添加

前端修改

<template>
    <div>
        <el-tree :data="menus" :props="defaultProps" :default-expanded-keys="expandedKey" node-key="catId" show-checkbox
            :expand-on-click-node="false">
            <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%">
            <el-form :model="category">
                <el-form-item label="分类名称">
                    <el-input v-model="category.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>
    </div>
</template>
<script>
// 这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
// 例如:import 《组件名称》 from '《组件路径》';

export default {
    // import引入的组件需要注入到对象中才能使用
    components: {},
    data() {
        return {
            category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
            dialogVisible: false,
            expandedKey: [],
            menus: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            }
        };
    },
    methods: {
        // 添加三级分类的方法
        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 }) => { 
                console.log("菜单保存成功");
                    this.$message({
                        message: '菜单保存成功',
                        type: 'success'
                    });
//                  关闭对话框
                    this.dialogVisible=false;
                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey = [this.category.parentCid];
            });
        },
        append(data) {
            console.log("append", data);
            this.dialogVisible = true;
            this.category.parentCid = data.catId;
            this.category.catLevel = data.catLevel * 1 + 1;
            this.category.showStatus = 1;
            this.category.sort = 0;
        },

        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 }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey = [node.parent.data.catId]
                    // if (data && data.code === 0) {
                    //     this.$message({
                    //         message: '操作成功',
                    //         type: 'success',
                    //         duration: 1500,
                    //         onClose: () => {
                    //             this.getDataList()
                    //         }
                    //     })
                    // } else {
                    //     this.$message.error(data.msg)
                    // }
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });

            console.log("remove", node, data);
        },
        getMenus() {
            this.dataListLoading = true
            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>

测试
在这里插入图片描述

商品服务三级分类修改

前端

<template>
    <div>
        <el-tree :data="menus" :props="defaultProps" :default-expanded-keys="expandedKey" node-key="catId" show-checkbox
            :expand-on-click-node="false">
            <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 type="text" size="mini" @click="() => edit(data)">
                        edit
                    </el-button>
                    <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">
                        Delete
                    </el-button>
                </span>
            </span>
        </el-tree>
        <el-dialog :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: {},
    data() {
        return {
            title: "",
            dialogType: "", //edit,add
            category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, catId: null, icon: "", productUnit: "" },
            dialogVisible: false,
            expandedKey: [],
            menus: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            }
        };
    },
    methods: {
        // 添加三级分类的方法
        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 }) => {
                console.log("菜单保存成功");
                this.$message({
                    message: '菜单保存成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        // 修改三级分类的方法
        editCategory() {
            var {catId,name,icon,productUnit} = this.category;
            var data = {catId,name,icon,productUnit}
            console.log("三级分类的对象", this.category);
            this.$http({
                url: this.$http.adornUrl('/product/category/update'),
                method: 'post',
                data: this.$http.adornData(data, false)
            }).then(({ data }) => {
                console.log("菜单修改成功");
                this.$message({
                    message: '菜单修改成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        submitData() {
            if (this.dialogType == "add") {
                this.addCategory();
            }
            if (this.dialogType == "edit") {
                this.editCategory();
            }
        },
        edit(data) {
            console.log("要修改的数据", data);
            this.title = "修改分类";
            this.dialogType = "edit";
            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;
            })
        },
        append(data) {
            console.log("append", data);
            this.title = "添加分类";
            this.dialogType = "add";
            this.dialogVisible = true;
            this.category.parentCid = data.catId;
            this.category.catLevel = data.catLevel * 1 + 1;
            this.category.showStatus = 1;
            this.category.sort = 0;
            this.category.name = "";
            this.category.catId = null;
            this.category.icon = "";
            this.category.productUnit = "";
        },
        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 }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey = [node.parent.data.catId]
                    // if (data && data.code === 0) {
                    //     this.$message({
                    //         message: '操作成功',
                    //         type: 'success',
                    //         duration: 1500,
                    //         onClose: () => {
                    //             this.getDataList()
                    //         }
                    //     })
                    // } else {
                    //     this.$message.error(data.msg)
                    // }
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });

            console.log("remove", node, data);
        },
        getMenus() {
            this.dataListLoading = true
            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>

拖拽节点

1.允许拖拽以及拖拽条件页面效果

<template>
    <div>
        <el-tree :data="menus" :props="defaultProps" :default-expanded-keys="expandedKey" node-key="catId" show-checkbox
            draggable :allow-drop="allowDrop" :expand-on-click-node="false">
            <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 type="text" size="mini" @click="() => edit(data)">
                        edit
                    </el-button>
                    <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">
                        Delete
                    </el-button>
                </span>
            </span>
        </el-tree>
        <el-dialog :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: {},
    data() {
        return {
            maxLevel: 0,
            title: "",
            dialogType: "", //edit,add
            category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, catId: null, icon: "", productUnit: "" },
            dialogVisible: false,
            expandedKey: [],
            menus: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            }
        };
    },
    methods: {
        allowDrop(draggingNode, dropNode, type) {
            //              1.被拖动的当前节点以及所在的父节点总层数不能大于3
            //          被拖动的当前节点的总层数
            console.log("allowDrop:", draggingNode, dropNode, type)
            this.countNodeLevel(draggingNode.data);
            // 当前正在拖动的节点+父节点所在的深度不大于3即可
            let deep = (this.maxLevel - draggingNode.parent.level);
            console.log("深度:",deep);
            console.log(this.maxLevel);
            if(type == "inner"){
                return deep + dropNode.data.catLevel <=3
            }else{
                // 前后
                return deep + dropNode.parent.level <=3
            }
            // return ;
        },
        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]);
                }
            }
        },
        // 添加三级分类的方法
        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 }) => {
                console.log("菜单保存成功");
                this.$message({
                    message: '菜单保存成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        // 修改三级分类的方法
        editCategory() {
            var { catId, name, icon, productUnit } = this.category;
            var data = { catId, name, icon, productUnit }
            console.log("三级分类的对象", this.category);
            this.$http({
                url: this.$http.adornUrl('/product/category/update'),
                method: 'post',
                data: this.$http.adornData(data, false)
            }).then(({ data }) => {
                console.log("菜单修改成功");
                this.$message({
                    message: '菜单修改成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        submitData() {
            if (this.dialogType == "add") {
                this.addCategory();
            }
            if (this.dialogType == "edit") {
                this.editCategory();
            }
        },
        edit(data) {
            console.log("要修改的数据", data);
            this.title = "修改分类";
            this.dialogType = "edit";
            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;
            })
        },
        append(data) {
            console.log("append", data);
            this.title = "添加分类";
            this.dialogType = "add";
            this.dialogVisible = true;
            this.category.parentCid = data.catId;
            this.category.catLevel = data.catLevel * 1 + 1;
            this.category.showStatus = 1;
            this.category.sort = 0;
            this.category.name = "";
            this.category.catId = null;
            this.category.icon = "";
            this.category.productUnit = "";
        },
        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 }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey = [node.parent.data.catId]
                    // if (data && data.code === 0) {
                    //     this.$message({
                    //         message: '操作成功',
                    //         type: 'success',
                    //         duration: 1500,
                    //         onClose: () => {
                    //             this.getDataList()
                    //         }
                    //     })
                    // } else {
                    //     this.$message.error(data.msg)
                    // }
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });

            console.log("remove", node, data);
        },
        getMenus() {
            this.dataListLoading = true
            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>

2.拖拽节点保存数据库

1.前端代码

<template>
    <div>
        <el-tree :data="menus" :props="defaultProps" :default-expanded-keys="expandedKey" node-key="catId" show-checkbox
            @node-drop="handleDrop" draggable :allow-drop="allowDrop" :expand-on-click-node="false">
            <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 type="text" size="mini" @click="() => edit(data)">
                        edit
                    </el-button>
                    <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">
                        Delete
                    </el-button>
                </span>
            </span>
        </el-tree>
        <el-dialog :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: {},
    data() {
        return {
            updateNodes: [],
            maxLevel: 0,
            title: "",
            dialogType: "", //edit,add
            category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, catId: null, icon: "", productUnit: "" },
            dialogVisible: false,
            expandedKey: [],
            menus: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            }
        };
    },
    methods: {
        handleDrop(draggingNode, dropNode, dropType, ev) {
            console.log('handleDrop: ', draggingNode, dropNode, dropType, ev);
            // 1.当前节点最新的父节点id
            var pCid = 0;
            let sibilings = null;
            if (dropType == "before" || dropType == "after") {
                pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId;
                sibilings = dropNode.parent.childNodes;
            } else {
                pCid = dropNode.data.catId;
                sibilings = dropNode.childNodes;
            }
            // 2.当前拖拽节点的最新顺序
            for (let i = 0; i < sibilings.length; i++) {
                if (sibilings[i].data.catId == draggingNode.data.catId) {
                    // 如果遍历是当前正在拖拽的节点
                    let catLevel = draggingNode.level
                    if (sibilings[i].level == draggingNode.level) {
                        //  当前拖拽节点的层级有变化
                        catLevel = sibilings[i].level;
                        // 递归修改子节点的层级
                        this.updateChildNodeLevel(sibilings[i]);
                    }
                    this.updateNodes.push({ catId: sibilings[i].data.catId, sort: i, parentCid: pCid, catLevel: catLevel });
                } else {
                    this.updateNodes.push({ catId: sibilings[i].data.catId, sort: i })
                }
            }
            // 3.当前拖拽节点的最新层级级
            console.log("updateNodes:", this.updateNodes);
            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];
                this.updateNodes = [];
                this.maxLevel = 0;

            });

        },
        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
            //          被拖动的当前节点的总层数
            console.log("allowDrop:", draggingNode, dropNode, type)
            this.countNodeLevel(draggingNode.data);
            // 当前正在拖动的节点+父节点所在的深度不大于3即可
            let deep = (this.maxLevel - draggingNode.parent.level);
            console.log("深度:", deep);
            console.log(this.maxLevel);
            if (type == "inner") {
                return deep + dropNode.data.catLevel <= 3
            } else {
                // 前后
                return deep + dropNode.parent.level <= 3
            }
            // return ;
        },
        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]);
                }
            }
        },
        // 添加三级分类的方法
        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 }) => {
                console.log("菜单保存成功");
                this.$message({
                    message: '菜单保存成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        // 修改三级分类的方法
        editCategory() {
            var { catId, name, icon, productUnit } = this.category;
            var data = { catId, name, icon, productUnit }
            console.log("三级分类的对象", this.category);
            this.$http({
                url: this.$http.adornUrl('/product/category/update'),
                method: 'post',
                data: this.$http.adornData(data, false)
            }).then(({ data }) => {
                console.log("菜单修改成功");
                this.$message({
                    message: '菜单修改成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        submitData() {
            if (this.dialogType == "add") {
                this.addCategory();
            }
            if (this.dialogType == "edit") {
                this.editCategory();
            }
        },
        edit(data) {
            console.log("要修改的数据", data);
            this.title = "修改分类";
            this.dialogType = "edit";
            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;
            })
        },
        append(data) {
            console.log("append", data);
            this.title = "添加分类";
            this.dialogType = "add";
            this.dialogVisible = true;
            this.category.parentCid = data.catId;
            this.category.catLevel = data.catLevel * 1 + 1;
            this.category.showStatus = 1;
            this.category.sort = 0;
            this.category.name = "";
            this.category.catId = null;
            this.category.icon = "";
            this.category.productUnit = "";
        },
        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 }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey = [node.parent.data.catId]
                    // if (data && data.code === 0) {
                    //     this.$message({
                    //         message: '操作成功',
                    //         type: 'success',
                    //         duration: 1500,
                    //         onClose: () => {
                    //             this.getDataList()
                    //         }
                    //     })
                    // } else {
                    //     this.$message.error(data.msg)
                    // }
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });

            console.log("remove", node, data);
        },
        getMenus() {
            this.dataListLoading = true
            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>

2.后端代码新增批量保存

    /**
     * 修改
     */
    @RequestMapping("/update/sort")
    public R update(@RequestBody CategoryEntity[] category){
        categoryService.updateBatchById(Arrays.asList(category));
        return R.ok();
    }

3.批量保存拖拽节点保存数据库优化加上拖拽按钮

<template>
    <div>
        <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽">
        </el-switch>
        <el-button v-if="draggable" @click="batchSave">批量保存</el-button>
        <el-tree :data="menus" :props="defaultProps" :default-expanded-keys="expandedKey" node-key="catId" show-checkbox
            @node-drop="handleDrop" :draggable="draggable" :allow-drop="allowDrop" :expand-on-click-node="false">
            <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 type="text" size="mini" @click="() => edit(data)">
                        edit
                    </el-button>
                    <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">
                        Delete
                    </el-button>
                </span>
            </span>
        </el-tree>
        <el-dialog :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: {},
    data() {
        return {
            pCid: [],
            draggable: false,
            updateNodes: [],
            maxLevel: 0,
            title: "",
            dialogType: "", //edit,add
            category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, catId: null, icon: "", productUnit: "" },
            dialogVisible: false,
            expandedKey: [],
            menus: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            }
        };
    },
    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.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = this.pCid;
                this.updateNodes = [];
                this.maxLevel = 0;
                this.pCid = [];
            });
        },
        handleDrop(draggingNode, dropNode, dropType, ev) {
            console.log('handleDrop: ', draggingNode, dropNode, dropType, ev);
            // 1.当前节点最新的父节点id
            var pCid = 0;
            let sibilings = null;
            if (dropType == "before" || dropType == "after") {
                pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId;
                sibilings = dropNode.parent.childNodes;
            } else {
                pCid = dropNode.data.catId;
                sibilings = dropNode.childNodes;
            }
            // 2.当前拖拽节点的最新顺序
            for (let i = 0; i < sibilings.length; i++) {
                if (sibilings[i].data.catId == draggingNode.data.catId) {
                    // 如果遍历是当前正在拖拽的节点
                    let catLevel = draggingNode.level
                    if (sibilings[i].level == draggingNode.level) {
                        //  当前拖拽节点的层级有变化
                        catLevel = sibilings[i].level;
                        // 递归修改子节点的层级
                        this.updateChildNodeLevel(sibilings[i]);
                    }
                    this.updateNodes.push({ catId: sibilings[i].data.catId, sort: i, parentCid: pCid, catLevel: catLevel });
                } else {
                    this.updateNodes.push({ catId: sibilings[i].data.catId, sort: i })
                }
            }
            // 3.当前拖拽节点的最新层级级
            console.log("updateNodes:", this.updateNodes);
            this.pCid.push(pCid);
        },
        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
            //   被拖动的当前节点的总层数
            console.log("allowDrop:", draggingNode, dropNode, type)
            this.countNodeLevel(draggingNode.data);
            // 当前正在拖动的节点+父节点所在的深度不大于3即可
            let deep = (this.maxLevel - draggingNode.parent.level);
            console.log("深度:", deep);
            console.log(this.maxLevel);
            if (type == "inner") {
                return deep + dropNode.data.catLevel <= 3
            } else {
                // 前后
                return deep + dropNode.parent.level <= 3
            }
            // return ;
        },
        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]);
                }
            }
        },
        // 添加三级分类的方法
        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 }) => {
                console.log("菜单保存成功");
                this.$message({
                    message: '菜单保存成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        // 修改三级分类的方法
        editCategory() {
            var { catId, name, icon, productUnit } = this.category;
            var data = { catId, name, icon, productUnit }
            console.log("三级分类的对象", this.category);
            this.$http({
                url: this.$http.adornUrl('/product/category/update'),
                method: 'post',
                data: this.$http.adornData(data, false)
            }).then(({ data }) => {
                console.log("菜单修改成功");
                this.$message({
                    message: '菜单修改成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        submitData() {
            if (this.dialogType == "add") {
                this.addCategory();
            }
            if (this.dialogType == "edit") {
                this.editCategory();
            }
        },
        edit(data) {
            console.log("要修改的数据", data);
            this.title = "修改分类";
            this.dialogType = "edit";
            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;
            })
        },
        append(data) {
            console.log("append", data);
            this.title = "添加分类";
            this.dialogType = "add";
            this.dialogVisible = true;
            this.category.parentCid = data.catId;
            this.category.catLevel = data.catLevel * 1 + 1;
            this.category.showStatus = 1;
            this.category.sort = 0;
            this.category.name = "";
            this.category.catId = null;
            this.category.icon = "";
            this.category.productUnit = "";
        },
        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 }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey = [node.parent.data.catId]
                    // if (data && data.code === 0) {
                    //     this.$message({
                    //         message: '操作成功',
                    //         type: 'success',
                    //         duration: 1500,
                    //         onClose: () => {
                    //             this.getDataList()
                    //         }
                    //     })
                    // } else {
                    //     this.$message.error(data.msg)
                    // }
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });

            console.log("remove", node, data);
        },
        getMenus() {
            this.dataListLoading = true
            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>

4.批量删除

前端

<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" :default-expanded-keys="expandedKey" node-key="catId" show-checkbox
            @node-drop="handleDrop" :draggable="draggable" :allow-drop="allowDrop" :expand-on-click-node="false"
            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)">
                        Append
                    </el-button>
                    <el-button type="text" size="mini" @click="() => edit(data)">
                        edit
                    </el-button>
                    <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">
                        Delete
                    </el-button>
                </span>
            </span>
        </el-tree>
        <el-dialog :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: {},
    data() {
        return {
            pCid: [],
            draggable: false,
            updateNodes: [],
            maxLevel: 0,
            title: "",
            dialogType: "", //edit,add
            category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, catId: null, icon: "", productUnit: "" },
            dialogVisible: false,
            expandedKey: [],
            menus: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            }
        };
    },
    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(() => {
                this.$http({
                    url: this.$http.adornUrl('/product/category/delete'),
                    method: 'post',
                    data: this.$http.adornData(catIds, false)
                }).then(({ data }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单批量删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });
        },
        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 = [];
            });
        },
        handleDrop(draggingNode, dropNode, dropType, ev) {
            console.log('handleDrop: ', draggingNode, dropNode, dropType, ev);
            // 1.当前节点最新的父节点id
            var pCid = 0;
            let sibilings = null;
            if (dropType == "before" || dropType == "after") {
                pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId;
                sibilings = dropNode.parent.childNodes;
            } else {
                pCid = dropNode.data.catId;
                sibilings = dropNode.childNodes;
            }
            // 2.当前拖拽节点的最新顺序
            for (let i = 0; i < sibilings.length; i++) {
                if (sibilings[i].data.catId == draggingNode.data.catId) {
                    // 如果遍历是当前正在拖拽的节点
                    let catLevel = draggingNode.level
                    if (sibilings[i].level == draggingNode.level) {
                        //  当前拖拽节点的层级有变化
                        catLevel = sibilings[i].level;
                        // 递归修改子节点的层级
                        this.updateChildNodeLevel(sibilings[i]);
                    }
                    this.updateNodes.push({ catId: sibilings[i].data.catId, sort: i, parentCid: pCid, catLevel: catLevel });
                } else {
                    this.updateNodes.push({ catId: sibilings[i].data.catId, sort: i })
                }
            }
            // 3.当前拖拽节点的最新层级级
            console.log("updateNodes:", this.updateNodes);
            this.pCid.push(pCid);
        },
        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
            //   被拖动的当前节点的总层数
            console.log("allowDrop:", draggingNode, dropNode, type)
            this.countNodeLevel(draggingNode.data);
            // 当前正在拖动的节点+父节点所在的深度不大于3即可
            let deep = (this.maxLevel - draggingNode.parent.level);
            console.log("深度:", deep);
            console.log(this.maxLevel);
            if (type == "inner") {
                return deep + dropNode.data.catLevel <= 3
            } else {
                // 前后
                return deep + dropNode.parent.level <= 3
            }
            // return ;
        },
        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]);
                }
            }
        },
        // 添加三级分类的方法
        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 }) => {
                console.log("菜单保存成功");
                this.$message({
                    message: '菜单保存成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        // 修改三级分类的方法
        editCategory() {
            var { catId, name, icon, productUnit } = this.category;
            var data = { catId, name, icon, productUnit }
            console.log("三级分类的对象", this.category);
            this.$http({
                url: this.$http.adornUrl('/product/category/update'),
                method: 'post',
                data: this.$http.adornData(data, false)
            }).then(({ data }) => {
                console.log("菜单修改成功");
                this.$message({
                    message: '菜单修改成功',
                    type: 'success'
                });
                //                  关闭对话框
                this.dialogVisible = false;
                // 刷新出新的菜单
                this.getMenus()
                // 设置删除后默认要展示的父菜单
                this.expandedKey = [this.category.parentCid];
            });
        },
        submitData() {
            if (this.dialogType == "add") {
                this.addCategory();
            }
            if (this.dialogType == "edit") {
                this.editCategory();
            }
        },
        edit(data) {
            console.log("要修改的数据", data);
            this.title = "修改分类";
            this.dialogType = "edit";
            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;
            })
        },
        append(data) {
            console.log("append", data);
            this.title = "添加分类";
            this.dialogType = "add";
            this.dialogVisible = true;
            this.category.parentCid = data.catId;
            this.category.catLevel = data.catLevel * 1 + 1;
            this.category.showStatus = 1;
            this.category.sort = 0;
            this.category.name = "";
            this.category.catId = null;
            this.category.icon = "";
            this.category.productUnit = "";
        },
        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 }) => {
                    console.log("删除成功");
                    this.$message({
                        message: '菜单删除成功',
                        type: 'success'
                    });

                    // 刷新出新的菜单
                    this.getMenus()
                    // 设置删除后默认要展示的父菜单
                    this.expandedKey = [node.parent.data.catId]
                    // if (data && data.code === 0) {
                    //     this.$message({
                    //         message: '操作成功',
                    //         type: 'success',
                    //         duration: 1500,
                    //         onClose: () => {
                    //             this.getDataList()
                    //         }
                    //     })
                    // } else {
                    //     this.$message.error(data.msg)
                    // }
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                });
            });

            console.log("remove", node, data);
        },
        getMenus() {
            this.dataListLoading = true
            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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值