Spring Cloud Alibaba商城实战项目基础篇(day03)

news2024/11/14 19:16:45

五、后台管理

5.1、商品服务

5.1.1、三级分类

5.1.1.1、查询所有菜单与子菜单

我们需要维护所有菜单以及各种子菜单,子菜单里面可能还有子菜单,所以我们采用递归的方式进行书写。

我们先在CategoryController中修改list方法,让他以组装树形结构进行返回。

    /**
     * 查询列表,并且以树形结构进行返回
     */
    @RequestMapping("/list")
    public R list(){
        List<CategoryEntity> categoryEntityList = categoryService.listWithTree();
        return R.ok().put("categoryEntityList", categoryEntityList);
    }

由于数据库表中是没有子菜单这个属性,所以我们需要在实体类中添加这个属性,一般开发中可以重新写一个VO,更加解耦。在CategoryEntity中添加一个children属性,但是需要加一个注解,告诉Mybatis-plus,这个属性我的表中没有,不需要理他。

	/**
	 * 子分类
	 */
	@TableField(exist = false)
	private List<CategoryEntity> children;

CategoryService中写一个接口。

    /**
     * 查出所有分类,并且组装成父子结构
     * @return
     */
    List<CategoryEntity> listWithTree();

CategoryServiceImpl也顺带写一写。

 @Override
    public List<CategoryEntity> listWithTree() {
        // 查询所有
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        // 找到所有一级分类
        List<CategoryEntity> collect = categoryEntities.stream().filter((categoryEntity) -> {
            // 返回父分类id为0的,父id等于0说明他是一级分类
            return categoryEntity.getParentCid() == 0;
        }).map((menu)->{
            // 设置子菜单
           menu.setChildren(getChildren(menu,categoryEntities));
           return menu;
        }).sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
                .collect(Collectors.toList());
        // 设置每一个父分类的子分类
        return collect;
    }

由于需要递归遍历,所以我们把遍历方法抽取出来。

  /**
     * 获取子菜单
     * @param currentMenu 当前菜单
     * @param allMenu 所有菜单
     * @return 所有子菜单
     */
    private List<CategoryEntity> getChildren(CategoryEntity currentMenu,List<CategoryEntity> allMenu){
        List<CategoryEntity> childrents = allMenu.stream().filter(categoryEntity -> categoryEntity.getParentCid() == currentMenu.getCatId())
                // 每个子菜单可能还有子菜单
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildren(categoryEntity, allMenu));
                    return categoryEntity;
                })
                // 排序
                .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
                .collect(Collectors.toList());;
        return childrents;

    }

然后启动商品服务就可以开始测试。

image-20230107110343803

5.1.1.2、配置网关和路由重写

我们需要维护后台管理系统,启动renren-fast-vue项目和mall-admin后端项目,直接npm run dev开跑。我们首先写的功能是分类维护。

renren-fast-vue的页面规则是http://localhost:8001/#/product-category,页面在src/views/modules/product下的目录里面。

image-20230107114627788

我们新建一个prodect文件夹和category.vue,用于保存目录管理页面。为了便于开发,我们队vscdoe进行一些配置。

首先是配置vue模板,输入vue时可以直接跳出模板。

image-20230107120207317

输入vue可以直接开始配置模板。

image-20230107120337810

把原来的注释掉,直接换成下面的这段配置。

{
    "Print to console": {
        "prefix": "vue",
        "body": [
            "<!-- $1 -->",
            "<template>",
            "<div class='$2'>$5</div>",
            "</template>",
            "",
            "<script>",
            "//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)",
            "//例如:import 《组件名称》 from '《组件路径》';",
            "",
            "export default {",
            "//import引入的组件需要注入到对象中才能使用",
            "components: {},",
            "data() {",
            "//这里存放数据",
            "return {",
            "",
            "};",
            "},",
            "//监听属性 类似于data概念",
            "computed: {},",
            "//监控data中的数据变化",
            "watch: {},",
            "//方法集合",
            "methods: {",
            "",
            "},",
            "//生命周期 - 创建完成(可以访问当前this实例)",
            "created() {",
            "",
            "},",
            "//生命周期 - 挂载完成(可以访问DOM元素)",
            "mounted() {",
            "",
            "},",
            "beforeCreate() {}, //生命周期 - 创建之前",
            "beforeMount() {}, //生命周期 - 挂载之前",
            "beforeUpdate() {}, //生命周期 - 更新之前",
            "updated() {}, //生命周期 - 更新之后",
            "beforeDestroy() {}, //生命周期 - 销毁之前",
            "destroyed() {}, //生命周期 - 销毁完成",
            "activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发",
            "}",
            "</script>",
            "<style lang='scss' scoped>",
            "//@import url($3); 引入公共css类",
            "$4",
            "</style>"
        ],
        "description": "Log output to console"
    }
}

模板设置好以后,我们需要配置格式化,首先先安装一个插件,Vetur。

image-20230107120515765

进入配置界面。进入json界面编辑。

image-20230107120541964

把原来的注释掉,直接换成下面这段配置。

