谷粒商城六商品服务三级分类

递归-树形结构数据获取

sql文件

sql文件太大了,这个博主写的非常厉害,看他的就ok了

CategoryController

package com.atguigu.gulistore.product.controller;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.atguigu.gulistore.product.entity.CategoryEntity;
import com.atguigu.gulistore.product.service.CategoryService;
import com.atguigu.common.utils.R;



/**
 * 商品三级分类
 *
 * @author lx
 * @email qazokmzjl@gmail.com
 * @date 2021-10-07 15:39:33
 */
@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);
    }
}

CategoryService

package com.atguigu.gulistore.product.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.gulistore.product.entity.CategoryEntity;

import java.util.List;
import java.util.Map;

/**
 * 商品三级分类
 *
 * @author lx
 * @email qazokmzjl@gmail.com
 * @date 2021-10-07 15:39:33
 */
public interface CategoryService extends IService<CategoryEntity> {

    PageUtils queryPage(Map<String, Object> params);

    List<CategoryEntity> listWithTree();
}

CategoryServiceImpl

这么查询,实际只查询了一次数据库

package com.atlinxi.gulimall.product.service.impl;

import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;

import com.atlinxi.gulimall.product.dao.CategoryDao;
import com.atlinxi.gulimall.product.entity.CategoryEntity;
import com.atlinxi.gulimall.product.service.CategoryService;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {


    //    之前是使用@Autowired注入
//    现在继承了 mybatisplus的ServiceImpl,泛型是CategoryDao,
//    我们可以直接使用baseMapper,它代表的也就是CategoryDao
//    @Autowired
//    private CategoryDao categoryDao;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }


    @Override
    public List<CategoryEntity> listWithTree() {

        // 1. 查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        // 2. 组装成父子的树形结构

        // 2.1 找到所有的一级分类
        List<CategoryEntity> level1Menu = entities.stream()
                // 过滤
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0
                // 上面的过滤会得到一个list,
                // menu只是一个变量名,指list返回的每一个元素
                ).map(menu -> {
                    menu.setChildren(getChildrens(menu, entities));
                    return menu;
                }).sorted((menu1, menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
                // 把结果收集成一个list
                .collect(Collectors.toList());

        return level1Menu;
    }

    /**
     * 递归查找所有菜单的子菜单
     *
     * @param root 当前分类
     * @param all  所有分类
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {

        List<CategoryEntity> children = all.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId())
                .map(categoryEntity -> {categoryEntity.setChildren(getChildrens(categoryEntity,all)); return categoryEntity;})
                .sorted((menu1,menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
                .collect(Collectors.toList());
        return children;
    }
}

CategoryEntity

package com.atguigu.gulistore.product.entity;

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

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

import lombok.Data;

/**
 * 商品三级分类
 * 
 * @author lx
 * @email qazokmzjl@gmail.com
 * @date 2021-10-07 15:39:33
 */
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 分类id
	 */
	@TableId
	private Long catId;
	/**
	 * 分类名称
	 */
	private String name;
	/**
	 * 父分类id
	 */
	private Long parentCid;
	/**
	 * 层级
	 */
	private Integer catLevel;
	/**
	 * 是否显示[0-不显示,1显示]
	 */
	private Integer showStatus;
	/**
	 * 排序
	 */
	private Integer sort;
	/**
	 * 图标地址
	 */
	private String icon;
	/**
	 * 计量单位
	 */
	private String productUnit;
	/**
	 * 商品数量
	 */
	private Integer productCount;

	/**
	 * 将当前菜单所有子分类保存到这个分类
	 */
//	这个注解代表,这个属性在数据库是没有相对应的字段的
	@TableField(exist = false)
	private List<CategoryEntity> children;

}

启动renren-fast服务和renren-fast-vue

会报set get方法找不到

lombok的依赖换到新版本就可以了

<lombok.version>1.18.14</lombok.version>

我的renren-fast在导入gulimall-common依赖的时候启动报错,大概是springboot和springcloud版本不兼容,

新增或者修改pom

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath></relativePath>
    </parent>


<spring-cloud.version>2020.0.4</spring-cloud.version>



<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

前端页面分析

在访问页面的http://localhost:8001/#/login,菜单栏新建商品系统,商品系统下新建分类维护
分类维护的菜单url填写product/category
当点击分类维护的时候,项目就会访问 http://localhost:8001/#/product-category

系统管理下的角色管理http://localhost:8001/#/sys-role,对应的项目路径是src-views-modules-sys-role.vue

根据这个规则,http://localhost:8001/#/product-category,对应的项目路径应该是src-views-modules-product-category.vue

application.yml

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

在启动类上添加@EnableDiscoveryClient

renren-fast-vue

static/config/index.js

// 访问网关gateway
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

src/views/modules/product/category.vue

创建这个文件

<template>
  <div>
    <el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick">
    </el-tree>
  </div>
</template>

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

export default {
  //import 引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    return {
      data: [],
      defaultProps: {
        children: "children",
        label: "label",
      },
    };
  },
  methods: {
    handleNodeClick(data) {
      console.log(data);
    },
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
        // params: this.$http.adornParams({
        //   page: this.pageIndex,
        //   limit: this.pageSize,
        //   roleName: this.dataForm.roleName,
        // }),
      }).then(({ data }) => {
        // if (data && data.code === 0) {
        //   this.dataList = data.page.list;
        //   this.totalPage = data.page.totalCount;
        // } else {
        //   this.dataList = [];
        //   this.totalPage = 0;
        // }
        // this.dataListLoading = false;

        console.log(data)
      });
    },
  },
  //计算属性 类似于 data 概念
  computed: {},
  //监控 data 中的数据变化
  watch: {},
  //生命周期 - 创建完成(可以访问当前 this 实例)
  created() {
    this.getMenus();
  },
  //生命周期 - 挂载完成(可以访问 DOM 元素)
  mounted() {},
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>

