分析 pms_category
表 - 之前执行sql文件导入表的时候数据同时导入了
编写业务查询出所有的 category
1、从默认逆向生成的 list 入手进行修改
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = categoryService.queryPage(params);
return R.ok().put("page", page);
}
}
2、创建业务方法查询所有分类及子分类
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 查出所有分类以及子分类,以树形结构组装
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
}
3、实体类添加属性作为子分类
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
...
// 不在数据表的字段需要添加该属性
@TableField(exist = false)
private List<CategoryEntity> children;
}
4、编写业务
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
...
@Override
public List<CategoryEntity> listWithTree() {
// 1、查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
// 2、组装成父子的树形结构
// 2.1、查出所有一级分类
List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == 0;
}).map((menu) -> {
menu.setChildren(getChildren(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> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
// 递归查询子菜单
categoryEntity.setChildren(getChildren(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return children;
}
}
1、新建 category.vue
进行前端渲染 - 参考 树形控件
<template>
<div>
<el-tree :data="menus" :props="defaultProps">el-tree>
div>
template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放菜单数据
return {
menus: [],
defaultProps: {
children:"children",
label:"label"
}
};
},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then((data) => {
console.log("成功获取菜单数据。。。",data);
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
};
script>
<style scoped>
style>
2、可以看到请求路径达不到预期 - 预期是给10000端口发送请求
3、因为默认是给 - http://localhost:8080/renren-fast
发送请求
4、修改为给网关发请求 - 以 /api
请求划分为前端请求
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api'
5、重新刷新页面发现验证码接口都报错了,因为验证码服务默认是给 renren-fast服务 发送请求,所以应该先配置网关路由直接将前端请求 /api
全部转给 renren-fast服务
下面都是修改 renren-fast服务
5.1、pom
<dependency>
<groupId>com.laptoy.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
5.2、application.yml - 添加
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 192.168.88.129:8848
5.3、主启动类开启服务发现
@SpringBootApplication
@EnableDiscoveryClient
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
6、网关配置路径映射 - 在配置中心的 gateway.yml 进行修改
spring:
cloud:
gateway:
routes:
######## 前端项目发送过来的都是/api/**的请求 #######
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
# http://localhost:88/api/captcha.jpg --> http://localhost:8080/renren-fast/captcha.jpg
7、启动发现验证码刷新出来了
从8001访问88,引发CORS跨域请求,浏览器会拒绝跨域请求
1、添加响应头配置解决跨域 - gateway模块添加config
@Configuration // gateway
public class GulimallCorsConfiguration {
@Bean // 添加过滤器
public CorsWebFilter corsWebFilter(){
// 基于url跨域,选择reactive包下的
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域的头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
2、修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类
(跟上面的配置冲突)
1、网关新增路由配置,将 /api/product
请求转给 gulimall-product
(配置在admin_route上方,否则导致 /api/**
请求先被admin_route拦截)
spring:
cloud:
gateway:
routes:
######## 前端项目发送过来的都是/api/**的请求 #######
# product服务路由
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
# http://localhost:88/api/product/category/list/tree --> http://localhost:10000/product/category/list/tree
# 人人开源后端服务路由
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
# http://localhost:88/api/captcha.jpg --> http://localhost:8080/renren-fast/captcha.jpg
2、修改 - category.vue
由于真实数据存在 data.data.data
中,所以可以先{{data}}
取出第二层data信息,再将this.menus = data.data
修改成如下
methods: {
getMenus() {
this.$http({ // http://localhost:10000/product/category/list/tree
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => { // success
console.log("成功获取到菜单数据:",data.data)
this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
}).catch(() => {});
},
}
data() {
//这里存放数据
return {
menus: [],
defaultProps: {
children: "children", //子节点属性为children
label: "name" // 显示data的name属性
}
};
},
1、新增 - append 和 remove 按钮
append
- 层级小于2时显示(因为只能为为三级分类)remove
- 当前节点无子节点时才可删除show-checkbox
- 展开多选框:expand-on-click-node=false
- 点击按钮不展开目录node-key="catId"
- 设置树节点唯一属性,最好设置,以唯一属性进行设置,这里catId为唯一属性<el-tree :data="menus" :props="defaultProps" :expand-on-click-node=false show-checkbox 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)">Appendel-button>
<el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">Deleteel-button>
span>
span>
el-tree>
2、添加 - append和remove方法(自行点击按钮进行测试)
methods: {
...
// 添加节点
append(data) {
console.log("append",data)
},
// 删除节点
remove(node, data) {
console.log("remove:",data,node)
},
},
1、逻辑删除配置
数据表中默认1代表展示数据
配置全局逻辑删除规则 修改 - 配置中心的mybatis.yml
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
# 0代表逻辑删除,1代表未逻辑删除
logic-delete-value: 0
logic-not-delete-value: 1
实体类属性添加注解 @TableLogin
@TableLogic
private Integer showStatus;
2、控制层
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 逻辑删除
* 请求体必须为json - 因为@RequestBody注解
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds) {
//1、检查当前删除的菜单是否被别的
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
}
3、业务层
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 检查当前删除的菜单,是否被别的地方引用
baseMapper.deleteBatchIds(asList);
}
}
4、设置日志级别 other.yml
添加
logging:
level:
com.laptoy.gulimall: debug
5、测试逻辑删除
postman发送 post请求http://localhost:88/api/product/category/delete
请求头携带json数据
查看数据表
查看日志 - 实际是更新操作
1、删除节点方法
- 添加 :default-expanded-keys
(默认展开的值)
data
- 添加 expandedKey: [],
<el-tree :default-expanded-keys="expandedKey">el-tree>
<script>
export default {
data() {
return {
...
// 添加默认展开数据
expandedKey: [],
};
},
//方法集合
methods: {
...
// 删除节点
remove(node, data) {
console.log("remove:", data, node);
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
})
.then(({ data }) => {
this.$message({
type: "success",
message: "菜单删除成功!",
});
// 刷新出新的菜单
this.getMenus();
// 删除成功展开父菜单
this.expandedKey = [node.parent.data.catId];
})
.catch(() => {});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
};
script>
<style scoped>
style>
1、新增弹出框
- 双向绑定 category
属性,弹出框只有一个文本框填写category.name
2、data - 新增 - 默认category数据 - 默认不展开弹出框数据
<template>
<div>
...
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off">el-input>
el-form-item>
el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消el-button>
<el-button type="primary" @click="addCategory">确 定el-button>
span>
el-dialog>
div>
template>
<script>
export default {
...
data() {
return {
...
// 弹出框数据
dialogVisible: false,
category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
};
},
//方法集合
methods: {
...
// 添加节点-赋值数据及调用弹出框
append(data) {
console.log("append:", data);
this.dialogVisible = true;
this.category.parentCid = data.catId;
// 防止catLevel为字符串,*1强转
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({
type: "success",
message: "菜单保存成功!",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
this.expandedKey = [this.category.parentCid];
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
};
script>
<style scoped>
style>
1、需要提交 catId
到数据库进行定位修改,以及其他数据:icon、productUnit 进行修改
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: "",
productUnit: "",
},
2、新增 edit 按钮 及 icon 和 productUnit 字段
<el-button type="text" size="mini" @click="() => edit(data)">Editel-button>
...
<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>
3、弹出框区分为 append edit 请求 - data新增属性用来区分
dialogType: "" // append / edit
title: "", // 对应标题
4、弹出框 - 修改
<el-button type="primary" @click="submitData">确 定el-button>
5、新增方法 - submitData 区分对应操作
// 区分方法
submitData(){
if(this.dialogType == "add"){
this.addCategory();
}
if(this.dialogType == "edit"){
this.editCategory();
}
},
6、添加节点方法 - 修改
// 添加节点-赋值数据及调用弹出框
append(data) {
console.log("append:", data);
this.dialogType = "add";
this.title = "添加节点";
this.dialogVisible = true;
this.category.parentCid = data.catId;
// 防止catLevel为字符串,*1强转
this.category.catLevel = data.catLevel * 1 + 1;
},
7、考虑并发情况的数据脏读问题,每次点击修改按钮回显数据应该从数据库调用查看
/**
* 信息
*/
@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId) {
CategoryEntity category = categoryService.getById(catId);
// 默认返回到前端都设置为 data
return R.ok().put("data", category);
}
8、修改 - 最终效果
<template>
<div>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node=false show-checkbox 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)">Appendel-button>
<el-button type="text" size="mini" @click="() => edit(data)">Editel-button>
<el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">Deleteel-button>
span>
span>
el-tree>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off">el-input>
el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off">el-input>
el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off">el-input>
el-form-item>
el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消el-button>
<el-button type="primary" @click="submitData">确 定el-button>
span>
el-dialog>
div>
template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
// 菜单数据
menus: [],
// 三级分类子分类数据
defaultProps: {
children: "children",
label: "name",
},
// 默认展开菜单
expandedKey: [],
// 是否展开弹出框
dialogVisible: false,
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: "",
productUnit: "",
},
// 弹出框类型
dialogType: "",
// 弹出框标题
title: "",
};
},
//方法集合
methods: {
// 展示菜单数据
getMenus() {
this.$http({
// http://localhost:10000/product/category/list/tree
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
})
.then(({ data }) => {
// success
console.log("成功获取到菜单", data.data);
this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
})
.catch(() => {});
},
// 区分方法
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
// 添加节点-赋值数据及调用弹出框
append(data) {
console.log("append:", data);
this.dialogType = "add";
this.title = "添加节点";
this.dialogVisible = true;
// 清除name数据
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.parentCid = data.catId;
// 防止catLevel为字符串,*1强转
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({
type: "success",
message: "菜单保存成功!",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
this.expandedKey = [this.category.parentCid];
});
},
// 修改节点-赋值数据及调用弹出框
edit(data) {
console.log("edit:", data);
this.dialogType = "edit";
this.dialogVisible = true;
this.title = "修改节点";
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
console.log("要回显的数据", data);
this.category.catId = data.data.catId;
this.category.name = data.data.name;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
});
},
// 修改节点-提交修改
editCategory() {
// 解构出数据进行提交,后端API接口为动态更新,不提交的数据就不更新,这里只需要通过catId更新三项
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false),
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单修改成功!",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
this.expandedKey = [this.category.parentCid];
});
},
// 删除节点
remove(node, data) {
console.log("remove:", data, node);
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
})
.then(({ data }) => {
this.$message({
type: "success",
message: "菜单删除成功!",
});
// 刷新出新的菜单
this.getMenus();
// 删除成功展开父菜单
this.expandedKey = [node.parent.data.catId];
})
.catch(() => {});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
};
script>
<style scoped>
style>
核心代码
// 是否允许拖入
allowDrop(draggingNode, dropNode, type) {
// 判断被拖动的当前节点以及所在的父节点总层数不能大于3
console.log("拖拽", draggingNode, dropNode, type);
// 被拖动节点的总层级
this.countNodeLevel(draggingNode.data);
// 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
console.log("深度", deep);
// 当前拖动的节点深度 + 父节点深度 <= 3
// 放入内部的需要 - (当前深度+放入位置深度<=3)
if (type == "inner") {
return deep + dropNode.level <= 3;
// 放入前/后只需要 - (当前深度+放入位置父节点深度<=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]);
}
}
},
全部代码
<template>
<div>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node=false show-checkbox 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)">Appendel-button>
<el-button type="text" size="mini" @click="() => edit(data)">Editel-button>
<el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">Deleteel-button>
span>
span>
el-tree>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off">el-input>
el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off">el-input>
el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off">el-input>
el-form-item>
el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消el-button>
<el-button type="primary" @click="submitData">确 定el-button>
span>
el-dialog>
div>
template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
// 菜单数据
menus: [],
// 三级分类子分类数据
defaultProps: {
children: "children",
label: "name",
},
// 默认展开框数据
expandedKey: [],
// 弹出框数据
dialogVisible: false,
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: "",
productUnit: "",
},
dialogType: "",
title: "",
maxLevel: 0,
};
},
//方法集合
methods: {
// 展示菜单数据
getMenus() {
this.$http({
// http://localhost:10000/product/category/list/tree
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
})
.then(({ data }) => {
// success
console.log("成功获取到菜单", data.data);
this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
})
.catch(() => {});
},
// 区分方法
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
// 添加节点-赋值数据及调用弹出框
append(data) {
console.log("append:", data);
this.dialogType = "add";
this.title = "添加节点";
this.dialogVisible = true;
// 清除name数据
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.parentCid = data.catId;
// 防止catLevel为字符串,*1强转
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({
type: "success",
message: "菜单保存成功!",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
this.expandedKey = [this.category.parentCid];
});
},
// 修改节点-赋值数据及调用弹出框
edit(data) {
console.log("edit:", data);
this.dialogType = "edit";
this.dialogVisible = true;
this.title = "修改节点";
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
console.log("要回显的数据", data);
this.category.catId = data.data.catId;
this.category.name = data.data.name;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
});
},
// 修改节点-提交修改
editCategory() {
// 解构出数据进行提交,后端API接口为动态更新,不提交的数据就不更新,这里只需要通过catId更新三项
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false),
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单修改成功!",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
this.expandedKey = [this.category.parentCid];
});
},
// 删除节点
remove(node, data) {
console.log("remove:", data, node);
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】当前菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
})
.then(({ data }) => {
this.$message({
type: "success",
message: "菜单删除成功!",
});
// 刷新出新的菜单
this.getMenus();
// 删除成功展开父菜单
this.expandedKey = [node.parent.data.catId];
})
.catch(() => {});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
// 是否允许拖入
allowDrop(draggingNode, dropNode, type) {
// 判断被拖动的当前节点以及所在的父节点总层数不能大于3
console.log("拖拽", draggingNode, dropNode, type);
// 被拖动节点的总层级
this.countNodeLevel(draggingNode.data);
// 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1
console.log("深度", deep);
// 当前拖动的节点深度 + 父节点深度 <= 3
// 放入内部的需要 - (当前深度+放入位置深度<=3)
if (type == "inner") {
return deep + dropNode.level <= 3;
// 放入前/后只需要 - (当前深度+放入位置父节点深度<=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() {
this.getMenus();
},
};
script>
<style scoped>
style>
1、拖拽成功绑定事件
<el-tree @node-drop="handleDrop" />
methods:{
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log('tree drop: ', dropNode.label, dropType);
},
}
2、新增 - updateNodes存放修改的数据
data:{
updateNodes: [],
}
3、新增 - 拖拽成功事件
// 拖拽成功事件
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log('handleDrop: ', draggingNode, dropNode, dropType)
let pCid = 0
let sibings = null
// 1、获取当前节点不同情况的 (最新的父节点id) 及 (所有兄弟节点)
if (dropType == 'inner') {
pCid = dropNode.data.catId
sibings = dropNode.childNodes
} else {
// 如果放在最顶层会导致找不到父节点id,三目运算符排除一下
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId
sibings = dropNode.parent.childNodes
}
// 2、当前拖拽节点和兄弟节点的最新顺序
for (let i = 0; i < sibings.length; i++) {
// 如果遍历的是当前节点
if (sibings[i].data.catId == draggingNode.data.catId) {
let catLevel = draggingNode.level;
// 如果当前节点的层级发生变化
if (sibings[i].level != draggingNode.level) {
// 层级修改为兄弟节点的层级
catLevel = sibings[i].level;
// 修改子节点层级
this.updateChildNodeLevel(sibings[i]);
}
// 设置父节点id
this.updateNodes.push({
catId: sibings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
})
}
// 其余节点直接修改顺序
this.updateNodes.push({ catId: sibings[i].data.catId, sort: i })
}
},
// 修改子节点层级
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])
}
}
}
1、添加API接口处理批量修改请求
/**
* 批量修改
*/
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] categorys) {
categoryService.updateBatchById(Arrays.asList(categorys));
return R.ok();
}
2、发送请求
// 拖拽成功事件
handleDrop(draggingNode, dropNode, dropType, ev) {
...
// 发送请求修改节点
this.$http({
url: this.$http.adornUrl('/product/category/update/sort'),
method: 'post',
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
type: 'success',
message: '菜单顺序修改成功!',
})
// 刷新出新的菜单
this.getMenus()
// 展开父菜单
this.expandedKey = [pCid]
// 每次拖拽成功重新赋值默认值
this.updateNodes =[];
this.maxLevel = 0;
});
},
1、添加开关并双向绑定draggable
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽">
<el-button type="primary" v-if="draggable" @click="batchSave">批量保存el-button>
<el-tree :draggable="draggable" >
2、添加 draggable并赋值默认值false,当true表示开启拖拽,新增全局pCid
data:{
draggable: false,
pCid:[]
}
// 拖拽成功事件
handleDrop(draggingNode, dropNode, dropType, ev) {
,,,
// 保存全局pCid给下述方法使用
this.pCid.push(pCid);
4、将单独保存的请求抽取成独立请求
// 批量保存
batchSave() {
this.$http({
url: this.$http.adornUrl('/product/category/update/sort'),
method: 'post',
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
type: 'success',
message: '菜单顺序修改成功!',
})
// 刷新出新的菜单
this.getMenus()
// 展开父菜单
this.expandedKey = this.pCid
// 每次拖拽成功重新赋值默认值
this.updateNodes = [];
this.maxLevel = 0;
this.pCid = 0;
});
}
5、之前的代码有点问题,因为未批量保存在每次操作都需要经数据库取数据,我们直接修改成取固定子树的数据
// 是否允许拖入
allowDrop(draggingNode, dropNode, type) {
// 判断被拖动的当前节点以及所在的父节点总层数不能大于3
console.log('拖拽', draggingNode, dropNode, type)
// 被拖动节点的总层级
this.countNodeLevel(draggingNode)
// 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1
console.log('深度', deep)
// 当前拖动的节点深度 + 父节点深度 <= 3
// 放入内部的需要 - (当前深度+放入位置深度<=3)
if (type == 'inner') {
return deep + dropNode.level <= 3
// 放入前/后只需要 - (当前深度+放入位置父节点深度<=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])
}
}
},
<el-button type="danger">批量删除el-button>
<el-tree ref="menuTree" >
// 批量删除
batchDelete() {
let catIds = [];
// 获取选中的节点内容
let checkNodes = this.$refs.menuTree.getCheckedNodes();
for (let i = 0; i < checkNodes.length; i++) {
catIds.push(checkNodes[i].catId)
}
console.log(checkNodes);
this.$confirm(`是否删除【${catIds}】所选菜单?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(catIds, false),
})
.then(({ data }) => {
this.$message({
type: 'success',
message: '菜单删除成功!',
})
// 刷新出新的菜单
this.getMenus()
// 删除成功展开父菜单
this.expandedKey = []
})
.catch(() => {
})
})
.catch(() => {
this.$message({
type: 'info',
message: '已取消删除',
})
})
}
<template>
<div>
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽">el-switch>
<el-button type="primary" v-if="draggable" @click="batchSave">批量保存el-button>
<el-button type="danger" @click="batchDelete">批量删除el-button>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node=false show-checkbox node-key="catId" :default-expanded-keys="expandedKey" :draggable="draggable" :allow-drop="allowDrop" @node-drop="handleDrop" ref="menuTree">
<span 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)">Appendel-button>
<el-button type="text" size="mini" @click="() => edit(data)">Editel-button>
<el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">Deleteel-button>
span>
span>
el-tree>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off">el-input>
el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off">el-input>
el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off">el-input>
el-form-item>
el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消el-button>
<el-button type="primary" @click="submitData">确 定el-button>
span>
el-dialog>
div>
template>
<script>
export default {
// import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
// 这里存放数据
return {
// 菜单数据
menus: [],
// 三级分类子分类数据
defaultProps: {
children: 'children',
label: 'name'
},
// 默认展开框数据
expandedKey: [],
// 弹出框数据
dialogVisible: false,
category: {
name: '',
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: '',
productUnit: '',
},
dialogType: '',
title: '',
maxLevel: 0,
updateNodes: [],
draggable: false,
pCid: [],
}
},
//方法集合
methods: {
// 展示菜单数据
getMenus() {
this.$http({
// http://localhost:10000/product/category/list/tree
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get',
})
.then(({ data }) => {
// success
console.log('成功获取到菜单', data.data)
this.menus = data.data // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
})
.catch(() => {
})
},
// 区分方法
submitData() {
if (this.dialogType == 'add') {
this.addCategory()
}
if (this.dialogType == 'edit') {
this.editCategory()
}
},
// 添加节点-赋值数据及调用弹出框
append(data) {
console.log('append:', data)
this.dialogType = 'add'
this.title = '添加节点'
this.dialogVisible = true
// 清除name数据
this.category.name = ''
this.category.icon = ''
this.category.productUnit = ''
this.category.parentCid = data.catId
// 防止catLevel为字符串,*1强转
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({
type: 'success',
message: '菜单保存成功!',
})
// 关闭对话框
this.dialogVisible = false
// 刷新出新的菜单
this.getMenus()
this.expandedKey = [this.category.parentCid]
})
},
// 修改节点-赋值数据及调用弹出框
edit(data) {
console.log('edit:', data)
this.dialogType = 'edit'
this.dialogVisible = true
this.title = '修改节点'
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: 'get',
}).then(({ data }) => {
console.log('要回显的数据', data)
this.category.catId = data.data.catId
this.category.name = data.data.name
this.category.icon = data.data.icon
this.category.productUnit = data.data.productUnit
this.category.parentCid = data.data.parentCid
})
},
// 修改节点-提交修改
editCategory() {
// 解构出数据进行提交,后端API接口为动态更新,不提交的数据就不更新,这里只需要通过catId更新三项
var { catId, name, icon, productUnit } = this.category
this.$http({
url: this.$http.adornUrl('/product/category/update'),
method: 'post',
data: this.$http.adornData({ catId, name, icon, productUnit }, false),
}).then(({ data }) => {
this.$message({
type: 'success',
message: '菜单修改成功!',
})
// 关闭对话框
this.dialogVisible = false
// 刷新出新的菜单
this.getMenus()
this.expandedKey = [this.category.parentCid]
})
},
// 删除节点
remove(node, data) {
console.log('remove:', data, node)
var ids = [data.catId]
this.$confirm(`是否删除【${data.name}】当前菜单?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids, false),
})
.then(({ data }) => {
this.$message({
type: 'success',
message: '菜单删除成功!',
})
// 刷新出新的菜单
this.getMenus()
// 删除成功展开父菜单
this.expandedKey = [node.parent.data.catId]
})
.catch(() => {
})
})
.catch(() => {
this.$message({
type: 'info',
message: '已取消删除',
})
})
},
// 是否允许拖入
allowDrop(draggingNode, dropNode, type) {
// 判断被拖动的当前节点以及所在的父节点总层数不能大于3
console.log('拖拽', draggingNode, dropNode, type)
// 被拖动节点的总层级
this.countNodeLevel(draggingNode)
// 当前节点最大深度 = (子节点最大层级 - 当前节点层级) + 1
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1
console.log('深度', deep)
// 当前拖动的节点深度 + 父节点深度 <= 3
// 放入内部的需要 - (当前深度+放入位置深度<=3)
if (type == 'inner') {
return deep + dropNode.level <= 3
// 放入前/后只需要 - (当前深度+放入位置父节点深度<=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)
let pCid = 0
let sibings = null
// 1、获取当前节点不同情况的 (最新的父节点id) 及 (所有兄弟节点)
if (dropType == 'inner') {
pCid = dropNode.data.catId
sibings = dropNode.childNodes
} else {
// 如果放在最顶层会导致找不到父节点id,三目运算符排除一下
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId
sibings = dropNode.parent.childNodes
}
this.pCid.push(pCid);
// 2、当前拖拽节点和兄弟节点的最新顺序
for (let i = 0; i < sibings.length; i++) {
// 如果遍历的是当前节点
if (sibings[i].data.catId == draggingNode.data.catId) {
let catLevel = draggingNode.level;
// 如果当前节点的层级发生变化
if (sibings[i].level != draggingNode.level) {
// 层级修改为兄弟节点的层级
catLevel = sibings[i].level;
// 修改子节点层级
this.updateChildNodeLevel(sibings[i]);
}
// 设置父节点id 及 层级
this.updateNodes.push({
catId: sibings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
})
}
// 其余节点直接修改顺序
this.updateNodes.push({ catId: sibings[i].data.catId, sort: i })
}
},
// 修改子节点层级
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({
type: 'success',
message: '菜单顺序修改成功!',
})
// 刷新出新的菜单
this.getMenus()
// 展开父菜单
this.expandedKey = this.pCid
// 每次拖拽成功重新赋值默认值
this.updateNodes = [];
this.maxLevel = 0;
this.pCid = 0;
});
},
// 批量删除
batchDelete() {
let catIds = [];
// 获取选中的节点内容
let checkNodes = this.$refs.menuTree.getCheckedNodes();
for (let i = 0; i < checkNodes.length; i++) {
catIds.push(checkNodes[i].catId)
}
console.log(checkNodes);
this.$confirm(`是否删除【${catIds}】所选菜单?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(catIds, false),
})
.then(({ data }) => {
this.$message({
type: 'success',
message: '菜单删除成功!',
})
// 刷新出新的菜单
this.getMenus()
// 删除成功展开父菜单
this.expandedKey = []
})
.catch(() => {
})
})
.catch(() => {
this.$message({
type: 'info',
message: '已取消删除',
})
})
}
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus()
},
}
script>
<style scoped>
style>