{
    // tab 大小为2个空格
  
    "editor.tabSize": 2,
  
    // 编辑器换行
  
    "editor.wordWrap": "off",
  
    // 保存时格式化
  
    "editor.formatOnSave": true,
  
    // 开启 vscode 文件路径导航
  
    "breadcrumbs.enabled": true,
  
    // prettier 设置语句末尾不加分号
  
    "prettier.semi": false,
  
    // prettier 设置强制单引号
  
    "prettier.singleQuote": true,
  
    // 选择 vue 文件中 template 的格式化工具
  
    "vetur.format.defaultFormatter.html": "js-beautify-html",
  
    // vetur 的自定义设置
  
    "vetur.format.defaultFormatterOptions": {
      "js-beautify-html": {
        "wrap_line_length": 30,
        "wrap_attributes": "auto",
        "end_with_newline": false
      },
  
      "prettier": {
        "singleQuote": true,
  
        "semi": false,
  
        "printWidth": 100,
  
        "wrapAttributes": false,
  
        "sortAttributes": false
      }
    },
    "[vue]": {
      "editor.defaultFormatter": "octref.vetur"
    },
    "vetur.completion.scaffoldSnippetSources": {
    
      "workspace": "💼",
      "user": "🗒️",
      "vetur": "✌"
    }
  }

后面你ctrl+s可以直接保存加格式化了。

这个后台管理系统我们使用的是Element-UI的树形控件:https://element.eleme.cn/#/zh-CN/component/tree, 我们先把官方文档的代码copy进去看看效果。

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

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

