商品服务-API-三级分类

三级分类增删改查

整个项目采用springboot 2.1.8.RELEASE版本

准备工作:

  1. 自动生成vue模板功能(新建vue组件,输入vue按回车,会自动生成vue模板)
  • 模板里面49行,这一段生成了之后会报错,建议删除 。"//@import url($3); 引入公共css类"。

https://www.cnblogs.com/songjilong/p/12635448.html

  1. element ui 官网地址

https://element.eleme.cn/#/zh-CN/component/tree#scoped-slot

  1. VScode格式化HTML代码保持标签属性不换行
    标签属性不换行方便阅读标签内容

文件-首选项-设置-Vetur-在setting.json中编辑

{
    "workbench.colorTheme": "Default Dark+",
    "editor.suggestSelection": "first",
    "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
    "java.requirements.JDK11Warning": false,
    "java.configuration.checkProjectSettingsExclusions": false,
    "java.project.importHint": false,
    "extensions.autoUpdate": false,
    "vetur.completion.scaffoldSnippetSources": {
        

        "workspace": "💼",
        "user": "🗒️",
        "vetur": "✌"
    },
    "vetur.format.defaultFormatter.html": "js-beautify-html",
 "vetur.format.defaultFormatterOptions": {
        "js-beautify-html": {
            "wrap_line_length": 220,
            "wrap_attributes": "auto",
            "end_with_newline": false
        }
    }
}

1. 配置网关路由与路径重写

1.1 前端编写vue组件。

新建组件所在位置:
src/views/modules/product/category.vue

创建组件(即调用前端请求打开该页面)即调用方法给后台发请求获取数据并在控制台打印

methods: {
    getMenus(){
        this.dataListLoading = true,
        this.$http({
          //获取所有三级分类数据,进行url拼接后发送请求。
          url: this.$http.adornUrl('/product/category/list/tree'),
          method: 'get'
        }).then(({data}) => {
          console.log("成功获取数据",data);
        })
    },
},

//组件创建完成即调用方法给后台发请求
created() {
    this.getMenus();
},

保存测试

  • 发现请求地址和我们想的不一样,这是因为前端请求没有统一交给网关进行管理。

1.2 解决请求地址问题

解决方案

  • 将请求地址前部分统一改为网关地址。这样无论发送什么请求,都会先经过网关进行处理。
  • static/config/index.js中修改网关地址前缀。
/**
 * 开发环境
 */
;(function () {
  window.SITE_CONFIG = {}
  // api接口请求地址
  window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api'
  // cdn地址 = 域名 + 版本号
  window.SITE_CONFIG['domain']  = './' // 域名
  window.SITE_CONFIG['version'] = ''  // 版本号(年月日时分)
  window.SITE_CONFIG['cdnUrl']  = window.SITE_CONFIG.domain + window.SITE_CONFIG.version
})()

  • 更改请求路径后刷新,发现页面验证码请求不到

1.3 解决验证码不显示问题

问题所在:

  • 前端将请求都交给网关来处理了,如果网关没配置,那么请求验证码的请求也会发给网关服务。
  • 网关服务没有处理验证码的controller。需要将请求转发给renren-fast这个服务才行。

将验证码请求交给网关管理。网关需要先发现renren-fast服务。

将renren-fast服务(8080端口)注册在nacos

  1. 引入common依赖(包含nacos依赖)
<dependency>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
  1. 编写配置文件,配置应用名和服务发现地址
spring:
  application:
    name: renren-fast
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  1. 开启服务发现功能
@EnableDiscoveryClient
@SpringBootApplication
public class RenrenApplication {
    public static void main(String[] args) {
	SpringApplication.run(RenrenApplication.class, args);
    }
}
  1. 登录nacos查看,服务已开启

  1. 在网关服务中配置路由规则
