当前位置:首页 » 《资源分享》 » 正文

Day398&399.三级分类 -谷粒商城_阿昌爱Java

5 人参与  2022年01月09日 12:17  分类 : 《资源分享》  评论

点击全文阅读


三级分类

0、表结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GntGqpql-1632323412719)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922203848085.png)]

1、sql脚本

根据老师提供的sql

2、查找所有分类以及子分类

  • 在achangmall-product的com.achang.achangmall.product.controller.CategoryController添加如下方法
/**
     * 查出所有分类以及子分类,以树形结构组装
     */
@RequestMapping("/list/tree")
public R list(){
    List<CategoryEntity> treeList = categoryService.listTree();
    return R.ok().put("list", treeList);
}
  • com.achang.achangmall.product.service.impl.CategoryServiceImpl
//查出所有分类以及子分类,以树形结构组装
@Override
public List<CategoryEntity> listTree() {
    List<CategoryEntity> allList = baseMapper.selectList(null);

    List<CategoryEntity> parentList = allList.stream()
        .filter(item -> item.getParentCid() == 0)
        .map(item -> {
            item.setChildren(getChildren(item, allList));
            return item;
        }).sorted((item1, item2) -> {
        return item1.getSort() - item2.getSort();
    })
        .collect(Collectors.toList());
    return parentList ;
}

//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> allList) {
    List<CategoryEntity> lastList = allList.stream()
        .filter(item -> {return root.getCatId().equals(item.getParentCid());})
        .map(item -> {
            item.setChildren(getChildren(item, allList));
            return item;
        })
        .sorted(
        (item1, item2) -> {
            return (item1.getSort()==null?0:item1.getSort()) - (item2.getSort()==null?0:item2.getSort());
        })
        .collect(Collectors.toList());

    return lastList;
}
  • 请求JSON结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ENFuZrV-1632323412722)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922211831278.png)]


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

  • 接着操作后台 localhost:8001 , 点击系统管理,菜单管理,新增 目录 商品系统 一级菜单

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8mfukTSs-1632323412735)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214246487.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aDI5mXnQ-1632323412743)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214332853.png)]

  • 继续新增: 菜单 分类维护 商品系统 product/category

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QtQlrG9S-1632323412745)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214346930.png)]

在左侧点击【分类维护】,希望在此展示3级分类 注意地址栏http://localhost:8001/#/product-category 可以注意到product-category我们的/被替换为了-

  • 比如sys-role具体的视图在renren-fast-vue/views/modules/sys/role.vue

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pu9o7lSY-1632323412748)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214536109.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgNrWAGG-1632323412758)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214618925.png)]

  • 所以要自定义我们的product/category视图的话,就是创建mudules/product/category.vue

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wugc5DVJ-1632323412759)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214651704.png)]

  • 此时我们使用elementui的树形结构组件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lm9FLq5j-1632323412762)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214732406.png)]

  • 他要给8080发请求读取数据,但是数据是在10000端口上,如果找到了这个请求改端口那改起来很麻烦。方法1是改vue项目里的全局配置,方法2是搭建个网关,让网关路由到10000

Ctrl+Shift+F全局搜索

在static/config/index.js里 window.SITE_CONFIG[‘baseUrl’] = ‘http://localhost:88/api’;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R7IZ4z5B-1632323412764)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922214905069.png)]

接着让重新登录http://localhost:8001/#/login,验证码是请求88的,所以不显示。而验证码是来源于fast后台的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8iq9cM0-1632323412765)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922215249092.png)]

  • 现在的验证码请求路径为,http://localhost:88/api/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
  • 原始的验证码请求路径:http://localhost:8001/renren-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
  • 88是gateway的端口
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=achangmall-gateway
server.port=88
  • 他要去nacos中查找api服务,但是nacos里是fast服务,就通过把api改成fast服务 ;需要将它交给nacos注册中心,所以给他加入achangmall-common的依赖
<dependency>
    <groupId>com.achang.achangmall</groupId>
    <artifactId>achangmall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
  • 在fast中添加如下配置,指定nacos地址和此服务名
spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  application:
    name: renren-fast

