谷粒商城项目实战——02商品服务之三级分类管理

一. 三级分类

1. 分类树形显示
1. 后端代码
1. Enerty层
  1. 在实体类CategoryEntity中添加一个children字段, 用于表示子分类

    // @TableField(exist = false) 注解表示该属性不是数据库中的字段
    @TableField(exist = false)
    private List<CategoryEntity> children;
    
2. Controller层
  1. 查询所有分类及其子分类, 并以树形结构组装起来

    @RequestMapping("/list/tree")
    // @RequiresPermissions("product:category:list")
    public R list(){
        List<CategoryEntity> entities = categoryService.listWithTree();
        return R.ok().put("data", entities);
    }
    
3. Service层
  1. 接口

    List<CategoryEntity> listWithTree();
    
  2. 实现

    /**
     * 查询所有分类及其子分类, 并以树形结构组装起来
     * @return
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        // 1. 查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        // 2. 组装成父子的树形结构
        List<CategoryEntity> level1Menus = entities.stream()
            // 2.1 找到所有的一级分类(特点: parent_cid = 0)
            .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
            // 2.2 查找所有一级分类的子分类
            .map((menu) -> {
                // 查找每个一级分类的子分类
                menu.setChildren(getChildrens(menu, entities));
                return menu;
            })
            // 2.3 将查询到的分类进行排序
            .sorted((menu1, menu2) -> {
                return (menu1.getSort() == null? 0: menu1.getSort()) -
                    (menu2.getSort() == null? 0: menu2.getSort());
            })
            // 2.4 组成List数组
            .collect(Collectors.toList());
        return level1Menus;
    }
    
    /**
     * 递归查找所有分类的子分类
     * 从all列表中获取当前root的子分类
     * @param root
     * @param all
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all){
        List<CategoryEntity> children = all.stream()
            .filter((categoryEntry) -> categoryEntry.getParentCid().equals(root.getCatId()))
            .map(categoryEntry -> {
                categoryEntry.setChildren(getChildrens(categoryEntry, all));
                return categoryEntry;
            }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null? 0: menu1.getSort()) -
                (menu2.getSort() == null? 0: menu2.getSort());
        })
            .collect(Collectors.toList());
        return children;
    }
    
4. 演示
  1. 演示效果

    image-20210428113616213

2. 前端代码
1. 配置项目路由
  1. 在index.js中修改项目的url

    // api接口请求地址
    window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
    

    image-20210428134133200

  2. 重新启动时, 会发现验证码无法显示, 因为验证码是调用的renren-fast的项目. 修改请求端口后, 就不能获取到验证码了

    image-20210428124746696

  3. 修改后端renren-fast

    1. 在pom文件中引入gulimall-common模块

    2. 配置nacos注册发现的配置信息

    image-20210428125141965

    1. 启动类中启用nacos
  4. 在gulimall-gateway中设置网关路由

    spring:
      cloud:
        gateway:
          routes:
            - id: admin_route
              # 负载均衡到指定的服务器
              uri: lb://renren-fast
              predicates:
                # 前端项目都带上/api的前缀
                - Path=/api/**
              filters:
                # http://localhost:80/api/captha.jpg ->
                # http://localhost:80/renren-fast/captha.jpg
                # 需要将url中的/api  修改为 /renren-fast
                - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
    
    
2. 解决跨域
  1. 此时验证码可以正常显示, 但是点击登录后会出现跨域问题

    image-20210428134020689

  2. 在gulimall-gateway模块中配置跨域相关的配置信息

    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;
    
    /**
     * @author Jiang锋时刻
     * @create 2021-04-28 13:45
     */
    @Configuration
    public class GulimallCorsConfig {
        @Bean
        public CorsWebFilter corsWebFilter(){
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            // 跨域的配置信息
            // 允许哪些请求头跨域访问
            corsConfiguration.addAllowedHeader("*");
            // 允许哪些请求方式跨域访问
            corsConfiguration.addAllowedMethod("*");
            // 允许哪些请求来源跨域访问
            corsConfiguration.addAllowedOrigin("*");
            // 是否允许携带cookie进行访问
            // 如果不设置, 则请求的信息会丢失
            corsConfiguration.setAllowCredentials(true);
    
            source.registerCorsConfiguration("/**",  corsConfiguration);
    
            return new CorsWebFilter(source);
        }
    }
    

    image-20210428140437987

  3. 重新启动项目并登录, 发现浏览器会出现以下错误, 原因是renren-fast中已经配置过跨域, 所以需要注释掉renren-fast中的跨域配置

    image-20210428140705494

    image-20210428140824844

  4. 此时项目前端终于可以正常获取到后端数据

    image-20210428141007618

