谷粒商城分布式基础(六)—— 商品服务API—三级分类(配置网关路由 & 跨域问题 & 树级菜单 & 拖拽效果)

 


一、基础概念

1、三级分类

2、SPU 和 SKU

 3、基本属性【规格参数】与销售属性

 

二、商品服务API—三级分类

1、sql 脚本

2、递归属性结构查询所有分类及子分类

复制代码
1)在gulimall-product的com.atguigu.gulimall.product.controller.CategoryController添加如下
  /**
     * 查询所有分类以及子分类,以树形结构组装起来
     * @return
     */
    @RequestMapping("/list/tree")
    public R list(){
        List<CategoryEntity> entities = categoryService.listWithTree();
        return R.ok().put("data", entities);
    }
复制代码
2)在gulimall-product的接口层com.atguigu.gulimall.product.service.CategoryService添加如下接口
  /**
     * 查询所有分类以及子分类,以树形结构组装起来
     * @return
     */
    List<CategoryEntity> listWithTree();
复制代码
(3)在gulimall-product的接口实现层com.atguigu.gulimall.product.service.impl.CategoryServiceImpl添加如下方法
  
/** * 查询所有分类以及子分类,以树形结构组装起来 * @return */ @Override public List<CategoryEntity> listWithTree() { //1、查询所有分类 List<CategoryEntity> entities = baseMapper.selectList(null); //2、组装成父子树形结构 //2.1、找到所有的一级分类 List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity -> //当前菜单的父id=0——>当前菜单为一级菜单 categoryEntity.getParentCid() == 0 ).map((menu)->{ //找每一个一级菜单的子菜单 menu.setChildren(getChildrens(menu, entities)); return menu; //排序菜单 }).sorted((menu1, menu2)->{ return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()); //收集当前所有菜单toList }).collect(Collectors.toList()); return level1Menus; } /** * 递归查找所有菜单的子菜单 * 查找当前菜单root的子菜单,总所有菜单all中过滤 * @param root * @param all * @return */ private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all){ //依次遍历从所有菜单all中过滤找到当前菜单root的子菜单集合children List<CategoryEntity> children = all.stream().filter(categoryEntity -> { //当前菜单root的id=过滤对象的父id——>找到当前菜单root的子菜单,即为被过滤到的all列表的当前对象categoryEntity return categoryEntity.getParentCid() == root.getCatId(); //root找到的子菜单列表children可能还有子菜单,继续查找列表children中每一个对象categoryEntity的子菜单 }).map(categoryEntity -> { //1、找到子菜单 //递归调用:查找列表children中每一个对象categoryEntity的子菜单,还是从所有菜单all中过滤 categoryEntity.setChildren(getChildrens(categoryEntity, all)); return categoryEntity; //2、菜单的排序 }).sorted((menu1,menu2)->{ return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()); //收集当前所有菜单toList }).collect(Collectors.toList()); return children; }
复制代码
4)测试
    启动gulimall-product
    访问:http://localhost:10000/product/category/list/tree
  注意:启动的时候报错是nacos配置文件还未修改,可以忽略

3、配置网关路由与路径重写

1)启动后端项目 renren-fast
   端口:8080
复制代码
(2)启动前端项目 renren-fast-vue
  (a)vscode 打开 renren-fast-vue
  (b)vscode终端 npm run dev 启动项目
    端口:8001,访问地址:http://localhost:8001/, 用户名:admin 密码:admin

复制代码
复制代码
3)添加菜单
  (a)添加目录“商品系统”
  系统管理——>菜单管理——>新增

  刷新,看到左侧多了商品系统,添加这个菜单实际是添加到了guli-admin.sys_menu表里

  (b)添加菜单“分类维护”

  guli-admin.sys_menu表又多了一行,父id是刚才的商品系统id 

复制代码
复制代码
4)添加vue组件
  (a)创建vue视图
  在左侧点击【分类维护】,希望在此展示3级分类 注意地址栏http://localhost:8001/#/product-category 可以注意到product-category我们的/被替换为了-

  比如sys-role具体的视图在renren-fast-vue/views/modules/sys/role.vue

  所以针对 路由地址 http://localhost:8001/#/product-category,要自定义我们的product/category视图的话,就是创建mudules/product/category.vue

  由于我们在前面配置过vue模板,这里直接使用即可

  得到以下模板

