两级分类的一种解决方案

两级分类的一种解决方案

解决方案:“数据库设计+vue的双向绑定特性” 实现

看懂以下代码前提知识:mysql、springBoot、mybatisPlus、了解vue生命周期、了解ElementUI基本使用,了解前后分离开发。

场景:现在要做一个课程的添加和修改功能,但是每一个课程都对应了课程分类(比如:Web基础课程,属于一级分类–>后端开发;属于二级分类–>java)。我们在添加课程的时候,进行选择课程分类时,应该是先选一级分类,然后才能选择二级分类,修改课程时,要求回显数据时下拉框显示该课程对应的一级分类和二级分类,效果演示见图5。

1、数据库设计:

(1)课程分类表:
在这里插入图片描述

(2)课程表
在这里插入图片描述

2、效果展示:

​ (1)添加课程时的效果:
在这里插入图片描述

在这里插入图片描述

(2)修改课程时的效果:

​ 因为是修改课程,需要进行数据回显
在这里插入图片描述

3、代码实现:

后端代码
(1)实体类Subject.java
@Data
@ApiModel(description = "Subject")
@TableName("subject")
public class Subject {

   private static final long serialVersionUID = 1L;
   @ApiModelProperty(value = "id")
   private Long id;

   @ApiModelProperty(value = "创建时间")
   @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
   @TableField("create_time")
   private Date createTime;

   @ApiModelProperty(value = "更新时间")
   @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
   @TableField("update_time")
   private Date updateTime;

   @ApiModelProperty(value = "逻辑删除(1:已删除,0:未删除)")
   @JsonIgnore
   @TableLogic
   @TableField("is_deleted")
   private Integer isDeleted;

   @ApiModelProperty(value = "其他参数")
   @TableField(exist = false)
   private Map<String,Object> param = new HashMap<>();

   @ApiModelProperty(value = "类别名称")
   @TableField("title")
   private String title;

   @ApiModelProperty(value = "父ID")
   @TableField("parent_id")
   private Long parentId;

   @ApiModelProperty(value = "排序字段")
   @TableField("sort")
   private Integer sort;

   @ApiModelProperty(value = "是否包含子节点")
   @TableField(exist = false)
   private boolean hasChildren;

}
(2)统一返回结果类Result
//统一返回结果类
@Data
public class Result<T> {
    private Integer code; //状态码
    private String message; //返回状态信息(成功 失败)
    private T data; //返回数据
    public Result() {}
    //成功的方法,有data数据
    public static<T> Result<T> ok(T data) {
        Result<T> result = new Result<>();
        if(data != null) {
            result.setData(data);
        }
        result.setCode(20000);
        result.setMessage("成功");
        return result;
    }
    //失败的方法,有data数据
    public static<T> Result<T> fail(T data) {
        Result<T> result = new Result<>();
        if(data != null) {
            result.setData(data);
        }
        result.setCode(20001);
        result.setMessage("失败");
        return result;
    }
    public Result<T> message(String msg){
        this.setMessage(msg);
        return this;
    }
    public Result<T> code(Integer code){
        this.setCode(code);
        return this;
    }
}

(3)课程分类控制层SubjectController:
@RestController
@RequestMapping(value="/admin/vod/subject")
//@CrossOrigin
public class SubjectController {

    @Autowired
    private SubjectService subjectService;

    //课程分类列表
    //懒加载,每次查询一层数据
    @ApiOperation("课程分类列表")
    @GetMapping("getChildSubject/{id}")
    public Result getChildSubject(@PathVariable Long id) {
        List<Subject> list = subjectService.selectSubjectList(id);
        return Result.ok(list);
    }
}

上述代码中selectSubjectList(id)

//课程分类列表
//懒加载,每次查询一层数据
@Override
public List<Subject> selectSubjectList(Long id) {
    //SELECT * FROM SUBJECT WHERE parent_id=0
    QueryWrapper<Subject> wrapper = new QueryWrapper<>();
    wrapper.eq("parent_id",id);
    List<Subject> subjectList = baseMapper.selectList(wrapper);
    //subjectList遍历,得到每个subject对象,判断是否有下一层数据,有hasChildren=true
    for (Subject subject:subjectList) {
        //获取subject的id值
        Long subjectId = subject.getId();
        //查询
        boolean isChild = this.isChildren(subjectId);
        //封装到对象里面
        subject.setHasChildren(isChild);
    }
    return subjectList;
}
//判断是否有下一层数据
private boolean isChildren(Long subjectId) {
    QueryWrapper<Subject> wrapper = new QueryWrapper<>();
    wrapper.eq("parent_id",subjectId);
    Integer count = baseMapper.selectCount(wrapper);
    // 1>0  true   0>0 false
    return count>0;
}
(4)课程控制类CourseController
@RestController
@RequestMapping(value="/admin/vod/course")
//@CrossOrigin
public class CourseController {

    @Autowired
    private CourseService courseService;