export default {
  //import引入的组件需要注入到对象中才能使用
  components: {},
  data() {
    return {
      data: [
        {
          label: '一级 1',
          children: [
            {
              label: '二级 1-1',
              children: [
                {
                  label: '三级 1-1-1',
                },
              ],
            },
          ],
        },
        {
          label: '一级 2',
          children: [
            {
              label: '二级 2-1',
              children: [
                {
                  label: '三级 2-1-1',
                },
              ],
            },
            {
              label: '二级 2-2',
              children: [
                {
                  label: '三级 2-2-1',
                },
              ],
            },
          ],
        },
        {
          label: '一级 3',
          children: [
            {
              label: '二级 3-1',
              children: [
                {
                  label: '三级 3-1-1',
                },
              ],
            },
            {
              label: '二级 3-2',
              children: [
                {
                  label: '三级 3-2-1',
                },
              ],
            },
          ],
        },
      ],
      defaultProps: {
        children: 'children',
        label: 'label',
      },
    }
  },
  methods: {
    handleNodeClick(data) {
      console.log(data)
    },
  },
  //监听属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {},
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style lang='scss' scoped>
//@import url(); 引入公共css类
</style>

image-20230107121205656

这里的数据都是写死的死数据,我们需要替换成数据库查询的数据。先将data中数据库删除掉,根据vue生命周期,我们直接把方法写在created里面。我们在methods中写一个方法。

  methods: {
    handleNodeClick(data) {
      console.log(data)
    },
    getMenus() {
      this.$http({
        url: this.$http.adornUrl('/product/category/list'),
        method: 'get',
      }).then(({ data }) => {
        console.log(data)
      })
    },
  },

在created里面调用即可。

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

一启动直接404,一看原来是端口不对,我们还需要修改端口。

image-20230107121934463

在src/config/index.js下定义了接口请求地址。

image-20230107122156844

为了统一地址,我们需要给网关发请求,由网关进行统一路由。

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

保存后发现发现他要求我们重新登录,且验证码都没有了,原来是他直接给我们网关发请求。

image-20230107122448356

由于需要网关路由,我们还需要把mall-admin这个项目注册到nacos中。在application.yml中加入这两段配置。

  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: mall-admin

最后在启动类上加入这这个注解,允许服务发现功能@EnableDiscoveryClient

image-20230107123006867

成果配置。我们接着去gateway配置中心的gateway.yml配置中心去配置网关,这里约定前端发的请求全部带上/api前缀。

spring:
  cloud:
    gateway:
      routes:
          # 路由到何处
        - id: admin-route
          # url规则
          uri: lb://mall-admin
          # 当请求为/api/xxx的全部路由到上面配置的url
          predicates:
            - Path=/api/**

启动网关和重启前端项目,再次看看是否可以拿到验证码。

image-20230107140729909

很不幸,还是404,这个时候我们需要使用网关的路径重写,使用gateway的filters的路径重写功能。

          filters:
            - RewritePath=/api/?(?<segment>.*), /mall-admin/$\{segment}

image-20230107142247335

开始登录!登录后发现出现了跨域问题。

image-20230107142531381

5.1.1.3、解决跨域问题

跨域指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。一般是使用同源策略进行限制。

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域。

跨域请求流程:

在这里插入图片描述

我们可以发现,他只是发了一个option请求,真正的登录请求还没有发过去就被跨域拦截了。

image-20230107143131694

解决办法:

  1. 使用nginx部署为同一域。
  2. 配置当次请求允许跨域:
  • 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,我们可以在网关写一个Filter,由网关统一进行跨域配置。我们在网关新建一个config目录用于存放配置类,新建一个MallConfiguration类用于解决跨域问题。

package cn.linstudy.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;

/**
 * @author XiaoLin
 * @date 2023/1/7
 * @description 解决跨域问题配置类
 */
@Configuration
public class MallConfiguration{

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

这是因为,脚手架项目也配置了跨域,我们需要把他原来的跨域配置给注释掉。

image-20230107150552753

注释掉后重启mall-admin即可。

5.1.1.4、三级分类树形展示

我们接着写网关路由,即在nacos的网关配置中书写路由规则即可。

               # 路由到何处
        - id: product-route
          # url规则
          uri: lb://mall-product
          # 当请求为/api/xxx的全部路由到上面配置的url
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/?(?<segment>.*), /$\{segment}

image-20230107151909877

启动后发现路由配置似乎没有生效。实际上是被上面的路由配置覆盖了。上面这条路由信息会先匹配上,所以会直接走上面的路由配置。我们可以调整一下顺序,把精确的路由放到高的优先级,模糊的路由放到低的优先级即可。

image-20230107152022428

刷新配置,完成!数据成果拿到

image-20230107152727913

接着开始显示数据,通过data.categoryEntityList可以获取到后台接口传过来的数据。

image-20230107163741890

这里即为展示数据的地方,我们把他改为menus,修改的地方要和这里对应上即可。由官方文档我们可以看到,

image-20230107164456601

因为我们的列表数据里面,标签名的属性是name,所以我们把label的值改为name即可展示数据。

image-20230107165709027

image-20230107165929799

5.1.1.4、菜单删除

我们可以使用Vue的slot功能。

image-20230107170933797

可以看到只需要给Vue Tree里面写一个span标签即可。

<span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
            type="text"
            size="mini"
            @click="() => append(data)">
            Append
          </el-button>
          <el-button
            type="text"
            size="mini"
            @click="() => remove(node, data)">
            Delete
          </el-button>
        </span>
      </span>

放到el-tree这个标签里面即可。

再把原生的append和remove方法复制过来即可,清空实现,自己实现。

 append(data) {
       
      },

remove(node, data) {
       
      },

我们可以发现,在点击append和remove的时候,菜单栏会展开和收缩,我们也需要把这个效果给去掉,参照文档,原来是这个属性在作怪,再把这个属性加到el-tree中。

:expand-on-click-node="false">

在这里插入图片描述

这样就去掉了这个烦人的效果,在点击按钮的时候就不会展开与合并了,只有点击箭头的时候才会。接下来就要开始判断啥时候展示append和remove菜单了,只有一级、二级菜单才可以添加,只有菜单下没有子菜单了以后才可以删除。

在slot插槽中有两个对象,node表示当前节点对象

image-20230107172250791

我们打印一下node对象,可以发现一个好东西。

image-20230107173434205

level表示当前的层级,是一级还是二级菜单。node.childNodes表示他的子节点

我们通过node.level这个属性来判断是否是一级、二级菜单,node.level <= 2表示为二级菜单。

我们在button中加上两个判断即可实现这个功能。

image-20230107174528548 image-20230107174544343

由于我们还需要做批量删除,所以我们还需要做上单选框,我们需要在el-tree上加入show-checkbox这个属性。

image-20230107180204817 image-20230107180244106

由于我们使用的是MyBatis-Plus,我们需要配置一下MyBatis-Plus的逻辑删除。老规矩先去官方文档上瞅一眼:https://baomidou.com/pages/6b03c5/。

我们要去实体类字段上加上@TableLogic注解。我们在CategoryEntity这个实体的showStatus字段上加入这个注解,但是发现一个问题,我们自己定义的规则是0表示不显示,1表示显示,别慌MyBatis-Plus会出手,我们点进去@TableLogic注解,可以看到,我们可以自己配置删除和不删除的规则。

image-20230108105812352

所以我们给这个注解上加一点东西。

	/**
	 * 是否显示[0-不显示,1显示],@TableLogic代表这是逻辑删除字段
	 */
	@TableLogic(value = "1",delval = "0")
	private Integer showStatus;

接下来我们可以开始去后台项目写逻辑删除接口。

    /**
     * RequestBody请求体,必须发post请求,SpringMVC会自动将请求体数据(JSON),转为对应的对象
     * @param catIds 商品id
     * @return
     */
    @RequestMapping("/delete")
    public R delete(@RequestBody Long[] catIds){
        // 检查当前菜单是否被其他地方引用
		categoryService.removeMenusByIds(Arrays.asList(catIds));
        return R.ok();
    }

去Service写接口。

    /**
     * 批量逻辑删除
     * @param asList
     */
    void removeMenusByIds(List<Long> asList);

接着去写实现类。

    /**
     * 用showStatus状态来完成逻辑删除
     * @param asList id列表
     */
    @Override
    public void removeMenusByIds(List<Long> asList) {
        baseMapper.deleteBatchIds(asList);
    }

后端接口写完以后,就去前端项目开始去测试。在category.vue中的remove方法去书写逻辑删除代码。为了便于开发方便,我们可以直配置好发送get、post请求模板。我们需要新建一个代码模板,不可以在原有的进行添加,否则会无法添加。

image-20230108112629112 image-20230108112650932
"http-get 请求": {
        "prefix": "httpget",
        "body": [
            "this.\\$http({",
            "url: this.\\$http.adornUrl(''),",
            "method: 'get',",
            "params: this.\\$http.adornParams({})",
            "}).then(({data}) => {",
            "})"
        ],
        "description": "httpGET 请求"
    },
    "http-post 请求": {
        "prefix": "httppost",
        "body": [
            "this.\\$http({",
            "url: this.\\$http.adornUrl(''),",
            "method: 'post',",
            "data: this.\\$http.adornData(data, false)",
            "}).then(({ data }) => { });"
        ],
        "description": "httpPOST 请求"
    }

接着我们去写remove方法。

    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()
      })
    },