3. 修改路由
  1. 问题: 登录后请求分类列表时, 前端仍然会将请求转发到renren-fast

    image-20210428141520526

  2. 修改后端路由配置

    - id: product_route
    	uri: lb://gulimall-product
    	predicates:
            - Path=/api/product/**
        filters:
            # http://localhost:88/api/product/category/list/tree
            # http://localhost:13000/product/category/list/tree
            - RewritePath=/api/(?<segment>.*),/$\{segment}
           
    

    image-20210428143151556

  3. 注意: 更精确的路由需要放在前面, 不然会被之前的路由规则解析

4. 前端调用
  1. 前端代码

    <template>
        <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
    
    </template>
    
    <script>
      export default {
        data() {
          return {
            menus: [],
            defaultProps: {
              // 子节点的属性
              children: 'children',
              // 要显示的内容
              label: 'name'
            }
          };
        },
        created(){
            this.getMenus()
        },
    
        methods: {
          handleNodeClick(data) {
            console.log(data);
          },
          getMenus(){
              this.$http({
                  url: this.$http.adornUrl('/product/category/list/tree'),
                  method: 'get'
              }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据
                  console.log("成功获取到菜单数据: ", data.data)
                  this.menus = data.data
              })
          }
        }
      };
    </script>
    
    <style>
    
    </style>
    

    image-20210428144515444 image-20210428144238424

  2. 显示效果

    image-20210428144644272

2. 分类删除
1. 后端代码
1. 配置逻辑删除
  1. 配置全局的逻辑删除规则(省略)

    mybatis-plus:
      global-config:
        db-config:
          id-type: auto
          # 1表示逻辑删除
          logic-delete-value: 1
          # 0表示没有逻辑删除
          logic-not-delete-value: 0
    
  2. 配置逻辑删除的组件bean(省略)

  3. 在entry中添加逻辑删除的注解

    /** 
     * 是否显示[0-不显示,1显示]
     */
    @TableLogic(value = "1", delval = "0")
    private Integer showStatus;
    
2. controller层
  1. CategoryController代码

    /**
     * 删除
     * @RequestBody: 获取请求体, 必须发送post请求
     * springMVC自动将请求体的数据(json), 转成对应的对象
     */
    @DeleteMapping("/delete")
    // @RequiresPermissions("product:category:delete")
    public R delete(@RequestBody Long[] catIds){
        // 这个方法太简单了, 不能满足业务需求, 需要重新写一个方法
        // categoryService.removeByIds(Arrays.asList(catIds));
        // 只实现了简单的删除, 还需检查当前删除的表单, 是否被别的地方引用
        categoryService.removeMenuByIds(Arrays.asList(catIds));
        return R.ok();
    }
    
3. service层
  1. 接口

    /**
     * 自定义删除分类的方法
     * @param asList
     */
    void removeMenuByIds(List<Long> asList);
    
  2. 实现

    @Override
    public void removeMenuByIds(List<Long> asList) {
        // Todo 检查当前删除的分类, 是否被别的地方引用
        baseMapper.deleteBatchIds(asList);
    }
    