如果报错gson依赖,就导入google的gson依赖

  • 这里阿昌出现了错误Could not find class [org.springframework.cloud.client.loadbalancer.reactive.OnNoRibbonDefaultCondit

发现是阿昌这里所使用的依赖版本是有误的,服务模块的项目使用的是2020.1.X的springcloud,所以需要修改:

  • springcloud的版本:Hoxton.RELEASE
  • springboot的版本:2.2.1.RELEASE
  • springcloud alibaba的版本:2.2.1.RELEASE
  • 在gateway中按格式加入
spring:
  cloud:
    gateway:
      routes:
        - id: renren_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
          		#将/api/的路径重写成/renren-fast/
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
  • 然后在nacos的服务列表里看到了renren-fast

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oklveFJK-1632323412766)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922223800421.png)]

  • 登录,还是报错!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcB1d3JF-1632323412767)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922224757337.png)]

  • 这里可知为跨域问题

那我们就在Gateway网关里写一个配置类去过来请求让他不跨域

package com.achang.achangmall.gateway.conf;

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 阿昌
 @create 2021-09-22 22:41
 *******
 */
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter(){
        // 基于url跨域,选择reactive包下的
        UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
        // 跨域配置信息
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许跨域的头
        corsConfiguration.addAllowedHeader("*");
        // 允许跨域的请求方式
        corsConfiguration.addAllowedMethod("*");
        // 允许跨域的请求来源
        corsConfiguration.addAllowedOrigin("*");
        // 是否允许携带cookie跨域
        corsConfiguration.setAllowCredentials(true);

        // 任意url都要进行跨域配置
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);
    }
}
  • 再次访问:http://localhost:8001/#/login,发现还是跨域,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfzBzvlk-1632323412768)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922230336014.png)]

  • 此时这里还跨域的问题是人人开源的服务里面他自己也配置了跨域,这里把他注释掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gUsiR5L4-1632323412769)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922230448509.png)]

  • 此时再次登录,成功!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jpxkjdaA-1632323412770)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210922230543449.png)]


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

在显示商品系统/分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在
这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree
但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。

  • 解决方法就是定义一个product路由规则,进行路径重写: 在网关增加三级分类的路由
        - id: product_route
          uri: lb://achangmall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
  • 在nacos中新建命名空间,用命名空间隔离项目,(可以在其中新建achangmall-product.yml)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJeMizV4-1632409025602)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210923203628884.png)]

spring:
  datasource:
    password: root
    username: root
    url: jdbc:mysql://192.168.109.101:3306/achangmall-pms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.jdbc.Driver

  application:
    name: achangmall-product
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto

server:
  port: 10000
  • 在product项目中新建bootstrap.properties
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=07a0c61b-ea34-4c89-9d7e-17a050124943
  • 主类上加上注解@EnableDiscoveryClient

  • 访问 localhost:88/api/product/category/list/tree invalid token,非法令牌,后台管理系统中没有登录,所以没有带令牌

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-62vPQadF-1632409025631)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210923204544528.png)]

  • 这里就需要调整网格Gateway里面的路由顺序,让越模糊的路由越在后面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xMeDINj4-1632409025637)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210923204656883.png)]

再次访问!!!成功!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQxlKjnL-1632409025645)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210923204800783.png)]


  • 接着修改前端部分,data解构,获取到后端传来的数据,并赋值给data
.<template>
    <el-tree
             :data="data"
             :props="defaultProps"
             @node-click="handleNodeClick"
             ></el-tree>
</template>

<script>
    export default {
        data() {
            return {
                data: [],//获取到后端来的数据,并赋值
                defaultProps: {
                    children: "children",
                    label: "name",
                },
            };
        },
        methods: {
            //获取所有菜单
            getMenus() {
                this.$http({
                    url: this.$http.adornUrl(`/product/category/list/tree`),
                    method: "get",
                }).then((resp) => {
                    this.data = resp.data.list;
                });
            },
            handleNodeClick(data) {
                console.log(data);
            },
        },
        created() {
            this.getMenus();
        },
    };
</script>
  • 前端效果图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XxJrFSc0-1632409025650)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210923210245251.png)]


5、删除数据

  • scoped slot(插槽):在el-tree标签里把内容写到span标签栏里即可

  • 修改category.vue的代码

.<template>
<el-tree
         :data="data"
         :props="defaultProps"
         show-checkbox
         @node-click="handleNodeClick"
         :expand-on-click-node="false"
         node-key="catId"
         >
    <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
            <el-button
                       v-if="node.level != 3"
                       type="text"
                       size="mini"
                       @click="() => append(data)"
                       >
                Append
    </el-button>
            <el-button
                       v-if="node.childNodes <= 0"
                       type="text"
                       size="mini"
                       @click="() => remove(node, data)"
                       >
                Delete
    </el-button>
    </span>
    </span></el-tree
    >