写到这里,删除大体功能就做完了,但是我们还需要一些细化细节:

  1. 删除前弹出提示框。
  2. 删除成功后有消息提示。
  3. 删除完后还是展开状态。

我们首先做删除前弹出提示框,提示框我们可以使用MessageBox弹框组件(https://element.eleme.cn/#/zh-CN/component/message-box)。

image-20230108122749306

开搞!

this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });

为了拿到删除菜单的名字,我们可以使用票号(`)和插值表达式来取值,带data中有一个name属性就是这个菜单的名字。

      this.$confirm(`是否删除【${data.name}】菜单, 是否继续?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => {
          this.$message({
            type: 'success',
            message: '删除成功!',
          })
        })
        .catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除',
          })
        })

接着我们改变一下代码顺序,点击确认删除后才发送删除请求即可。

    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.getMenus()
        })
        this.$message({
          type: 'success',
          message: '删除成功!',
        })
      })
    }

成功是成功了,但是在点击取消的时候控制台报错了。

image-20230108123641913

原因是取消的按钮没处理,这是因为我们之前删了catch代码,加回去即可。

    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.getMenus()
          })

          this.$message({
            type: 'success',
            message: '删除成功!',
          })
        })
		 // 取消的操作,先留空,不然控制会报错
        .catch(() => {})
    }

接着我们来做,删除成功后还是依然处于展开状态,不收回去。需要做到依然展开,我们就需要参照官方文档的属性,去瞅瞅看有没有合适的属性。

在这里插入图片描述

恰好找到这个属性default-expanded-keys

我们可以在el-tree中动态绑定这个属性。

  <el-tree :data="menus"
    :props="defaultProps"
    @node-click="handleNodeClick"
    :expand-on-click-node="false"
    show-checkbox
    node-key="catId"
    :default-expanded-keys="expandedKey">

在data中添加expandedKey数据值,这个是一个数组

  data() {
    return {
      menus: [],
      expandedKey: [],
      defaultProps: {
        children: 'children',
        label: 'name',
      },
    }
  },

在删除成功,刷新菜单后,修改expandedKey的值,默认展示删除的菜单的父菜单。我们在getMenus方法后调用这个方法即可。

image-20230108125238504
// 设置需要默认展开的菜单,赋值为当前节点的父节点的id即可,当前节点的父节点为
this.expandedKey = [node.parent.data.catId]

很好,很完美。

5.1.1.5、菜单添加

弹框添加依然去瞅瞅官方文档,使用Dialog对话框组件(https://element.eleme.cn/#/zh-CN/component/dialog)。

image-20230108144533757
<el-dialog
  title="提示"
  :visible.sync="dialogVisible"
  width="30%"
  :before-close="handleClose">
  <span>这是一段信息</span>
  <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>

复制过去之后可以发现报错了。

image-20230108150317469

原来是他需要一根元素,我们需要用一个div包裹住他。

image-20230108150659062

dialogVisible属性为false表示这个对话框不打开。当我们点击appen也就是添加的时候,就讲将dialogVisible属性设置为true即可。我们首先先定义一个变量,初值为false。

image-20230108214344640
dialogVisible: false,

点击append方法的时候修改为true即可。

    append(data) {
      this.dialogVisible = true
    },

他此时还是会报错,因为在对话框中定义了点击关闭的事件,但是我们还没有定义事件,所以先把这个也删掉。

  :before-close="handleClose"

由于我们需要内嵌一个表单,提交的时候提交一个表单给后台添加到数据库中,去瞅一眼官方文档可以发现,他提供了一个内嵌表单的方法。

image-20230108215031474

把对话框里面的span标签替换成form表单即可。

      <el-form
        :model="category">
        <el-form-item
          label="分类名称">
          <el-input
            v-model="category.name"
            autocomplete="off">
          </el-input>
        </el-form-item>
      </el-form>

v-model="category.name"这里双向绑定了一个值,我们还需要在data中声明出来。

category: { name: ' ' },
image-20230108231252625

我们给确定按钮提交表单,为确定按钮绑定一个点击事件。

@click="addCategory"

image-20230108231543900

根据数据库表字段可知,除了名字需要添加外,我们还需要一些其他的字段,这些字段不是前台输入的,而是我们自己赋值设置的。

image-20230109083329094

再点击append的时候,我们就需要赋值,我们先点击append看看可以拿到什么数据。

image-20230109083503788

点击append后可以拿到这些数据,catId就是添加的parentId,而catLevel菜单层级是当前点击菜单的层级+1(由于害怕他是数字,所以我们先*1再+1,把他转为字符串。)

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

当我们点击确认后,看看提交的数据是什么?

image-20230109090451947

可以看到,数据都拿到了。前端完成后,可以直接发请求即可,因为后端接口都逆向工程生成好了。

    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()
        // 设置需要默认展开的菜单,赋值为当前节点的父节点的id即可,当前节点的父节点为
        this.expandedKey = [this.category.parentCid]
      })
    },