    //根据id获取课程信息
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
        CourseFormVo courseFormVo = courseService.getCourseInfoById(id);
        return Result.ok(courseFormVo);
    }

    
}

上述代码中getCourseInfoById(id)

//根据id查询课程信息
    @Override
    public CourseFormVo getCourseInfoById(Long id) {
        //课程基本信息
        Course course = baseMapper.selectById(id);
        if(course == null) {
            return null;
        }
        //课程描述信息
        CourseDescription courseDescription = descriptionService.getById(id);
        //封装
        CourseFormVo courseFormVo = new CourseFormVo();
        BeanUtils.copyProperties(course,courseFormVo);
        //封装描述
        if(courseDescription != null) {
            courseFormVo.setDescription(courseDescription.getDescription());
        }
        return courseFormVo;
    }

前端代码

a、引入后端api接口

​ 1)课程类别接口

import request from '@/utils/request'
const api_name = '/admin/vod/subject'
export default {
    //课程分类列表
    getChildList(id) {
        return request({
        url: `${api_name}/getChildSubject/${id}`,
        method: 'get'
        })
    }
}

​ 2)课程接口:

import request from '@/utils/request'

const api_name = '/admin/vod/course'

export default {
  //id获取课程信息
  getCourseInfoById(id) {
    return request({
      url: `${api_name}/get/${id}`,
      method: 'get'
    })
  },
}
b、页面代码
<template>
  <div class="app-container">
    <!-- 课程信息表单 -->
    <el-form label-width="120px">
      <el-form-item label="课程标题">
        <el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写"/>
      </el-form-item>
      <!-- 课程讲师 -->
      <el-form-item label="课程讲师">
        <el-select
          v-model="courseInfo.teacherId"
          placeholder="请选择">
          <el-option
            v-for="teacher in teacherList"
            :key="teacher.id"
            :label="teacher.name"
            :value="teacher.id"/>
        </el-select>
      </el-form-item>
      <!-- 所属分类:级联下拉列表 -->
      <el-form-item label="课程类别">
        <!-- 一级分类 -->
        <!-- 看仔细了,双向绑定v-model="courseInfo.subjectParentId"中的subjectParentId表示:当通过课程id查询到课程的信息(包括subjectParentId一级分类ID)后,
        就能实现根据courseInfo.subjectParentId 和 subjectList 中的所有数据比对,显示出该课程“具体”的一级菜单 -->
        <el-select
          v-model="courseInfo.subjectParentId"
          placeholder="请选择"
          @change="subjectChanged">
          <el-option
            v-for="subject in subjectList"
            :key="subject.id"
            :label="subject.title"
            :value="subject.id"/>
        </el-select>
        <!-- 二级分类 -->
        <!-- v-model="courseInfo.subjectId" 这里是二级菜单subjectId和上面一级分类subjectParentId不一样,其他道理一样 -->
        <el-select v-model="courseInfo.subjectId" placeholder="请选择">
          <el-option
            v-for="subject in subjectLevelTwoList"
            :key="subject.id"
            :label="subject.title"
            :value="subject.id"/>
        </el-select>
      </el-form-item>
      <el-form-item label="总课时">
        <el-input-number :min="0" v-model="courseInfo.lessonNum" controls-position="right" placeholder="请填写课程的总课时数"/>
      </el-form-item>
      <!-- 课程简介-->
      <el-form-item label="课程简介">
        <el-input v-model="courseInfo.description" type="textarea" rows="5"/>
      </el-form-item>
      <!-- 课程封面 -->
      <el-form-item label="课程封面">
        <el-upload
          :show-file-list="false"
          :on-success="handleCoverSuccess"
          :before-upload="beforeCoverUpload"
          :on-error="handleCoverError"
          :action="BASE_API+'/admin/vod/file/upload'"
          class="cover-uploader">
          <img v-if="courseInfo.cover" :src="courseInfo.cover">
          <i v-else class="el-icon-plus avatar-uploader-icon"/>
        </el-upload>
      </el-form-item>
      <el-form-item label="课程价格">
        <el-input-number :min="0" v-model="courseInfo.price" controls-position="right" placeholder="免费课程请设置为0元"/> 元
      </el-form-item>
    </el-form>

    <div style="text-align:center">
      <el-button :disabled="saveBtnDisabled" type="primary" @click="saveAndNext()">保存并下一步</el-button>
    </div>
  </div>
</template>
<script>
import courseApi from '@/api/vod/course'
import teacherApi from '@/api/vod/teacher'
import subjectApi from '@/api/vod/subject'

