2022年最新《谷粒商城开发教程》:3 - 商品服务-API-三级分类



一、三级分类

1.1、查询 - 递归树形结构数据获取

分析 pms_category表 - 之前执行sql文件导入表的时候数据同时导入了
在这里插入图片描述
编写业务查询出所有的 category

1、从默认逆向生成的 list 入手进行修改

@RestController
@RequestMapping("product/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;
    
	@RequestMapping("/list")
	public R list(@RequestParam Map<String, Object> params){
    	PageUtils page = categoryService.queryPage(params);
	    return R.ok().put("page", page);
	}
}

2、创建业务方法查询所有分类及子分类

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

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

3、实体类添加属性作为子分类

@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {

	...
	
	// 不在数据表的字段需要添加该属性
	@TableField(exist = false)
	private List<CategoryEntity> children;

}

4、编写业务

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
	
	...
   
    @Override
    public List<CategoryEntity> listWithTree() {
        // 1、查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        // 2、组装成父子的树形结构
        // 2.1、查出所有一级分类
        List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == 0;
        }).map((menu) -> {
            menu.setChildren(getChildren(menu, entities));
            return menu;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return level1Menus;
    }

    // 递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == root.getCatId();
        }).map(categoryEntity -> {
            // 递归查询子菜单
            categoryEntity.setChildren(getChildren(categoryEntity, all));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return children;
    }

}

5、效果
在这里插入图片描述


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

1、新建 category.vue 进行前端渲染 - 参考 树形控件

<template>
  <div>
    <el-tree :data="menus" :props="defaultProps"></el-tree>
  </div>
</template>

<script>
export default {
  //import引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    //这里存放菜单数据
    return {
      menus: [],
      defaultProps: {
         children:"children",
         label:"label"
      }
    };
  },
  //方法集合
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then((data) => {
        console.log("成功获取菜单数据。。。",data);
      });
    },
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
};
</script>
<style scoped>
</style>

2、可以看到请求路径达不到预期 - 预期是给10000端口发送请求
在这里插入图片描述
3、因为默认是给 - http://localhost:8080/renren-fast 发送请求
在这里插入图片描述
4、修改为给网关发请求 - 以 /api 请求划分为前端请求

window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api'

5、重新刷新页面发现验证码接口都报错了,因为验证码服务默认是给 renren-fast服务 发送请求,所以应该先配置网关路由直接将前端请求 /api 全部转给 renren-fast服务

下面都是修改 renren-fast服务

5.1、pom

<dependency>
	<groupId>com.laptoy.gulimall</groupId>
	<artifactId>gulimall-common</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>

5.2、application.yml - 添加

spring:
  application:
      name: renren-fast
  cloud:
    nacos:
      discovery:
       server-addr: 192.168.88.129:8848

5.3、主启动类开启服务发现

@SpringBootApplication
@EnableDiscoveryClient
public class RenrenApplication {
	public static void main(String[] args) {
		SpringApplication.run(RenrenApplication.class, args);
	}
}

6、网关配置路径映射 - 在配置中心的 gateway.yml 进行修改

spring:
  cloud:
    gateway:
      routes:
######## 前端项目发送过来的都是/api/**的请求 #######
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
# http://localhost:88/api/captcha.jpg -->  http://localhost:8080/renren-fast/captcha.jpg

7、启动发现验证码刷新出来了


1.3、网关统一配置跨域

从8001访问88,引发CORS跨域请求,浏览器会拒绝跨域请求在这里插入图片描述


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

在这里插入图片描述
解决方案
在这里插入图片描述


1、添加响应头配置解决跨域 - gateway模块添加config

@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);
    }
}

2、修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类(跟上面的配置冲突)


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