2. 前端代码
  1. 前端样式

    <template>
        <!-- 
            :data: 获取的数据源
            :props: 配置选项
            @node-click: 节点被点击时的回调
            show-checkbox: 节点是否可被选择
            :expand-on-click-node: 是否在点击节点的时候展开或者收缩节点, 默认值为 true,
                                   如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
            node-key: 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的(一定要设置)
         -->
        <el-tree 
            :data="menus" 
            :props="defaultProps" 
            @node-click="handleNodeClick" 
            node-key="catId"
            show-checkbox
            :expand-on-click-node="false">
            <!-- node: 当前节点信息, data: 从数据库中获取到的内容 -->
            <span class="custom-tree-node" slot-scope="{ node, data }">
            <span>{{ node.label }}</span>
            <span>
                <!-- 
                    只有一级分类和二级分类才能追加子分类: v-if="node.level <= 2"
                    只有没有子分类的节点才能删除当前分类: v-if="node.childNodes.length == 0"
                -->
              <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 {
        data() {
          return {
            menus: [],
            defaultProps: {
              // 子节点的属性
              children: 'children',
              // 要显示的内容
              label: 'name'
            }
          };
        },
        created(){
            this.getMenus()
        },
        methods: {
          handleNodeClick(data) {
            console.log(data);
          },
          getMenus(){
              this.$http({
                  url: this.$http.adornUrl('/product/category/list/tree'),
                  method: 'get'
              }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据
                  console.log("成功获取到菜单数据: ", data.data)
                  this.menus = data.data
              })
          },
          append(data) {
            console.log(data)
          },
    
          remove(node, data) {
            console.log(node + ":" + data)
          }
        }
      };
    </script>
    

    image-20210428224837330

  2. 删除功能实现后的完整代码

    <template>
        <!-- 
            :data: 获取的数据源
            :props: 配置选项
            @node-click: 节点被点击时的回调
            show-checkbox: 节点是否可被选择
            :expand-on-click-node: 是否在点击节点的时候展开或者收缩节点, 默认值为 true,
                                   如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
            node-key: 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的(一定要设置)
            :default-expanded-keys: 默认展开的节点
         -->
        <el-tree 
            :data="menus" 
            :props="defaultProps" 
            @node-click="handleNodeClick" 
            node-key="catId"
            :default-expanded-keys="expandedKey"
            show-checkbox
            :expand-on-click-node="false">
            <!-- node: 当前节点信息, data: 从数据库中获取到的内容 -->
            <span class="custom-tree-node" slot-scope="{ node, data }">
            <span>{{ node.label }}</span>
            <span>
                <!-- 
                    只有一级分类和二级分类才能追加子分类: v-if="node.level <= 2"
                    只有没有子分类的节点才能删除当前分类: v-if="node.childNodes.length == 0"
                -->
              <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 {
        data() {
          return {
            menus: [],
            // 展开删除分类的父id
            expandedKey: [],
            defaultProps: {
              // 子节点的属性
              children: 'children',
              // 要显示的内容
              label: 'name'
            }
          };
        },
        created(){
            this.getMenus()
        },
        methods: {
          handleNodeClick(data) {
            console.log(data);
          },
          getMenus(){
              this.$http({
                  url: this.$http.adornUrl('/product/category/list/tree'),
                  method: 'get'
              }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据
                  console.log("成功获取到菜单数据: ", data.data)
                  this.menus = data.data
              })
          },
          append(data) {
            console.log(data)
          },
    
          remove(node, data) {
            // 获取选中的分类id列表
            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]
                });
            });
          }
    
        }
      };
    </script>
    
    <style>
    
    </style>
    
3. 分类添加
1. 后端代码
  1. 分类添加的后端代码在脚手架工具中就生成了, 这里可以不用编写

    @PostMapping("/save")
    public R save(@RequestBody CategoryEntity category){
        categoryService.save(category);
        return R.ok();
    }
    
2. 前端代码
  1. 样式

    <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="dialogVisible = false">确 定</el-button>
        </span>
    </el-dialog>
    
    ...
    
    data() {
          return {
          	...
            // 弹出框是否开启
            dialogVisible: false,
            category: {
                name: "",
            }
          };
        },
    
  2. 实现

    // 添加三级分类
    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.expandedKey =  [this.category.parentCid]
        });
    }
    
4. 分类修改
1. 后端代码
  1. 分类修改的后端代码在脚手架工具中就生成了, 这里可以不用编写

    @PostMapping("/update")
    // @RequiresPermissions("product:category:update")
    public R update(@RequestBody CategoryEntity category){
        categoryService.updateById(category);
        return R.ok();
    }
    
