前言

在我们做项目时,可能会遇到许多树形展示的数据,在编辑时要求选择当前数据所在的上级,这时就要用到级联,如图:
在这里插入图片描述
如何实现这种功能?在这里分三部份来说明,数据及实体类,前端展示,后台数据查询。

一、创建相关实体类及数据表

1、创建商品分类表:

CREATE TABLE `mall_pms`.`pms_category`  (
  `cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id',
  `name` char(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '分类名称',
  `parent_cid` bigint(20) NULL DEFAULT NULL COMMENT '父分类id',
  `cat_level` int(11) NULL DEFAULT NULL COMMENT '层级',
  `show_status` tinyint(4) NULL DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',
  `sort` int(11) NULL DEFAULT NULL COMMENT '排序',
  `icon` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图标地址',
  `product_unit` char(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '计量单位',
  `product_count` int(11) NULL DEFAULT NULL COMMENT '商品数量',
  PRIMARY KEY (`cat_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1405048572491190275 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '商品三级分类' ROW_FORMAT = Dynamic;

2、创建属性分组表

CREATE TABLE `mall_pms`.`pms_attr_group`  (
  `attr_group_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分组id',
  `attr_group_name` char(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组名',
  `sort` int(11) NULL DEFAULT NULL COMMENT '排序',
  `descript` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组图标',
  `catelog_id` bigint(20) NULL DEFAULT NULL COMMENT '所属分类id',
  PRIMARY KEY (`attr_group_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '属性分组' ROW_FORMAT = Dynamic;

3、二者关系
一个分类对应多个属性分组,其中pms_attr_group表中的catelog_id对应pms_category表中的cat_id
在这里插入图片描述

4、对应实体类

CategoryEntity 中的children用来加载子分类,这空或null时不显示

@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 分类id
	 */
	@TableId
	private 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(JsonInclude.Include.NON_EMPTY) //为空或null时不出现此字段
	@TableField(exist = false)
	private List<CategoryEntity> children;

}

AttrGroupEntity类中的catelogPath用来加载当前caetlog_id的所在所有父类的cat_id,主要用于数据的回显

@Data
@TableName("pms_attr_group")
public class AttrGroupEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 分组id
	 */
	@TableId
	private Long attrGroupId;
	/**
	 * 组名
	 */
	private String attrGroupName;
	/**
	 * 排序
	 */
	private Integer sort;
	/**
	 * 描述
	 */
	private String descript;
	/**
	 * 组图标
	 */
	private String icon;
	/**
	 * 所属分类id
	 */
	private Long catelogId;

	/**
	 * 当前catelogId所在的完整路径集合
	 */
	@TableField(exist = false)
	private Long[] catelogPath;
}

二、前端使用基于Vue的ElementUI实现

1、前端页面结构及效果图
在这里插入图片描述
attrgroup.vue页面
在这里插入图片描述
新增页面
在这里插入图片描述
2、在attrgroup.vue引入组件attrgroup-add-or-update.vue
引入

import AddOrUpdate from './attrgroup-add-or-update';

定义组件

export default {
  //import引入的组件需要注入到对象中才能使用
  components: { 
    AddOrUpdate
  },

在template中使用组件,其中:

  • addOrUpdateVisible控制组件是否显示
  • addOrUpdate组件名称
  • @refreshDataList="getDataList"组件保存后用来重新加载刷新表格
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>

3、新增与修改实现
当点新增时,弹出如上图的对话框
新增与修改代码:

<el-button type="primary" @click="addOrUpdateHandle()">新增</el-button>

//修改时要传入scope.row.attrGroupId
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button>
 // 新增 / 修改
 addOrUpdateHandle(id) {
   this.addOrUpdateVisible = true;
   this.$nextTick(() => {
     this.$refs.addOrUpdate.init(id);
   });
 },

4、attrgroup-add-or-update.vue实现
引入级联组件:

<el-cascader v-model="dataForm.catelogPath" :options="categorys" :props="props"></el-cascader>

props表示组件加载映射哪些数据

 props: {
        value: 'catId',
        label: 'name',
        children: 'children'
      },

v-model=“dataForm.catelogPath” 用于双向绑定级联数据,catelogPath存放回显的数据
:options="categorys"动态绑定全部数据

完整代码

<template>
  <el-dialog :title="!dataForm.attrGroupId ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible">
    <el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
      <el-form-item label="组名" prop="attrGroupName"><el-input v-model="dataForm.attrGroupName" placeholder="组名"></el-input></el-form-item>
      <el-form-item label="排序" prop="sort"><el-input v-model="dataForm.sort" placeholder="排序"></el-input></el-form-item>
      <el-form-item label="描述" prop="descript"><el-input v-model="dataForm.descript" placeholder="描述"></el-input></el-form-item>
      <el-form-item label="组图标" prop="icon"><el-input v-model="dataForm.icon" placeholder="组图标"></el-input></el-form-item>
      <el-form-item label="所属分类id" prop="catelogId">
        <!-- <el-input v-model="dataForm.catelogId" placeholder="所属分类id"></el-input> -->
        <el-cascader v-model="dataForm.catelogPath" :options="categorys" :props="props"></el-cascader>
      </el-form-item>
    </el-form>
    <span slot="footer" class="dialog-footer">
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" @click="dataFormSubmit()">确定</el-button>
    </span>
  </el-dialog>
</template>

<script>
export default {
  data() {
    return {
      props: {
        value: 'catId',
        label: 'name',
        children: 'children'
      },
      categorys: [],
      visible: false,
      dataForm: {
        attrGroupId: 0,
        attrGroupName: '',
        sort: '',
        descript: '',
        icon: '',
        catelogPath: [],
        catelogId: 0
      },
      dataRule: {
        attrGroupName: [{ required: true, message: '组名不能为空', trigger: 'blur' }],
        sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
        descript: [{ required: true, message: '描述不能为空', trigger: 'blur' }],
        icon: [{ required: true, message: '组图标不能为空', trigger: 'blur' }],
        catelogId: [{ required: true, message: '所属分类id不能为空', trigger: 'blur' }]
      }
    };
  },
  created() {
    this.getCategorys();
  },
  methods: {
    getCategorys() {
      this.$http({
        url: this.$http.adornUrl('/product/category/list/tree'),
        method: 'get'
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.categorys = data.data;
        }
      });
    },
    init(id) {
      this.dataForm.attrGroupId = id || 0;
      this.visible = true;
      this.$nextTick(() => {
        this.$refs['dataForm'].resetFields();
        if (this.dataForm.attrGroupId) {
          this.$http({
            url: this.$http.adornUrl(`/product/attrgroup/info/${this.dataForm.attrGroupId}`),
            method: 'get',
            params: this.$http.adornParams()
          }).then(({ data }) => {
            if (data && data.code === 0) {
              this.dataForm.attrGroupName = data.attrGroup.attrGroupName;
              this.dataForm.sort = data.attrGroup.sort;
              this.dataForm.descript = data.attrGroup.descript;
              this.dataForm.icon = data.attrGroup.icon;
              this.dataForm.catelogId = data.attrGroup.catelogId;
              //当前catelogId所在的路径集合
              this.dataForm.catelogPath = data.attrGroup.catelogPath;
            }
          });
        }
      });
    },
    // 表单提交
    dataFormSubmit() {
      this.$refs['dataForm'].validate(valid => {
        if (valid) {
          this.$http({
            url: this.$http.adornUrl(`/product/attrgroup/${!this.dataForm.attrGroupId ? 'save' : 'update'}`),
            method: 'post',
            data: this.$http.adornData({
              attrGroupId: this.dataForm.attrGroupId || undefined,
              attrGroupName: this.dataForm.attrGroupName,
              sort: this.dataForm.sort,
              descript: this.dataForm.descript,
              icon: this.dataForm.icon,
              //绑定最后一个
              catelogId: this.dataForm.catelogPath[this.dataForm.catelogPath.length - 1]
            })
          }).then(({ data }) => {
            if (data && data.code === 0) {
              this.$message({
                message: '操作成功',
                type: 'success',
                duration: 1500,
                onClose: () => {
                  this.visible = false;
                  this.$emit('refreshDataList');
                }
              });
            } else {
              this.$message.error(data.msg);
            }
          });
        }
      });
    }
  }
};
</script>
<style>
</style>