</template>

<script>
    export default {
        data() {
            return {
                data: [],
                defaultProps: {
                    children: "children",
                    label: "name",
                },
            };
        },
        methods: {
            //添加节点
            append(data) {},
            //删除节点
            remove(node, data) {
                console.log(node);
            },
            //获取所有菜单
            getMenus() {
                this.$http({
                    url: this.$http.adornUrl(`/product/category/list/tree`),
                    method: "get",
                }).then((resp) => {
                    this.data = resp.data.list;
                });
            },
            handleNodeClick(data) {
                console.log(data);
            },
        },
        created() {
            this.getMenus();
        },
    };
</script>
  • 编写后台的删除接口
/**
* 删除
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
    categoryService.removeCategory(catIds);
    return R.ok();
}

@Override
public void removeCategory(Long[] catIds) {
    baseMapper.deleteBatchIds(Arrays.asList(catIds));
}
  • 编写后台的删除接口,这里采用逻辑删除,那么首先我们就需要通过MybaitsPlus配置逻辑删除

在achangmall-product的application.yaml配置如下

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1 #逻辑删除的值为1
      logic-not-delete-value: 0 #无逻辑删除的值0

在CategoryEntity中指定字段指定逻辑删除

public class CategoryEntity implements Serializable {

	/**
	 * 是否显示[0-不显示,1显示]
	 */
	@TableLogic(value = "1",delval = "0")
	private Integer showStatus;
    
    //省略其他属性.........
}
  • 另外在“src/main/resources/application.yml”文件中,设置日志级别,打印出SQL语句:
logging:
  level: debug

  • 前端代码
.<template>
<el-tree
         :data="data"
         :props="defaultProps"
         show-checkbox
         @node-click="handleNodeClick"
         :expand-on-click-node="false"
         node-key="catId"
         :default-expanded-keys="expandedKey"
         >
    <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
            <el-button
                       v-if="node.level != 3"
                       type="text"
                       size="mini"
                       @click="() => append(data)"
                       >
                Append
    </el-button>
            <el-button
                       v-if="node.childNodes <= 0"
                       type="text"
                       size="mini"
                       @click="() => remove(node, data)"
                       >
                Delete
    </el-button>
    </span>
    </span></el-tree
    >
</template>

<script>
    export default {
        data() {
            return {
                data: [],
                defaultProps: {
                    children: "children",
                    label: "name",
                },
            };
        },
        methods: {
            //添加节点
            append(data) {},
            //删除节点
            remove(node, data) {
                var ids = [data.catId]; //删除节点的id
                this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
                    confirmButtonText: "确定",
                    cancelButtonText: "取消",
                    type: "warning",
                })
                    .then(() => {
                    this.$http({
                        url: this.$http.adornUrl("/product/category/delete"),
                        method: "post",
                        data: this.$http.adornData(ids, false),
                    })
                        .then(({ data }) => {
                        this.$message({
                            type: "success",
                            message: "菜单删除成功!",
                        });
                        // 刷新出新的菜单
                        this.getMenus();
                        this.expandedKey = [node.parent.data.catId];
                    })
                        .catch(() => {});
                })
                    .catch(() => {
                    this.$message({
                        type: "info",
                        message: "已取消删除",
                    });
                });
            },
            //获取所有菜单
            getMenus() {
                this.$http({
                    url: this.$http.adornUrl(`/product/category/list/tree`),
                    method: "get",
                }).then((resp) => {
                    this.data = resp.data.list;
                });
            },
            handleNodeClick(data) {
                console.log(data);
            },
        },
        created() {
            this.getMenus();
        },
    };
</script>

6、新增分类

  • 后端代码,已经逆向生成
@RequestMapping("/save")
public R save(@RequestBody CategoryEntity category){
    categoryService.save(category);

    return R.ok();
}
  • 前端代码