复制代码
复制代码
<template>
<div></div>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具 js,第三方插件 js,json 文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';

export default {
//import 引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {

};
},
//计算属性 类似于 data 概念
computed: {},
//监控 data 中的数据变化
watch: {},
//方法集合
methods: {

},
//生命周期 - 创建完成(可以访问当前 this 实例)
created() {

},
//生命周期 - 挂载完成(可以访问 DOM 元素)
mounted() {

},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发 
}
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类

</style>
复制代码
(b)构写vue视图文件测试
  访问element-ui网站 https://element.eleme.cn/#/zh-CN/component/tree,组件里面找到“Tree树形组件”,复制代码
  生命周期挂载完成就调取后端接口获取数据,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: {},
  props: {},
  data() {
    return {
      data: [],
      defaultProps: {
        children: "children",
        label: "label",
      },
    };
  },
  methods: {
    handleNodeClick(data) {
      console.log(data);
    },
    getMenus(){
        this.$http({
          url: this.$http.adornUrl('/product/category/list/tree'),
          method: 'get',
        }).then(data=>{
            console.log("成功获取到菜单数据..."+data);
        })
    }
  },

  //计算属性 类似于 data 概念
  computed: {},
  //监控 data 中的数据变化
  watch: {},

  //生命周期 - 创建完成(可以访问当前 this 实例)
  created() {},
  //生命周期 - 挂载完成(可以访问 DOM 元素)
  mounted() {
      this.getMenus();
  },
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发
};
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类
</style>
复制代码
    查看“分类维护菜单”,发现访问结果404

  他默认给8001端口(renren-fast)发请求读取数据,但是数据是在10000端口(gulimall-product)上,如果找到了这个请求改端口那改起来很麻烦。

复制代码
5)renren-fast-vue统一发送请求到网关88(gulimall-gateway)
   我们学习SpringCloud Alibaba的时候搭建了一个网关项目gulimall-gateway,网关端口为88,也就是说可以配置网关路由,我们只需要给网关发送请求,让网关路由到指定的地方
  修改renren-fast-vue全局配置api接口请求地址
   在static/config/index.js里
   修改前

    修改后

    接着让重新登录http://localhost:8001/#/login,验证码是请求88的,所以不显示。而验证码是来源于fast后台的

  现在的验证码请求路径为,http://localhost:88/captcha.jpg?uuid=d13e164d-e129-4d71-87b4-f3aeddee6da4

  原始的验证码请求路径:http://localhost:8080/renren-fast/captcha.jpg?uuid=d13e164d-e129-4d71-87b4-f3aeddee6da4

  也就是说所有的请求都被发送到网关了,但是验证码所在的服务在 renren-fast(8080)上

  所以接下来我们需要让网关gulimall-gateway将所有请求默认转给服务renren-fast,要转的话网关必须从注册中心nacos中发现这个服务,所以这个服务必须先注册到注册中心nacos中去

复制代码
复制代码
6)renren-fast服务注册到nacos注册中心
  (a)修改pom.xml引入公共依赖gulimall-common
  因为gulimall-common中有引入nacos的注册中心和配置中心,所以相当于renren-fast也引入了

  (b)修改application.yml,增加以下配置

  (c) 主启动类 RenrenApplication加上注解@EnableDiscoveryClient

  (d)重启renren-fast,查看nacos控制台的服务列表

  然后在nacos的服务列表里看到了renren-fast

  注意:启动过程中的错误是nacos配置中心还没配置,可以忽略 

