两级分类的一种解决方案
解决方案:“数据库设计+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代码。