.<template>
    <div>
        <el-tree
                 :data="data"
                 :props="defaultProps"
                 show-checkbox
                 @node-click="handleNodeClick"
                 :expand-on-click-node="false"
                 node-key="catId"
                 :default-expanded-keys="expandedKey"
                 >
            <span class="custom-tree-node" slot-scope="{ node, data }">
                <span>{{ node.label }}</span>
                <span>
                    <el-button
                               v-if="node.level != 3"
                               type="text"
                               size="mini"
                               @click="() => append(data)"
                               >
                        Append
                    </el-button>
                    <el-button
                               v-if="node.childNodes <= 0"
                               type="text"
                               size="mini"
                               @click="() => remove(node, data)"
                               >
                        Delete
                    </el-button>
                </span>
            </span></el-tree
            >

        <el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
            <el-form :model="category">
                <el-form-item label="分类名称">
                    <el-input v-model="category.name" autocomplete="off"></el-input>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="addCategory">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                dialogVisible: false,
                expandedKey: [],
                category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
                data: [],
                defaultProps: {
                    children: "children",
                    label: "name",
                },
            };
        },
        methods: {
            //添加节点
            append(data) {
                this.category.parentCid = data.catId;
                this.category.catLevel = data.catLevel * 1 + 1;
                this.dialogVisible = true;
            },
            // 添加三级分类
            addCategory() {
                console.log("提交的三级分类数据", this.category);
                this.$http({
                    url: this.$http.adornUrl("/product/category/save"),
                    method: "post",
                    data: this.$http.adornData(this.category, false),
                })
                    .then(({ data }) => {
                    this.$message({
                        type: "success",
                        message: "菜单保存成功!",
                    });
                    this.dialogVisible = false;
                    // 刷新出新的菜单
                    this.getMenus();
                    this.expandedKey = [this.category.parentCid];
                })
                    .catch(() => {});
            },
            //删除节点
            remove(node, data) {
                var ids = [data.catId]; //删除节点的id
                this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
                    confirmButtonText: "确定",
                    cancelButtonText: "取消",
                    type: "warning",
                })
                    .then(() => {
                    this.$http({
                        url: this.$http.adornUrl("/product/category/delete"),
                        method: "post",
                        data: this.$http.adornData(ids, false),
                    })
                        .then(({ data }) => {
                        this.$message({
                            type: "success",
                            message: "菜单删除成功!",
                        });
                        // 刷新出新的菜单
                        this.getMenus();
                        this.expandedKey = [node.parent.data.catId];
                    })
                        .catch(() => {});
                })
                    .catch(() => {
                    this.$message({
                        type: "info",
                        message: "已取消删除",
                    });
                });
            },
            //获取所有菜单
            getMenus() {
                this.$http({
                    url: this.$http.adornUrl(`/product/category/list/tree`),
                    method: "get",
                }).then((resp) => {
                    this.data = resp.data.list;
                });
            },
            handleNodeClick(data) {
                console.log(data);
            },
        },
        created() {
            this.getMenus();
        },
    };
</script>

<style>
</style>

7、修改分类

  • 后端代码
//数据回显
@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId){
    CategoryEntity category = categoryService.getById(catId);
    return R.ok().put("category", category);
}
  • 前端代码
