接 商品服务的三级分类——上
七、完成逻辑删除后端接口
1、使用mybatis-plus自带的逻辑删除功能
application.xml 配置逻辑删除规则:
1 2 3 4 5 6 7 8 9 | mybatis-plus: # 需要在启动类上增加@MapperScan("com.jiguiquan.zidanmall.product.dao") # 还有一部分有Mapper对应的xml文件,用于编写我们自定义的SQL语句 mapper-locations: classpath:/mapper/**/*.xml global-config: db-config: id-type: auto #主键自增 logic-delete-value: 1 logic-not-delete-value: 0 |
2、在实体类的制定字段上增加逻辑删除注解
1 2 | @TableLogic (value = "1" , delval = "0" ) //1显示、0删除 //故意和第一步矛盾的,有了这一步,第一步也可以省略 |
3、service层核心API:
1 2 3 4 5 | public void removeMenusByIds(List<Long> ids) { //TODO //逻辑删除 baseMapper.deleteBatchIds(ids); } |
4、开启debug级别日志,方便我们查看 Dao 层实际执行的SQL语句
1 2 3 | logging: level: com.jiguiquan.zidanmall: debug |
5、再启动测试
调用:POST localhost:10000/product/category/delete [1432]
可以看到,Dao曾实际执行的是Update方法,即为逻辑删除:
八、完成前端删除逻辑
1、修改vue文件中的remove方法:向后台发送http请求:
1 2 3 4 5 6 7 8 9 10 11 | remove(node, data) { console.log( "remove" , node); var ids = [data.catId] this .$http({ url: this .$http.adornUrl( '/product/category/delete' ), method: 'post' , data: this .$http.adornData(ids, false ) }).then(({ data }) => { console.log( "删除成功" ) this .getMenus(); }); |
2、为删除增加“确认删除”确认框:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | remove(node, data) { console.log( "remove" , 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 }) => { console.log( "删除成功" ); this .getMenus(); }); }). catch (() => {}); //确认删除结束 } |
3、我们还可以在删除成功后,给一个“删除成功”的提示消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | remove(node, data) { console.log( "remove" , 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({ message: "分类删除成功" , type: "success" }); this .getMenus(); }); }) . catch (() => {}); //确认删除结束 } |
4、在删除成功后,界面定位到操作的位置,即将操作节点的父节点再次展开
这里我们将会用到树节点的这个属性: default-expanded-keys
1 2 3 4 5 6 | //动态绑定属性 :default-expanded-keys = "expandedKey" //在data中定义expandedKey的值,默认为空 expandedKey: [], //成功刷新后,设置需要被展开的树节点,当前节点的父节点 this.expandedKey = [node.parent.data.catId]; |
5、最后的完善效果如下:
九、快速完成新增效果
1、使用element-ui对话框组件“Dialog 对话框”,使用效果如下:
2、表单我们使用带嵌套内容的对话框
3、核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //在data中增加一个category对象,用来存放新增数据 data() { return { category: { name: "" , parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 }, menus: [], dialogVisible: false , expandedKey: [], defaultProps: { children: "children" , label: "name" } }; //两个相关方法 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] }); }, |
4、最后的效果如下:
十、实现基本修改效果
1、修改还是基于与新增相同的模态框 el-dialog
只需要有标识位区分新增和修改:
重要方法:
2、后端更新接口:
/product/category/update
只更新部位null的字段;
3、设置el-dialog模态框点击周围不关闭:
:close-on-click-modal="false"
十一、拖拽树节点实现类别更新
这部分内容,需要前端考虑得点非常多,每一个操作可能引起的哪些节点的哪些数据会发生改变,需要考虑得非常周到!
1、通过树节点的 draggable 属性来使得树支持拖拽功能;
2、当实现了支持拖拽功能后,节点就可以被拖拽了
拖拽有两种情况,
一种是放置在某几点前后,作为兄弟节点;type为prev或者next
一种是防止在节点上,作为它的子节点;type为inner
3、allow-drag 属性可以用来判断词节点是否允许被拖拽至此,是一个函数Function;
4、整个拖拽功能要考虑得点非常多:
4.1、首先要考虑什么样的情况可以拖拽,什么样的情况不可以拖拽:
——当前被拖拽的节点的深度为m,目标位置的节点层级为n,如果是拖拽在前后为兄弟节点,那么最终总层数为 m + (n – 1),此时(n-1)相当于就是最终父节点的层级;
——当前被拖拽的节点的深度为m,目标位置的节点层级为n,如果是拖拽在目标节点内部,那么最终总层数为 m + n,此时n相当于是最终父节点的层级,与上一条就呼应上了;
无论如何,最终的结果都不可以大于3,即 result <= 3,才可以拖拽,否则拖拽不成功;
拖拽完成后会触发 node-drop事件,事件绑定方法会被传入4个参数,如下图:
4.2、当每一次拖拽成功后,会有哪些节点的哪些信息会被影响到:
——当前节点最新的父id=》pcid
——新的父节点的所有子节点(即最终的坐在位置的所有兄弟姐妹节点 siblings 的顺序sort)
——当前被拖拽节点的所有子节点的所在层级level会发生改变;
总结:
自己的pcid、sort,level都会发生改变;
目标位置的兄弟节点的sort会发生改变;
子节点的所在层级level会发生改变;
5、当将所有需要修改的节点全部统计出来后,使用数组,一次性提交给后台执行批量更新操作;
后端接口:
1 2 3 4 5 6 | @RequestMapping ( "/update/sort" ) //@RequiresPermissions("product:category:update") public R updateSort( @RequestBody CategoryEntity[] categorys){ categoryService.updateBatchById(Arrays.asList(categorys)); return R.ok(); } |
注意:
最后我们同样需要刷新菜单,展开到最总位置的父节点;
一定要记得将需要更新的数组 updateNodes清空,不然下一次就没法玩了;
以及maxLevel置为零;
6、为了防止误操作,我们可以将开启拖拽做成开关,批量提交按钮做成“批量保存”按钮,
这样,我们只有打开“开启拖拽”开关,才会支持拖拽,不然 draggable 一直为 false;
拖拽完成后,收集好数据,只有点击“批量保存”按钮,才会批量提交后台处理;
最终效果如下:
十二、批量删除功能的实现
1、批量删除的时候,我们肯定需要知道Tree上面所有被选择的节点数组:
可以使用 getCheckedNodes 方法,我们获取到当前组件使用:this.$refs.menuTree,
其中this.$refs代表所有组件(DOM元素),menuTree是我们为Tree设置的ref,可以理解为组件的唯一标识符;
代码如下:
2、操作效果如下:
十三、总结:
到这里,整个三级分类树的开发就全部完成了;更多细节,在实际工作中当然还会有更多的细节需要处理;
完整的 category.vue 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | <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> <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)" >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= "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> </div> </template> <script> //这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等) //例如:import 《组件名称》 from '《组件路径》'; export default { //import引入的组件需要注入到对象中才能使用 components: {}, props: {}, data() { return { pCid: [], draggable: false , updateNodes: [], maxLevel: 0, title: "" , dialogType: "" , //edit,add category: { name: "" , parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, productUnit: "" , icon: "" , catId: null }, dialogVisible: false , menus: [], expandedKey: [], defaultProps: { 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; }); }, batchDelete() { let catIds = []; let catNames = []; let checkedNodes = this .$refs.menuTree.getCheckedNodes(); console.log( "被选中的元素" , checkedNodes); for (let i = 0; i < checkedNodes.length; i++) { catIds.push(checkedNodes[i].catId); catNames.push(checkedNodes[i].name); } this .$confirm(`是否批量删除【${catNames}】菜单?`, "提示" , { 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 (() => {}); }, 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 .expandedKey = this .pCid; this .updateNodes = []; this .maxLevel = 0; // this.pCid = 0; }); }, 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; } this .pCid.push(pCid); //2、当前拖拽节点的最新顺序, for (let i = 0; i < siblings.length; i++) { if (siblings[i].data.catId == draggingNode.data.catId) { //如果遍历的是当前正在拖拽的节点 let catLevel = draggingNode.level; if (siblings[i].level != draggingNode.level) { //当前节点的层级发生变化 catLevel = siblings[i].level; //修改他子节点的层级 this .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]); } } }, allowDrop(draggingNode, dropNode, type) { //1、被拖动的当前节点以及所在的父节点总层数不能大于3 //1)、被拖动的当前节点总层数 console.log( "allowDrop:" , draggingNode, dropNode, type); // this .countNodeLevel(draggingNode); //当前正在拖动的节点+父节点所在的深度不大于3即可 let deep = Math.abs( this .maxLevel - draggingNode.level) + 1; console.log( "maxLevel: " , this .maxLevel); console.log( "draggingNode.level: " ,draggingNode.level); console.log( "深度:" , deep); // this.maxLevel if (type == "inner" ) { // console.log( // `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}` // ); return deep + dropNode.level <= 3; } else { return deep + dropNode.parent.level <= 3; } }, countNodeLevel(node) { //找到所有子节点,求出最大深度 if (node.childNodes != null && node.childNodes.length > 0) { for (let i = 0; i < node.childNodes.length; i++) { if (node.childNodes[i].level > this .maxLevel) { this .maxLevel = node.childNodes[i].level; } this .countNodeLevel(node.childNodes[i]); } } else { this .maxLevel = node.level; } }, edit(data) { console.log( "要修改的数据" , data); this .dialogType = "edit" ; this .title = "修改分类" ; this .dialogVisible = true ; //发送请求获取当前节点最新的数据 this .$http({ url: this .$http.adornUrl(`/product/category/info/${data.catId}`), method: "get" }).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; /** * parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, */ }); }, append(data) { console.log( "append" , data); this .dialogType = "add" ; this .title = "添加分类" ; 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; }, submitData() { if ( this .dialogType == "add" ) { this .addCategory(); } if ( this .dialogType == "edit" ) { this .editCategory(); } }, //修改三级分类数据 editCategory() { var { catId, name, icon, productUnit } = this .category; this .$http({ url: this .$http.adornUrl( "/product/category/update" ), method: "post" , data: this .$http.adornData({ catId, name, icon, productUnit }, false ) }).then(({ data }) => { this .$message({ message: "菜单修改成功" , type: "success" }); //关闭对话框 this .dialogVisible = false ; //刷新出新的菜单 this .getMenus(); //设置需要默认展开的菜单 this .expandedKey = [ this .category.parentCid]; }); }, //添加三级分类 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( "remove" , node, data); } }, //生命周期 - 创建完成(可以访问当前this实例) created() { this .getMenus(); }, //生命周期 - 挂载完成(可以访问DOM元素) mounted() {}, beforeCreate() {}, //生命周期 - 创建之前 beforeMount() {}, //生命周期 - 挂载之前 beforeUpdate() {}, //生命周期 - 更新之前 updated() {}, //生命周期 - 更新之后 beforeDestroy() {}, //生命周期 - 销毁之前 destroyed() {}, //生命周期 - 销毁完成 activated() {} //如果页面有keep-alive缓存功能,这个函数会触发 }; </script> <style scoped> </style> |