1、网关新增路由配置,将 /api/product 请求转给 gulimall-product(配置在admin_route上方,否则导致 /api/** 请求先被admin_route拦截)

spring:
  cloud:
    gateway:
      routes:
######## 前端项目发送过来的都是/api/**的请求 #######
        
        # product服务路由
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
# http://localhost:88/api/product/category/list/tree -->  http://localhost:10000/product/category/list/tree
        
        # 人人开源后端服务路由
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
# http://localhost:88/api/captcha.jpg -->  http://localhost:8080/renren-fast/captcha.jpg

2、修改 - category.vue

由于真实数据存在 data.data.data中,所以可以先{{data}}取出第二层data信息,再将this.menus = data.data
在这里插入图片描述
修改成如下

methods: {
    getMenus() {
      this.$http({      // http://localhost:10000/product/category/list/tree
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get"
      }).then(({ data }) => { // success
      	  console.log("成功获取到菜单数据:",data.data)
          this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
      }).catch(() => {});
    },
}

在这里插入图片描述
3、修改节点显示数据

data() {
  //这里存放数据
  return {
    menus: [],
    defaultProps: {
       children: "children", //子节点属性为children
       label: "name"  	     // 显示data的name属性
    }
  };
},

至此,分类数据成功显示
在这里插入图片描述


1.5、优化页面效果

1、新增 - append 和 remove 按钮

  • append - 层级小于2时显示(因为只能为为三级分类)
  • remove - 当前节点无子节点时才可删除
  • show-checkbox - 展开多选框
  • :expand-on-click-node=false - 点击按钮不展开目录
  • node-key="catId" - 设置树节点唯一属性,最好设置,以唯一属性进行设置,这里catId为唯一属性
<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>

在这里插入图片描述

2、添加 - append和remove方法(自行点击按钮进行测试)

methods: {
  ...
  // 添加节点
  append(data) {
    console.log("append",data)
  },

  // 删除节点
  remove(node, data) {
    console.log("remove:",data,node)
  },
},

1.6、删除-逻辑删除-API

1、逻辑删除配置

数据表中默认1代表展示数据
在这里插入图片描述配置全局逻辑删除规则 修改 - 配置中心的mybatis.yml

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      # 0代表逻辑删除,1代表未逻辑删除
      logic-delete-value: 0
      logic-not-delete-value: 1

实体类属性添加注解 @TableLogin

@TableLogic
private Integer showStatus;

2、控制层

@RestController
@RequestMapping("product/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;
    
	/**
	 * 逻辑删除
	 * 请求体必须为json - 因为@RequestBody注解
	 */
	@RequestMapping("/delete")
	public R delete(@RequestBody Long[] catIds) {
	    //1、检查当前删除的菜单是否被别的
	    categoryService.removeMenuByIds(Arrays.asList(catIds));
	    return R.ok();
	}
}

3、业务层

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
	@Override
	public void removeMenuByIds(List<Long> asList) {
	    //TODO 检查当前删除的菜单,是否被别的地方引用
	    baseMapper.deleteBatchIds(asList);
	}
}

4、设置日志级别 other.yml 添加

logging:
  level: 
    com.laptoy.gulimall: debug

5、测试逻辑删除

postman发送 post请求http://localhost:88/api/product/category/delete 请求头携带json数据
在这里插入图片描述
查看数据表
在这里插入图片描述
查看日志 - 实际是更新操作
在这里插入图片描述


1.7、删除-删除效果完成

1、删除节点方法

<el-tree> - 添加 :default-expanded-keys(默认展开的值)
data - 添加 expandedKey: [],

<el-tree :default-expanded-keys="expandedKey"></el-tree>

<script>
export default {
  data() {
    return {
      ...
      // 添加默认展开数据
      expandedKey: [],
    };
  },
  //方法集合
  methods: {
	...

    // 删除节点
    remove(node, data) {
      console.log("remove:", data, node);
      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(() => {});
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
};
</script>
<style scoped>
</style>

1.8、新增-新增效果完成

1、新增弹出框 <el-dialog> - 双向绑定 category 属性,弹出框只有一个文本框填写category.name
2、data - 新增 - 默认category数据 - 默认不展开弹出框数据

<template>
  <div>
  	...
    <!-- Append按钮专属弹出框 -->
    <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 {
  ...
  data() {
    return {
      ...
      // 弹出框数据
      dialogVisible: false,
      category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
    };
  },

  //方法集合
  methods: {
  	...

    // 添加节点-赋值数据及调用弹出框
    append(data) {
      console.log("append:", data);
      this.dialogVisible = true;
      this.category.parentCid = data.catId;
      // 防止catLevel为字符串,*1强转
      this.category.catLevel = data.catLevel * 1 + 1;
    },

    // 添加节点-提交添加
    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];
      });
    },
    
  },

  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
};
</script>
<style scoped>
</style>