.<template>
    <div>
        <el-tree
                 :data="data"
                 :props="defaultProps"
                 show-checkbox
                 @node-click="handleNodeClick"
                 :expand-on-click-node="false"
                 node-key="catId"
                 :default-expanded-keys="expandedKey"
                 >
            <span class="custom-tree-node" slot-scope="{ node, data }">
                <span>{{ node.label }}</span>
                <span>
                    <el-button
                               v-if="node.level != 3"
                               type="text"
                               size="mini"
                               @click="() => append(data)"
                               >
                        Append
                    </el-button>
                    <el-button
                               v-if="node.childNodes <= 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
            >

        <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-item label="图标">
                    <el-input v-model="category.icon" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="计量单位">
                    <el-input
                              v-model="category.productUnit"
                              autocomplete="off"
                              ></el-input>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="submitData">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                dialogType: "", //edit,add
                title: "",
                dialogVisible: false,
                expandedKey: [],
                category: {
                    name: "",
                    parentCid: 0,
                    catLevel: 0,
                    showStatus: 1,
                    sort: 0,
                    icon: "",
                    productUnit: "",
                    catId: null,
                },
                data: [],
                defaultProps: {
                    children: "children",
                    label: "name",
                },
            };
        },
        methods: {
            //添加节点
            append(data) {
                console.log("append----", data);
                this.dialogType = "add";
                this.title = "添加分类";
                this.category.parentCid = data.catId;
                this.category.catLevel = data.catLevel * 1 + 1;
                this.category.catId = null;
                this.category.name = null;
                this.category.icon = "";
                this.category.productUnit = "";
                this.category.sort = 0;
                this.category.showStatus = 1;
                this.dialogVisible = true;
            },
            // 修改三级分类数据
            editCategory() {
                var { catId, name, icon, productUnit } = this.category;
                this.$http({
                    url: this.$http.adornUrl("/product/category/update"),
                    method: "post",
                    data: this.$http.adornData({ catId, name, icon, productUnit }, false),
                })
                    .then(({ data }) => {
                    this.$message({
                        type: "success",
                        message: "菜单修改成功!",
                    });
                    // 关闭对话框
                    this.dialogVisible = false;
                    // 刷新出新的菜单
                    this.getMenus();
                    // 设置需要默认展开的菜单
                    this.expandedKey = [this.category.parentCid];
                })
                    .catch(() => {});
            },
            edit(data) {
                console.log("要修改的数据", data);
                this.dialogType = "edit";
                this.title = "修改分类";
                // 发送请求获取节点最新的数据,数据回显
                this.$http({
                    url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
                    method: "get",
                }).then((data) => {
                    // 请求成功
                    console.log("要回显得数据", data);
                    console.log(data);
                    this.category = data.data.category;
                    // console.log(this.category);
                    this.dialogVisible = true;
                });
            },
            //上交
            submitData() {
                if (this.dialogType == "add") {
                    this.addCategory();
                }
                if (this.dialogType == "edit") {
                    this.editCategory();
                }
            },

            // 添加三级分类
            addCategory() {
                console.log("提交的三级分类数据", this.category);
                this.$http({
                    url: this.$http.adornUrl("/product/category/save"),
                    method: "post",
                    data: this.$http.adornData(this.category, false),
                })
                    .then(({ data }) => {
                    this.$message({
                        type: "success",
                        message: "菜单保存成功!",
                    });
                    this.dialogVisible = false;
                    // 刷新出新的菜单
                    this.getMenus();
                    this.expandedKey = [this.category.parentCid];
                })
                    .catch(() => {});
            },
            //删除节点
            remove(node, data) {
                var ids = [data.catId]; //删除节点的id
                this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
                    confirmButtonText: "确定",
                    cancelButtonText: "取消",
                    type: "warning",
                })
                    .then(() => {
                    this.$http({
                        url: this.$http.adornUrl("/product/category/delete"),
                        method: "post",
                        data: this.$http.adornData(ids, false),
                    })
                        .then(({ data }) => {
                        this.$message({
                            type: "success",
                            message: "菜单删除成功!",
                        });
                        // 刷新出新的菜单
                        this.getMenus();
                        this.expandedKey = [node.parent.data.catId];
                    })
                        .catch(() => {});
                })
                    .catch(() => {
                    this.$message({
                        type: "info",
                        message: "已取消删除",
                    });
                });
            },
            //获取所有菜单
            getMenus() {
                this.$http({
                    url: this.$http.adornUrl(`/product/category/list/tree`),
                    method: "get",
                }).then((resp) => {
                    this.data = resp.data.list;
                });
            },
            handleNodeClick(data) {
                console.log(data);
            },
        },
        created() {
            this.getMenus();
        },
    };
</script>

8、拖拽效果

为了防止误操作,我们通过edit把拖拽功能开启后才能进行操作。所以添加switch标签,操作是否可以拖拽。我们也可以体会到el-switch这个标签是一个开关

批量保存
但是现在存在的一个问题是每次拖拽的时候,都会发送请求,更新数据库这样频繁的与数据库交互

现在想要实现一个拖拽过程中不更新数据库,拖拽完成后,统一提交拖拽后的数据。

<el-button v-if="draggable" @click="batchSave">批量保存</el-button>

  • 前端代码