gateway 网关路由配置及路径重写

这步完成之后,正常页面是会报404的,但我这儿报了503,可以简单的理解为和500差不多,就是服务器错误

原因是lb是负载均衡的操作,但是由于nacos排除了ribbon的负载均衡依赖,所以得自己引入负载均衡的依赖,我这里直接引入loadbalancer依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: https://www.baidu.com
          predicates:
            #              根据参数来匹配
            - Query=url,baidu


        #  前端项目发送请求都以 /api 开头
        - id: admin_route
          #  lb 负载均衡  到renren-fast服务
          uri: lb://renren-fast
          # 匹配所有以api开头的请求
          predicates:
            - Path=/api/**
          filters:
            #  路径重写
            # http://localhost:88/api/captcha.jpg 在网关匹配到相应的规则后
            #  就变成了 http://localhost:8080/api/captcha.jpg
            # 但实际上我们需要真正访问的是 http://localhost:8080/renren-fast/captcha.jpg
            # (?<segment>.*)  $\{segment} 相当于片段
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

网关统一配置跨域

完成上面的部分,发现还是不能正常访问,那是因为发生CROS(跨域)

跨域
指的是浏览器不能执行远程网站的脚本或者发送请求。它是由浏览器的同源策略造成的,是浏览器对javascript添加的安全限制
同源策略:协议、域名、端口都要相同
注意:域名和其对应的ip也属于跨域

在这里插入图片描述

跨域流程

options,类似于get,post,也是http一种请求方式。

简单请求指,get,head,post
并且Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

有兴趣的可以看这篇文章,跨域资源共享
在这里插入图片描述

解决跨域方案一

使用nginx同时代理前端和后端,这种比较麻烦

  1. 我们将前端项目和后端项目都部署到nginx服务器,
  2. 浏览器访问前端项目时访问nginx地址
  3. nginx将静态请求都代理给前端项目,将动态请求反向代理给网关

在这里插入图片描述

解决跨域方案二

网关统一配置跨域

这个文件需要新建,

然后把renren-fast服务中的config/CorsConfig内容全部注释掉,否则两个解决跨域的冲突了

package com.atlinxi.gulimall.gateway.config;

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

@Configuration
public class GulimallCorsConfiguration {

    // springboot提供的CorsWebFilter
    // 只需要将它放入容器即可
    @Bean
    public CorsWebFilter corsWebFilter(){

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 配置跨域
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        // 请求来源
        // 我的springboot版本是2.5.5,用下面那个配置
//        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedOriginPattern("*");
        // 允许携带cookie
        corsConfiguration.setAllowCredentials(true);


        // 允许任意路径跨域
        source.registerCorsConfiguration("/**",corsConfiguration);

        return new CorsWebFilter(source);
    }
}

三级分类增删改查

我们这里的crud界面是用element ui的组件自己写的,效果是这样的
在这里插入图片描述

renren-fast-vue自带的crud界面是这样的
在这里插入图片描述

用哪个都行,但老师第一次带着做了一次完整的crud,之后将使用renren-fast-vue自带的,

我们在todo谷粒商城二本地虚拟机环境搭建及项目初始化
使用逆向工程生成代码的时候,当时是删掉了resources下的src文件,其实就是对应的前端crud的代码,我们以后就用这个

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

此时url是这个,因为我们在gateway配置的路由是所有api都进入renren-fast,但是我们商品服务是在product服务的,所以我们需要给gateway添加路由规则

配置文件

gulimall-gateway/application.yml

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: https://www.baidu.com
          predicates:
            #              根据参数来匹配
            - Query=url,baidu

        # 和admin_route顺序不能乱,否则页面访问报404,因为被它拦截了
        # 我们一般把精确的路由放在上面,优先级高
        # 匹配了这个路由之后,不会匹配下面的路由
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          # 前端的请求是 http://localhost:88/api/product/category/list/tree
          # 后端实际需要的请求是,http://localhost:12000/product/category/list/tree
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

        #  前端项目发送请求都以 /api 开头
        - id: admin_route
          #  lb 负载均衡  到renren-fast服务
          uri: lb://renren-fast
          # 匹配所有以api开头的请求
          predicates:
            - Path=/api/**
          filters:
            #  路径重写
            # http://localhost:88/api/captcha.jpg 在网关匹配到相应的规则后
            #  就变成了 http://localhost:8080/api/captcha.jpg
            # 但实际上我们需要真正访问的是 http://localhost:8080/renren-fast/captcha.jpg
            # (?<segment>.*)  $\{segment} 相当于片段
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}


gulimall-product配置

因为之前老师是有部分的配置在nacos,部分配置在本地idea,我觉得太乱了,而且后期应该会整合到nacos

所以我这里把所有配置文件都写到了nacos

# bootstrap.properties
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=4853bbe2-4518-4916-b8de-37ef773f8ca6

spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true

spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true

spring.cloud.nacos.config.extension-configs[2].data-id=other.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true



# datasource.yml group dev
spring:
  datasource:
    url: jdbc:mysql://192.168.56.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root




# mybatis.yml group dev
# classpath* 依赖的依赖也扫描
mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  #  主键自增
  # 在实体类中,每个主键都有@TableId,默认为None,可以设置为自增
  #  但是如果在每个实体类中都设置的话,如果有一天需要修改,将相当麻烦
  global-config:
    db-config:
      id-type: auto
      # 这里不能用这个全局配置的字段,需要在CategoryEntity
      # 的showStatus字段加@TableLogic
      # 因为其他的表也可能有这个字段,如果有的话,
      # 在使用mybatis-plus的uodate方法时,就不能更新showStatus了
      # sql语句会变成这样
      # update pms_category where catId = 1 and showStatus = 1
      # 我明明实体类传入了catId和showStatus两个值,结果我的showStatus为什么不见了。。。。
      # todo 这个有时间看吧
      # logic-delete-field: showStatus # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 0 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 1 # 逻辑未删除值(默认为 0)





# other.yml group dev
server:
  port: 12000
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
logging:
  level: 
    com.atlinxi.gulimall: debug

前端

renren-fast-vue/category.vue

<template>
  <div>
    <!-- data绑定数据 -->
    <!-- expand-on-click-node 是否在点击节点的时候展开或者收缩节点, 
          默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 

        show-checkbox 节点是否可被选择,其实就是有没有复选框

        node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的      

        default-expanded-keys 默认展开的节点的 key 的数组
        allow-drop  拖拽时判定目标节点能否被放置。
          type 参数有三种情况:'prev''inner''next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后

        draggable 是否开启拖拽节点功能

        @node-drop  拖拽成功完成时触发的事件

          共四个参数,依次为:
            被拖拽节点对应的 Node、结束拖拽时最后进入的节点、被拖拽节点的放置位置(before、after、inner)、event


        

        ref="menuTree"<el-tree></el-tree> 起个名字,可以通过vue实例来进行调用该组件
    -->
    <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="">
    </el-switch>
    <el-button v-if="draggable" @click="batchSave">批量保存</el-button>
    <el-button type="danger" @click="batchDelete">批量删除</el-button>
    <el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      :draggable="draggable"
      :allow-drop="allowDrop"
      @node-drop="handleDrop"
      ref="menuTree"
    >
      <!-- 每一个分类都会跟一个这个span 也就是 Append Delete -->
      <!-- node为当前节点,例如有没有展开,有没有选中之类
            data是这个节点真正的数据 -->
      <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>
          <el-button type="text" size="mini" @click="() => edit(data)">
            edit
          </el-button>
        </span>
      </span>
    </el-tree>
    <!-- dialogVisible 打开或者关闭对话框 
        close-on-click-modal 是否可以点击对话框的外面关闭对话框
    -->
    <el-dialog
      :close-on-click-modal="false"
      :title="title"
      :visible.sync="dialogVisible"
      width="30%"
    >
      <!-- model 表单数据 -->
      <el-form :model="category">
        <el-form-item label="分类名称" :label-width="formLabelWidth">
          <el-input v-model="category.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="图标" :label-width="formLabelWidth">
          <el-input v-model="category.icon" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="计量单位" :label-width="formLabelWidth">
          <el-input
            v-model="category.productUnit"
            autocomplete="off"
          ></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitData()">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

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

export default {
  //import 引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    return {
      pCid: [],
      draggable: false,
      updateNodes: [],
      maxLevel: 0,
      title: "",
      dialogType: "", // edit,add
      formLabelWidth: "",
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        catId: null,
        productUnit: "",
        icon: "",
      },
      dialogVisible: false,
      menus: [],
      expandedKey: [],
      defaultProps: {
        // 以哪个字段展开分类
        children: "children",
        // 哪个字段为分类名称
        label: "name",
      },
    };
  },
  //计算属性 类似于 data 概念
  computed: {},
  //监控 data 中的数据变化
  watch: {},
  //方法集合
  methods: {
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        this.menus = data.data;
      });
    },
    // 批量删除
    batchDelete() {
      // this vue实例,refs 获取vue实例的所有组件,

      // getCheckedNodes
      //       若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组

      //         (leafOnly, includeHalfChecked) 接收两个 boolean 类型的参数,
      //           1. 是否只是叶子节点(子节点),默认值为 false
      //           2. 是否包含半选节点(当子节点没有被全部选中,父节点则是半选节点),默认值为 false
      let catIds = [];
      let checkedNodes = this.$refs.menuTree.getCheckedNodes();
      console.log(checkedNodes);
      for (let i = 0; i < checkedNodes.length; i++) {
        catIds.push(checkedNodes[i].catId);
      }

      this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(catIds, false),
          }).then(({ data }) => {
            this.$message({
              type: "success",
              message: "菜单批量删除成功",
            });
            this.getMenus();
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
    // 多次拖拽排序的時候,统一提交数据库更新,避免了每拖拽一次就和后端交互一次
    batchSave() {
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.updateNodes, false),
      }).then(({ data }) => {
        this.$message({
          type: "success",
          message: "菜单顺序等修改成功",
        });
        this.getMenus();
        this.expandedKey = this.pCid;
        this.updateNodes = [];
        this.maxLevel = 0;
        // this.pCid = 0;
      });
    },
    // 拖拽成功完成时触发的事件

    // 我们需要收集的信息有,
    // 1. 拖拽节点的父节点,
    // 2. 拖拽节点最新的排序(就是将父节点的children重新排序)
    // 3. 拖拽节点的层级
    handleDrop(draggingNode, dropNode, dropType) {
      // 1. 当前节点最新的父节点id
      let pCid = 0;
      let siblings = null;

      // 被拖拽节点的放置位置 如果是dropNode 之前或者之后
      if (dropType == "before" || dropType == "after") {
        // 被拖拽节点的父节点,也就是dropNode的父节点
        // 如果把二级分类和三级分类拖拽到一级分类的前后,那么此时dropNode的parent的data不是一个节点,而是所有一级分类的数组
        //    此时pCid将会是undefined
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId;

        // 重新排序的就是父节点的children
        siblings = dropNode.parent.childNodes;
      } else {
        // 被拖拽节点的放置位置 如果是dropNode 内
        // 那么被拖拽节点的父节点 就是 dropNode
        pCid = dropNode.data.catId;

        // 这里的siblings,实际上是已经排序好的一个数组

        // 重新排序的就是dropNode的children
        siblings = dropNode.childNodes;
      }
      this.pCid.push(pCid);

      // 2. 当前拖拽节点的最新顺序
      // for (let i = 0; i < siblings.length; i++) {
      //   // 如果遍历的层级和拖拽节点的层级不一样,说明拖拽节点的层级发生了变化
      //   let catLevel = draggingNode.level;
      //   if (siblings[i].level != draggingNode.level) {
      //     // 在拖拽的时候,层级发生变化,element ui是会将level改变的,我们只需要将我们的level与其同步即可
      //     catLevel = siblings[i].level;

      //     console.log("层级发生变化的节点是,", siblings[i]);
      //     // 修改子节点的层级
      //     this.updateChildNodeLevel(siblings[i]);
      //   }

      //   this.updateNodes.push({
      //     catgId: siblings[i].data.catId,
      //     sort: i,
      //     parentCid: pCid,
      //     name: siblings[i].data.name,
      //     catLevel: catLevel,
      //   });
      // }
      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) {
            // 3. 当前拖拽节点的最新层级
            // element ui 给我们提供了一个字段 level,直接赋值就可以了
            // 如果有子节点的话,它的子节点的层级也是需要变化的
            // 在拖拽的时候,层级发生变化,element ui是会将level改变的,我们只需要将我们的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 != null && node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) {
          let cNode = node.childNodes[i].data;

          this.updateNodes.push({
            catId: cNode.catId,
            catLevel: node.childNodes[i].level,
          });

          this.updateChildNodeLevel(node.childNodes[i]);
        }
      }
    },
    // draggingNode 当前正在拖拽的节点
    // dropNode 插入的节点
    // type 参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后

    // 拖拽的层级这块儿有些不太好理解
    // draggingNode,dropNode level 代表的是当前节点的level,而不是该节点我们数据库存储的level

    // 拖拽时判断目标节点能否被放置
    allowDrop(draggingNode, dropNode, type) {
      // todo data代表的是数据库中的数据,我理解这儿做任何level的操作,都不应该用data的level
      // 因为如果你拖动多次的话,数据库中的catLevel肯定暂时是没有变的,
      // 而draggingNode 本身node的level已经和dta中的level可能不一样了
      // 因此用data中的level有可能是出错的
      // 但现在我没有时间去验证这个事情,先跟着老师敲下面的代码吧,

      // todo 果然,上面的是被我说中了的,老师在后面两级都改了,但是我是由于自己实现的部分方法
      // 而数据依然是跟着老师的,所以这块儿实在是不好改,会耗费大量精力,所以这块儿暂时先这样

      // 1. 被拖动的当前节点及所在的父节点总层数不能大于3

      // // 1)递归找出被拖动的当前节点的总层数

      // // 例如:手机-iphone-iphone12  其对应的总层数为3,2,1
      // this.countNodeLevel(draggingNode.data);

      // // 这个算出来的是被拖动的当前节点共有
      // let deep = this.maxLevel - draggingNode.data.catLevel + 1;

      // if (type == "inner") {
      //   return deep + dropNode.level <= 3;
      // } else {
      //   return deep + dropNode.parent.level <= 3;
      // }

      // 算出当前节点一共有多少层,手机-iphone-iphone12  其对应的总层数为3,2,1
      this.countNodeLevel(draggingNode.data);

      if (type == "inner") {
        return this.maxLevel + dropNode.level <= 3;
      } else {
        return this.maxLevel + 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]);
      //   }
      // }
      if (node.children != null && node.children.length > 0) {
        // 只要被拖动的当前节点有子节点,那层级最少是2级
        this.maxLevel = 2;
        for (let i = 0; i < node.children.length; i++) {
          // 循环当前节点的子节点是否有子节点,如果有,当前节点的层级就是3层
          if (
            node.children[i].children != null &&
            node.children[i].children.length > 0
          ) {
            this.maxLevel = 3;
            break;
          }
        }
      } else {
        // 如果被拖动的当前节点没有子节点,那层级就是1级
        // maxLevel 此时变量叫这个名字就不太对了,但是还是和老师保持一致吧
        this.maxLevel = 1;
      }
    },
    edit(data) {
      this.dialogVisible = true;
      // 回显直接拿前端的数据有风险
      // 假设10分钟前的页面回显,那数据肯定是错误的,所以回显是需要调后台接口的
      // 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 }) => {
        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;
      });

      this.dialogType = "edit";
      this.title = "修改分类";
    },
    append(data) {
      this.dialogVisible = true;
      // 父分类的id
      this.category.parentCid = data.catId;
      // *1 可以将字符串转换为数字
      this.category.catLevel = data.catLevel * 1 + 1;
      this.dialogType = "add";
      this.title = "添加分类";
      this.category.catId = null;
      this.category.name = "";
      this.category.icon = "";
      this.category.productUnit = "";
    },
    submitData() {
      if (this.dialogType == "add") {
        this.addCategory();
      }
      if (this.dialogType == "edit") {
        this.editCategory();
      }
    },
    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];
        this.category.name = "";
      });
    },
    // 添加三级分类
    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];
        this.category.name = "";
      });
    },
    remove(node, data) {
      var ids = [data.catId];

      this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            this.$message({
              type: "success",
              message: "菜单删除成功",
            });
            this.getMenus();
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },

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

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

后端

CategoryController

package com.atlinxi.gulimall.product.controller;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.atlinxi.gulimall.product.entity.CategoryEntity;
import com.atlinxi.gulimall.product.service.CategoryService;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.R;


/**
 * 商品三级分类
 *
 * @author linxi
 * @email qazokmzjl@gmail.com
 * @date 2022-10-12 23:06:12
 */