1.9、修改-修改效果完成

1、需要提交 catId 到数据库进行定位修改,以及其他数据:icon、productUnit 进行修改

category: {
  name: "",
  parentCid: 0,
  catLevel: 0,
  showStatus: 1,
  sort: 0,
  catId: null,
  icon: "",
  productUnit: "",
},

2、新增 edit 按钮 及 icon 和 productUnit 字段

<el-button type="text" size="mini" @click="() => edit(data)">Edit</el-button>

...

<!-- Append按钮专属弹出框 -->
<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>

3、弹出框区分为 append edit 请求 - data新增属性用来区分

dialogType: ""	// append / edit
title: "",      // 对应标题

4、弹出框 - 修改

<el-button type="primary" @click="submitData">确 定</el-button>

5、新增方法 - submitData 区分对应操作

// 区分方法
submitData(){
  if(this.dialogType == "add"){
    this.addCategory();
  }
  if(this.dialogType == "edit"){
    this.editCategory();
  }
},

6、添加节点方法 - 修改

// 添加节点-赋值数据及调用弹出框
append(data) {
  console.log("append:", data);
  this.dialogType = "add";
  this.title = "添加节点";
  this.dialogVisible = true;
  
  this.category.parentCid = data.catId;
  // 防止catLevel为字符串,*1强转
  this.category.catLevel = data.catLevel * 1 + 1;
},

7、考虑并发情况的数据脏读问题,每次点击修改按钮回显数据应该从数据库调用查看

/**
 * 信息
 */
@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId) {
    CategoryEntity category = categoryService.getById(catId);
    // 默认返回到前端都设置为 data
    return R.ok().put("data", category);
}

8、修改 - 最终效果

<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>
    <!-- Append按钮专属弹出框 -->
    <el-dialog :title="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-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 {
      // 菜单数据
      menus: [],
      // 三级分类子分类数据
      defaultProps: {
        children: "children",
        label: "name",
      },
      // 默认展开菜单
      expandedKey: [],
      // 是否展开弹出框
      dialogVisible: false,
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        catId: null,
        icon: "",
        productUnit: "",
      },
      // 弹出框类型
      dialogType: "",
      // 弹出框标题
      title: "",
    };
  },

  //方法集合
  methods: {
    // 展示菜单数据
    getMenus() {
      this.$http({
        // http://localhost:10000/product/category/list/tree
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      })
        .then(({ data }) => {
          // success
          console.log("成功获取到菜单", data.data);
          this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
        })
        .catch(() => {});
    },

    // 区分方法
    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      }
      if (this.dialogType == "edit") {
        this.editCategory();
      }
    },

    // 添加节点-赋值数据及调用弹出框
    append(data) {
      console.log("append:", data);
      this.dialogType = "add";
      this.title = "添加节点";
      this.dialogVisible = true;
      // 清除name数据
      this.category.name = "";
      this.category.icon = "";
      this.category.productUnit = "";
      this.category.parentCid = data.catId;
      // 防止catLevel为字符串,*1强转
      this.category.catLevel = data.catLevel * 1 + 1;
    },

    // 添加节点-提交添加
    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];
      });
    },

    // 修改节点-赋值数据及调用弹出框
    edit(data) {
      console.log("edit:", data);
      this.dialogType = "edit";
      this.dialogVisible = true;
      this.title = "修改节点";

      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
      }).then(({ data }) => {
        console.log("要回显的数据", data);
        this.category.catId = data.data.catId;
        this.category.name = data.data.name;
        this.category.icon = data.data.icon;
        this.category.productUnit = data.data.productUnit;
        this.category.parentCid = data.data.parentCid;
      });
    },

    // 修改节点-提交修改
    editCategory() {
      // 解构出数据进行提交,后端API接口为动态更新,不提交的数据就不更新,这里只需要通过catId更新三项
      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];
      });
    },

    // 删除节点
    remove(node, data) {
      console.log("remove:", data, node);
      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(() => {});
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },

  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
};
</script>
<style scoped>
</style>