复制代码
复制代码
7)gulimall-gateway配置路由规则
(a)修改application.yml文件,新增以下内容
- id: admin_route
uri: lb://renren-fast # 路由给renren-fast,lb代表负载均衡
predicates: # 什么情况下路由给它
- Path=/api/** # 默认前端项目都带上api前缀
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

 (b)重启网关gulimall-gateway

  访问:http://localhost:8001/

复制代码

4、跨域问题解决

从上面我们发现登录还是报错,显示跨域
从8001访问88,引发CORS跨域请求,浏览器会拒绝跨域请求
(1)什么是跨域
  问题描述:已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。
  问题分析:这是一种跨域问题。访问的域名和端口和原来的请求不同,请求就会被限制
  跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对js施加的安全限制。(ajax可以)
  同源策略:是指协议,域名,端囗都要相同,其中有一个不同都会产生跨域;

 (2)跨域流程

    这个跨域请求的实现是通过预检请求实现的,先发送一个OPSTIONS探路,收到响应允许跨域后再发送真实请求

复制代码
(3)解决方案
    方法1:设置nginx包含admin和gateway
    方法2:让服务器告诉预检请求能跨域

  解决方法:在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。 package com.atguigu.gulimall.gateway.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; @Configuration
// gateway public class GulimallCorsConfiguration { @Bean // 添加过滤器 public CorsWebFilter corsWebFilter(){ // 基于url跨域,选择reactive包下的 UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource(); // 跨域配置信息 CorsConfiguration corsConfiguration = new CorsConfiguration(); // 允许跨域的头 corsConfiguration.addAllowedHeader("*"); // 允许跨域的请求方式 corsConfiguration.addAllowedMethod("*"); // 允许跨域的请求来源 corsConfiguration.addAllowedOrigin("*"); // 是否允许携带cookie跨域 corsConfiguration.setAllowCredentials(true); // 任意url都要进行跨域配置 source.registerCorsConfiguration("/**",corsConfiguration); return new CorsWebFilter(source); } }   重启,再次访问:http://localhost:8001/#/login

  http://localhost:8001/renren已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。
  (原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)n-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
  出现了多个请求,并且也存在多个跨源请求。   为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问。
  解决完毕,正常进入后台
复制代码

5、树形展示三级分类数据   

复制代码
1)配置商品服务网关路由

    在显示商品系统/分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在
    这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8080/renren-fast/product/category/list/tree
    但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。

    解决方法就是定义一个product路由规则,进行路径重写:

  在网关增加三级分类的路由

- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
复制代码
复制代码
2)gulimall-product配置中心配置
    上面我们只是配置了网关路由规则,但是还没有把gulimall-product(商品服务)注册到注册中心,为了方便修改,我们把所有配置放到配置中心
  (a)在nacos中新建命名空间,用命名空间隔离项目
  (b)在gulimall-product项目中新建 bootstrap.properties文件
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=85e4d525-d8aa-4da5-a496-36ce8c83f1eb

  (c)application.yml配置注册中心相关地址

  (d)主启动类加上服务注册注解@EnableDiscoveryClient,并重新启动商品服务和网关服务查看nacos的服务列表

   全部注册成功

复制代码
复制代码
(3)测试商品服务API  
  至此,商品服务已经注册到nacos中了,网关路由也已经配置好了,访问商品服务:http://localhost:88/api/product/category/list/tree,查看是否正常
  响应结果:

   非法令牌,后台管理系统中没有登录,所以没有带令牌

  原因:先匹配的先路由,fast和product路由重叠,fast要求登录

  修正:在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,

    类似于异常体系中try catch子句中异常的处理顺序。

   重启网关gulimall-gateway再次访问

    http://localhost:88/api/product/category/list/tree 正常

    http://localhost:8001/#/product-category  正常

  原因是:先访问网关88,网关路径重写后访问nacos8848,nacos找到服务

复制代码
4)完成三级分类前端vue
  data解构,加上{},把data的地方改成menus
复制代码
<template>
  <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"
  ></el-tree>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具 js,第三方插件 js,json 文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';

export default {
  //import 引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    return {
      menus: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
  methods: {
    handleNodeClick(data) {
      console.log(data);
    },
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取到菜单数据..." + data.data);
        this.menus = data.data;// 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
      })//fail
      .catch(() => {

      });
    },
  },

  //计算属性 类似于 data 概念
  computed: {},
  //监控 data 中的数据变化
  watch: {},

  //生命周期 - 创建完成(可以访问当前 this 实例)
  created() {},
  //生命周期 - 挂载完成(可以访问 DOM 元素)
  mounted() {
    this.getMenus();
  },
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发
};
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类
</style>

  最终效果

复制代码

6、删除数据

1)前端VUE页面处理
  scoped slot(插槽):在el-tree标签里把内容写到span标签栏里即可修改category.vue的代码
复制代码
<template>
  <el-tree
    :data="menus"
    :props="defaultProps"
    :expand-on-click-node="false"
    show-checkbox
    node-key="catId"
  >
    <span class="custom-tree-node" slot-scope="{ node, data }">
      <span>{{ node.label }}</span>
      <span>
        <el-button
          v-if="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 {
  //import 引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    return {
      menus: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      })
        .then(({ data }) => {
          console.log("成功获取到菜单数据..." + data.data);
          this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
        }) //fail
        .catch(() => {});
    },
    append(data) {
      console.log("append", data);
    },
    remove(node, data) {
      console.log("remove", node, data);
    },
  },
  created() {
      this.getMenus();
  },
 
};
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类
</style>

前端效果:
复制代码
复制代码
2)postman测试
  由于delete请求接收的是一个数组,所以这里使用JSON方式,传入了一个数组:   测试链接:http://localhost:88/api/product/category/delete

  再次查询数据库能够看到cat_id为1432的数据已经被删除了。

复制代码
复制代码
3)删除校验
    但是我们需要修改检查当前菜单是否被引用,gulimall-product操作如下
  (a)修改 CategoryController,添加以下代码
/**
* 删除
* @RequestBody:获取请求体,必须发送post请求
* springmvc会自动将请求体的数据(json)转为对应的对象
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//categoryService.removeByIds(Arrays.asList(catIds));
//1、检查当前删除的菜单,是否被别的地方引用
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
  (b)创建其对应的接口和实现类
@Override
public void removeMenuByIds(List<Long> longs) {
//TODO 1、检查当前删除的菜单,是否被别的地方引用
baseMapper.deleteBatchIds(longs);
}

复制代码
复制代码
4mybatis-plus逻辑删除
    然而多数时候,我们并不希望删除数据,而是标记它被删除了,这就是逻辑删除;
  逻辑删除是mybatis-plus 的内容,会在项目中配置一些内容,告诉此项目执行delete语句时并不删除,只是标志位
  可以设置show_status为0,标记它已经被删除。


  https://baomidou.com/guide/logic-delete.html



(a)配置全局逻辑删除规则
  修改application.yml


(b)
实体类字段上加上@TableLogic注解
  修改修改 com.atguigu.gulimall.product.entity.CategoryEntity 实体类,添加上@TableLogic,表明使用逻辑删除:

 (c)调整日志级别

    修改application.yml,设置日志级别打印出sql语句

  

 (c)重启gulimall-product,用postman测试

  至此,逻辑删除成功 

复制代码
复制代码
5)修改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 请求"
    }
复制代码
复制代码
6)前端vue删除效果细化
  (a)发送的请求:delete
  (b)发送的数据:this.$http.adornData(ids, false)
  (c)util/httpRequest.js中,封装了一些拦截器
  (d)http.adornParams是封装get请求的数据
  (e)http.adornData封装post请求的数据
  (f)ajax的get请求会被缓存,就不会请求服务器了。
  (g)所以我们在url后面拼接个date,让他每次都请求服务器
  (h)删除弹窗
  (i)删除成功弹窗
  (j)删除后重新展开父节点:重新ajax请求数据,指定展开的基准是:default-expanded-keys=“expandedKey”,返回数据后刷新this.expandedKey = [node.parent.data.catId]; 注意 node-key="catId"指定default-expanded-keys的数组中的key值只认准属性为catId
复制代码
复制代码
<template>
  <el-tree
    :data="menus"
    :props="defaultProps"
    :expand-on-click-node="false"
    show-checkbox
    node-key="catId"
    :default-expanded-keys="expandedKey"
  >
    <span class="custom-tree-node" slot-scope="{ node, data }">
      <span>{{ node.label }}</span>
      <span>
        <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 {
  //import 引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    return {
      menus: [],
      expandedKey:[],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      })
        .then(({ data }) => {
          console.log("成功获取到菜单数据..." + data.data);
          this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
        }) //fail
        .catch(() => {});
    },
    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 }) => {
            this.$message({
              type: "success",
              message: "菜单删除成功!",
            });
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = [node.parent.data.catId]
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },
  created() {
    this.getMenus();
  },
};
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类
</style>
复制代码

7、新增分类

复制代码
<template>
  <div>
    <el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <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>
export default {
  //import 引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    return {
      category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
      dialogVisible: false,
      menus: [],
      expandedKey: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      })
        .then(({ data }) => {
          console.log("成功获取到菜单数据..." + data.data);
          this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
        }) //fail
        .catch(() => {});
    },
    append(data) {
      console.log("append----", data);
      this.category.parentCid = data.catId;
      this.category.catLevel = data.catLevel * 1 + 1;
      this.dialogVisible = true;
    },
    //添加三级分类
    addCategory() {
      console.log("提交的三级分类数据", this.category);
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        this.$message({
          type: "success",
          message: "菜单保存成功!",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新出新的菜单
        this.getMenus();
        this.expandedKey = [this.category.parentCid];
      });
    },
    remove(node, data) {
      var ids = [data.catId];
      this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            this.$message({
              type: "success",
              message: "菜单删除成功!",
            });
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },
  created() {
    this.getMenus();
  },
};
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类
</style>

效果:
复制代码

 

8、修改分类

复制代码
1)后端修改代码
/**
* 信息
*/
@RequestMapping("/info/{catId}")
//@RequiresPermissions("product:category:info")
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
复制代码

 