export default {
  data() {
    return {
      BASE_API: 'http://localhost:8301',
      saveBtnDisabled: false, // 按钮是否禁用
      courseInfo: {// 表单数据
        price: 0,
        lessonNum: 0,
        // 以下解决表单数据不全时insert语句非空校验
        teacherId: '',
        subjectId: '',
        subjectParentId: '',
        cover: '',
        description: ''
      },
      teacherList: [], // 讲师列表
      subjectList: [], // 一级分类列表
      subjectLevelTwoList: []// 二级分类列表
    }
  },
  created() {
    // this.$parent.courseId 获取父组件中的课程courdeID
    if (this.$parent.courseId) { // 回显
      this.fetchCourseInfoById(this.$parent.courseId)
    } else { // 新增
      // 初始化分类列表
      this.initSubjectList()
    }
    // 获取讲师列表
    this.initTeacherList()
  },
  methods: {
    // 获取课程信息(回显数据时调用)
    // 注意:我们要做的是把 “所有的一级菜单数据” 和 “该一级菜单下的二级菜单” 的数据放到subjectList和subjectLevelTwoList中即可,具体显示哪个交给div中的代码实现
    fetchCourseInfoById(id) {
      // 调用后端接口,根据课程id查询课程信息
      courseApi.getCourseInfoById(id).then(response => {
        // 将接口返回数据复制到data中的courseInf(表单数据),实现回显
        this.courseInfo = response.data
        // 初始化分类列表
        subjectApi.getChildList(0).then(response => {
          // subjectList存储的是所有的一级分类
          this.subjectList = response.data
          // 填充二级菜单:遍历subjectList,subjectList存储的是所有一级分类,而不是某一个一级分类,所以需要遍历找出该课程所属的一级分类
          this.subjectList.forEach(subject => {
            // subject是遍历时一级菜单中的某一个,courseInfo.subjectParentId是根据课程id查询到的课程信息中的一级菜单,都是一级菜单,比对符合即赋值即可
            if (subject.id === this.courseInfo.subjectParentId) {
              // 拿到当前类别下的子类别列表,将子类别列表填入二级下拉菜单列表
              subjectApi.getChildList(subject.id).then(response => {
                this.subjectLevelTwoList = response.data
              })
            }
          })
        })
      })
    },
    // 获取讲师列表
    initTeacherList() {
      teacherApi.list().then(response => {
        this.teacherList = response.data
      })
    },

    // 初始化分类列表
    initSubjectList() {
      subjectApi.getChildList(0).then(response => {
        // 给一级分类列表赋值
        this.subjectList = response.data
      })
    },

    // 选择一级分类,切换二级分类
    subjectChanged(value) {
      subjectApi.getChildList(value).then(response => {
        this.courseInfo.subjectId = ''
        // 给二级分类列表赋值
        this.subjectLevelTwoList = response.data
      })
    },

    // 上传成功回调
    handleCoverSuccess(res, file) {
      this.courseInfo.cover = res.data
    },

    // 上传校验
    beforeCoverUpload(file) {
      const isJPG = file.type === 'image/jpeg'
      const isLt2M = file.size / 1024 / 1024 < 2

      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!')
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!')
      }
      return isJPG && isLt2M
    },

    // 错误处理
    handleCoverError() {
      console.log('error')
      this.$message.error('上传失败2')
    },

    // 保存并下一步
    saveAndNext() {
      this.saveBtnDisabled = true
      if (!this.$parent.courseId) {
        this.saveData()
      } else {
        this.updateData()
      }
    },
    // 修改
    updateData() {
      courseApi.updateCourseInfoById(this.courseInfo).then(response => {
        this.$message.success(response.message)
        this.$parent.courseId = response.data // 获取courseId
        this.$parent.active = 1 // 下一步
      })
    },

    // 保存
    saveData() {
      courseApi.saveCourseInfo(this.courseInfo).then(response => {
        this.$message.success(response.message)
        this.$parent.courseId = response.data // 获取courseId
        this.$parent.active = 1 // 下一步
      })
    }
  }
}
</script>
<style scoped>
.tinymce-container {
  position: relative;
  line-height: normal;
}
.cover-uploader .avatar-uploader-icon {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;

  font-size: 28px;
  color: #8c939d;
  width: 640px;
  height: 357px;
  line-height: 357px;
  text-align: center;
}
.cover-uploader .avatar-uploader-icon:hover {
  border-color: #409EFF;
}
.cover-uploader img {
  width: 640px;
  height: 357px;
  display: block;
}
</style>

解释:

(1)添加课程时,我们只需要初始化分类列表,供用户进行选择。二级菜单默认为空,用户选择了一级菜单后(@change=“subjectChanged”,发生变换就调用subjectChanged方法)会调用subjectChanged方法给二级分类列表赋值。

(2)修改课程时,我们也要做初始化分类列表,但是因为路由到这个页面时路径中会带有该课程的id,先根据该课程id查询出课程的数据(包括subjectId、subjectParentId)赋值给data中的courseInfo,在中使用利用双向绑定(v-model=“courseInfo.subjectParentId”、v-model=“courseInfo.subjectId”)即可实现下拉框直接显示该课程“具体”的一级分类和二级分类,而无需写过多的js代码。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值