1.10、修改-拖拽效果

在这里插入图片描述

核心代码

  • 递归求出子节点层级
  • 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
  • 当前拖动的节点深度 + 父节点深度 <= 3
// 是否允许拖入
allowDrop(draggingNode, dropNode, type) {
  // 判断被拖动的当前节点以及所在的父节点总层数不能大于3
  console.log("拖拽", draggingNode, dropNode, type);
  // 被拖动节点的总层级
  this.countNodeLevel(draggingNode.data);
  // 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
  let deep = this.maxLevel - draggingNode.data.catLevel + 1;
  console.log("深度", deep);
  // 当前拖动的节点深度 + 父节点深度 <= 3
  // 放入内部的需要 - (当前深度+放入位置深度<=3)
  if (type == "inner") {
    return deep + dropNode.level <= 3;
    // 放入前/后只需要 - (当前深度+放入位置父节点深度<=3)
  } else {
    return deep + dropNode.parent.level <= 3;
  }
},

// 统计被拖动节点的总层数
countNodeLevel(node) {
  // 递归找出所有子节点,求出最大层级
  if (node.children != null && node.children.length > 0) {
    for (let i = 0; i < node.children.length; i++) {
      if (node.children[i].catLevel > this.maxLevel) {
        this.maxLevel = node.children[i].catLevel;
      }
      this.countNodeLevel(node.children[i]);
    }
  }
},

全部代码

<template>
  <div>
    <el-tree :data="menus" :props="defaultProps" :expand-on-click-node=false show-checkbox node-key="catId" :default-expanded-keys="expandedKey" draggable :allow-drop="allowDrop">
      <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%">
      <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 {
      // 菜单数据
      menus: [],
      // 三级分类子分类数据
      defaultProps: {
        children: "children",
        label: "name",
      },
      // 默认展开框数据
      expandedKey: [],
      // 弹出框数据
      dialogVisible: false,
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        catId: null,
        icon: "",
        productUnit: "",
      },
      dialogType: "",
      title: "",
      maxLevel: 0,
    };
  },

  //方法集合
  methods: {
    // 展示菜单数据
    getMenus() {
      this.$http({
        // http://localhost:10000/product/category/list/tree
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      })
        .then(({ data }) => {
          // success
          console.log("成功获取到菜单", data.data);
          this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
        })
        .catch(() => {});
    },

    // 区分方法
    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      }
      if (this.dialogType == "edit") {
        this.editCategory();
      }
    },

    // 添加节点-赋值数据及调用弹出框
    append(data) {
      console.log("append:", data);
      this.dialogType = "add";
      this.title = "添加节点";
      this.dialogVisible = true;
      // 清除name数据
      this.category.name = "";
      this.category.icon = "";
      this.category.productUnit = "";
      this.category.parentCid = data.catId;
      // 防止catLevel为字符串,*1强转
      this.category.catLevel = data.catLevel * 1 + 1;
    },

    // 添加节点-提交添加
    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];
      });
    },

    // 修改节点-赋值数据及调用弹出框
    edit(data) {
      console.log("edit:", data);
      this.dialogType = "edit";
      this.dialogVisible = true;
      this.title = "修改节点";

      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
      }).then(({ data }) => {
        console.log("要回显的数据", data);
        this.category.catId = data.data.catId;
        this.category.name = data.data.name;
        this.category.icon = data.data.icon;
        this.category.productUnit = data.data.productUnit;
        this.category.parentCid = data.data.parentCid;
      });
    },

    // 修改节点-提交修改
    editCategory() {
      // 解构出数据进行提交,后端API接口为动态更新,不提交的数据就不更新,这里只需要通过catId更新三项
      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];
      });
    },

    // 删除节点
    remove(node, data) {
      console.log("remove:", data, node);
      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(() => {});
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },

    // 是否允许拖入
    allowDrop(draggingNode, dropNode, type) {
      // 判断被拖动的当前节点以及所在的父节点总层数不能大于3
      console.log("拖拽", draggingNode, dropNode, type);
      // 被拖动节点的总层级
      this.countNodeLevel(draggingNode.data);

      // 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
	  let deep = Math.abs(this.maxLevel - draggingNode.level) + 1		
      console.log("深度", deep);

      // 当前拖动的节点深度 + 父节点深度 <= 3
      // 放入内部的需要 - (当前深度+放入位置深度<=3)
      if (type == "inner") {
        return deep + dropNode.level <= 3;
        // 放入前/后只需要 - (当前深度+放入位置父节点深度<=3)
      } else {
        return deep + dropNode.parent.level <= 3;
      }

    },

    // 统计被拖动节点的总层数
    countNodeLevel(node) {
      // 递归找出所有子节点,求出最大层级
      if (node.children != null && node.children.length > 0) {
        for (let i = 0; i < node.children.length; i++) {
          if (node.children[i].catLevel > this.maxLevel) {
            this.maxLevel = node.children[i].catLevel;
          }
          this.countNodeLevel(node.children[i]);
        }
      }
    },
  },

  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
};
</script>
<style scoped>
</style>