保存成功后关闭对话框,我们只需要把dialogVisible属性置为false即可。接着刷新菜单,再展开当前菜单,这个在删除时都做过了。直接copy即可。

5.1.5.6、修改菜单

想要实现修改功能,肯定要先添加修改按钮,这个修改按钮是任何时候都显示的,所以去掉v-if。点击修改的时候弹出对话框,我们需要将dialogVisible值设置为true即可。

我们先来做回显数据。点击修改菜单的时候就可以拿到数据,里面有catId,直接回显即可,都无需向后台发请求。

image-20230109112339315

由于输入框绑定的数据是 v-model="category.name",所以我们只需要把category.name的值修改即可。

      // 回显编辑框
      this.category.name = data.name

我们再给catId赋值,因为需要拿catId去修改数据。

this.category.catId = data.catId

接下来就遇到了一个难题,由于我们需要复用对话框,所以我们需要区分是添加还是修改。我们可以定义一个dialogType的数据,当点击新增的时候就把他的值赋值为save,点击修改的时候把他的值赋值为update。

image-20230109113655569

点击新增的时候我们就把他的值修改为save。

image-20230109113732566

image-20230109113828813

我们还需要把原来确定按钮事件修改一下,自己写一个方法去判断是新增还是编辑。

image-20230109114419344

// 判断是修改还是新增
    submitData(){
      if(this.dialogType == "save"){
        this.addCategory();
      }
      if(this.dialogType == "update"){
        this.updateCategory();
      }
    },

我们再来做一个小效果,当点击新增的时候,提示框标题为新增,点击编辑的时候,提示框标题为编辑。

image-20230109115557496

点击新增的时候把这个变量的值改了。

this.dialogTitle = '新增分类'

在这里插入图片描述

同理,点击修改的时候也是一样。

this.dialogTitle = '修改分类'

image-20230109115844557

新增菜单还需要图标和计量单位两个属性,我们新增两个输入框。这个时候就出问题,如果有多人同时使用的话会出现数据不一致的问题,所以我们的回显数据不可以偷懒,必须是从数据库里面访问得到最新的数据进行回显。

 update(data) {
      this.dialogTitle = '修改分类'
      this.dialogVisible = true
      // 回显编辑框
      this.$http({
  	url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: 'post',
        data: this.$http.adornData(data, false),
      }).then(({ data }) => {
        this.category.name = data.category.name
        this.category.icon = data.category.icon
        this.category.productUnit = data.category.productUnit
        this.parentCid = data.parentCid;
      })

image-20230109121743200

接着就开始真正修改,由于我们只需要修改部分数据,于是乎,我们还需要进行数据拼接。我们在方法中去拼接参数

let data = {catId,name,icon,productUnit}

发完请求以后仿照着删除,去掉对话框、重新获取菜单,展开到当前菜单。

    // 修改菜单方法
    updateCategory() {
      let { catId, name, icon, productUnit } = this.category
      let 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()
        // 设置需要默认展开的菜单,赋值为当前节点的父节点的id即可,当前节点的父节点为
        console.log(this.category)
        this.expandedKey = [this.category.parentCid]
      })
    },

我们还会发现,如果我们点击空白页面,对话框会消失,我想让对话框一直在的话可以设置这个属性:close-on-click-modal="false"为false即可。

image-20230109154606613

image-20230109154648159

接着我们还会发现一个问题,先修改,再添加,会发现,在添加的对话框中会显示我们刚刚修改的值。这是因为我们在回显的时候将对话框的值都设置进去了。

image-20230109154939300

所以我们在append打开对话框的时候需要修改回默认值,否则会出现这个问题。

      this.category.name = ''
      this.category.icon = ''
      this.category.productUnit = ''

image-20230109155440550

5.1.5.7、拖拽效果

在Element-UI中还可以实现可拖拽修改。

image-20230109155710560

我们把他写到el-tree属性中。

image-20230109155903027

但是不是所有的节点都可以这样随意被拖拽,于是乎Element-UI还提供了allow-drag这个方法给我们,我们自己通过业务逻辑来判断是否可以被拖拽。是否可以拖拽的核心就判断总层数是否大于3,大于3则不可以拖拽,否则可以拖拽。

image-20230109160139063

我们需要在el-tree中动态绑定这个方法。

image-20230109162201724

在这个方法里面做逻辑判断。

allowDrop(draggingNode, dropNode, type) {
      // 判断是否可以拖拽的核心是当前节点以及所在父节点的总层数不可以超过3
      /// 1. 计算被拖拽的节点的总层数
      console.log(draggingNode) // draggingNode表示的是拖拽的节点
      let level = countNodeLevel(draggingNode)
    },

draggingNode表示的是拖拽的东西,而draggingNode.data可以获取到拖拽的节点信息。

在这里插入图片描述