2. 前端代码
  1. 完整代码

    <!-- 
        title: 标题
        visible: 是否显示 Dialog,支持 .sync 修饰符
        		 .sync 修饰符,同时监听 Dialog 的 open 和 close 事件
        :close-on-click-modal: 是否可以通过点击 modal 关闭 Dialog(默认未true)
    -->
    <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>
    
    <script>
      export default {
        data() {
          return {
            menus: [],
            // 展开删除分类的父id
            expandedKey: [],
            defaultProps: {
              // 子节点的属性
              children: 'children',
              // 要显示的内容
              label: 'name'
            },
            // 弹出框是否开启
            dialogVisible: false,
            // 添加弹出框中的属性
            category: {
                name: "",
                parentCid: 0,
                catLevel: 0,
                showStatus: 1,
                sort: 0,
                // 菜单id, 用于分类的修改
                catId: null,
                // 图标
                icon: "",
                // 计量单位
                productUnit: ""           
            },
            // 用于区分是修改还是添加
            dialogType: "",
            title: ""
          };
        },
        created(){
            this.getMenus()
        },
        methods: {
          handleNodeClick(data) {
            console.log(data);
          },
            
          // 获取属性分类列表
          getMenus(){
              this.$http({
                  url: this.$http.adornUrl('/product/category/list/tree'),
                  method: 'get'
              }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据
                  console.log("成功获取到菜单数据: ", data.data)
                  this.menus = data.data
              })
          },
            
          // 获取要添加的分类
          append(data) {
            // 声明为添加
            this.dialogType = "add"
            this.title = "add"
            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
            console.log(data)
          },
          
          // 删除分类
          remove(node, data) {
            // 获取选中的分类id列表
            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]
                });
            });
          },
            
          // 添加三级分类
          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.expandedKey =  [this.category.parentCid]
    
              });
              //console.log("添加三级分类: ", this.category)
          },
            
          // 修改时数据回显
          edit(data){
              // 声明为修改
              this.dialogType = "edit"
              console.log("要修改的数据:", data)
              // 显示弹出框
              this.dialogVisible = true,
              this.title = "edit"
    
              // 数据回显, 不能直接回显上一次刷新的内容
              // 因为如果有其他用户修改过该数据, 则会回显错误
              // this.category.name = data.name
              // this.category.catId = data.catId
    
              // 正确的方法: 发送请求获取当前节点最新的数据
              this.$http({
              url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
              method: 'get'
              }).then(({data}) => { // 这里的data时从后台获取到的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
              })
          },
            
          // 修改三级分类数据
          editCategory(){
              // 获取category中需要的数据
              // 这里不能直接传入category, 因为前面数据回显的时候只回显了这几个数据
              // 如果直接传category, 则未回显的属性会赋值未默认值, 从而出错
              // 也可以在前面回显的时候回显所有的属性, 然后治理就可以直接使用category
              var {catId, name, icon, productUnit} = this.category
              var data = {catId, name, icon, productUnit}
              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.expandedKey =  [this.category.parentCid]
              });
          },
            
          // 提交, 通过判断dialogType的值来确定是调用添加还是修改
          submitData(){
              if(this.dialogType == "edit") {
                  this.editCategory()
              } else if(this.dialogType == "add") {
                  this.addCategory()
              }
          }
        }
      };
    </script>
    
5. 实现拉拽
1. 后端代码

在后端创建批量修改节点的方法

1. controller层
  1. 代码

    /**
     * 拖拽时批量修改排序
     * @param category
     * @return
     */
    @PostMapping("/update/sort")
    public R updateSort(@RequestBody CategoryEntity[] category){
        categoryService.updateBatchById(Arrays.asList(category));
        return R.ok();
    }
    
2. 前端代码
  1. 实现样式:

    在<el-tree>标签中添加属性draggable

    image-20210429235953866

  2. 发现问题: 通过拖拽会出现层级大于3的情况