1.11、修改-拖拽结果收集

1、拖拽成功绑定事件

<el-tree @node-drop="handleDrop" />

methods:{
	handleDrop(draggingNode, dropNode, dropType, ev) {
	   console.log('tree drop: ', dropNode.label, dropType);
	},
}

2、新增 - updateNodes存放修改的数据

data:{
	updateNodes: [],
}

3、新增 - 拖拽成功事件

// 拖拽成功事件
handleDrop(draggingNode, dropNode, dropType, ev) {
  console.log('handleDrop: ', draggingNode, dropNode, dropType)
  let pCid = 0
  let sibings = null
  // 1、获取当前节点不同情况的 (最新的父节点id) 及 (所有兄弟节点)
  if (dropType == 'inner') {
    pCid = dropNode.data.catId
    sibings = dropNode.childNodes
  } else {
    // 如果放在最顶层会导致找不到父节点id,三目运算符排除一下
    pCid =
      dropNode.parent.data.catId == undefined
        ? 0
        : dropNode.parent.data.catId
    sibings = dropNode.parent.childNodes
  }

  // 2、当前拖拽节点和兄弟节点的最新顺序
  for (let i = 0; i < sibings.length; i++) {
    // 如果遍历的是当前节点
    if (sibings[i].data.catId == draggingNode.data.catId) {
      let catLevel = draggingNode.level;
      // 如果当前节点的层级发生变化
      if (sibings[i].level != draggingNode.level) {
        // 层级修改为兄弟节点的层级
        catLevel = sibings[i].level;
        // 修改子节点层级
        this.updateChildNodeLevel(sibings[i]);

      }
      // 设置父节点id
      this.updateNodes.push({
        catId: sibings[i].data.catId,
        sort: i,
        parentCid: pCid,
        catLevel: catLevel,
      })
    }

    // 其余节点直接修改顺序
    this.updateNodes.push({ catId: sibings[i].data.catId, sort: i })
  }
},

// 修改子节点层级
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])
    }
  }

}

1.12、拖拽功能完成

1、添加API接口处理批量修改请求

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

2、发送请求

// 拖拽成功事件
handleDrop(draggingNode, dropNode, dropType, ev) {
  ...
  // 发送请求修改节点
  this.$http({
    url: this.$http.adornUrl('/product/category/update/sort'),
    method: 'post',
    data: this.$http.adornData(this.updateNodes, false)
  }).then(({ data }) => {
    this.$message({
      type: 'success',
      message: '菜单顺序修改成功!',
    })
    // 刷新出新的菜单
    this.getMenus()
    // 展开父菜单
    this.expandedKey = [pCid]
    // 每次拖拽成功重新赋值默认值
    this.updateNodes =[];
    this.maxLevel = 0;
  });

},

1.13、批量拖拽效果

1、添加开关并双向绑定draggable

<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽">
<el-button type="primary" v-if="draggable" @click="batchSave">批量保存</el-button>
<el-tree :draggable="draggable" >

2、添加 draggable并赋值默认值false,当true表示开启拖拽,新增全局pCid

data:{
	draggable: false,
	pCid:[]
}