我们先来写一个方法,来判断他的最大深度,来确定是否可以拖动,这里用到了递归计算。

    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])
        }
      }
    },

最后再写一个方法判断是否可以拖动即可。

    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])
        }
      }
    },

5.1.5.7、拖拽数据修改

上面我们只是实现了拖拽效果,但是数据库数据并没有改变,我们现在要做的就是在拖拽的同时把数据库的信息也改了,在Element-UI提供了一个监听事件给我们。

在这里插入图片描述

我们还需要在el-tree上加上这个属性 @node-drop="handleDrop",拖拽成功后会调用handleDrop函数。

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

他有三个参数:

  1. draggingNode:拖动的节点
  2. dropNode:拖动到哪个节点
  3. dropType:拖动类型
  4. ev:事件,暂时用不上

接着我们去自定义handleDrop方法,来完成我们需要实现的功能。

image-20230110115534582

    handleDrop(draggingNode, dropNode, dropType) {
      console.log('tree drop: ', dropNode.label, dropType)
      // 当前节点的父节点id
      let pCid = 0
      let sillings = null // 兄弟节点
      // 以前面或者后面的方式进入
      if (dropType == 'before' || dropType == 'after') {
        pCid =
          dropNode.parent.data.catId == undefined // 防止出现undefined
            ? 0
            : dropNode.parent.data.catId
        sillings = dropNode.parent.childNodes
      } else {
        pCid = dropNode.data.catId
        sillings = dropNode.childNodes
      }
      // 当前拖拽节点的最新顺序
      for (let i = 0; i < sillings.length; i++) {
        // 如果遍历的是当前正在拖拽的节点
        if (sillings[i].data.catId == draggingNode.data.catId) {
          // 如果遍历的是当前正在拖拽的节点
          let catLevel = draggingNode.level
          // 如果当前节点的层级发生变化
          if (sillings[i].level != draggingNode.level) {
            // 修改他的子节点的层级
            catLevel = sillings[i].level
            this.updateChildNodeLevel(sillings[i])
          }
          this.updateNodes.push({ catId: sillings[i].data.catId, sort: i, parentCid: pCid })
        } else {
          this.updateNodes.push({ catId: sillings[i].data.catId, sort: i })
        }
        this.pCid.push(pCid)
      }
    },
    updateChildNodeLevel(node) {
      // 他有子节点
      if (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])
        }
      }
    },

前端写好后,我们要把数据交给数据库,真正修改数据。我们先去productController中写一个方法。

    /**
     * 批量拖拽修改功能
     * @param category 需要批量拖拽修改的数组分类对象
     * @return
     */
    @RequestMapping("/update/sort")
    public R updateSort(@RequestBody CategoryEntity[] category){
        categoryService.updateBatchById(Arrays.asList(category));
        return R.ok();
    }

直接调用MyBatis-Plus里面的方法即可。接下来就是开始细化,有些时候我们不一定是需要拖拽,会有一定的误触概率,我们需要定义一个开关,当开启开关的时候才允许拖拽。还是去看看Element-UI的组件,有一个Switch开关(https://element.eleme.cn/#/zh-CN/component/switch)。

image-20230110130308943

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

active-text表示开关开启的时候显示的文字, inactive-text表示关闭的时候显示的文字。v-model绑定了一个value值,这个value值决定了拖拽功能是否开启。我们再给el-tree中的动态拖拽功能也动态绑定一个属性,用于动态控制是否可以拖拽。

:draggable="draggable"

image-20230110151712183

我们再将原来官方文档动态绑定的value1的值改为我们自己设置的draggable这个值,即可实现动态绑定,最后再在data中设置这个值即可。

image-20230110152126617

接下来还有一点不足,那就是我们每拖动一次都会与数据库交互,这样会造成频繁与数据库进行交互,我们其实可以做一个按钮,当所有操作拖动完成后一次性点击保存即可。使用el-button可以新增一个按钮,用于批量保存。

 <el-button type="primary"
      @click="batchUpdate">
      保存</el-button>

定义的点击事件中,我们直接将刚才写的与数据库交互的代码复制过去,这里还需要注意一个点。

image-20230110153100061

所以我们需要把pCid定义成全局的变量image-20230110153322280

直接在data中定义成全局变量即可。
在这里插入图片描述

当修改了pCid的值以后我们再去将修改后的pCid赋值给全局的pCid。

在这里插入图片描述

在我们修改完后,需要展示当前节点时要用到pCid,所以我们需要使用到全局变量的pCid,同时在修改成功后,不仅仅需要重置对话框的值和最大等级的值,此时还需要把pCid的值重置为0.

image-20230110154225229

此时还有点问题,因为我们在不断地拖拽的时候,在以前是实时和数据库交互,我们可以随时拿到最新的最大深度,但是这次不是实时和数据库交互,所以我们判断能否拖动的条件需要改变,因为我们不可以实时从数据库中拿到最新的值,所以我们的判断条件应该拿到当前页面最新的值来计算最大深度,超过了3就无法拖拽。我们去allowDrop方法里面去修改计算最大深度的值即可。image-20230110154605294

此时为了避免负数,我们还应该使用绝对值来避免负数。

let currentDepth = Math.abs(this.maxLevel - draggingNode.data.catLevel) + 1

image-20230110154739322

如果不实时和数据库交互,那么我们使用的对象也需要发生改变。

image-20230110155210499

    updateChildNodeLevel(node) {
      // 他有子节点
      if (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])
        }
      }
    },
    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])
        }
      }
    },
    allowDrop(draggingNode, dropNode, type) {
      // 判断是否可以拖拽的核心是当前节点以及所在父节点的总层数不可以超过3
      /// 1. 计算被拖拽的节点的总层数
      // draggingNode表示的是拖拽的节点
      this.countNodeLevel(draggingNode)
      // 计算最大深度
      let currentDepth = Math.abs(this.maxLevel - draggingNode.data.catLevel) + 1
      // 再根据拖动类型来判断计算公式
      if (type == 'inner') {
        // 如果是拖动到里面
        // 如果深度小于3,说明可以拖动
        return currentDepth + dropNode.level <= 3
      } else {
        // 如果是拖动到外面
        return currentDepth + dropNode.parent.level <= 3
      }
    },