复制代码
2)前端代码
<template>
  <div>
    <el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <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"
      :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>
      <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>
export default {
  //import 引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    return {
      title: "",
      dialogType: "", //edit,add
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        productUnit: "",
        icon: "",
        catId: null,
      },
      dialogVisible: false,
      menus: [],
      expandedKey: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      })
        .then(({ data }) => {
          console.log("成功获取到菜单数据..." + data.data);
          this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
        }) //fail
        .catch(() => {});
    },
    edit(data) {
      console.log("要修改的数据", data);
      this.dialogType = "edit";
      this.title = "修改分类";
      this.dialogVisible = true;

      //发送请求获取当前节点最新的数据
      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
        params: this.$http.adornParams({}),
      }).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;
      });
    },
    append(data) {
      console.log("append----", data);
      this.dialogType = "add";
      this.title = "添加分类";
      this.category.parentCid = data.catId;
      this.category.catLevel = data.catLevel * 1 + 1;
      this.category.catId = null;
      this.category.name = null;
      this.category.icon = "";
      this.category.productUnit = "";
      this.category.sort = 0;
      this.category.showStatus = 1;
      this.dialogVisible = true;
    },
    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      }
      if (this.dialogType == "edit") {
        this.editCategory();
      }
    },
    //修改三级分类
    editCategory() {
      var { catId, name, icon, productUnit } = this.category;
      this.$http({
        url: this.$http.adornUrl("/product/category/update"),
        method: "post",
        data: this.$http.adornData({ catId, name, icon, productUnit }, false),
      }).then(({ data }) => {
        this.$message({
          type: "success",
          message: "菜单修改成功!",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新出新的菜单
        this.getMenus();
        this.expandedKey = [this.category.parentCid];
      });
    },
    //添加三级分类
    addCategory() {
      console.log("提交的三级分类数据", this.category);
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        this.$message({
          type: "success",
          message: "菜单保存成功!",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新出新的菜单
        this.getMenus();
        this.expandedKey = [this.category.parentCid];
      });
    },
    remove(node, data) {
      var ids = [data.catId];
      this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            this.$message({
              type: "success",
              message: "菜单删除成功!",
            });
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },
  created() {
    this.getMenus();
  },
};
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类
</style>
复制代码

 

8、拖拽效果

为了防止误操作,我们通过edit把拖拽功能开启后才能进行操作。所以添加switch标签,操作是否可以拖拽。我们也可以体会到el-switch这个标签是一个开关

批量保存
但是现在存在的一个问题是每次拖拽的时候,都会发送请求,更新数据库这样频繁的与数据库交互

现在想要实现一个拖拽过程中不更新数据库,拖拽完成后,统一提交拖拽后的数据。

<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
复制代码

<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"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      :draggable="draggable"
      :allow-drop="allowDrop"
      @node-drop="handleDrop"
      ref="menuTree"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
            v-if="node.level <= 2"
            type="text"
            size="mini"
            @click="() => append(data)"
            >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: {},
  props: {},
  data() {
    return {
      pCid: [],
      draggable: false,
      updateNodes: [],
      maxLevel: 0,
      title: "",
      dialogType: "", //edit,add
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        productUnit: "",
        icon: "",
        catId: null,
      },
      dialogVisible: false,
      menus: [],
      expandedKey: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },

  //计算属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取到菜单数据...", data.data);
        this.menus = data.data;
      });
    },
    batchSave() {
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.updateNodes, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单顺序等修改成功",
          type: "success",
        });
        //刷新出新的菜单
        this.getMenus();
        //设置需要默认展开的菜单
        this.expandedKey = this.pCid;
        this.updateNodes = [];
        this.maxLevel = 0;
        // this.pCid = 0;
      });
    },
    handleDrop(draggingNode, dropNode, dropType, ev) {
      console.log("handleDrop: ", draggingNode, dropNode, dropType);
      //1、当前节点最新的父节点id
      let pCid = 0;
      let siblings = null;
      if (dropType == "before" || dropType == "after") {
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId;
        siblings = dropNode.parent.childNodes;
      } else {
        pCid = dropNode.data.catId;
        siblings = dropNode.childNodes;
      }
      this.pCid.push(pCid);

      //2、当前拖拽节点的最新顺序,
      for (let i = 0; i < siblings.length; i++) {
        if (siblings[i].data.catId == draggingNode.data.catId) {
          //如果遍历的是当前正在拖拽的节点
          let catLevel = draggingNode.level;
          if (siblings[i].level != draggingNode.level) {
            //当前节点的层级发生变化
            catLevel = siblings[i].level;
            //修改他子节点的层级
            this.updateChildNodeLevel(siblings[i]);
          }
          this.updateNodes.push({
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
          });
        } else {
          this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
        }
      }

      //3、当前拖拽节点的最新层级
      console.log("updateNodes", this.updateNodes);
    },
    updateChildNodeLevel(node) {
      if (node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) {
          var cNode = node.childNodes[i].data;
          this.updateNodes.push({
            catId: cNode.catId,
            catLevel: node.childNodes[i].level,
          });
          this.updateChildNodeLevel(node.childNodes[i]);
        }
      }
    },
    allowDrop(draggingNode, dropNode, type) {
      //1、被拖动的当前节点以及所在的父节点总层数不能大于3

      //1)、被拖动的当前节点总层数
      console.log("allowDrop:", draggingNode, dropNode, type);
      //
      this.countNodeLevel(draggingNode);
      //当前正在拖动的节点+父节点所在的深度不大于3即可
      let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
      console.log("深度:", deep);

      //   this.maxLevel
      if (type == "inner") {
        // console.log(
        //   `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
        // );
        return deep + dropNode.level <= 3;
      } else {
        return deep + dropNode.parent.level <= 3;
      }
    },
    countNodeLevel(node) {
      //找到所有子节点,求出最大深度
      if (node.childNodes != null && node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) {
          if (node.childNodes[i].level > this.maxLevel) {
            this.maxLevel = node.childNodes[i].level;
          }
          this.countNodeLevel(node.childNodes[i]);
        }
      }
    },
    edit(data) {
      console.log("要修改的数据", data);
      this.dialogType = "edit";
      this.title = "修改分类";
      this.dialogVisible = true;

      //发送请求获取当前节点最新的数据
      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
      }).then(({ data }) => {
        //请求成功
        console.log("要回显的数据", data);
        this.category.name = data.data.name;
        this.category.catId = data.data.catId;
        this.category.icon = data.data.icon;
        this.category.productUnit = data.data.productUnit;
        this.category.parentCid = data.data.parentCid;
        this.category.catLevel = data.data.catLevel;
        this.category.sort = data.data.sort;
        this.category.showStatus = data.data.showStatus;
        /**
         *         parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
         */
      });
    },
    append(data) {
      console.log("append", data);
      this.dialogType = "add";
      this.title = "添加分类";
      this.dialogVisible = true;
      this.category.parentCid = data.catId;
      this.category.catLevel = data.catLevel * 1 + 1;
      this.category.catId = null;
      this.category.name = "";
      this.category.icon = "";
      this.category.productUnit = "";
      this.category.sort = 0;
      this.category.showStatus = 1;
    },

    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      }
      if (this.dialogType == "edit") {
        this.editCategory();
      }
    },
    //修改三级分类数据
    editCategory() {
      var { catId, name, icon, productUnit } = this.category;
      this.$http({
        url: this.$http.adornUrl("/product/category/update"),
        method: "post",
        data: this.$http.adornData({ catId, name, icon, productUnit }, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单修改成功",
          type: "success",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新出新的菜单
        this.getMenus();
        //设置需要默认展开的菜单
        this.expandedKey = [this.category.parentCid];
      });
    },
    //添加三级分类
    addCategory() {
      console.log("提交的三级分类数据", this.category);
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单保存成功",
          type: "success",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新出新的菜单
        this.getMenus();
        //设置需要默认展开的菜单
        this.expandedKey = [this.category.parentCid];
      });
    },

    remove(node, data) {
      var ids = [data.catId];
      this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            this.$message({
              message: "菜单删除成功",
              type: "success",
            });
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {});

      console.log("remove", node, data);
    },
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {},
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>