// 拖拽成功事件
handleDrop(draggingNode, dropNode, dropType, ev) {
  ,,,
  // 保存全局pCid给下述方法使用
  this.pCid.push(pCid);

4、将单独保存的请求抽取成独立请求

// 批量保存
batchSave() {
  this.$http({
    url: this.$http.adornUrl('/product/category/update/sort'),
    method: 'post',
    data: this.$http.adornData(this.updateNodes, false)
  }).then(({ data }) => {
    this.$message({
      type: 'success',
      message: '菜单顺序修改成功!',
    })
    // 刷新出新的菜单
    this.getMenus()
    // 展开父菜单
    this.expandedKey = this.pCid
    // 每次拖拽成功重新赋值默认值
    this.updateNodes = [];
    this.maxLevel = 0;
    this.pCid = 0;
  });
}

5、之前的代码有点问题,因为未批量保存在每次操作都需要经数据库取数据,我们直接修改成取固定子树的数据

// 是否允许拖入
allowDrop(draggingNode, dropNode, type) {
  // 判断被拖动的当前节点以及所在的父节点总层数不能大于3
  console.log('拖拽', draggingNode, dropNode, type)
  // 被拖动节点的总层级
  this.countNodeLevel(draggingNode)
  // 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
  let deep = Math.abs(this.maxLevel - draggingNode.level) + 1
  console.log('深度', deep)
  // 当前拖动的节点深度 + 父节点深度 <= 3
  // 放入内部的需要 - (当前深度+放入位置深度<=3)
  if (type == 'inner') {
    return deep + dropNode.level <= 3
    // 放入前/后只需要 - (当前深度+放入位置父节点深度<=3)
  } else {
    return deep + dropNode.parent.level <= 3
  }
},
// 统计被拖动节点的总层数
countNodeLevel(node) {
  // 递归找出所有子节点,求出最大层级
  if (node.childNodes != null && node.childNodes.length > 0) {
    for (let i = 0; i < node.childNodes.length; i++) {
      if (node.childNodes[i].level > this.maxLevel) {
        this.maxLevel = node.childNodes[i].level;
      }
      this.countNodeLevel(node.childNodes[i])
    }
  }
},

1.14、批量删除

<el-button type="danger">批量删除</el-button>
<el-tree ref="menuTree" >
// 批量删除
batchDelete() {
  let catIds = [];
  // 获取选中的节点内容
  let checkNodes = this.$refs.menuTree.getCheckedNodes();
  for (let i = 0; i < checkNodes.length; i++) {
    catIds.push(checkNodes[i].catId)
  }
  console.log(checkNodes);
  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({
            type: 'success',
            message: '菜单删除成功!',
          })
          // 刷新出新的菜单
          this.getMenus()
          // 删除成功展开父菜单
          this.expandedKey = []
        })
        .catch(() => {
        })
    })
    .catch(() => {
      this.$message({
        type: 'info',
        message: '已取消删除',
      })
    })
} 

1.15、全部代码展示

