实现三级分类
后端实现:
在controller中
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities=categoryService.listWithTree();
return R.ok().put("data",entities);
}
创建一个istWithTree()方法
查出所有的父子关系
//查出所有分类,组装成父子的树形结构
@Override
public List<CategoryEntity> listWithTree() {
//查询所有分类
List<CategoryEntity> entities = categoryDao.selectList(null);
//分类
//查询所有一级分类
List<CategoryEntity> level1Menus = entities.stream().filter((categoryEntity -> {
return categoryEntity.getParentCid() == 0;
})).map((menu)->{
menu.setChildren(getChildrens(menu,entities));
return menu;
}).sorted((menu1,menu2)->{
return (menu1.getSort()==null?0:menu1.getSort())-(menu2.getSort()==null?0:menu2.getSort());//通过表中的排序字段进行排序
})
.collect(Collectors.toList());
return level1Menus;
}
//递归查找所有的子菜单
private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all ){
List<CategoryEntity> collect = all.stream().filter((categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
})).map((childrenEntity) -> {
childrenEntity.setChildren(getChildrens(childrenEntity, all));
return childrenEntity;
}).sorted((child1, child2) -> {
return (child1.getSort()==null?0:child1.getSort())-(child2.getSort()==null?0:child2.getSort());//通过表中的排序字段进行排序
}).collect(Collectors.toList());
return collect;
}
前端实现:
1、使用人人开发平台创建出来一个目录(商品系统),然后在商品系统的目录下创建一个分类维护(路径为propduct/catory)
2、在renren-fast-vue中的models中创建product文件夹,在该文件夹下创建category.vue
<template>
<div><el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree></div>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
data:[
],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus(){
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(data=>{
console.log("成功获取到数据",data)
})
}
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
//声明周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//声明周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
此时打开分类维护的目录,页面会发送请求,但是请求的地址是http://localhost:8080/product/category/list/tree
因为在static下的config中的index.js中指定了请求地址是8080端口
所以我们将前端的请求转发给gateway网关,由网关做转发
修改前端中的 window.SITE_CONFIG[‘baseUrl’] = ‘http://localhost:88’
测试:发现我们的验证码没了,这是由于验证码是来源于renren-fast这个项目中的,所以我们要将renren-fast也注册到nacos中
直接导入gulimall-common的依赖
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
配置nacos的地址
在启动类上使用注解开启服务
为了使前端发送的请求都交给renren-fast去处理
给前端的请求路径上加一个api
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';//修改为给网关发请求
在gateway中
当遇到由api的路径时交给renren-fast去处理
修改配置文件
- id: admin_root
uri: lb://renren-fast
predicates:
- Path=/api/**
但是这时候发送的验证码请求变为了http://renren-fast/api/captcha.jpg,路径中带由了api
所以需要将请求的路径修改为http://renren-fast/captcha.jpg
需要设置The RewritePath GatewayFilter Factory
- id: admin_root
uri: lb://renren-fast
predicates:
- Path=/api/**
## http://localhost:88/api/captcha.jpg http://renren-fast/api/captcha.jpg
filters:
- RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
此时就不会出现验证码的问题了
解决跨域问题
此时点击登录,出现跨域问题
在gate网关配置解决跨域问题
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource urlsorce = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration=new CorsConfiguration();
//配置跨域
corsConfiguration.addAllowedHeader("*");//允许的请求头
corsConfiguration.addAllowedMethod("*");//允许的请求方法
corsConfiguration.addAllowedOrigin("*");//允许的请求来源
corsConfiguration.setAllowCredentials(true);//是否允许携带cookie
urlsorce.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(urlsorce);
}
}
并且将renrnen-fast中的跨域问题的配置注释了
登录页面成功进入后台
当组件加载完成会发送一个请求获取三级分类数据
我们使用gateway进行转发
1、配置gulimall-product模块到nacos中
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-product
2、创建product的命名空间,将prodcut的配置拆分(暂时不做)
3、新建bootstarp.yml文件,配置地址和配置文件
spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: 765a7765-9d98-47ca-9050-95b2450d4457
4、使用注解*@EnableDiscoveryClient*开启服务发现
启动测试是否被注册到注册中心
5、完成gateway的转发
修改gateway的配置文件
配置新的路由
- id: product
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>/?.*), /$\{segment}
重新启动网关测试
成功返回到数据
将数据添加到页面上
下一步就是将这些数据添加到页面上
<template>
<div><el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree></div>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
//数据存放在menus中
menus:[
],
defaultProps: {
children: 'children',
label: 'name'//修改label,展示name数据中的属性
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus(){
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
//对data进行解构
}).then(({data})=>{
// console.log("成功获取到数据",data.data)
this.menus=data.data;
})
}
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
//声明周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//声明周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
给页面添加按钮(添加和删除节点的按钮)并且节点前面添加复选框,规定只有1级和2级节点可以使用append按钮,没有子节点的才能使用delete按钮
实现
<template>
<div><el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" node-key="catId"
show-checkbox>
<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></div>
</template>
<script>
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus(){
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data})=>{
// console.log("成功获取到数据",data.data)
this.menus=data.data;
});
},
//点击append和remove后触发的方法
append(data) {
console.log("append"+data)
},
remove(node, data) {
console.log("remove",node,data)
}
删除功能实现
逻辑删除(当show-status的值为1时表示没有删除,为0时表示已删除)
查看官方文档所给出的逻辑删除功能的使用方法逻辑删除 | MyBatis-Plus (baomidou.com)
步骤 1: 配置
application.yml
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
步骤 2: 实体类字段上加上@TableLogic
注解
@TableLogic
private Integer deleted;
由于这里我们是1表示未删除,0表示已删除
所有在@TableLogic注解中,规定我们自己的逻辑
@TableLogic(value ="1",delval = "0")
private Integer showStatus;
删除代码生成器生成的删除方法
自定义一个deleteMenusByIds()方法
@Override
public void removeMenusByIds(List<Long> asList) {
//TODO 检查当前删除的菜单是否被别的地方引用
categoryDao.deleteBatchIds(asList);
}
通过前端的删除按钮发送请求
点击删除弹出确认删除对话框,删除成功后由成功提示,且删除之后默认展开父节点
<template>
<div><el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" node-key="catId"
show-checkbox
:default-expanded-keys="isCheckArrays"
>
<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></div>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
menus:[],
isCheckArrays:[],
defaultProps: {
children: 'children',
label: 'name'
}
};
},
methods: {
getMenus(){
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data})=>{
// console.log("成功获取到数据",data.data)
this.menus=data.data;
});
},
//点击append和remove后触发的方法
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.isCheckArrays=[node.parent.data.catId];
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
},
新增功能实现
功能描述:点击append后弹出对话框嵌套表单,填好要添加节点的name后点击确定关闭,对话框关闭,请求发送到后端实现新增。点击确定后页面刷新,展开当时点击append的父节点
最终的前端代码
<template>
<div>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" node-key="catId"
show-checkbox
:default-expanded-keys="isCheckArrays"
>
<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-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>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory">确 定</el-button>
</div>
</el-dialog>
</el-dialog>
</div>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
category:{
name:"",
parentCid:"1",
cateLevel:"",
showStatus:"",
sort:"1"
},//添加对话框中所填数据
dialogVisible: false, //添加对话框是否可见
menus: [],
isCheckArrays: [],//默认选中打开节点
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
//提交新增表单
addCategory(){
//发送post请求添加
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.isCheckArrays=[this.category.parentCid]
});
// console.log(this.category)
},
getMenus() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
// console.log("成功获取到数据",data.data)
this.menus = data.data;
});
},
//点击append和remove后触发的方法
append(data) {
console.log("append" , data);
this.dialogVisible = true;
//获取添加新节点的属性
this.category.parentCid=data.catId;
this.category.showStatus=data.showStatus;
this.category.catLevel=data.catLevel*1+1;
this.category.sort=data.sort;
},
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.isCheckArrays = [node.parent.data.catId];
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除"
});
});
}
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
//声明周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//声明周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
编辑功能实现
具体效果:
1、新增编辑按钮
2、点击编辑,弹出对话框,使用新增的对话框,进行数据回显,且对话框的title根据业务的不同有所变化
3、进行数据回显时,需要发送get请求查询数据
4、当打开的对话框是endit时,点击确定发送的updata请求,打开的对话框的save时发送save请求
5、点击append按钮时,清空表单中回显的数据
注意:发送修改请求时,只需要修改form中的三个属性即可
前端代码:
<template>
<div>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" node-key="catId"
show-checkbox
:default-expanded-keys="isCheckArrays"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="node.level<=2"
type="text"
size="mini"
@click="() => append(data)">
Append
</el-button>
<el-button
v-if="node.childNodes.length==0"
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
<el-button
type="text"
size="mini"
@click="() => endit(data)">
Endit
</el-button>
</span>
</span>
</el-tree>
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
>
<!-- 嵌套表单 -->
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</el-dialog>
</el-dialog>
</div>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
title:"",//对话框的title
formType:"",
category:{
catId:null,
name:"",
parentCid:"0",
cateLevel:"",
showStatus:"",
sort:"1",
icon:"",
productUnit:""
},//添加对话框中所填数据
dialogVisible: false, //添加对话框是否可见
menus: [],
isCheckArrays: [],//默认选中打开节点
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
//提交表单
submitForm(){
if(this.formType=="append"){
this.addCategory();
}
if(this.formType=="endit"){
//执行的是保存请求
this.enditData();
}
},
//提交新增表单
addCategory(){
//发送post请求添加
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.isCheckArrays=[this.category.parentCid]
});
// console.log(this.category)
},
//修改
enditData(){
var {catId,name,icon,productUnit}=this.category;
var somdata={
catId:catId,
name:name,
icon:icon,
productUnit:productUnit
};
// console.log("修改成功");
this.$http({
url: this.$http.adornUrl('/product/category/update'),
method: 'post',
data: this.$http.adornData(somdata, false)
}).then(({ data }) => {
//修改成功
//对话框消失
this.dialogVisible=false;
//刷新界面
this.getMenus();
this.$message({
message: '修改成功',
type: 'success'
});
});
},
getMenus() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
// console.log("成功获取到数据",data.data)
this.menus = data.data;
});
},
//点击append和remove后触发的方法
append(data) {
this.title="新增";
console.log("append" , data);
this.dialogVisible = true;
this.formType="append";//展示的是不回显数据的对话框
//获取添加新节点的属性
this.category.parentCid=data.catId;
this.category.showStatus=data.showStatus;
this.category.catLevel=data.catLevel*1+1;
this.category.sort=data.sort;
//清空输入框
this.category.icon="";
this.category.sort=0;
this.category.catId=null;
this.productUnit="";
},
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.isCheckArrays = [node.parent.data.catId];
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除"
});
});
},
//修改按钮绑定的方法
endit(data){
this.category.catId=data.catId;
//设置要固定的对话框
this.isCheckArrays = [data.parentCid];
console.log("endit" , data);
this.title="编辑";
//弹出对话框--还是使用那个对话框
this.dialogVisible = true;
this.formType="endit";//展示的是回显数据的对话框
//回显数据--发送查询请求(可能别的管理员登录也要进行修改,导致我们回显的是旧数据,所以需要查询一下数据库)
// this.category.name=data.name;
// this.category.icon=data.icon;
// this.productUnit=data.productUnit;
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: 'get'
}).then(({ data }) => {
//返回数据--记得console一下
// console.log(data)
this.category.name=data.category.name;
this.category.icon=data.category.icon;
this.productUnit=data.category.productUnit;
})
}
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
//声明周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//声明周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
拖拽效果的实现
给树形控件添加参数:allow-drag=“allowDrag”
:allow-drop=“allowDrop”
绑定一个方法allowDrop用来判断是否能被拖动
//判断是否能被拖拽
allowDrop(draggingNode, dropNode, type) {
console.log("draggingNode", draggingNode, dropNode, type);
//查找最深的level
this.selectMaxLeven(draggingNode.data);
let deep = this.MaxLevel - draggingNode.data.catLevel + 1;
console.log("深度", this.MaxLevel);
if (type == "inner") {
//如果是拖动到里面
return deep + dropNode.level <= 3;
} else {
// console.log(false);
return deep + dropNode.parent.level <= 3;
}
},
selectMaxLeven(node) {
//判断是否有子节点
if (node.children != null && node.children.length > 0) {
//有子节点,存放在一个Array的数组里面,遍历子节点
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.MaxLevel) {
this.MaxLevel = node.children[i].catLevel;
}
//递归查找
this.selectMaxLeven(node.children[i]);
}
}
},
拖拽后进行数据更新
使用事件@node-drag-end=“handleDragEnd” 拖拽结束时(可能未成功)触发的事件
1、更新他的父节点
2、对移动后的兄弟节点进行排序
3、更新他的level
4、更新他的子节点的level
handleDrop(draggingNode, dropNode, dropType, ev) {
//先初始化
this.updateNodes=[];
this.MaxLevel= 0;
console.log("draggingNode ", draggingNode, dropNode, dropType, ev);
let pCid = 0; //父节点
let sibling = null; //拖动后的兄弟节点
//1、获取拖拽后的节点的父节点
//判断当前的拖动type
if (dropType == "before" || dropType == "after") {
pCid=dropNode.parent.data.catId==undefined?0:dropNode.parent.data.catId;
sibling = dropNode.parent.childNodes;
} else {
//如果是inner,则父节点为
pCid = dropNode.data.catId;
sibling = dropNode.childNodes;
}
//2、拿到拖拽后的兄弟节点,并且完成新排序
for (let i = 0; i < sibling.length; i++) {
if (sibling[i].data.catId == draggingNode.data.catId) {
//遍历到当前节点是拖拽的节点,还需要更新parentCid,和当前层级,还有它子节点的层级
let catLevel = draggingNode.data.catLevel; //层级
// console.log("当前层级为",catLevel)
if (sibling[i].level !=catLevel) {
// console.log("层级发生变化")
//当前节点层级发生变化
//层级发生变化
catLevel=sibling[i].level;
//修改他的子节点
this.updateChildNodeLevel(sibling[i])
}
this.updateNodes.push({
catId: sibling[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel
});
}
else {
//遍历的是其他子节点
this.updateNodes.push({
catId: sibling[i].data.catId,
sort: i
});
}
}
//当前拖拽节点的最新层级
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]);
}
},
后端处理
/*
批量修改
*/
@RequestMapping("/update/sort")
public R updateByList(@RequestBody CategoryEntity[] categorys){
categoryService.updateBatchById(Arrays.asList(categorys));
return R.ok();
}
前端发送请求
//当前拖拽节点的最新层级
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.isCheckArrays = [pCid];
});
},
拖拽功能优化
添加一个按钮,点击后开启拖拽,并且显示批量保存的按钮
点击批量保存实现保存所有拖动过的节点
由于之前判断是否能拖拽的方法中调用的是数据库中的属性,我们修改为当前节点下的属性
前端代码实现:
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽">
</el-switch>
<el-button v-if="draggable" @click="saveAll">批量保存</el-button>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" node-key="catId"
show-checkbox
:default-expanded-keys="isCheckArrays"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
saveAll(){
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.isCheckArrays =this.pCid;//这里的pCid为一个数组
//初始化
this.updateNodes=[];
this.MaxLevel= 0;
});
修改判断是否能被拖拽的方法的修改
//判断是否能被拖拽
allowDrop(draggingNode, dropNode, type) {
console.log("draggingNode", draggingNode, dropNode, type);
//查找最深的level
this.selectMaxLeven(draggingNode);
let deep = Math.abs(this.MaxLevel - draggingNode.level) + 1;
console.log("深度", this.MaxLevel);
if (type == "inner") {
//如果是拖动到里面
return deep + dropNode.level <= 3;
} else {
// console.log(false);
return deep + dropNode.parent.level <= 3;
}
},
批量删除实现
使用下面这个方法获取选中的 cateId
getCheckedNodes | 若节点可被选择(即 show-checkbox 为 true ),则返回目前被选中的节点所组成的数组 |
---|---|
ref 被用来给元素或子组件注册引用信息, 引用信息将会注册在父组件的 $refs 对象上
给树形控件加上ref
deleteAll(){
let nums=[];
//批量删除
nums=this.$refs.menuTree.getCheckedNodes();//使用$refs指向这个控件
//遍历这个数组
for(var i=0;i<nums.length;i++){
this.catIds.push(nums[i].catId);
}
//弹出对话框
this.$confirm('是否删除选中的节点?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(this.catIds, false)
}).then(({ data }) => {
this.$message({
type: 'success',
message: '删除成功!'
});
//刷新界面
this.getMenus();
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
category.vue的全部代码
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽">
</el-switch>
<el-button v-if="draggable" @click="saveAll">批量保存</el-button>
<el-button type="danger" round @click="deleteAll">批量删除</el-button>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" node-key="catId"
show-checkbox
:default-expanded-keys="isCheckArrays"
: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
v-if="node.childNodes.length==0"
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
<el-button
type="text"
size="mini"
@click="() => endit(data)">
Endit
</el-button>
</span>
</span>
</el-tree>
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
>
<!-- 嵌套表单 -->
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</el-dialog>
</el-dialog>
</div>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
catIds:[],
pCid:[],
draggable: false,
updateNodes: [],
MaxLevel: 0,
title: "", //对话框的title
formType: "",
category: {
catId: null,
name: "",
parentCid: "0",
cateLevel: "",
showStatus: "",
sort: "1",
icon: "",
productUnit: ""
}, //添加对话框中所填数据
dialogVisible: false, //添加对话框是否可见
menus: [],
isCheckArrays: [], //默认选中打开节点
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
deleteAll(){
let nums=[];
//批量删除
nums=this.$refs.menuTree.getCheckedNodes();
//遍历这个数组
for(var i=0;i<nums.length;i++){
this.catIds.push(nums[i].catId);
}
//弹出对话框
this.$confirm('是否删除选中的节点?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(this.catIds, false)
}).then(({ data }) => {
this.$message({
type: 'success',
message: '删除成功!'
});
//刷新界面
this.getMenus();
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
saveAll(){
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.isCheckArrays =this.pCid;
//初始化
this.updateNodes=[];
this.MaxLevel= 0;
});
},
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("draggingNode ", draggingNode, dropNode, dropType, ev);
let pCid = 0; //父节点
let sibling = null; //拖动后的兄弟节点
//1、获取拖拽后的节点的父节点
//判断当前的拖动type
if (dropType == "before" || dropType == "after") {
pCid=dropNode.parent.data.catId==undefined?0:dropNode.parent.data.catId;
sibling = dropNode.parent.childNodes;
} else {
//如果是inner,则父节点为
pCid = dropNode.data.catId;
sibling = dropNode.childNodes;
}
this.pCid.push(pCid);
//2、拿到拖拽后的兄弟节点,并且完成新排序
for (let i = 0; i < sibling.length; i++) {
if (sibling[i].data.catId == draggingNode.data.catId) {
//遍历到当前节点是拖拽的节点,还需要更新parentCid,和当前层级,还有它子节点的层级
let catLevel = draggingNode.data.catLevel; //层级
// console.log("当前层级为",catLevel)
if (sibling[i].level !=catLevel) {
// console.log("层级发生变化")
//当前节点层级发生变化
//层级发生变化
catLevel=sibling[i].level;
//修改他的子节点
this.updateChildNodeLevel(sibling[i])
}
this.updateNodes.push({
catId: sibling[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel
});
}
else {
//遍历的是其他子节点
this.updateNodes.push({
catId: sibling[i].data.catId,
sort: i
});
}
}
//当前拖拽节点的最新层级
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]);
}
},
//判断是否能被拖拽
allowDrop(draggingNode, dropNode, type) {
console.log("draggingNode", draggingNode, dropNode, type);
//查找最深的level
this.selectMaxLeven(draggingNode);
let deep = Math.abs(this.MaxLevel - draggingNode.level) + 1;
console.log("深度", this.MaxLevel);
if (type == "inner") {
//如果是拖动到里面
return deep + dropNode.level <= 3;
} else {
// console.log(false);
return deep + dropNode.parent.level <= 3;
}
},
selectMaxLeven(node) {
//判断是否有子节点
if (node.childNodes != null && node.childNodes.length > 0) {
//有子节点,存放在一个Array的数组里面,遍历子节点
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].level > this.MaxLevel) {
this.MaxLevel = node.childNodes[i].level;
}
//递归查找
this.selectMaxLeven(node.childNodes[i]);
}
}
},
//提交表单
submitForm() {
if (this.formType == "append") {
this.addCategory();
}
if (this.formType == "endit") {
//执行的是保存请求
this.enditData();
}
},
//提交新增表单
addCategory() {
//发送post请求添加
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.isCheckArrays = [this.category.parentCid];
});
// console.log(this.category)
},
//修改
enditData() {
var { catId, name, icon, productUnit } = this.category;
var somdata = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit
};
// console.log("修改成功");
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(somdata, false)
}).then(({ data }) => {
//修改成功
//对话框消失
this.dialogVisible = false;
//刷新界面
this.getMenus();
this.$message({
message: "修改成功",
type: "success"
});
});
},
getMenus() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
// console.log("成功获取到数据",data.data)
this.menus = data.data;
});
},
//点击append和remove后触发的方法
append(data) {
this.title = "新增";
console.log("append", data);
this.dialogVisible = true;
this.formType = "append"; //展示的是不回显数据的对话框
//获取添加新节点的属性
this.category.parentCid = data.catId;
this.category.showStatus = data.showStatus;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.sort = data.sort;
//清空输入框
this.category.icon = "";
this.category.sort = 0;
this.category.catId = null;
this.productUnit = "";
},
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.isCheckArrays = [node.parent.data.catId];
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除"
});
});
},
//修改按钮绑定的方法
endit(data) {
this.category.catId = data.catId;
//设置要固定的对话框
this.isCheckArrays = [data.parentCid];
console.log("endit", data);
this.title = "编辑";
//弹出对话框--还是使用那个对话框
this.dialogVisible = true;
this.formType = "endit"; //展示的是回显数据的对话框
//回显数据--发送查询请求(可能别的管理员登录也要进行修改,导致我们回显的是旧数据,所以需要查询一下数据库)
// this.category.name=data.name;
// this.category.icon=data.icon;
// this.productUnit=data.productUnit;
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
//返回数据--记得console一下
// console.log(data)
this.category.name = data.category.name;
this.category.icon = data.category.icon;
this.productUnit = data.category.productUnit;
});
}
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
//声明周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//声明周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>