两级分类的一种解决方案
解决方案:“数据库设计+vue的双向绑定特性” 实现
看懂以下代码前提知识:mysql、springBoot、mybatisPlus、了解vue生命周期、了解ElementUI基本使用,了解前后分离开发。
场景:现在要做一个课程的添加和修改功能,但是每一个课程都对应了课程分类(比如:Web基础课程,属于一级分类–>后端开发;属于二级分类–>java)。我们在添加课程的时候,进行选择课程分类时,应该是先选一级分类,然后才能选择二级分类,修改课程时,要求回显数据时下拉框显示该课程对应的一级分类和二级分类,效果演示见图5。
1、数据库设计:
(1)课程分类表: ![在这里插入图片描述](https://img-blog.csdnimg.cn/494997c9f65c42dda4f9d42d3a19e9c2.jpeg#pic_center)
(2)课程表 ![在这里插入图片描述](https://img-blog.csdnimg.cn/964ff4dbe15f400580eee198e24e5291.jpeg#pic_center)
2、效果展示:
(1)添加课程时的效果: ![在这里插入图片描述](https://img-blog.csdnimg.cn/2a35dfe6f4554699a83f671953c98679.jpeg#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/0a7394b17fba4905b8beed2c9b83ea84.jpeg#pic_center)
(2)修改课程时的效果:
因为是修改课程,需要进行数据回显 ![在这里插入图片描述](https://img-blog.csdnimg.cn/3b63e12643f04c74a850c90fd664ed31.jpeg#pic_center)
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 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 {
private Integer code; //状态码
private String message; //返回状态信息(成功 失败)
private T data; //返回数据
public Result() {}
//成功的方法,有data数据
public static Result ok(T data) {
Result result = new Result();
if(data != null) {
result.setData(data);
}
result.setCode(20000);
result.setMessage("成功");
return result;
}
//失败的方法,有data数据
public static Result fail(T data) {
Result result = new Result();
if(data != null) {
result.setData(data);
}
result.setCode(20001);
result.setMessage("失败");
return result;
}
public Result message(String msg){
this.setMessage(msg);
return this;
}
public Result 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 list = subjectService.selectSubjectList(id);
return Result.ok(list);
}
}
上述代码中selectSubjectList(id)
//课程分类列表
//懒加载,每次查询一层数据
@Override
public List selectSubjectList(Long id) {
//SELECT * FROM SUBJECT WHERE parent_id=0
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("parent_id",id);
List 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 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、页面代码
元
保存并下一步
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 // 下一步
})
}
}
}
.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;
}
解释:
(1)添加课程时,我们只需要初始化分类列表,供用户进行选择。二级菜单默认为空,用户选择了一级菜单后(@change=“subjectChanged”,发生变换就调用subjectChanged方法)会调用subjectChanged方法给二级分类列表赋值。
(2)修改课程时,我们也要做初始化分类列表,但是因为路由到这个页面时路径中会带有该课程的id,先根据该课程id查询出课程的数据(包括subjectId、subjectParentId)赋值给data中的courseInfo,在中使用利用双向绑定(v-model=“courseInfo.subjectParentId”、v-model=“courseInfo.subjectId”)即可实现下拉框直接显示该课程“具体”的一级分类和二级分类,而无需写过多的js代码。
|