保存后发现,我们在批量保存的时候,他只展开了一个父节点,此时我们需要展开多个父节点,那么我们就需要将父节点声明成一个数组。

pCid: [],

image-20230110155708347

每修改一个节点我们需要把节点的pCid赋值到数组中,便于展开。

 this.pCid.push(pCid);

image-20230110155939737

在批量保存方法中还需要把pCid这个数组中的所有值都直接展开。

image-20230110160124266

5.1.5.8、批量删除

这是最后一个功能——批量删除,我们先搞一个按钮出来。

 <el-button type="danger">批量删除</el-button>

在点击按钮后需要触发批量删除方法。

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

我们需要拿到节点的选中信息,所以我们需要拿到组件信息,所以我们需要使用ref的内置组件。

image-20230112190606575

      ref="menuTree"

在这里插入图片描述

通过这个可以拿到选中节点相关信息,而通过getCheckedNodes方法可以拿到已选中的菜单信息,通过控制台我们可以看到,他打印的恰好就是我们选中的节点信息。

image-20230112191229526

然后我们遍历这个数组,拿到每一个被选中的id即可实现批量删除,我们这样就可以拿

    batchDelete() {
      let cheaked = this.$refs.menuTree.getCheckedNodes()
      for (let i = 0; i < cheaked.length; i++) {
        console.log(cheaked[i].catId)
      }
    }

再组装成一个数组即可,接着发请求即可实现批量删除。

    batchUpdate() {
      // 当前拖拽节点的最新层级
      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()
        console.log(this.pCid)
        this.expandedKey = this.pCid
        this.updateNodes = []
        this.maxLevel = 0
        this.pCid = 0
      })
    },

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/176968.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Day868.索引(上) -MySQL实战

索引&#xff08;上&#xff09; Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于索引&#xff08;上&#xff09;的内容。 某一个 SQL 查询比较慢&#xff0c;分析完原因之后&#xff0c;可能就会说“给某个字段加个索引吧”之类的解决方案。但到底什么是索引&…

linux部署java项目cpu占用100%的排除故障