复制代码

 

9、批量删除

复制代码
<template>
  <div>
    <el-switch
      v-model="draggable"
      active-text="开启拖拽"
      inactive-text="关闭拖拽"
    ></el-switch>
    <el-button v-if="draggable" @click="batchSave">批量保存</el-button>
    <el-button type="danger" @click="batchDelete">批量删除</el-button>
    <el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      :draggable="draggable"
      :allow-drop="allowDrop"
      @node-drop="handleDrop"
      ref="menuTree"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
            v-if="node.level <= 2"
            type="text"
            size="mini"
            @click="() => append(data)"
            >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: {},
  props: {},
  data() {
    return {
      pCid: [],
      draggable: false,
      updateNodes: [],
      maxLevel: 0,
      title: "",
      dialogType: "", //edit,add
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        productUnit: "",
        icon: "",
        catId: null,
      },
      dialogVisible: false,
      menus: [],
      expandedKey: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },

  //计算属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取到菜单数据...", data.data);
        this.menus = data.data;
      });
    },
    batchDelete() {
      let catIds = [];
      let checkedNodes = this.$refs.menuTree.getCheckedNodes();
      console.log("被选中的元素", checkedNodes);
      for (let i = 0; i < checkedNodes.length; i++) {
        catIds.push(checkedNodes[i].catId);
      }
      this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(catIds, false)
          }).then(({ data }) => {
            this.$message({
              message: "菜单批量删除成功",
              type: "success"
            });
            this.getMenus();
          });
        })
        .catch(() => {});
    },
    batchSave() {
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.updateNodes, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单顺序等修改成功",
          type: "success",
        });
        //刷新出新的菜单
        this.getMenus();
        //设置需要默认展开的菜单
        this.expandedKey = this.pCid;
        this.updateNodes = [];
        this.maxLevel = 0;
        // this.pCid = 0;
      });
    },
    handleDrop(draggingNode, dropNode, dropType, ev) {
      console.log("handleDrop: ", draggingNode, dropNode, dropType);
      //1、当前节点最新的父节点id
      let pCid = 0;
      let siblings = null;
      if (dropType == "before" || dropType == "after") {
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId;
        siblings = dropNode.parent.childNodes;
      } else {
        pCid = dropNode.data.catId;
        siblings = dropNode.childNodes;
      }
      this.pCid.push(pCid);

      //2、当前拖拽节点的最新顺序,
      for (let i = 0; i < siblings.length; i++) {
        if (siblings[i].data.catId == draggingNode.data.catId) {
          //如果遍历的是当前正在拖拽的节点
          let catLevel = draggingNode.level;
          if (siblings[i].level != draggingNode.level) {
            //当前节点的层级发生变化
            catLevel = siblings[i].level;
            //修改他子节点的层级
            this.updateChildNodeLevel(siblings[i]);
          }
          this.updateNodes.push({
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
          });
        } else {
          this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
        }
      }

      //3、当前拖拽节点的最新层级
      console.log("updateNodes", this.updateNodes);
    },
    updateChildNodeLevel(node) {
      if (node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) {
          var cNode = node.childNodes[i].data;
          this.updateNodes.push({
            catId: cNode.catId,
            catLevel: node.childNodes[i].level,
          });
          this.updateChildNodeLevel(node.childNodes[i]);
        }
      }
    },
    allowDrop(draggingNode, dropNode, type) {
      //1、被拖动的当前节点以及所在的父节点总层数不能大于3

      //1)、被拖动的当前节点总层数
      console.log("allowDrop:", draggingNode, dropNode, type);
      //
      this.countNodeLevel(draggingNode);
      //当前正在拖动的节点+父节点所在的深度不大于3即可
      let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
      console.log("深度:", deep);

      //   this.maxLevel
      if (type == "inner") {
        // console.log(
        //   `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
        // );
        return deep + dropNode.level <= 3;
      } else {
        return deep + dropNode.parent.level <= 3;
      }
    },
    countNodeLevel(node) {
      //找到所有子节点,求出最大深度
      if (node.childNodes != null && node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) {
          if (node.childNodes[i].level > this.maxLevel) {
            this.maxLevel = node.childNodes[i].level;
          }
          this.countNodeLevel(node.childNodes[i]);
        }
      }
    },
    edit(data) {
      console.log("要修改的数据", data);
      this.dialogType = "edit";
      this.title = "修改分类";
      this.dialogVisible = true;

      //发送请求获取当前节点最新的数据
      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
      }).then(({ data }) => {
        //请求成功
        console.log("要回显的数据", data);
        this.category.name = data.data.name;
        this.category.catId = data.data.catId;
        this.category.icon = data.data.icon;
        this.category.productUnit = data.data.productUnit;
        this.category.parentCid = data.data.parentCid;
        this.category.catLevel = data.data.catLevel;
        this.category.sort = data.data.sort;
        this.category.showStatus = data.data.showStatus;
        /**
         *         parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
         */
      });
    },
    append(data) {
      console.log("append", data);
      this.dialogType = "add";
      this.title = "添加分类";
      this.dialogVisible = true;
      this.category.parentCid = data.catId;
      this.category.catLevel = data.catLevel * 1 + 1;
      this.category.catId = null;
      this.category.name = "";
      this.category.icon = "";
      this.category.productUnit = "";
      this.category.sort = 0;
      this.category.showStatus = 1;
    },

    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      }
      if (this.dialogType == "edit") {
        this.editCategory();
      }
    },
    //修改三级分类数据
    editCategory() {
      var { catId, name, icon, productUnit } = this.category;
      this.$http({
        url: this.$http.adornUrl("/product/category/update"),
        method: "post",
        data: this.$http.adornData({ catId, name, icon, productUnit }, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单修改成功",
          type: "success",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新出新的菜单
        this.getMenus();
        //设置需要默认展开的菜单
        this.expandedKey = [this.category.parentCid];
      });
    },
    //添加三级分类
    addCategory() {
      console.log("提交的三级分类数据", this.category);
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        this.$message({
          message: "菜单保存成功",
          type: "success",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新出新的菜单
        this.getMenus();
        //设置需要默认展开的菜单
        this.expandedKey = [this.category.parentCid];
      });
    },

    remove(node, data) {
      var ids = [data.catId];
      this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            this.$message({
              message: "菜单删除成功",
              type: "success",
            });
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {});

      console.log("remove", node, data);
    },
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {},
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
复制代码

 

 

 

 

 

/**
* 信息
*/
@RequestMapping("/info/{catId}")
//@RequiresPermissions("product:category:info")
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
posted @   沧海一粟hr  阅读(708)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
点击右上角即可分享
微信分享提示

目录导航