@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);
    }


    /**
     * 信息
     */
    @RequestMapping("/info/{catId}")
    public R info(@PathVariable("catId") Long catId) {
        CategoryEntity category = categoryService.getById(catId);

        return R.ok().put("category", category);
    }

    /**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@RequestBody CategoryEntity category) {
        categoryService.save(category);

        return R.ok();
    }

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

        return R.ok();
    }

    /**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@RequestBody CategoryEntity category) {
        categoryService.updateById(category);

        return R.ok();
    }

    /**
     * 删除
     */
    @RequestMapping("/delete")
    public R delete(@RequestBody Long[] catIds) {


//		categoryService.removeByIds(Arrays.asList(catIds));

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

        return R.ok();
    }

}

CategoryServiceImpl

package com.atlinxi.gulimall.product.service.impl;

import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;

import com.atlinxi.gulimall.product.dao.CategoryDao;
import com.atlinxi.gulimall.product.entity.CategoryEntity;
import com.atlinxi.gulimall.product.service.CategoryService;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {


    //    之前是使用@Autowired注入
//    现在继承了 mybatisplus的ServiceImpl,泛型是CategoryDao,
//    我们可以直接使用baseMapper,它代表的也就是CategoryDao
//    @Autowired
//    private CategoryDao categoryDao;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }


    @Override
    public List<CategoryEntity> listWithTree() {

        // 1. 查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        List<CategoryEntity> entities2 = entities.stream()
                .sorted((categoryEntity1,categoryEntity2)->(categoryEntity1.getCatLevel() - categoryEntity2.getCatLevel()))
                .collect(Collectors.toList());

        // 2. 组装成父子的树形结构

        // 2.1 找到所有的一级分类
        List<CategoryEntity> level1Menu = entities.stream()
                // 过滤
                // 这里用的是 == ,0应该是int,categoryEntity.getParentCid() 是Long,==就是true,equals就是false,因为equals底层会先比较类型
                // getChildrens 中 categoryEntity.getParentCid().equals(root.getCatId()) 这俩都是Long类型,==就是false,equals就是true
                // equals是true很好理解,因为类型一样,值一样,==是false是因为,不是在常量池中创建的对象就是在堆中创建的对象,所以地址值肯定是不一样的
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0
                // 上面的过滤会得到一个list,
                // menu只是一个变量名,指list返回的每一个元素
                ).map(menu -> {
                    menu.setChildren(getChildrens(menu, entities2));
                    return menu;
                }).sorted((menu1, menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
                // 把结果收集成一个list
                .collect(Collectors.toList());

        return level1Menu;
    }



    /**
     * 递归查找所有菜单的子菜单
     *
     * @param root 当前分类
     * @param all  所有分类
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {

        List<CategoryEntity> children = all.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId()))
                .map(categoryEntity -> {categoryEntity.setChildren(getChildrens(categoryEntity,all)); return categoryEntity;})
                .sorted((menu1,menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
                .collect(Collectors.toList());
        return children;
    }


    @Override
    public void removeMenuByIds(List<Long> asList) {
        //todo 1.检查当前删除的菜单,是否被别的地方引用

        // 逻辑删除
        baseMapper.deleteBatchIds(asList);
    }
}

所有内容全部来源于尚硅谷视频:
https://www.bilibili.com/video/BV1np4y1C7Yf?p=36&spm_id_from=pageDriver

因为他最清楚,识字多的人会做出什么样的事。

房思琪的初恋乐园
林奕含

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值