吕梁seo网站建设网站关键词优化案例
概述:
本三级联动分类服务端使用的是: Springboot + MyBatis-plus,前端使用的是:Vue+ElementUI,树形控件使用的是el-tree。本三级联动分类可以把任一子项拖拽到其它目录,可以添加、编辑、删除分类。
效果图:
实现流程:
一、树形数据展示实现
1、创建表
- cat_id使用bigint类型目的是为了使用雪花算法,加快查询的速度;
- parent_cid父ID,用于子节点指向父亲节点,同理我们可根据当前节点等于子节点点父ID,然后递归,找到所属的所有子节点
- cat_level层级,用于识别当前节点是一级、二级还是三级节点
- show_status是否显示,控制是否显示,使用了逻辑删除功能
- product_unit计量单位,使用什么单位统计当前分类下产品的数量
- product_count产品数量,在添加商品时进行统计更新
- sort排序,进行分类排序
- icon图表,类别的图片,使用iconfont
CREATE TABLE `kmall_product`.`pro_category` (`cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id',`name` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类名称',`parent_cid` bigint(20) NULL DEFAULT NULL COMMENT '父分类id',`cat_level` int(4) NULL DEFAULT NULL COMMENT '层级',`show_status` tinyint(2) NULL DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',`sort` int(4) NULL DEFAULT NULL COMMENT '排序',`icon` char(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标地址',`product_unit` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '计量单位',`product_count` int(11) NULL DEFAULT NULL COMMENT '商品数量',PRIMARY KEY (`cat_id`) USING BTREE,INDEX `parent_cid`(`parent_cid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1433 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品三级分类' ROW_FORMAT = Dynamic;
2、生成实体类
把上面点表内能映射为实体类,便于java中操作
@Data
@TableName("pro_category")
public class CategoryEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 分类id*/@TableIdprivate Long catId;/*** 分类名称*/private String name;/*** 父分类id*/private Long parentCid;/*** 层级*/private Integer catLevel;/*** 是否显示[0-不显示,1显示]*/@TableLogic(value = "1", delval = "0")private Integer showStatus;/*** 排序*/private Integer sort;/*** 图标地址*/private String icon;/*** 计量单位*/private String productUnit;/*** 商品数量*/private Integer productCount;@JsonInclude(value = JsonInclude.Include.NON_EMPTY)@TableField(exist = false)private List<CategoryEntity> children;}
说明:下面的children是用来保存子节点数据,是暂存在内存中,所以要加上@TableField(exist = false)注解
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)@TableField(exist = false)private List<CategoryEntity> children;
3、获取树形数据
创建CategoryController控制层,添加请求方法,使用的是get请求,查询到结果后以data为名称返回给前端
/*** 查出所有分类以及子分类,以树形结构组装起来*/@RequestMapping("/list/tree")public R listWithTree(){List<CategoryEntity> entities = categoryService.listWithTree();return R.ok().put("data", entities);}
4、查询封装树形数据
创建CategoryServiceImpl实现层,实现listWithTree方法
- 首先查出所有分类
- 选出一级分类
- 递归获取一级分类的所有子节点,设置到children属性中
@Overridepublic List<CategoryEntity> listWithTree() {//查出所有分类List<CategoryEntity> entities = baseMapper.selectList(null);//组装成父子树形结构//1级分类List<CategoryEntity> menus = entities.stream().filter(entity -> entity.getParentCid() == 0).map(entity -> {entity.setChildren(getChildren(entity, entities));return entity;}).sorted((menu1, menu2) ->(menu1.getSort() == null ? 0 : menu1.getSort()) -(menu2.getSort() == null ? 0 : menu2.getSort())).collect(Collectors.toList());return menus;}
/*** 递归查找所有菜单的子菜单*/private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {List<CategoryEntity> children = all.stream().filter(entity -> entity.getParentCid().equals(root.getCatId())).map(entity -> {entity.setChildren(getChildren(entity, all)); //递归设置return entity;}).sorted((menu1, menu2) ->(menu1.getSort() == null ? 0 : menu1.getSort()) -(menu2.getSort() == null ? 0 : menu2.getSort())).collect(Collectors.toList());return children;}
5、前端展现实现
5.1、树形控件el-tree设置
- :data=“menus”:绑定数据menus
- :props=“defaultProps” :设置默认属性defaultProps,便于与后端数据转换
- expand-on-click-node:设置为false,表示需要单击才展开
- show-checkbox:显示checkbox复选框
- node-key=“catId” :绑定主键
- :default-expanded-keys=“expandedKey” :设置默认展开分类
- :draggable=“draggable” :是否能拖拽节点数据
- :allow-drop=“allowDrop”:拖拽时判定目标节点能否被放置。type 参数有三种情况:‘prev’、‘inner’ 和 ‘next’,分别表示放置在目标节点前、插入至目标节点和放置在目标节点后
- @node-drop=“handleDrop”:拖拽成功完成时触发的事件
<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"> </el-tree>
5.2、树形控件el-tree数据初始化
data() {return {pCid: [],draggable: false,updateNodes: [],maxLevel: 0,title: "",dialogType: "", //edit,addcategory: {name: "",parentCid: 0,catLevel: 0,showStatus: 1,sort: 0,productUnit: "",icon: "",catId: null},dialogVisible: false,menus: [],expandedKey: [],defaultProps: {children: "children",label: "name"}};},
5.3、加载menus数据,绑定到树形控件
说明:/product/category/list/tree此路径对应的是controller里查找树形数据的方法
getMenus() {this.$http({url: this.$http.adornUrl("/product/category/list/tree"),method: "get"}).then(({data}) => {console.log("成功获取到菜单数据...", data.data);this.menus = data.data;});},
二、添加编辑删除树形控件实现
1、前端添加、编辑、删除实现
1.1、 html内容
- 只有叶子节点才显示删除功能
- 所有的分类都可以编辑
- 只有一级、二级分类有添加功能
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
<el-button size="mini" 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)">添加</el-button><el-button type="text" size="mini" @click="edit(data)">编辑</el-button><el-button v-if="node.childNodes.length==0" type="text" size="mini"@click="() => remove(node, data)">删除</el-button></span></span></el-tree>
1.2、js实现
- 添加:
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;},
//添加三级分类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];});},
submitData() {if (this.dialogType == "add") {this.addCategory();}if (this.dialogType == "edit") {this.editCategory();}},
- 批量添加
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; });},
- 编辑
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; });},
//修改三级分类数据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];});},
- 删除单个节点
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);}
- 批量删除
1)首先根据选中的节点获取选中的节点 catId
2)提示是否删除
3)如果确定删除,则调用服务层的批量删除方法
4)删除成功后提示,并重新加载菜单
batchDelete() {let catIds = [];let checkedNodes = this.$refs.menuTree.getCheckedNodes();console.log("被选中的元素", checkedNodes);for (let i = 0; i < checkedNodes.length; i++) {catIds.push(checkedNodes[i].catId);}this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {this.$http({url: this.$http.adornUrl("/product/category/delete"),method: "post",data: this.$http.adornData(catIds, false)}).then(({data}) => {this.$message({message: "菜单批量删除成功",type: "success"});this.getMenus();});}).catch(() => {});},
2、服务端添加、编辑、删除实现
- 添加
@RequestMapping("/save")public R save(@RequestBody CategoryEntity category){categoryService.save(category);return R.ok();}
- 编辑
@RequestMapping("/info/{catId}")public R info(@PathVariable("catId") Long catId){CategoryEntity category = categoryService.getById(catId);return R.ok().put("data", category);}@RequestMapping("/update")public R update(@RequestBody CategoryEntity category){categoryService.updateCascade(category);return R.ok();}
- 删除
@RequestMapping("/delete")public R delete(@RequestBody Long[] catIds){//1.检查当前删除的分类,是否被别的地方引用categoryService.removeMenuByIds(Arrays.asList(catIds));return R.ok();}@Overridepublic void removeMenuByIds(List<Long> asList) { //逻辑删除baseMapper.deleteBatchIds(asList);}
三、树形控件拖拽功能实现
1、前端实现
1.1、html部分,添加拖拽控制开关
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch><el-button v-if="draggable" @click="batchSave">批量保存</el-button><el-button size="mini" 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)">添加</el-button><el-button type="text" size="mini" @click="edit(data)">编辑</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>
1.2、js部分
- 计算可拖拽的信息,找到所有子节点,求出最大深度
1)被拖动的当前节点以及所在的父节点总层数不能大于3
2)计算当前节点所在的最大层级,一般是<= 3 ,
3)计算当前节点所在的最大深度,一般是<= 3 , 公式:let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
4)判断拖拽是在内部,还是前后,如果是拖拽到某节点的内部,必须:deep + dropNode.level <= 3;
5)否则 deep + dropNode.parent.level <= 3;
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("深度:", deep);// this.maxLevelif (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]);}}else{this.maxLevel = node.level}},
- 处理拖拽的节点,排序
handleDrop(draggingNode, dropNode, dropType, ev) {console.log("handleDrop: ", draggingNode, dropNode, dropType);//1、当前节点最新的父节点idlet 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);},
2、服务端实现
@RequestMapping("/update/sort")public R updateSort(@RequestBody List<CategoryEntity> categorys){categoryService.updateBatchById(categorys);return R.ok();}
源码下载:
https://gitee.com/charlinchenlin/koo-erp