<template>
  <div>
    <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>
    <el-button type="primary" 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%">
      <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 {
      // 菜单数据
      menus: [],
      // 三级分类子分类数据
      defaultProps: {
        children: 'children',
        label: 'name'
      },
      // 默认展开框数据
      expandedKey: [],
      // 弹出框数据
      dialogVisible: false,
      category: {
        name: '',
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        catId: null,
        icon: '',
        productUnit: '',
      },
      dialogType: '',
      title: '',
      maxLevel: 0,
      updateNodes: [],
      draggable: false,
      pCid: [],
    }
  },

  //方法集合
  methods: {
    // 展示菜单数据
    getMenus() {
      this.$http({
        // http://localhost:10000/product/category/list/tree
        url: this.$http.adornUrl('/product/category/list/tree'),
        method: 'get',
      })
        .then(({ data }) => {
          // success
          console.log('成功获取到菜单', data.data)
          this.menus = data.data // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
        })
        .catch(() => {
        })
    },

    // 区分方法
    submitData() {
      if (this.dialogType == 'add') {
        this.addCategory()
      }
      if (this.dialogType == 'edit') {
        this.editCategory()
      }
    },

    // 添加节点-赋值数据及调用弹出框
    append(data) {
      console.log('append:', data)
      this.dialogType = 'add'
      this.title = '添加节点'
      this.dialogVisible = true
      // 清除name数据
      this.category.name = ''
      this.category.icon = ''
      this.category.productUnit = ''
      this.category.parentCid = data.catId
      // 防止catLevel为字符串,*1强转
      this.category.catLevel = data.catLevel * 1 + 1
    },

    // 添加节点-提交添加
    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]
      })
    },

    // 修改节点-赋值数据及调用弹出框
    edit(data) {
      console.log('edit:', data)
      this.dialogType = 'edit'
      this.dialogVisible = true
      this.title = '修改节点'

      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: 'get',
      }).then(({ data }) => {
        console.log('要回显的数据', data)
        this.category.catId = data.data.catId
        this.category.name = data.data.name
        this.category.icon = data.data.icon
        this.category.productUnit = data.data.productUnit
        this.category.parentCid = data.data.parentCid
      })
    },

    // 修改节点-提交修改
    editCategory() {
      // 解构出数据进行提交,后端API接口为动态更新,不提交的数据就不更新,这里只需要通过catId更新三项
      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]
      })
    },

    // 删除节点
    remove(node, data) {
      console.log('remove:', data, node)
      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(() => {
            })
        })
        .catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除',
          })
        })
    },

    // 是否允许拖入
    allowDrop(draggingNode, dropNode, type) {
      // 判断被拖动的当前节点以及所在的父节点总层数不能大于3
      console.log('拖拽', draggingNode, dropNode, type)
      // 被拖动节点的总层级
      this.countNodeLevel(draggingNode)

      // 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
      let deep = Math.abs(this.maxLevel - draggingNode.level) + 1
      console.log('深度', deep)

      // 当前拖动的节点深度 + 父节点深度 <= 3
      // 放入内部的需要 - (当前深度+放入位置深度<=3)
      if (type == 'inner') {
        return deep + dropNode.level <= 3
        // 放入前/后只需要 - (当前深度+放入位置父节点深度<=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])
        }
      }
    },

    // 拖拽成功事件
    handleDrop(draggingNode, dropNode, dropType, ev) {
      console.log('handleDrop: ', draggingNode, dropNode, dropType)
      let pCid = 0
      let sibings = null
      // 1、获取当前节点不同情况的 (最新的父节点id) 及 (所有兄弟节点)
      if (dropType == 'inner') {
        pCid = dropNode.data.catId
        sibings = dropNode.childNodes
      } else {
        // 如果放在最顶层会导致找不到父节点id,三目运算符排除一下
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId
        sibings = dropNode.parent.childNodes
      }
      this.pCid.push(pCid);

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

        // 其余节点直接修改顺序
        this.updateNodes.push({ catId: sibings[i].data.catId, sort: i })
      }

    },

    // 修改子节点层级
    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])
        }
      }

    },

    // 批量保存
    batchSave() {
      this.$http({
        url: this.$http.adornUrl('/product/category/update/sort'),
        method: 'post',
        data: this.$http.adornData(this.updateNodes, false)
      }).then(({ data }) => {
        this.$message({
          type: 'success',
          message: '菜单顺序修改成功!',
        })
        // 刷新出新的菜单
        this.getMenus()
        // 展开父菜单
        this.expandedKey = this.pCid
        // 每次拖拽成功重新赋值默认值
        this.updateNodes = [];
        this.maxLevel = 0;
        this.pCid = 0;
      });

    },

    // 批量删除
    batchDelete() {
      let catIds = [];
      // 获取选中的节点内容
      let checkNodes = this.$refs.menuTree.getCheckedNodes();
      for (let i = 0; i < checkNodes.length; i++) {
        catIds.push(checkNodes[i].catId)
      }
      console.log(checkNodes);
      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({
                type: 'success',
                message: '菜单删除成功!',
              })
              // 刷新出新的菜单
              this.getMenus()
              // 删除成功展开父菜单
              this.expandedKey = []
            })
            .catch(() => {
            })
        })
        .catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除',
          })
        })
    } 
  },

    //生命周期 - 创建完成(可以访问当前this实例)
    created() {
      this.getMenus()
    },
  }
</script>
<style scoped>
</style>
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Laptoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值