image-20210429235837508

  1. 解决

    data() {
        return {
            ...
            // 分类的最大深度
            maxLevel: 0
        };
    },
    
    /**
     * 判断是否可以拖拽到指定位置
     * draggingNode: 被拖拽的节点
     * dropNode: 目标节点
     * type: 当前节点被拖到目标节点的1什么位置
     */
    allowDrop(draggingNode, dropNode, type) {
        // 1. 被拖动的当前节点以及所在的父节点总层数不能大于3
        // 1) 被拖动的当前节点总层数
        console.log(draggingNode,  dropNode, type)
        this.countNodeLevel(draggingNode.data)
        // 2) 当前正在拖动的节点 + 父节点所在的深度不大于4即可
        // 当前深度 = 子节点的maxLevel - 当前拖动的节点的level  + 1
        let deep = this.maxLevel - draggingNode.data.catLevel + 1
        console.log("深度: ", deep)
        if(type == "inner") {   // 如果是拖到某个节点里面
            return (deep + dropNode.level) <= 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])
            }
        }
    }
    
  2. 拖拽成功后, 需要修改节点的值, 需要使用监听事件

    添加拖拽成功执行的事件

    image-20210430210948532

    data() {
        return {
            ...
            // 要修改的节点集合
            updateNodes: []
        };
    },
    
    // 拖拽成功后执行
    handleDrop(draggingNode, dropNode, dropType, ev) {
        console.log('handleDrop: ', draggingNode, dropNode, dropType);
        let pCid = 0
        let siblings = null
        // 1. 需要获取节点的最新父节点
        // 如果是以"before" 或 "after"方式拖拽, 则当前节点的父id为dropNode
        // 如果是以"inner"方式拖拽, 则当前节点的父节点id为dropNode的catId
    
        // 2. 当前拖拽节点的最新顺序
        // 找到当前节点的父节点, 然后将其子节点的顺序从0开始排序
        // 如果是以"before" 或 "after"方式拖拽, 则当前节点的兄弟节点是dropNode父节点的子节点列表
        // 如果是以"inner"方式拖拽, 则当前节点的兄弟节点是dropNode节点的子节点列表
        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
        }
        // 3. 当前拖拽节点的最新层级
        // 遍历所有子节点, 然后排序
        for(let i = 0; i < siblings.length; i++) {
            // 获取要改的节点id以及sort要改的值
            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])
                }
                // 如果遍历的是当前正在拖拽的节点, 还需要更改当前节点的父id
                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})
            }
        }
        // console.log("updateNodes", this.updateNodes)
        // console.log("updateNodes", this.updateNodes)
        // 3. 当前拖拽节点的最新层级
        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 = []
            thihs.maxLevel = 0
        });
    },
        
    // 修改子节点的层级
    updateChildNodeLevel(node) {
        if(node.childNodes.length > 0) {
            for(let i  = 0; i < node.childNodes.length; i++) {
                var cNode = node.childNodes[i].data
                this.updateNodes.push({catId: cNode.catId, catLevel: node.childNodes[i].level})
                this.updateChildNodeLevel(node.childNodes[i])
            }
        }
    } 
    
  3. 优化1: 为拖拽功能添加一个是否开启的按钮, 防止误操作

    <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"> </el-switch>
    

    image-20210430214838154

    image-20210430214914476

  4. 优化2: 现在每拖拽一次就会和数据库进行交互, 实现拖拽完成后统一写入数据库

    1. 添加保存按钮
    <el-button v-if="draggable" @click="batchSave" type="primary">批量保存</el-button>
    

    image-20210430215724728

    1. handleDrop方法中的post提交部分的代码放到batchSave
    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 = [pCid]
            // 清空回默认值
            this.updateNodes = []
            this.maxLevel = 0
        });
    }
    

    image-20210430220012526

    1. 由于pCid是在handleDrop方法中定义的, 所以需要在data中定义, 且需要在handleDrop中修改后重新赋值

    image-20210430220558204

    image-20210430220535541

  5. 优化3: 之前在判断是否可以拖拽时, 是从数据库中读取的层级, 但是现在是批量操作, 所以应该改为当前树的实际层级; 而且之前获取层级时会出现负数的情况, 所以我们改为使用绝对值

    // 拖拽成功后执行
    handleDrop(draggingNode, dropNode, dropType, ev) {
        console.log('handleDrop: ', draggingNode, dropNode, dropType);
        let pCid = 0
        let siblings = null
        // 1. 需要获取节点的最新父节点
        // 如果是以"before" 或 "after"方式拖拽, 则当前节点的父id为dropNode
        // 如果是以"inner"方式拖拽, 则当前节点的父节点id为dropNode的catId
    
        // 2. 当前拖拽节点的最新顺序
        // 找到当前节点的父节点, 然后将其子节点的顺序从0开始排序
        // 如果是以"before" 或 "after"方式拖拽, 则当前节点的兄弟节点是dropNode父节点的子节点列表
        // 如果是以"inner"方式拖拽, 则当前节点的兄弟节点是dropNode节点的子节点列表
        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 = pCid
    
        // 遍历所有子节点, 然后排序
        for(let i = 0; i < siblings.length; i++) {
            // 获取要改的节点id以及sort要改的值
            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])
                }
                // 如果遍历的是当前正在拖拽的节点, 还需要更改当前节点的父id
                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})
            }
        }
    },
        
    // 修改子节点的层级
    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])
            }
        }
    },
    

    image-202104302217137168.

  6. 优化4: 批量修改时会有多个父节点, 所以需要将data的的pCid改为数组形式, 然后将要展开的所有节点都放到数组中

    image-20210430231502030

    image-20210430231525247

    image-20210430231604881

  7. JavaScript部分的完整代码

      export default {
        data() {
          return {
            pCid: [],
            draggable: false,
            menus: [],
            expandedKey: [],
            defaultProps: { children: 'children', label: 'name'},
            dialogVisible: false,
            category: {name: "", parentCid: 0, catLevel: 0, showStatus: 1,
                sort: 0, catId: null, icon: "", productUnit: ""},
            dialogType: "",
            title: "",
            maxLevel: 0,
            updateNodes: []
          };
        },
        created(){
            this.getMenus()
        },
        methods: {
          handleNodeClick(data) {
          },
          getMenus(){
              this.$http({
                  url: this.$http.adornUrl('/product/category/list/tree'),
                  method: 'get'
              }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据
                  this.menus = data.data
              })
          },
          append(data) {
            this.dialogType = "add"
            this.title = "add"
            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
          },
          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]
                });
            });
          },
          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.expandedKey =  [this.category.parentCid]
              });
          },
          edit(data){
              this.dialogType = "edit"
              this.dialogVisible = true,
              this.title = "edit"
              this.$http({
              url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
              method: 'get'
              }).then(({data}) => { // 这里的data时从后台获取到的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
              })
          },
          editCategory(){
              var {catId, name, icon, productUnit} = this.category
              var data = {catId, name, icon, productUnit}
              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.expandedKey =  [this.category.parentCid]
              });
          },
          submitData(){
              if(this.dialogType == "edit") {
                  this.editCategory()
              } else if(this.dialogType == "add") {
                  this.addCategory()
              }
          },
          allowDrop(draggingNode, dropNode, type) {
            this.countNodeLevel(draggingNode)
            let deep = this.maxLevel - draggingNode.data.catLevel + 1
            if(type == "inner") {
               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])
                  }
              }
          },
          handleDrop(draggingNode, dropNode, dropType, ev) {
            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)
            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})
                }
            }    
          },
          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
            });
          }
        }
      };
    
