文章目录
1 三级分类
1.1 数据准备
导入数据pms_catelog.sql
到表 psm_category
文件下载地址:https://share.weiyun.com/bO0OZMCv
连接失效可以到尚硅谷微信公众号输入“谷粒商城”获取
1.2 查询功能实现
1.2.1 controller
修改CategoryController.java里的list方法
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
// @RequiresPermissions("product:category:list")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
1.2.2 entity
修改CategoryEntity.java,添加 children属性
@TableField(exist = false)
private List<CategoryEntity> children;
1.2.2 service
在CategoryService.java声明方法
在CategoryServiceImpl.java实现方法
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);
return new PageUtils(page);
}
@Override
public List<CategoryEntity> listWithTree() {
//1、查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2、组装成父子的树形结构
List<CategoryEntity> level1Menus = entities.stream()
// 过滤,取出所有1级分类的记录
.filter(categoryEntity -> categoryEntity.getParentCid() == 0)
// 递归映射子分类
.map((menu) -> {
menu.setChildren(getChildrens(menu, entities));
return menu;
})
.sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
.collect(Collectors.toList());
return level1Menus;
}
//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream()
// 过滤出分类的父级与传入参数的等级一样的记录,级1级分类找2级分类
.filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId()))
.map(categoryEntity -> {
//1、递归找子菜单
categoryEntity.setChildren(getChildrens(categoryEntity, all));
return categoryEntity;
})
.sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
.collect(Collectors.toList());
return children;
}
}
1.2.3 测试
启动服务,测试,注意,先启动nacos
访问http://localhost:10000/product/category/list/tree
1.3 配置网关路由与路径重写
1、idea启动renren-fast
2、vscode打开renren-fast-vue启动
3、进入前端开发平台,进入菜单管理新增菜单
可以在gulimall-admin数据库里的sys_menu表里看到
4、新增分类维护
角色管理视图对应renren-fast-vue里的src/views/modules/sys/role.vue文件
5、在modules文件夹下创建product文件夹
6、新建编写category.vue
<!-- -->
<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概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
7、static\config\index.js里定义了api接口请求地址,改写成全部给网关地址发请求
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
验证码失效:需要将renren-fast注册进nacos
1.3.2 配置renren-fast
1、让renren-fast依赖gulimall-common模块
坑:各种报错
**问题原因:**可能是因为renren-fast各种依赖包的版本和common里包的版本不同
**解决方法:**单独导入依赖包,例如我的renren-fast的springboot是2.6.6版本
只导入nacos-discover
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.0.1.0</version>
<exclusions>
<exclusion>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
2、在application.yml添加配置
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: localhost:8848
3、开启服务的注册发现功能
@SpringBootApplication
@EnableDiscoveryClient
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
4、重启服务
坑:启动服务报错java: 找不到符号 符号: 方法setOperation(java.lang.String)
解决方法:
将renren-fast里的的lombok依赖的版本改为<lombok.version>1.18.14</lombok.version>
5、到nacos查看是否注册成功
6、配置网关规则
spring:
cloud:
gateway:
routes:
- id: baidu_route
uri: https://www.baidu.com
predicates:
- Query=url, baidu # 如果参数url的值为baidu,则页面跳转至https://www.baidu.com
- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url, qq
- id: admin_route
uri: lb://renren-fast
predicates:
# 设定前端项目发送请求都带上api前缀
- Path=/api/**
7、重启renren-fast和gateway服务
8、发现验证码还是不行
原因是http请求发送http://localhost:88/api被网关匹配后经负载均衡转至了http://localhost:8080/api/captcha.jpg,而正常的验证码请求为http://localhost:8080/renren-fast/captcha.jpg
9、路径进行重写
spring:
cloud:
gateway:
routes:
- id: baidu_route
uri: https://www.baidu.com
predicates:
- Query=url, baidu # 如果参数url的值为baidu,则页面跳转至https://www.baidu.com
- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url, qq
- id: admin_route
uri: lb://renren-fast
predicates:
# 设定前端项目发送请求都带上api前缀
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}
可以看到验证码成功显示,但是因为跨域原因不允许登陆
1.4 跨域
1.4.1 什么是跨域
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域
1.4.2 跨域流程
1.4.3 解决跨域的方法
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:表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
1.4.4 网关配置跨域
1、在gateway服务里新建config.GulimallCorsConfiguration.java
package com.atguigu.gulimall.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
/**
* @Author: xjhqre
* @DateTime: 2022/4/21 15:58
*/
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1、配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
坑:配置完成之后还是报错Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
**解决方法:**boot2.4以上的把addAllowedOrigin("*");
改成addAllowedOriginPattern("*");
2、重启服务查看效果,报错说有多个策略
原因renren-fast里也配置了跨域,将其注释掉即可
@Configuration
public class CorsConfig implements WebMvcConfigurer {
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOriginPatterns("*")
// .allowCredentials(true)
// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .maxAge(3600);
// }
}
3、最后重启服务登陆页面
1.5 树形展示分类数据
1.5.1 添加网关路由规则
精确的路由规则放在上面
spring:
cloud:
gateway:
routes:
####################### 产品请求路由 ########################
# 当进入分页维护界面时,会发送http://localhost:88/api/product/category/list/tree请求
# 该请求经此规则匹配处理后变成http://localhost:88/product/category/list/tree请求
# 经过负载均衡后转至http://localhost:10000/product/category/list/tree,返回查询到的数据
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*), /$\{segment}
####################### 登陆页面路由 ########################
- id: admin_route
uri: lb://renren-fast
predicates:
# 设定前端项目发送请求都带上api前缀
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}
1.5.2 配置注册nacos
1、新建product服务的命名空间
2、新建bootstrap.yml标明配置中心地址
spring:
application:
name: gulimall-product
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: 1a68e0de-7cea-4360-b817-8d4eba43ff76
3、在product里的application.yml配置服务注册中心的地址和服务名称,PS:不知道在哪一步已经配置好了
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.56.10:3306/gulimall_pms
driver-class-name: com.mysql.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-product
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto # 自增主键
server:
port: 10000
4、开启服务发现注解
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.atguigu.gulimall.product.dao")
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
1.5.3 前端页面
1、修改category.vue页面
<!-- -->
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick">
</el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
//这里存放数据
return {
menus: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.5.4 测试
1、启动服务
- vue
- gulimall-gateway
- gulimall-product
- renren-fast
- nacos
访问http://localhost:8001/
登陆后进入分类维护页面即可看到效果
1.6 删除分类实现
1.6.1 前端页面
修改category.vue
实现最末尾的分类可以删除,其他有子节点的节点不能被删除
末尾节点不能添加,其他节点可以添加
添加选项框
<!-- -->
<template>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox="true"
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
//这里存放数据
return {
menus: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
},
remove(node, data) {
console.log("append", node, data);
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.6.2 测试删除逻辑
使用postman测试删除逻辑
1.6.3 实现逻辑删除
1、修改CategoryController里原来的删除逻辑
/**
* 删除
* @RequestBody:获取请求体,只有POST请求才有请求体,必须发送POST请求
* SpringMVC自动将请求体的数据(json),转为对应的对象
*/
@RequestMapping("/delete")
//@RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds){
//categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
2、创建removeMenuByIds接口并实现
CategoryServiceImpl:
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 1、检查当前删除的菜单,是否被别的地方引用
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
3、配置全局的逻辑删除规则并调整日志打印级别,gulimall-product\src\main\resources\application.yml
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.56.10:3306/gulimall_pms
driver-class-name: com.mysql.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-product
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto # 自增主键
logic-delete-value: 0 # 删除逻辑为0
logic-not-delete-value: 1 # 不删除逻辑为1
server:
port: 10000
4、实体类字段上加上@TableLogic注解,逻辑不删除用value,逻辑删除用delval,gulimall-product\src\main\java\com\atguigu\gulimall\product\entity\CategoryEntity.java
@TableLogic
private Integer showStatus;
或
@TableLogic(value = "1", delval = "0")
private Integer showStatus;
6、重启服务,用postman测试逻辑删除,查看打印的日志
1.6.4 删除效果细化
1、前端发送删除请求
2、删除确认
3、消息提示
4、依旧展开父节点
修改后的category.vue代码
<!-- -->
<template>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox="true"
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 <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId]
});
})
.catch(() => {});
console.log("append", node, data);
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
设置数据库所有数据的show_status为1
UPDATE pms_category SET show_status=1
1.7 新增分类实现
1、点击新增弹出对话框
2、填写表单数据
3、发送post请求保存分类
4、关闭对话框
5、刷新菜单并展开
修改后的category.vue
<!-- -->
<template>
<div>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox="true"
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 <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<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>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
},
// 添加三级分类
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({
message: "菜单保存成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("append", node, data);
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.8 修改效果实现
1、添加修改按钮
2、区分对话框功能
3、不同提示信息
4、新增表单填写选项
5、回显图标和计量单位
6、回显最新的数据
7、修改后端的info方法,改完后记得重启
@RequestMapping("/info/{catId}")
// @RequiresPermissions("product:category:info")
public R info(@PathVariable("catId") Long catId) {
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
修改后的category.vue:
<!-- -->
<template>
<div>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox="true"
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 <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog :title="dialogTitle" :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>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
dialogTitle: "",
dialogType: "",
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.dialogTitle = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
// 因为修改后数据变化,需要改回默认数据
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
// 添加三级分类
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({
message: "菜单保存成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改方法
edit(data) {
console.log("要修改的数据:", data);
this.dialogType = "edit";
this.dialogTitle = "修改分类";
this.dialogVisible = true;
// 发送请求查询最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
params: this.$http.adornParams({}),
}).then(({ data }) => {
// 请求成功后的处理
console.log("要回显的数据:", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
// 修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var categoryData = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(categoryData, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 判断是修改请求还是添加请求
submitData() {
if (this.dialogType == "add") {
this.addCategory();
} else if ((this.dialogType = "edit")) {
this.editCategory();
}
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("append", node, data);
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.9 拖拽效果实现
1.9.1 前端效果实现
1、在树形结构中允许拖拽
2、编写拖拽函数
修改后的category.vue:
<!-- -->
<template>
<div>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:show-checkbox="true"
node-key="catId"
:default-expanded-keys="expandedKey"
draggable
:allow-drop="allowDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog :title="dialogTitle" :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>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
maxLevel: 0,
dialogTitle: "",
dialogType: "",
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.dialogTitle = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
// 因为修改后数据变化,需要改回默认数据
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
// 添加三级分类
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({
message: "菜单保存成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改方法
edit(data) {
console.log("要修改的数据:", data);
this.dialogType = "edit";
this.dialogTitle = "修改分类";
this.dialogVisible = true;
// 发送请求查询最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
params: this.$http.adornParams({}),
}).then(({ data }) => {
// 请求成功后的处理
console.log("要回显的数据:", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
// 修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var categoryData = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(categoryData, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 判断是修改请求还是添加请求
submitData() {
if (this.dialogType == "add") {
this.addCategory();
} else if ((this.dialogType = "edit")) {
this.editCategory();
}
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("append", node, data);
},
// 允许拖拽函数
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3
//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//
this.countNodeLevel(draggingNode.data);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
console.log("深度:", deep);
// this.maxLevel
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
//找到所有子节点,求出最大深度
countNodeLevel(node) {
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.9.2 前端数据更新
1、影响的数据有parent_cid、cat_level、sort
2、添加拖拽结束函数
修改后的category.vue
<!-- -->
<template>
<div>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:show-checkbox="true"
node-key="catId"
:default-expanded-keys="expandedKey"
draggable
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog :title="dialogTitle" :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>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
updateNodes: [],
maxLevel: 0,
dialogTitle: "",
dialogType: "",
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.dialogTitle = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
// 因为修改后数据变化,需要改回默认数据
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
// 添加三级分类
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({
message: "菜单保存成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改方法
edit(data) {
console.log("要修改的数据:", data);
this.dialogType = "edit";
this.dialogTitle = "修改分类";
this.dialogVisible = true;
// 发送请求查询最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
params: this.$http.adornParams({}),
}).then(({ data }) => {
// 请求成功后的处理
console.log("要回显的数据:", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
// 修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var categoryData = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(categoryData, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 判断是修改请求还是添加请求
submitData() {
if (this.dialogType == "add") {
this.addCategory();
} else if ((this.dialogType = "edit")) {
this.editCategory();
}
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("append", node, data);
},
// 允许拖拽函数
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3
//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//
this.countNodeLevel(draggingNode.data);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
console.log("深度:", deep);
// this.maxLevel
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
//找到所有子节点,求出最大深度
countNodeLevel(node) {
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
// 拖拽事件结束后的回调函数
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
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;
}
//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.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
},
// 递归更新子节点的层级
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.9.3 后端数据更新
1、后端编写批量修改方法,改完后记得重启
/**
* 拖拽后批量修改
*/
@RequestMapping("/update/sort")
// @RequiresPermissions("product:category:update")
public R updateSort(@RequestBody CategoryEntity[] category) {
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}
2、用postman测试方法
3、前端发送请求
4、拖拽完成后初始化updateNodes和maxLevel
修改后的category.vue:
<!-- -->
<template>
<div>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:show-checkbox="true"
node-key="catId"
:default-expanded-keys="expandedKey"
draggable
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog :title="dialogTitle" :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>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
updateNodes: [],
maxLevel: 0,
dialogTitle: "",
dialogType: "",
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.dialogTitle = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
// 因为修改后数据变化,需要改回默认数据
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
// 添加三级分类
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({
message: "菜单保存成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改方法
edit(data) {
console.log("要修改的数据:", data);
this.dialogType = "edit";
this.dialogTitle = "修改分类";
this.dialogVisible = true;
// 发送请求查询最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
params: this.$http.adornParams({}),
}).then(({ data }) => {
// 请求成功后的处理
console.log("要回显的数据:", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
// 修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var categoryData = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(categoryData, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 判断是修改请求还是添加请求
submitData() {
if (this.dialogType == "add") {
this.addCategory();
} else if ((this.dialogType = "edit")) {
this.editCategory();
}
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("append", node, data);
},
// 允许拖拽函数
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3
//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//
this.countNodeLevel(draggingNode.data);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
console.log("深度:", deep);
// this.maxLevel
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
//找到所有子节点,求出最大深度
countNodeLevel(node) {
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
// 拖拽事件结束后的回调函数
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
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;
}
//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.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
// 发送请求
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [pCid];
this.updateNodes = [];
this.maxLevel = 0;
});
},
// 递归更新子节点的层级
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.9.4 批量保存拖拽效果
1、添加可选拖拽功能
2、统一提交拖拽结果
3、这里我做了一下修改,在点击保存拖拽结果后不展开最后的拖拽的父节点,就不设置pCid了
4、修改计算深度的代码逻辑
修改后的category.vue:
<!-- -->
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽"
></el-switch>
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:show-checkbox="true"
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog :title="dialogTitle" :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>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
draggable: false,
updateNodes: [],
dialogTitle: "",
dialogType: "",
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.dialogTitle = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
// 因为修改后数据变化,需要改回默认数据
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
// 添加三级分类
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({
message: "菜单保存成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改方法
edit(data) {
console.log("要修改的数据:", data);
this.dialogType = "edit";
this.dialogTitle = "修改分类";
this.dialogVisible = true;
// 发送请求查询最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
params: this.$http.adornParams({}),
}).then(({ data }) => {
// 请求成功后的处理
console.log("要回显的数据:", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
// 修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var categoryData = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(categoryData, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 判断是修改请求还是添加请求
submitData() {
if (this.dialogType == "add") {
this.addCategory();
} else if ((this.dialogType = "edit")) {
this.editCategory();
}
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("append", node, data);
},
// 允许拖拽函数
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3
//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
this.maxLevel = draggingNode.level;
this.countNodeLevel(draggingNode);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度:", deep);
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
countNodeLevel(node) {
//找到所有子节点,求出最大深度
if (node.childNodes != null && node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].level > this.maxLevel) {
this.maxLevel = node.childNodes[i].level;
}
this.countNodeLevel(node.childNodes[i]);
}
}
},
// 拖拽事件结束后的回调函数
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
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;
}
//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.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
// 展开节点
this.expandedKey = [pCid];
},
// 递归更新子节点的层级
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
batchSave() {
// 发送请求
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
this.updateNodes = [];
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.10 批量删除节点实现
1、添加删除按钮
2、给tree添加引用名
3、编写删除函数
改写后的category.vue:
<!-- -->
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽"
></el-switch>
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<!-- expand-on-click-node:是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 -->
<!-- show-checkbox 节点是否可被选择 -->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
:show-checkbox="true"
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
ref="menuTree"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog :title="dialogTitle" :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>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
draggable: false,
updateNodes: [],
dialogTitle: "",
dialogType: "",
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null,
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
// children和name都是menus里的数据
children: "children",
label: "name",
},
};
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.dialogTitle = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
// 因为修改后数据变化,需要改回默认数据
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
// 添加三级分类
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({
message: "菜单保存成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改方法
edit(data) {
console.log("要修改的数据:", data);
this.dialogType = "edit";
this.dialogTitle = "修改分类";
this.dialogVisible = true;
// 发送请求查询最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
params: this.$http.adornParams({}),
}).then(({ data }) => {
// 请求成功后的处理
console.log("要回显的数据:", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
});
},
// 修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var categoryData = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(categoryData, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 判断是修改请求还是添加请求
submitData() {
if (this.dialogType == "add") {
this.addCategory();
} else if ((this.dialogType = "edit")) {
this.editCategory();
}
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
// 刷新菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});
console.log("append", node, data);
},
// 允许拖拽函数
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3
//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
this.maxLevel = draggingNode.level;
this.countNodeLevel(draggingNode);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度:", deep);
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
countNodeLevel(node) {
//找到所有子节点,求出最大深度
if (node.childNodes != null && node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].level > this.maxLevel) {
this.maxLevel = node.childNodes[i].level;
}
this.countNodeLevel(node.childNodes[i]);
}
}
},
// 拖拽事件结束后的回调函数
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
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;
}
//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.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
// 展开节点
this.expandedKey = [pCid];
},
// 递归更新子节点的层级
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
batchSave() {
// 发送请求
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
this.updateNodes = [];
});
},
// 批量删除函数
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({
message: "菜单批量删除成功",
type: "success",
});
this.getMenus();
});
})
.catch(() => {});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
// 在实例创建时使用该方法,获取分类数据,保存到menus中
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>