spring:
  cloud:
    gateway:
      routes:
        - id: admin_router
          # lb 表示负载均衡
          uri: lb://renren-fast
          # 前端请求都是/api开头的。都转给renren-fast服务
          predicates:
            - Path=/api/**
          # 目前请求:http://localhost:88/api/captcha.jpg
          # 实际需要:http://localhost:8080/renren-fast/captcha.jpg
          filters:
            - RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}
  1. 重启服务,刷新页面 。此时,能收到验证码了。

  1. 输入账号密码,验证码,点击登录,发现跨域问题。

1.4 解决跨域问题

什么是跨域:

解决方案:

  1. 使用nginx代理。浏览器将所有请求都交给nginx处理。开发过程不方便使用该方法。
  1. 配置当次请求允许跨域

添加响应头

  • Access-Control-Allow-Origin:支持哪些来源的请求跨域.
  • Access-Control-Allow-Methods:支持哪些方法跨域
  • Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含cookie.
  • Access-Control-Expose-Headers:跨域请求暴露的字段
    • CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
  • Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

具体实现:

在网关服务写一个filter类,所有请求都会先经过该过滤器。添加响应头后放行。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;

@Configuration
public class MyCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration configuration = new CorsConfiguration();

        //1.配置跨域 
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.addAllowedOrigin("*");
        configuration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**",configuration);
        return new CorsWebFilter(source);
    }
}

重启测试,跨域问题解决。

  • 请求报错是因为renren-fast项目设置的也有跨域配置,将其注释即可

2. 树形展示三级分类数据(重要)

2.1 在网关服务配置product相关路由

  • filters少写个s。让我找了1个小时的错.....
  • 网关配置是按照配置文件上下顺序来确定优先级的。所以请求拦截/api/product/**要在/api/**前面。
spring:
  cloud:
    gateway:
      routes:
        - id: product_router
          # 去服务注册中心找该服务
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          # 目前请求:http://localhost:88/api/product/category/list/tree
          # 实际需要:http://localhost:10001/product/category/list/tree
          # /api舍弃,剩余部分不变。
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
        - id: admin_router
          # lb 表示负载均衡
          uri: lb://renren-fast
          # 前端请求都是/api开头的。都转给renren-fast服务
          predicates:
            - Path=/api/**
          # 目前请求:http://localhost:88/api/captcha.jpg
          # 实际需要:http://localhost:8080/renren-fast/captcha.jpg
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

2.2 product服务(10001端口)在nacos注册

  • 编写配置文件(服务注册地址和服务名),添加服务发现注解。
spring:
  application:
    name: gulimall-product
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

刷新页面

  • 启动renren-fast、product、gateway服务

2.3 后端逻辑实现

  • 逆向工程生成了一个获取所有数据的方法。不符合要求(不是以三级分类格式返回的)。
  • 递归树形结构数据获取全部数据。

controller层,新增一个方法

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

entity层

  • 新增一个属性用来存储该分类下的所有子分类(children)
  • 用来存放该分类的子分类。不存在数据库表,需要加注解标注。之后使用VO,现在先使用和数据库字段相匹配的entiy。
// 用来存放该分类的子分类。不存在数据库表,需要加注解标注
@TableField(exist =false)
private List<CategoryEntity> children;

service层

  • 通过递归获取该分类下的所有子分类
  • 先找出一级分类,再根据一级分类找出它的子分类(二级分类)。
  • 再使用递归方式,找到二级分类的子分类(三级分类)。
  • 使用java8新特性,函数式编程https://www.cnblogs.com/cxnph/articles/15730101.html

Mybatis-Plus为我们封装好了一系列方法,继承baseMapper即可直接使用

public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {}
public interface CategoryDao extends BaseMapper<CategoryEntity> {}
CategoryDao.xml文件不用编写任何sql语句(单表简单查询使用Mybatis-Plus为我们封装好了一系列方法即可)
-----------------------------------------------------------------------------------------------------------------
@Override
public List<CategoryEntity> listWithTree() {
    //1.查出所有分类。baseMapper就是CategoryDao。参数是null即查找所有分类列表。
    List<CategoryEntity> entities = baseMapper.selectList(null);

    //2.组装父子树形结构
    //2.1 找出所有一级分类(getParentCid==0的分类数据)
    List<CategoryEntity> level1Menus = entities.stream().filter(e -> e.getParentCid() == 0)
            .map(memu->{
                //2.2将该分类的子菜单添加到children字段。映射并返回
                memu.setChildren(getChildrens(memu,entities));
                return memu;
            })
            .sorted((menu1,menu2)->
                    (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
            .collect(Collectors.toList());  //将处理好的流收集为一个List集合。
    //2.2 找到每个分类的子分类
    return level1Menus;
}
//递归方法(传递当前实体和全部实体集合)
private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
    List<CategoryEntity> children = all.stream().filter(e -> {
        //找到父id是一级分类(上面传递的是一级分类)的分类(即二级分类)
        //return e.getParentCid() == root.getCatId();  //这是一个坑,用==比较会丢失数据
       return Objects.equals(e.getParentCid(), root.getCatId());
    }).map(e -> {
        //递归查找三级分类,并将其设置给二级分类的children。
        e.setChildren(getChildrens(e, all));
        return e;
    }).sorted((menu1, menu2) ->
         (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort())
    ).collect(Collectors.toList());
    return children;
}

测试发送请求

2.4 前端展示

<template>
  <!-- 展示menus数据 -->
  <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>

<script>
export default {
  components: {},
  data() {
    //这里存放数据
    return {
      //menus接收后端数据
      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.page)
        // 将后端请求获取的data传递给menus。
        this.menus = data.page;
      });
    },
  },
  created() {
    this.getMenus();
  },
};
</script>

这里也出了个错。label写成laber了....

  • 只要获取到后端数据,前端可以根据控制台进行调试。
  • 后端返回数据是json格式的,返回对象名是data
console.log("成功获取数据", data)

所以这里需要将data.page传给menus参数进行显示。

3. 删除分类需求

3.1 前端展示(添加删除和新增按钮)

  • 点击append,调用append(data)方法
  • 点击delete,调用remove(node, data)方法
  • 一级和二级节点才显示添加按钮 ,三级节点才显示删除按钮(使用v-if判断)
<template>
  <!-- 展示menus数据 -->
  <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>

显示效果:

3.2 后端逻辑

controller层 逆向工程自动生成的delete方法

  • @RequestBody 获取请求体的内容,springMVC将其转换为所需对象。
  • 只有post方法才有请求体。请求体是json格式
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
    //将前端传递的数组转为集合
    categoryService.removeMenusByIds(Arrays.asList(catIds));
    return R.ok();
}

使用mybatis-plus实现逻辑删除

  • 使用数据库的某个字段作为标志位。删除该条数据时,只更改该字段的值,页面不进行展示。不会彻底删除该条数据。
  • 官网介绍:https://baomidou.com/pages/6b03c5/

mybatis-plus 3.3.0之后

  • 可以在配置文件全局配置逻辑删除字段名
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

mybatis-plus 3.3.0之前

  • 该项目使用的是3.2.0。在逻辑字段上加@TableLogic注解。
  • value表示显示该值。delval表示不显示该值。可自定义
  • 只需要在该字段上加上注解即可实现逻辑删除。
@TableLogic(value = "1" ,delval = "0")
private Integer showStatus;

测试逻辑删除

  • 设置日志级别。显示数据库操作语句
logging:
  level:
    com.atguigu.gulimall: debug
  • postman发送删除请求。

  • 实际执行sql语句

3.3 前后端联调逻辑删除某个或多个菜单

  • 前面后端已实现逻辑删除功能。前端需要点击删除按钮,调用删除方法,发送post请求给后端。请求成功后调用方法获取所有数据。刷新页面

前端功能:

<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>

remove(node, data) {
  var ids=[data.catId];
  this.$http({
    url: this.$http.adornUrl("/product/category/delete"),
    method: "post",
    data: this.$http.adornData(ids, false),
  }).then(({ data }) => {
    //刷新页面,重新获取数据
    this.getMenus();
    console.log('删除成功');
  });
},

后端功能:

  • 启动reren-fast后端,启动gateway网关服务,启动product商品服务。
  • 点击delete

需求一:点击删除按钮后,弹出提示框。确认是否删除该菜单。点击确认,调用post请求,执行逻辑删除。点击取消,什么也不干。

remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除[${data.name}]菜单?`, "提示", {
  confirmButtonText: "确定",
  cancelButtonText: "取消",
  type: "warning",
}).then(() => {
  //点击确定按钮,发送post请求
  this.$http({
    url: this.$http.adornUrl("/product/category/delete"),
    method: "post",
    data: this.$http.adornData(ids, false),
  }).then(({ data }) => {
    this.getMenus();
    console.log("删除成功");
  });
  this.$message({
    type: "success",
    message: "菜单删除成功!",
  });
});
},

效果: 删除成功弹出删除成功。

需求二:数据删除成功后,应该依旧展开所在菜单的父节点。而不是刷新整个页面关闭所有子节点。

  • elementui组件有介绍
  • 删除菜单成功后,先刷新页面,显示全部菜单,然后传递默认展开菜单(已删除菜单的父菜单id)。

<el-tree :default-expanded-keys="expanded">
data() {
  return {
    menus: [],
    //初始默认为空,默认不展开
    expanded: [],
    defaultProps: {
      children: "children",
      label: "name",
    },
  };
},

//点击确定按钮,发送post请求删除
this.$http({
  url: this.$http.adornUrl("/product/category/delete"),
  method: "post",
  data: this.$http.adornData(ids, false),
}).then(({ data }) => {
  //刷新出新的菜单
  this.getMenus();
  //设置需要默认展开的菜单,传递参数。
  this.expanded=[node.parent.data.catId]
});

3.4 实现批量删除功能

新增一个批量删除按钮,绑定单击事件

<el-button type="danger" size="mini" @click="batchDelete">批量删除</el-button>

单击事件处理

  • 定义一个数组,用来存放选中的catId。
  • 获取选中的数组元素checkNodes(element ui定义的),将元素catId添加到自定义数组中。
  • 弹出是否删除框
  • 点击是,给后端发删除请求。传递需要删除的catId数组。
  • 后端执行成功,刷新菜单。弹出提示框“菜单批量删除成功!”。
batchDelete() {
    let catIds=[];
    let checkNodes=this.$refs.menuTree.getCheckedNodes();
    for (let i = 0; i < checkNodes.length; i++) {
      catIds.push(checkNodes[i].catId);
    }
      this.$confirm(`是否批量删除菜单?`, "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        //点击确定按钮,发送post请求删除
        this.$http({
          url: this.$http.adornUrl("/product/category/delete"),
          method: "post",
          data: this.$http.adornData(catIds, false),
        }).then(({ data }) => {
          //刷新出新的菜单
          this.getMenus();
        });
        this.$message({
          type: "success",
          message: "菜单批量删除成功!",
        });
      })
      .catch(() => {});
  },

4. 新增需求

https://element.eleme.cn/#/zh-CN/component/dialog

思路:

  • 点击append按钮。弹出一个框,输入要添加的菜单名字。点击确认,菜单添加成功。刷新页面,默认展示添加分类的父分类id。

4.1 显示输入框

点击append按钮,调用单击函数。显示输入框

  • 点击append按钮时,双向绑定category对象。
  • 计算需要传递给后端的数据[name(用户输入),parentCid(展开的这个id),catLevel(层级,父id+1)]
<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">append</el-button>

append(data) {
    console.log(data);
    this.dialogVisible = true;
    this.category.parentCid=data.catId;
    this.category.catLevel=data.catLevel*1+1;
},
data() {
  return {
    dialogVisible: false,
  };
},

4.2 输入新增数据

显示输入框,输入值。

  • 输入框需要双向绑定data中的一个对象category。用来获取或者给其传递值。
<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>

data() {
  return {
    dialogVisible: false,
    //data中放一个对象,给属性默认值。(需要传递给后端的对象)
    category:{name:"",patentCid:0,catLevel:0,showStatus:1,sort:0},
  };
},

4.3 点击确认按钮

点击确认按钮,调用单击addCategory方法。给后端发请求。

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 }) => { 
  });

4.4 后续操作

后端执行成功,弹出添加成功,关闭输入框,刷新页面,默认选择分类框进行展示

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.expanded=[this.category.parentCid];
  });

5. 修改需求

5.1 前端增加修改按钮和修改框

  • 为每级菜单都添加一个修改按钮。点击修改按钮,会弹出一个输入框。回显数据。和添加菜单框可以共用一个。
  • 添加框显示添加。修改框显示修改。添加框点确认调用添加方法。修改框点确认调用修改方法。

html:

  • :title实现双向绑定data中的title数据。实现添加框显示添加。修改框显示修改功能。
  • submitData方法实现添加框点确认调用添加方法,修改框点确认调用修改方法功能。
<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>
  <el-button type="text" size="mini" @click="() => edit(data)">edit</el-button>
</span>

<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>

js:

  • 发送请求时:`符号可以传递参数。{}使用解构表达式。
  • 多人操作时,如果停留在页面时间过长。点击编辑时,需要从数据库实时获取最新的数据。
  • 点击确认后,关闭输入框,刷新页面。展示默认菜单(当前菜单的父id)。
data() {
  return {
    title: "",
    dialogType: "", //修改设置为edit,添加设置为add
    menus: [], //menus接收后端数据
    dialogVisible: false,
    category: {
      name: "",
      parentCid: 0,
      catLevel: 0,
      showStatus: 1,
      sort: 0,
      catId: null,
      productUnit: "",
      icon: "",
    },
    expanded: [],
    defaultProps: {
      children: "children",
      label: "name",
    },
  };
},

//点击添加按钮。
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;
},

//点击修改按钮。
edit(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.category.name;
    this.category.catId = data.category.catId;
    this.category.icon = data.category.icon;
    this.category.productUnit = data.category.productUnit;
    this.category.parentCid=data.category.parentCid
  });
},

//弹框选择。
submitData() {
  if (this.dialogType = "add") {
    this.addCategory();
  } if (this.dialogType = "edit") {
    this.editCategory();
  }
},

//添加点击确认后。
addCategory() {
  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.expanded = [this.category.parentCid];
  });
},

//编辑点击确认后。
editCategory() {
  var { catId, name, icon, productUnit } = this.category; //解构表达式,解构需要传递给后端的属性
  var data={catId, name, icon, productUnit}; //key和value相同可以省略。
  this.$http({
    url: this.$http.adornUrl("/product/category/update"),
    method: "post",
    data: this.$http.adornData(data, false),
  }).then(({ data }) => {
    this.$message({
      type: "success",
      message: "菜单修改成功!",
    });
    //关闭对话框
    this.dialogVisible = false;
    //刷新菜单
    this.getMenus();
    //设置默认展示菜单
    this.expanded = [this.category.parentCid];
  });
},

5.2 拖拽效果实现

时间有限,暂未实现。
https://element.eleme.cn/#/zh-CN/component/tree

6. 前端全部代码

  • 至此,除了拖拽功能未实现外。其余代码都在下方。
<template>

  <div>
    <el-button type="danger" size="mini" @click="batchDelete">批量删除</el-button>
    <!-- 展示menus数据 -->
    <el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" show-checkbox node-key="catId" :default-expanded-keys="expanded" 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)"><i class="el-icon-circle-plus"></i></el-button>
          <!-- 三级节点才显示删除按钮 -->
          <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)"> <i class="el-icon-delete"></i></el-button>
          <el-button type="text" size="mini" @click="() => edit(data)"><i class="el-icon-edit"></i></el-button>
        </span>
      </span>
    </el-tree>

    <!-- 显示对话框,修改和添加都会出现该对话框wrh -->
    <el-dialog :title="title" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
      <el-form :model="category">
        <el-form-item label="分类名称">
          <el-input v-model="category.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="图标">
          <el-input v-model="category.icon" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="计量单位">
          <el-input v-model="category.productUnit" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>

      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitData">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

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

export default {
  //import引入的组件需要注入到对象中才能使用
  components: {},
  data() {
    //这里存放数据,wrh
    return {
      title: "",
      dialogType: "", //修改设置为edit,添加设置为add
      menus: [], //menus接收后端数据wrh
      dialogVisible: false,
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        catId: null,
        productUnit: "",
        icon: "",
      },
      expanded: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },

  //方法集合wrh
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        // 将后端请求获取的data传递给menus。
        this.menus = data.page;
      });
    },

    batchDelete() {
      let catIds = [];
      let checkNodes = this.$refs.menuTree.getCheckedNodes();
      for (let i = 0; i < checkNodes.length; i++) {
        catIds.push(checkNodes[i].catId);
      }
      this.$confirm(`是否批量删除菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          //点击确定按钮,发送post请求删除
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(catIds, false),
          }).then(({ data }) => {
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expanded = [catIds[1].parentCid];
          });
          this.$message({
            type: "success",
            message: "菜单批量删除成功!",
          });
        })
        .catch(() => {});
    },

    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;
    },
    edit(data) {
      this.dialogType = "edit";
      this.title = "编辑分类";
      this.dialogVisible = true; //弹出编辑框

      this.$http({
        //多人操作时,保证获取的是节点最新的数据(不能偷懒)
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
      }).then(({ data }) => {
        this.category.name = data.category.name;
        this.category.catId = data.category.catId;
        this.category.icon = data.category.icon;
        this.category.productUnit = data.category.productUnit;
        this.category.parentCid = data.category.parentCid;
      });
    },

    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      } else if (this.dialogType == "edit") {
        this.editCategory();
      }
    },
    addCategory() {
      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.expanded = [this.category.parentCid];
      });
    },

    editCategory() {
      var { catId, name, icon, productUnit } = this.category; //解构表达式,解构需要传递给后端的属性
      var data = { catId, name, icon, productUnit }; //key和value相同可以省略。
      this.$http({
        url: this.$http.adornUrl("/product/category/update"),
        method: "post",
        data: this.$http.adornData(data, false),
      }).then(({ data }) => {
        this.$message({
          type: "success",
          message: "菜单修改成功!",
        });
        //关闭对话框
        this.dialogVisible = false;
        //刷新菜单
        this.getMenus();
        //设置默认展示菜单
        this.expanded = [this.category.parentCid];
      });
    },

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

  //生命周期 - 创建完成(可以访问当前this实例)wrh
  //创建组件即调用方法给后台发请求
  created() {
    this.getMenus();
  },

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

  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {},
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
posted @ 2021-12-30 17:06  初夏那片海  阅读(147)  评论(0)    收藏  举报