用top -c命令查看cpu占用高的进程 ![在这里插入图片描述](https://img-blog.csdnimg.cn/12af3f060fb84ce98b24c7247546b50b.png 发现cpu占用为99%的进程pid为24857 用top -Hp 24857查看cpu占用最高的线程 发现占用cpu97.3%的线程id为24926 将24926转为16进制 通过jstack查看进…

袋式除尘器—选型计算

1.处理气体量的计算计算袋式除尘器的处理气体时&#xff0c;首先要求出工况条件下的气体量&#xff0c;即实际通过袋式除尘器的气体量&#xff0c;并且还要考虑除尘器本身的漏风量。这些数据&#xff0c;应根据已有工厂的实际运行经验或检测资料来确定&#xff0c;如果缺乏必要…

Day09 - 子类父类多继承多层继承多态

1. 子类中访问父类中的私有属性和私有方法 如果想使用父类中的私有属性和私有方法,那么就需要在父类中,为这些私有的属性和方法,提供相应的公有的接口方法来间接访问2. 如何为父类中的属性进行初始化 在子类中如果定义了自己的初始化方法,那么这时父类的初始化方法就不会再执…

尚硅谷AJAX教程

优点&#xff1a;无需刷新页面获取数据&#xff0c;允许你根据用户事件来更新部分页面内容 缺点&#xff1a;没有浏览历史&#xff0c;不能回退&#xff0c;存在跨域&#xff0c;SEO不友好 原生XHR请求 get请求 <body><button>获取数据</button><scri…

弹性可微调,基于LCN光敏材料的触觉模拟系统

对于AR/VR体验来讲&#xff0c;体感、触觉模拟很重要&#xff0c;但现阶段还没有一种方便消费者使用、轻便的体感方案&#xff0c;因此Meta等公司不断在探索更好的体感技术。比如近期&#xff0c;Nature发表了一项来自荷兰埃因霍芬理工大学的新研究&#xff0c;该研究由Meta Re…

svg的path标签的d属性

<svgwidth"200"height"200"viewBox"0 0 200 200"style"border: 1px solid red"><pathd"M10 10 L110 10 L110 110 L10 110 Z"fill"none "stroke"green"></path></svg>运行效果…

Redux Toolkit 调用 API 的四种方式

Redux Toolkit 调用 API 的四种方式 上篇笔记写的比较乱&#xff0c;加上这回又学了点新的东西&#xff0c;所以重新整合一下。 本地 API 用的是 json-server&#xff0c;端口设置在 3005&#xff0c;数据包含&#xff1a; {"users": [{"id": 1,"n…

数据分析-深度学习 Pytorch Day9

迁移学习通过利用数据、任务或模型之间的相似性&#xff0c;将在旧领域学习过的模型应用于新领域来求解新问题。生活中常用的“举一反三”、“照猫画虎”就很好地体现了迁移学习的思想。利用迁移学习的思想&#xff0c;可以将已有的一些训练好的模型&#xff0c;迁移到我们的任…

【Kotlin】泛型 ② ( 可变参数 vararg 关键字与泛型结合使用 | 使用 [] 运算符获取指定可变参数对象 )

文章目录一、可变参数 vararg 关键字与泛型结合使用二、使用 [] 运算符获取指定可变参数对象一、可变参数 vararg 关键字与泛型结合使用 如果 泛型类型 T 的参数 是 vararg 可变参数 , 则在接收 可变参数 时 , 需要使用 Array<out T> 类型 的变量进行接收 ; 参数为 vara…

分房管理系统Rose模型设计过程

文章目录 一、模型总体设计 1 创建系统的Use Case 视图 2 创建系统的 Logical 视图 3 创建系统的 Class 框图 4 创建系统的 StateChart 框图 5 创建系统的 Activity 框图 二、软件模块结构图设计 1 根据系统功能进行第一级分解 2 完成第二级分解 3 完成第三级分解 4 整合得到完…

从零开始的python基础教程

一、Getting Started 如果使用的是mac或linux系统&#xff0c;需要输入 python3 比如运行 python3 app.py 可以直接在终端的>>>符号后执行python代码 print("*" * 10) 1、实现 在相关终端当运行“python”为CPython&#xff0c;这是python的默认实现 Jy…

大数据--spark

什么是SparkApache Spark 的架构基础是弹性分布式数据集 (RDD)&#xff0c;这是一种只读的多组数据项&#xff0c;分布在机器集群上&#xff0c;以容错方式维护。[2] Dataframe API 作为 RDD 之上的抽象发布&#xff0c;随后是 Dataset API。在 Spark 1.x 中&#xff0c;RDD 是…

39-剑指 Offer 41. 数据流中的中位数

题目 如何得到一个数据流中的中位数&#xff1f;如果从数据流中读出奇数个数值&#xff0c;那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值&#xff0c;那么中位数就是所有数值排序之后中间两个数的平均值。 例如&#xff0c; [2,3,4] 的中位…

50个常用的 Numpy 函数详解

目录 一、创建数组 1、Array 2、Linspace 3、Arange 4、Uniform 5、Random.randint 6、Random.random 7、Logspace 8、zeroes 9、ones 10、full 11、Identity 二、数组操作 12、min 13、max 14、unique 15、mean 16、medain 17、digitize 18、reshape 19、…

详解1247:河中跳房子(二分经典例题)

1247&#xff1a;河中跳房子【题目描述】每年奶牛们都要举办各种特殊版本的跳房子比赛&#xff0c;包括在河里从一个岩石跳到另一个岩石。这项激动人心的活动在一条长长的笔直河道中进行&#xff0c;在起点和离起点L远 (1 ≤ L≤ 1,000,000,000) 的终点处均有一个岩石。在起点和…

《Unity Shader 入门精要》第6章 Unity 中的基础光照

第6章 Unity 中的基础光照 6.1 我们是如何看到这个世界的 通常来说我们要模拟真实的光照环境来生成一张图像&#xff0c;需要考虑3种物理现象&#xff1a; 首先&#xff0c;光线从光源&#xff08;light source&#xff09;中被发射出来然后&#xff0c;光线和场景中的一些物…

JavaScript while 循环

文章目录JavaScript while 循环while 循环do/while 循环比较 for 和 while笔记列表JavaScript while 循环 只要指定条件为 true&#xff0c;循环就可以一直执行代码块。 while 循环 while 循环会在指定条件为真时循环执行代码块。 语法 while (条件) {需要执行的代码 }本例中…

Redis内部的阻塞式操作以及应对方法

Redis之所以被广泛应用&#xff0c;很重要的一个原因就是它支持高性能访问&#xff0c;也正因为这样&#xff0c;我们必须要重视所有可能影响Redis性能的因素&#xff0c;不仅要知道具体的机制&#xff0c;尽可能避免异常的情况出现&#xff0c;还要提前准备好应对异常的方案。…

MySQL进阶篇之索引2

02、索引 前四节内容&#xff1a;https://blog.csdn.net/kuaixiao0217/article/details/128753999 2.5、SQL性能分析 2.5.1、查看执行频次 1、SQL执行频率 MySQL客户端连接成功后&#xff0c;通过show [session|global] status命令可以提供服务器状态信息。 通过如下指令…