.<template>
    <div>
        <el-switch
                   v-model="draggable"
                   active-text="开启拖拽"
                   inactive-text="关闭拖拽"
                   >
        </el-switch>
        <el-button @click="batchSave" v-if="draggable">批量保存</el-button>
        <el-tree
                 :data="data"
                 :props="defaultProps"
                 show-checkbox
                 @node-click="handleNodeClick"
                 :expand-on-click-node="false"
                 node-key="catId"
                 :default-expanded-keys="expandedKey"
                 :draggable="draggable"
                 :allow-drop="allowDrop"
                 >
            <span class="custom-tree-node" slot-scope="{ node, data }">
                <span>{{ node.label }}</span>
                <span>
                    <el-button
                               v-if="node.level != 3"
                               type="text"
                               size="mini"
                               @click="() => append(data)"
                               >
                        Append
                    </el-button>
                    <el-button
                               v-if="node.childNodes <= 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
            >

        <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-item label="图标">
                    <el-input v-model="category.icon" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="计量单位">
                    <el-input
                              v-model="category.productUnit"
                              autocomplete="off"
                              ></el-input>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="submitData">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                pCid: [],
                draggable: false,
                updateNodes: [],
                maxLevel: 0,
                dialogType: "", //edit,add
                title: "",
                dialogVisible: false,
                expandedKey: [],
                category: {
                    name: "",
                    parentCid: 0,
                    catLevel: 0,
                    showStatus: 1,
                    sort: 0,
                    icon: "",
                    productUnit: "",
                    catId: null,
                },
                data: [],
                defaultProps: {
                    children: "children",
                    label: "name",
                },
            };
        },
        methods: {
            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;
                })
                    .catch(() => {});
            },
            handleDrop(draggingNode, dropNode, dropType, ev) {
                console.log("handleDrop: ", draggingNode, dropNode, dropType);

                //1 当前节点最新的父节点
                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);
                //2 当前拖拽节点的最新顺序
                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.updateChildNodeLevlel(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 });
                    }
                }
                //3 当前拖拽节点的最新层级
                console.log("updateNodes", this.updateNodes);
            },
            updateChildNodeLevlel(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.updateChildNodeLevlel(node.childNodes[i]);
                    }
                }
            },
            allowDrop(draggingNode, dropNode, type) {
                //1 被拖动的当前节点以及所在的父节点总层数不能大于3

                //1 被拖动的当前节点总层数
                console.log("allowDrop:", draggingNode, dropNode, type);

                var level = this.countNodeLevel(draggingNode);

                // 当前正在拖动的节点+父节点所在的深度不大于3即可
                let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
                console.log("深度:", deep);

                // this.maxLevel
                if (type == "innner") {
                    // console.log(
                    //   `this.maxLevel: ${this.maxLevel}; draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level: ${dropNode.level}`
                    // );
                    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);
                    }
                }
            },
            //添加节点
            append(data) {
                console.log("append----", data);
                this.dialogType = "add";
                this.title = "添加分类";
                this.category.parentCid = data.catId;
                this.category.catLevel = data.catLevel * 1 + 1;
                this.category.catId = null;
                this.category.name = null;
                this.category.icon = "";
                this.category.productUnit = "";
                this.category.sort = 0;
                this.category.showStatus = 1;
                this.dialogVisible = true;
            },
            // 修改三级分类数据
            editCategory() {
                var { catId, name, icon, productUnit } = this.category;
                this.$http({
                    url: this.$http.adornUrl("/product/category/update"),
                    method: "post",
                    data: this.$http.adornData({ catId, name, icon, productUnit }, false),
                })
                    .then(({ data }) => {
                    this.$message({
                        type: "success",
                        message: "菜单修改成功!",
                    });
                    // 关闭对话框
                    this.dialogVisible = false;
                    // 刷新出新的菜单
                    this.getMenus();
                    // 设置需要默认展开的菜单
                    this.expandedKey = [this.category.parentCid];
                })
                    .catch(() => {});
            },
            edit(data) {
                console.log("要修改的数据", data);
                this.dialogType = "edit";
                this.title = "修改分类";
                // 发送请求获取节点最新的数据,数据回显
                this.$http({
                    url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
                    method: "get",
                }).then((data) => {
                    // 请求成功
                    console.log("要回显得数据", data);
                    console.log(data);
                    this.category = data.data.category;
                    // console.log(this.category);
                    this.dialogVisible = true;
                });
            },
            //上交
            submitData() {
                if (this.dialogType == "add") {
                    this.addCategory();
                }
                if (this.dialogType == "edit") {
                    this.editCategory();
                }
            },

            // 添加三级分类
            addCategory() {
                console.log("提交的三级分类数据", this.category);
                this.$http({
                    url: this.$http.adornUrl("/product/category/save"),
                    method: "post",
                    data: this.$http.adornData(this.category, false),
                })
                    .then(({ data }) => {
                    this.$message({
                        type: "success",
                        message: "菜单保存成功!",
                    });
                    this.dialogVisible = false;
                    // 刷新出新的菜单
                    this.getMenus();
                    this.expandedKey = [this.category.parentCid];
                })
                    .catch(() => {});
            },
            //删除节点
            remove(node, data) {
                var ids = [data.catId]; //删除节点的id
                this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
                    confirmButtonText: "确定",
                    cancelButtonText: "取消",
                    type: "warning",
                })
                    .then(() => {
                    this.$http({
                        url: this.$http.adornUrl("/product/category/delete"),
                        method: "post",
                        data: this.$http.adornData(ids, false),
                    })
                        .then(({ data }) => {
                        this.$message({
                            type: "success",
                            message: "菜单删除成功!",
                        });
                        // 刷新出新的菜单
                        this.getMenus();
                        this.expandedKey = [node.parent.data.catId];
                    })
                        .catch(() => {});
                })
                    .catch(() => {
                    this.$message({
                        type: "info",
                        message: "已取消删除",
                    });
                });
            },
            //获取所有菜单
            getMenus() {
                this.$http({
                    url: this.$http.adornUrl(`/product/category/list/tree`),
                    method: "get",
                }).then((resp) => {
                    this.data = resp.data.list;
                });
            },
            handleNodeClick(data) {
                console.log(data);
            },
        },
        created() {
            this.getMenus();
        },
    };
