一. 三级分类
1. 分类树形显示
1. 后端代码
1. Enerty层
-
在实体类
CategoryEntity
中添加一个children字段, 用于表示子分类// @TableField(exist = false) 注解表示该属性不是数据库中的字段 @TableField(exist = false) private List<CategoryEntity> children;
2. Controller层
-
查询所有分类及其子分类, 并以树形结构组装起来
@RequestMapping("/list/tree") // @RequiresPermissions("product:category:list") public R list(){ List<CategoryEntity> entities = categoryService.listWithTree(); return R.ok().put("data", entities); }
3. Service层
-
接口
List<CategoryEntity> listWithTree();
-
实现
/** * 查询所有分类及其子分类, 并以树形结构组装起来 * @return */ @Override public List<CategoryEntity> listWithTree() { // 1. 查出所有分类 List<CategoryEntity> entities = baseMapper.selectList(null); // 2. 组装成父子的树形结构 List<CategoryEntity> level1Menus = entities.stream() // 2.1 找到所有的一级分类(特点: parent_cid = 0) .filter(categoryEntity -> categoryEntity.getParentCid() == 0) // 2.2 查找所有一级分类的子分类 .map((menu) -> { // 查找每个一级分类的子分类 menu.setChildren(getChildrens(menu, entities)); return menu; }) // 2.3 将查询到的分类进行排序 .sorted((menu1, menu2) -> { return (menu1.getSort() == null? 0: menu1.getSort()) - (menu2.getSort() == null? 0: menu2.getSort()); }) // 2.4 组成List数组 .collect(Collectors.toList()); return level1Menus; } /** * 递归查找所有分类的子分类 * 从all列表中获取当前root的子分类 * @param root * @param all * @return */ private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all){ List<CategoryEntity> children = all.stream() .filter((categoryEntry) -> categoryEntry.getParentCid().equals(root.getCatId())) .map(categoryEntry -> { categoryEntry.setChildren(getChildrens(categoryEntry, all)); return categoryEntry; }).sorted((menu1, menu2) -> { return (menu1.getSort() == null? 0: menu1.getSort()) - (menu2.getSort() == null? 0: menu2.getSort()); }) .collect(Collectors.toList()); return children; }
4. 演示
-
演示效果
2. 前端代码
1. 配置项目路由
-
在index.js中修改项目的url
// api接口请求地址 window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
-
重新启动时, 会发现验证码无法显示, 因为验证码是调用的
renren-fast
的项目. 修改请求端口后, 就不能获取到验证码了 -
修改后端
renren-fast
-
在pom文件中引入
gulimall-common
模块 -
配置nacos注册发现的配置信息
- 启动类中启用nacos
-
-
在gulimall-gateway中设置网关路由
spring: cloud: gateway: routes: - id: admin_route # 负载均衡到指定的服务器 uri: lb://renren-fast predicates: # 前端项目都带上/api的前缀 - Path=/api/** filters: # http://localhost:80/api/captha.jpg -> # http://localhost:80/renren-fast/captha.jpg # 需要将url中的/api 修改为 /renren-fast - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
2. 解决跨域
-
此时验证码可以正常显示, 但是点击登录后会出现跨域问题
-
在gulimall-gateway模块中配置跨域相关的配置信息
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 Jiang锋时刻 * @create 2021-04-28 13:45 */ @Configuration public class GulimallCorsConfig { @Bean public CorsWebFilter corsWebFilter(){ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); // 跨域的配置信息 // 允许哪些请求头跨域访问 corsConfiguration.addAllowedHeader("*"); // 允许哪些请求方式跨域访问 corsConfiguration.addAllowedMethod("*"); // 允许哪些请求来源跨域访问 corsConfiguration.addAllowedOrigin("*"); // 是否允许携带cookie进行访问 // 如果不设置, 则请求的信息会丢失 corsConfiguration.setAllowCredentials(true); source.registerCorsConfiguration("/**", corsConfiguration); return new CorsWebFilter(source); } }
-
重新启动项目并登录, 发现浏览器会出现以下错误, 原因是
renren-fast
中已经配置过跨域, 所以需要注释掉renren-fast中的跨域配置
-
此时项目前端终于可以正常获取到后端数据
3. 修改路由
-
问题: 登录后请求分类列表时, 前端仍然会将请求转发到
renren-fast
-
修改后端路由配置
- id: product_route uri: lb://gulimall-product predicates: - Path=/api/product/** filters: # http://localhost:88/api/product/category/list/tree # http://localhost:13000/product/category/list/tree - RewritePath=/api/(?<segment>.*),/$\{segment}
-
注意: 更精确的路由需要放在前面, 不然会被之前的路由规则解析
4. 前端调用
-
前端代码
<template> <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree> </template> <script> export default { data() { return { menus: [], defaultProps: { // 子节点的属性 children: 'children', // 要显示的内容 label: 'name' } }; }, created(){ this.getMenus() }, methods: { handleNodeClick(data) { console.log(data); }, getMenus(){ this.$http({ url: this.$http.adornUrl('/product/category/list/tree'), method: 'get' }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据 console.log("成功获取到菜单数据: ", data.data) this.menus = data.data }) } } }; </script> <style> </style>
-
显示效果
2. 分类删除
1. 后端代码
1. 配置逻辑删除
-
配置全局的逻辑删除规则(省略)
mybatis-plus: global-config: db-config: id-type: auto # 1表示逻辑删除 logic-delete-value: 1 # 0表示没有逻辑删除 logic-not-delete-value: 0
-
配置逻辑删除的组件bean(省略)
-
在entry中添加逻辑删除的注解
/** * 是否显示[0-不显示,1显示] */ @TableLogic(value = "1", delval = "0") private Integer showStatus;
2. controller层
-
CategoryController
代码/** * 删除 * @RequestBody: 获取请求体, 必须发送post请求 * springMVC自动将请求体的数据(json), 转成对应的对象 */ @DeleteMapping("/delete") // @RequiresPermissions("product:category:delete") public R delete(@RequestBody Long[] catIds){ // 这个方法太简单了, 不能满足业务需求, 需要重新写一个方法 // categoryService.removeByIds(Arrays.asList(catIds)); // 只实现了简单的删除, 还需检查当前删除的表单, 是否被别的地方引用 categoryService.removeMenuByIds(Arrays.asList(catIds)); return R.ok(); }
3. service层
-
接口
/** * 自定义删除分类的方法 * @param asList */ void removeMenuByIds(List<Long> asList);
-
实现
@Override public void removeMenuByIds(List<Long> asList) { // Todo 检查当前删除的分类, 是否被别的地方引用 baseMapper.deleteBatchIds(asList); }
2. 前端代码
-
前端样式
<template> <!-- :data: 获取的数据源 :props: 配置选项 @node-click: 节点被点击时的回调 show-checkbox: 节点是否可被选择 :expand-on-click-node: 是否在点击节点的时候展开或者收缩节点, 默认值为 true, 如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 node-key: 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的(一定要设置) --> <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick" node-key="catId" show-checkbox :expand-on-click-node="false"> <!-- node: 当前节点信息, data: 从数据库中获取到的内容 --> <span class="custom-tree-node" slot-scope="{ node, data }"> <span>{{ node.label }}</span> <span> <!-- 只有一级分类和二级分类才能追加子分类: v-if="node.level <= 2" 只有没有子分类的节点才能删除当前分类: v-if="node.childNodes.length == 0" --> <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> export default { data() { return { menus: [], defaultProps: { // 子节点的属性 children: 'children', // 要显示的内容 label: 'name' } }; }, created(){ this.getMenus() }, methods: { handleNodeClick(data) { console.log(data); }, getMenus(){ this.$http({ url: this.$http.adornUrl('/product/category/list/tree'), method: 'get' }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据 console.log("成功获取到菜单数据: ", data.data) this.menus = data.data }) }, append(data) { console.log(data) }, remove(node, data) { console.log(node + ":" + data) } } }; </script>
-
删除功能实现后的完整代码
<template> <!-- :data: 获取的数据源 :props: 配置选项 @node-click: 节点被点击时的回调 show-checkbox: 节点是否可被选择 :expand-on-click-node: 是否在点击节点的时候展开或者收缩节点, 默认值为 true, 如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 node-key: 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的(一定要设置) :default-expanded-keys: 默认展开的节点 --> <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick" node-key="catId" :default-expanded-keys="expandedKey" show-checkbox :expand-on-click-node="false"> <!-- node: 当前节点信息, data: 从数据库中获取到的内容 --> <span class="custom-tree-node" slot-scope="{ node, data }"> <span>{{ node.label }}</span> <span> <!-- 只有一级分类和二级分类才能追加子分类: v-if="node.level <= 2" 只有没有子分类的节点才能删除当前分类: v-if="node.childNodes.length == 0" --> <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> export default { data() { return { menus: [], // 展开删除分类的父id expandedKey: [], defaultProps: { // 子节点的属性 children: 'children', // 要显示的内容 label: 'name' } }; }, created(){ this.getMenus() }, methods: { handleNodeClick(data) { console.log(data); }, getMenus(){ this.$http({ url: this.$http.adornUrl('/product/category/list/tree'), method: 'get' }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据 console.log("成功获取到菜单数据: ", data.data) this.menus = data.data }) }, append(data) { console.log(data) }, remove(node, data) { // 获取选中的分类id列表 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] }); }); } } }; </script> <style> </style>
3. 分类添加
1. 后端代码
-
分类添加的后端代码在脚手架工具中就生成了, 这里可以不用编写
@PostMapping("/save") public R save(@RequestBody CategoryEntity category){ categoryService.save(category); return R.ok(); }
2. 前端代码
-
样式
<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="dialogVisible = false">确 定</el-button> </span> </el-dialog> ... data() { return { ... // 弹出框是否开启 dialogVisible: false, category: { name: "", } }; },
-
实现
// 添加三级分类 addCategory(){ this.$http({ url: this.$http.adornUrl('/product/category/save'), method: 'post', data: this.$http.adornData(this.category, false) }).then(({ data }) => { this.$message({ type: 'success', message: '保存成功' }); // 关闭对话框 this.dialogVisible = false // 刷新出新的菜单 this.getMenus() // 设置默认展开的菜单 this.expandedKey = [this.category.parentCid] }); }
4. 分类修改
1. 后端代码
-
分类修改的后端代码在脚手架工具中就生成了, 这里可以不用编写
@PostMapping("/update") // @RequiresPermissions("product:category:update") public R update(@RequestBody CategoryEntity category){ categoryService.updateById(category); return R.ok(); }
2. 前端代码
-
完整代码
<!-- title: 标题 visible: 是否显示 Dialog,支持 .sync 修饰符 .sync 修饰符,同时监听 Dialog 的 open 和 close 事件 :close-on-click-modal: 是否可以通过点击 modal 关闭 Dialog(默认未true) --> <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> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="submitData">确 定</el-button> </span> </el-dialog> <script> export default { data() { return { menus: [], // 展开删除分类的父id expandedKey: [], defaultProps: { // 子节点的属性 children: 'children', // 要显示的内容 label: 'name' }, // 弹出框是否开启 dialogVisible: false, // 添加弹出框中的属性 category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, // 菜单id, 用于分类的修改 catId: null, // 图标 icon: "", // 计量单位 productUnit: "" }, // 用于区分是修改还是添加 dialogType: "", title: "" }; }, created(){ this.getMenus() }, methods: { handleNodeClick(data) { console.log(data); }, // 获取属性分类列表 getMenus(){ this.$http({ url: this.$http.adornUrl('/product/category/list/tree'), method: 'get' }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据 console.log("成功获取到菜单数据: ", data.data) this.menus = data.data }) }, // 获取要添加的分类 append(data) { // 声明为添加 this.dialogType = "add" this.title = "add" 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 console.log(data) }, // 删除分类 remove(node, data) { // 获取选中的分类id列表 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] }); }); }, // 添加三级分类 addCategory(){ this.$http({ url: this.$http.adornUrl('/product/category/save'), method: 'post', data: this.$http.adornData(this.category, false) }).then(({ data }) => { this.$message({ type: 'success', message: '保存成功' }); // 关闭对话框 this.dialogVisible = false // 刷新出新的菜单 this.getMenus() // 设置默认展开的菜单 this.expandedKey = [this.category.parentCid] }); //console.log("添加三级分类: ", this.category) }, // 修改时数据回显 edit(data){ // 声明为修改 this.dialogType = "edit" console.log("要修改的数据:", data) // 显示弹出框 this.dialogVisible = true, this.title = "edit" // 数据回显, 不能直接回显上一次刷新的内容 // 因为如果有其他用户修改过该数据, 则会回显错误 // this.category.name = data.name // this.category.catId = data.catId // 正确的方法: 发送请求获取当前节点最新的数据 this.$http({ url: this.$http.adornUrl(`/product/category/info/${data.catId}`), method: 'get' }).then(({data}) => { // 这里的data时从后台获取到的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 }) }, // 修改三级分类数据 editCategory(){ // 获取category中需要的数据 // 这里不能直接传入category, 因为前面数据回显的时候只回显了这几个数据 // 如果直接传category, 则未回显的属性会赋值未默认值, 从而出错 // 也可以在前面回显的时候回显所有的属性, 然后治理就可以直接使用category var {catId, name, icon, productUnit} = this.category var data = {catId, name, icon, productUnit} this.$http({ url: this.$http.adornUrl('/product/category/update'), method: 'post', data: this.$http.adornData(data, false) }).then(({ data }) => { this.$message({ type: 'success', message: '修改成功' }); // 关闭对话框 this.dialogVisible = false // 刷新出新的菜单 this.getMenus() // 设置默认展开的菜单 this.expandedKey = [this.category.parentCid] }); }, // 提交, 通过判断dialogType的值来确定是调用添加还是修改 submitData(){ if(this.dialogType == "edit") { this.editCategory() } else if(this.dialogType == "add") { this.addCategory() } } } }; </script>
5. 实现拉拽
1. 后端代码
在后端创建批量修改节点的方法
1. controller层
-
代码
/** * 拖拽时批量修改排序 * @param category * @return */ @PostMapping("/update/sort") public R updateSort(@RequestBody CategoryEntity[] category){ categoryService.updateBatchById(Arrays.asList(category)); return R.ok(); }
2. 前端代码
-
实现样式:
在<el-tree>标签中添加属性
draggable
-
发现问题: 通过拖拽会出现层级大于3的情况
-
解决
data() { return { ... // 分类的最大深度 maxLevel: 0 }; }, /** * 判断是否可以拖拽到指定位置 * draggingNode: 被拖拽的节点 * dropNode: 目标节点 * type: 当前节点被拖到目标节点的1什么位置 */ allowDrop(draggingNode, dropNode, type) { // 1. 被拖动的当前节点以及所在的父节点总层数不能大于3 // 1) 被拖动的当前节点总层数 console.log(draggingNode, dropNode, type) this.countNodeLevel(draggingNode.data) // 2) 当前正在拖动的节点 + 父节点所在的深度不大于4即可 // 当前深度 = 子节点的maxLevel - 当前拖动的节点的level + 1 let deep = this.maxLevel - draggingNode.data.catLevel + 1 console.log("深度: ", deep) 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]) } } }
-
拖拽成功后, 需要修改节点的值, 需要使用监听事件
添加拖拽成功执行的事件
data() { return { ... // 要修改的节点集合 updateNodes: [] }; }, // 拖拽成功后执行 handleDrop(draggingNode, dropNode, dropType, ev) { console.log('handleDrop: ', draggingNode, dropNode, dropType); let pCid = 0 let siblings = null // 1. 需要获取节点的最新父节点 // 如果是以"before" 或 "after"方式拖拽, 则当前节点的父id为dropNode // 如果是以"inner"方式拖拽, 则当前节点的父节点id为dropNode的catId // 2. 当前拖拽节点的最新顺序 // 找到当前节点的父节点, 然后将其子节点的顺序从0开始排序 // 如果是以"before" 或 "after"方式拖拽, 则当前节点的兄弟节点是dropNode父节点的子节点列表 // 如果是以"inner"方式拖拽, 则当前节点的兄弟节点是dropNode节点的子节点列表 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 } // 3. 当前拖拽节点的最新层级 // 遍历所有子节点, 然后排序 for(let i = 0; i < siblings.length; i++) { // 获取要改的节点id以及sort要改的值 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]) } // 如果遍历的是当前正在拖拽的节点, 还需要更改当前节点的父id 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}) } } // console.log("updateNodes", this.updateNodes) // console.log("updateNodes", this.updateNodes) // 3. 当前拖拽节点的最新层级 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 = [] thihs.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]) } } }
-
优化1: 为拖拽功能添加一个是否开启的按钮, 防止误操作
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"> </el-switch>
-
优化2: 现在每拖拽一次就会和数据库进行交互, 实现拖拽完成后统一写入数据库
- 添加保存按钮
<el-button v-if="draggable" @click="batchSave" type="primary">批量保存</el-button>
- 将
handleDrop
方法中的post提交部分的代码放到batchSave
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 = [pCid] // 清空回默认值 this.updateNodes = [] this.maxLevel = 0 }); }
- 由于pCid是在
handleDrop
方法中定义的, 所以需要在data中定义, 且需要在handleDrop
中修改后重新赋值
-
优化3: 之前在判断是否可以拖拽时, 是从数据库中读取的层级, 但是现在是批量操作, 所以应该改为当前树的实际层级; 而且之前获取层级时会出现负数的情况, 所以我们改为使用绝对值
// 拖拽成功后执行 handleDrop(draggingNode, dropNode, dropType, ev) { console.log('handleDrop: ', draggingNode, dropNode, dropType); let pCid = 0 let siblings = null // 1. 需要获取节点的最新父节点 // 如果是以"before" 或 "after"方式拖拽, 则当前节点的父id为dropNode // 如果是以"inner"方式拖拽, 则当前节点的父节点id为dropNode的catId // 2. 当前拖拽节点的最新顺序 // 找到当前节点的父节点, 然后将其子节点的顺序从0开始排序 // 如果是以"before" 或 "after"方式拖拽, 则当前节点的兄弟节点是dropNode父节点的子节点列表 // 如果是以"inner"方式拖拽, 则当前节点的兄弟节点是dropNode节点的子节点列表 if(dropType == "before" || dropType == "after") { pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId siblings = dropNode.parent.childNodes } else { pCid = dropNode.data.catId // 要遍历的节点 siblings = dropNode.childNodes } this.pCid = pCid // 遍历所有子节点, 然后排序 for(let i = 0; i < siblings.length; i++) { // 获取要改的节点id以及sort要改的值 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]) } // 如果遍历的是当前正在拖拽的节点, 还需要更改当前节点的父id 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}) } } }, // 修改子节点的层级 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]) } } },
8.
-
优化4: 批量修改时会有多个父节点, 所以需要将data的的pCid改为数组形式, 然后将要展开的所有节点都放到数组中
-
JavaScript部分的完整代码
export default { data() { return { pCid: [], draggable: false, menus: [], expandedKey: [], defaultProps: { children: 'children', label: 'name'}, dialogVisible: false, category: {name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, catId: null, icon: "", productUnit: ""}, dialogType: "", title: "", maxLevel: 0, updateNodes: [] }; }, created(){ this.getMenus() }, methods: { handleNodeClick(data) { }, getMenus(){ this.$http({ url: this.$http.adornUrl('/product/category/list/tree'), method: 'get' }).then(({data}) => { // {data}, 使用解构的方式获取对象中的data数据 this.menus = data.data }) }, append(data) { this.dialogType = "add" this.title = "add" 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 }, 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({ type: 'success', message: '分类删除成功' }); this.getMenus() this.expandedKey = [node.parent.data.catId] }); }); }, addCategory(){ this.$http({ url: this.$http.adornUrl('/product/category/save'), method: 'post', data: this.$http.adornData(this.category, false) }).then(({ data }) => { this.$message({ type: 'success', message: '保存成功' }); this.dialogVisible = false this.getMenus() this.expandedKey = [this.category.parentCid] }); }, edit(data){ this.dialogType = "edit" this.dialogVisible = true, this.title = "edit" this.$http({ url: this.$http.adornUrl(`/product/category/info/${data.catId}`), method: 'get' }).then(({data}) => { // 这里的data时从后台获取到的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 }) }, editCategory(){ var {catId, name, icon, productUnit} = this.category var data = {catId, name, icon, productUnit} this.$http({ url: this.$http.adornUrl('/product/category/update'), method: 'post', data: this.$http.adornData(data, false) }).then(({ data }) => { this.$message({ type: 'success', message: '修改成功' }); this.dialogVisible = false this.getMenus() this.expandedKey = [this.category.parentCid] }); }, submitData(){ if(this.dialogType == "edit") { this.editCategory() } else if(this.dialogType == "add") { this.addCategory() } }, allowDrop(draggingNode, dropNode, type) { this.countNodeLevel(draggingNode) let deep = this.maxLevel - draggingNode.data.catLevel + 1 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) { let pCid = 0 let siblings = null if(dropType == "before" || dropType == "after") { pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId siblings = dropNode.parent.childNodes } else { pCid = dropNode.data.catId siblings = dropNode.childNodes } this.pCid.push(pCid) 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}) } } }, 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 }); } } };
6. 分类批量删除
1. 后端代码
-
分类修改的后端代码在脚手架工具中就生成了, 这里可以不用编写
/** * 删除 * @RequestBody: 获取请求体, 必须发送post请求 * springMVC自动将请求体的数据(json), 转成对应的对象 */ @RequestMapping("/delete") public R delete(@RequestBody Long[] catIds){ // 这个方法太简单了, 不能满足业务需求, 需要重新写一个方法 // categoryService.removeByIds(Arrays.asList(catIds)); // 只实现了简单的删除, 还需检查当前删除的表单, 是否被别的地方引用 categoryService.removeMenuByIds(Arrays.asList(catIds)); return R.ok(); }
2. 前端代码
-
vue代码
<el-button @click="batchDelete" type="danger">批量删除</el-button> <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick" node-key="catId" :default-expanded-keys="expandedKey" show-checkbox :expand-on-click-node="false" :allow-drop="allowDrop" @node-drop="handleDrop" :draggable = "draggable" ref="menuTree"> ... </el-tree> </el-button>
-
JavaScript代码
// 批量删除 batchDelete(){ let catIds = [] // 被选中的元素 let checkedNodes = this.$refs.menuTree.getCheckedNodes(); console.log(checkedNodes) // 遍历所有被选中的元素, 获取catId 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() }); }) }