三、后台使用SpringBoot与MyBatisPlus实现

1、加载树形数据
控制层

 @RequestMapping("/list/tree")
    //@RequiresPermissions("member:pmscategory:list")
    public R listWithTree(){
        List<CategoryEntity> entities = categoryService.listWithTree();
        return R.ok().put("data", entities);
    }

实现层

  • 首先查询出所有的数据
  • 组装,找到一级菜单,然后给一级菜单的子菜单采用递归方法添加数据
  • 最后以list方式返回
   /**
     * 㞡现树形结构
     * @return
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        //1.查询所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2.组装
        //2.1) 所到所有的一级类别 ,parentId = 0
        List<CategoryEntity> levelOneMenus = entities.stream()
                .filter(item ->  item.getParentCid() == 0)
                .map(item -> {
                    item.setChildren(this.getChildrens(item, entities));
                    return item;
                })
                .sorted((item1, item2) -> {
                    return (item1.getSort() == null ? 0 : item1.getSort())  - (item2.getSort() == null ? 0 : item2.getSort());
                })
                .collect(Collectors.toList());
        return levelOneMenus;
    }

    /**
     * 递归查找菜单所有的子菜单
     * @param root
     * @param all
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all){
        List<CategoryEntity> treeMenus = all.stream()
                //如果菜单中的父菜单Id == 当前菜单的id,则说明是子菜单
                .filter(item ->  Objects.equals(item.getParentCid(), root.getCatId()))
                .map(item -> {
                    //递归添加子菜单
                    List<CategoryEntity> childrens = getChildrens(item, all);
                    item.setChildren(childrens);
                    return item;
                })
                //排序
                .sorted((item1, item2) -> {
                    return (item1.getSort() == null ? 0 : item1.getSort())  - (item2.getSort() == null ? 0 : item2.getSort());
                })
                .collect(Collectors.toList());
        return treeMenus;
    }

2、编辑回显级联数据
控制层

  @RequestMapping("/info/{attrGroupId}")
    //@RequiresPermissions("product:attrgroup:info")
    public R info(@PathVariable("attrGroupId") Long attrGroupId){
		AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
        //查出categoryId 所在路径
        if(attrGroup != null && attrGroup.getCatelogId() != null){
           Long[] categoryPath = categoryService.findCategoryPath(attrGroup.getCatelogId());
           attrGroup.setCatelogPath(categoryPath);
        }
        return R.ok().put("attrGroup", attrGroup);
    }

实现层,采用递归方法查找当前分类的所有上级分类Id,拼装到数组中

 @Override
    public Long[] findCategoryPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPaths = this.findParentPath(catelogId, paths);
        Collections.reverse(parentPaths);
        return parentPaths.toArray(new Long[parentPaths.size()]);
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths){
        paths.add(catelogId);
        CategoryEntity entity = this.getById(catelogId);
        if(entity.getParentCid() != null && entity.getParentCid() != 0){
            this.findParentPath(entity.getParentCid(), paths);
        }
        return paths;
    }
Logo

前往低代码交流专区

更多推荐