6. 分类批量删除
1. 后端代码
  1. 分类修改的后端代码在脚手架工具中就生成了, 这里可以不用编写

    /**
     * 删除
     * @RequestBody: 获取请求体, 必须发送post请求
     * springMVC自动将请求体的数据(json), 转成对应的对象
     */
    @RequestMapping("/delete")
    public R delete(@RequestBody Long[] catIds){
        // 这个方法太简单了, 不能满足业务需求, 需要重新写一个方法
        // categoryService.removeByIds(Arrays.asList(catIds));
        // 只实现了简单的删除, 还需检查当前删除的表单, 是否被别的地方引用
        categoryService.removeMenuByIds(Arrays.asList(catIds));
        return R.ok();
    }
    
2. 前端代码
  1. vue代码

    <el-button  @click="batchDelete" type="danger">批量删除</el-button>
        <el-tree 
            :data="menus" 
            :props="defaultProps" 
            @node-click="handleNodeClick" 
            node-key="catId"
            :default-expanded-keys="expandedKey"
            show-checkbox
            :expand-on-click-node="false"
            :allow-drop="allowDrop"
            @node-drop="handleDrop"
            :draggable = "draggable"
            ref="menuTree">
         ...
        </el-tree>
    </el-button>
    

    image-20210430235352680

  2. JavaScript代码

    // 批量删除
    batchDelete(){
        let catIds = []
        // 被选中的元素
        let checkedNodes = this.$refs.menuTree.getCheckedNodes();
        console.log(checkedNodes)
        // 遍历所有被选中的元素, 获取catId
        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()
            });
        })
    }
    
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值