</script>

<style>
</style>
  • 后端代码
@RestController
@RequestMapping("product/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;
    /**
     * 修改分类
     */
    @RequestMapping("/update/sort")
    // @RequiresPermissions("product:category:update")
    public R update(@RequestBody CategoryEntity[] category){

        categoryService.updateBatchById(Arrays.asList(category));
        return R.ok();
    }

}

9、批量删除

  • 前端代码
.<template>
    <div>
        <el-switch
                   v-model="draggable"
                   active-text="开启拖拽"
                   inactive-text="关闭拖拽"
                   >
        </el-switch>
        <el-button @click="batchSave" v-if="draggable">批量保存</el-button>
        <el-button type="danger" @click="batchDelete">批量删除</el-button>
        <el-tree
                 :data="data"
                 :props="defaultProps"
                 show-checkbox
                 @node-click="handleNodeClick"
                 :expand-on-click-node="false"
                 node-key="catId"
                 :default-expanded-keys="expandedKey"
                 :draggable="draggable"
                 :allow-drop="allowDrop"
                 ref="menuTree"
                 >
            <span class="custom-tree-node" slot-scope="{ node, data }">
                <span>{{ node.label }}</span>
                <span>
                    <el-button
                               v-if="node.level != 3"
                               type="text"
                               size="mini"
                               @click="() => append(data)"
                               >
                        Append
                    </el-button>
                    <el-button
                               v-if="node.childNodes <= 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
            >

        <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-item label="图标">
                    <el-input v-model="category.icon" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="计量单位">
                    <el-input
                              v-model="category.productUnit"
                              autocomplete="off"
                              ></el-input>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="submitData">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                pCid: [],
                draggable: false,
                updateNodes: [],
                maxLevel: 0,
                dialogType: "", //edit,add
                title: "",
                dialogVisible: false,
                expandedKey: [],
                category: {
                    name: "",
                    parentCid: 0,
                    catLevel: 0,
                    showStatus: 1,
                    sort: 0,
                    icon: "",
                    productUnit: "",
                    catId: null,
                },
                data: [],
                defaultProps: {
                    children: "children",
                    label: "name",
                },
            };
        },
        methods: {
            // 批量删除
            batchDelete() {
                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(() => {});
                })
                    .catch(() => {});
            },
            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;
                })
                    .catch(() => {});
            },
            handleDrop(draggingNode, dropNode, dropType, ev) {
                console.log("handleDrop: ", draggingNode, dropNode, dropType);

                //1 当前节点最新的父节点
                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);
                //2 当前拖拽节点的最新顺序
                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.updateChildNodeLevlel(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 });
                    }
                }
                //3 当前拖拽节点的最新层级
                console.log("updateNodes", this.updateNodes);
            },
            updateChildNodeLevlel(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.updateChildNodeLevlel(node.childNodes[i]);
                    }
                }
            },
            allowDrop(draggingNode, dropNode, type) {
                //1 被拖动的当前节点以及所在的父节点总层数不能大于3

                //1 被拖动的当前节点总层数
                console.log("allowDrop:", draggingNode, dropNode, type);

                var level = this.countNodeLevel(draggingNode);

                // 当前正在拖动的节点+父节点所在的深度不大于3即可
                let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
                console.log("深度:", deep);

                // this.maxLevel
                if (type == "innner") {
                    // console.log(
                    //   `this.maxLevel: ${this.maxLevel}; draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level: ${dropNode.level}`
                    // );
                    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);
                    }
                }
            },
            //添加节点
            append(data) {
                console.log("append----", data);
                this.dialogType = "add";
                this.title = "添加分类";
                this.category.parentCid = data.catId;
                this.category.catLevel = data.catLevel * 1 + 1;
                this.category.catId = null;
                this.category.name = null;
                this.category.icon = "";
                this.category.productUnit = "";
                this.category.sort = 0;
                this.category.showStatus = 1;
                this.dialogVisible = true;
            },
            // 修改三级分类数据
            editCategory() {
                var { catId, name, icon, productUnit } = this.category;
                this.$http({
                    url: this.$http.adornUrl("/product/category/update"),
                    method: "post",
                    data: this.$http.adornData({ catId, name, icon, productUnit }, false),
                })
                    .then(({ data }) => {
                    this.$message({
                        type: "success",
                        message: "菜单修改成功!",
                    });
                    // 关闭对话框
                    this.dialogVisible = false;
                    // 刷新出新的菜单
                    this.getMenus();
                    // 设置需要默认展开的菜单
                    this.expandedKey = [this.category.parentCid];
                })
                    .catch(() => {});
            },
            edit(data) {
                console.log("要修改的数据", data);
                this.dialogType = "edit";
                this.title = "修改分类";
                // 发送请求获取节点最新的数据,数据回显
                this.$http({
                    url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
                    method: "get",
                }).then((data) => {
                    // 请求成功
                    console.log("要回显得数据", data);
                    console.log(data);
                    this.category = data.data.category;
                    // console.log(this.category);
                    this.dialogVisible = true;
                });
            },
            //上交
            submitData() {
                if (this.dialogType == "add") {
                    this.addCategory();
                }
                if (this.dialogType == "edit") {
                    this.editCategory();
                }
            },

            // 添加三级分类
            addCategory() {
                console.log("提交的三级分类数据", this.category);
                this.$http({
                    url: this.$http.adornUrl("/product/category/save"),
                    method: "post",
                    data: this.$http.adornData(this.category, false),
                })
                    .then(({ data }) => {
                    this.$message({
                        type: "success",
                        message: "菜单保存成功!",
                    });
                    this.dialogVisible = false;
                    // 刷新出新的菜单
                    this.getMenus();
                    this.expandedKey = [this.category.parentCid];
                })
                    .catch(() => {});
            },
            //删除节点
            remove(node, data) {
                var ids = [data.catId]; //删除节点的id
                this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
                    confirmButtonText: "确定",
                    cancelButtonText: "取消",
                    type: "warning",
                })
                    .then(() => {
                    this.$http({
                        url: this.$http.adornUrl("/product/category/delete"),
                        method: "post",
                        data: this.$http.adornData(ids, false),
                    })
                        .then(({ data }) => {
                        this.$message({
                            type: "success",
                            message: "菜单删除成功!",
                        });
                        // 刷新出新的菜单
                        this.getMenus();
                        this.expandedKey = [node.parent.data.catId];
                    })
                        .catch(() => {});
                })
                    .catch(() => {
                    this.$message({
                        type: "info",
                        message: "已取消删除",
                    });
                });
            },
            //获取所有菜单
            getMenus() {
                this.$http({
                    url: this.$http.adornUrl(`/product/category/list/tree`),
                    method: "get",
                }).then((resp) => {
                    this.data = resp.data.list;
                });
            },
            handleNodeClick(data) {
                console.log(data);
            },
        },
        created() {
            this.getMenus();
        },
    };
</script>

<style>
</style>
  • 后端代码
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
    categoryService.removeCategory(catIds);
    return R.ok();
}
  • 效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S2AYFyHx-1632409025660)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210923225501258.png)]


点击全文阅读


本文链接:http://zhangshiyu.com/post/32956.html

节点  菜单  删除  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1