1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 谷粒学院16万字笔记+1600张配图(九)——课程管理

谷粒学院16万字笔记+1600张配图(九)——课程管理

时间:2022-06-27 13:41:15

相关推荐

谷粒学院16万字笔记+1600张配图(九)——课程管理

项目源码与所需资料

链接:/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59

提取码:8z59

文章目录

demo09-课程管理1.课程大纲列表后端1.1创建两个实体类1.2控制层1.3业务层1.3.1业务层接口1.3.2业务层实现类 1.4测试 2.课程大纲列表前端2.1在api中定义方法2.2调用api中的方法2.3初始化课程大纲列表2.4获取路径中的id值后初始化课程大纲列表2.5让数据在页面显示2.6测试 3.修改课程基本信息后端3.1数据回显的接口3.1.1控制层3.1.2业务层接口3.1.3业务层实现类 3.2修改课程基本信息的接口3.2.1控制层3.2.2业务层接口3.2.3业务层实现类 4.修改课程基本信息前端4.1在api中定义方法4.2修改chapter.vue4.3在info.vue实现数据回显4.3.1获取路径中的id值4.3.2调用api中的方法4.3.3调用根据id查询课程基本信息的方法4.3.4测试4.3.5测试中发现的问题4.3.6初始化一级分类、二级分类、讲师列表4.3.7测试4.3.8点击添加课程路由后清空数据4.3.9渲染问题 4.4修改课程基本信息 5.章节前端(部分)5.1添加按钮5.2添加弹框组件5.3给按钮绑定事件 6.章节后端6.1添加章节6.2修改章节6.3删除章节6.3.1控制层6.3.2业务层接口6.3.3业务层实现类 7.章节前端7.1在api中定义方法7.2添加章节7.2.1调用api中的方法7.2.2给属性添加注解7.2.3测试7.2.4bug7.2.5完善saveOrUpdate方法 7.3修改章节7.3.1给编辑按钮绑定事件7.3.2调用api中的方法7.3.3定义修改章节的方法7.3.4完善saveOrUpdate方法7.3.5测试 7.4删除章节7.4.1给删除按钮绑定事件7.4.2调用api中的方法7.4.3测试 8.小节后端8.1修改请求路径8.2添加小节8.3删除小节8.4修改小节 9.小节前端9.1添加小节弹框组件9.2在api中定义方法9.3添加小节9.3.1引入js文件9.3.2给"添加课时"按钮绑定事件9.3.3定义弹出添加小节页面的方法openVideo9.3.4调用api中的方法9.3.5编写saveOrUpdateVideo方法9.3.6给属性添加注解9.3.7测试9.3.8bug9.3.9编写错误 9.4删除小节9.4.1绑定事件9.4.2调用api中的方法9.4.3测试 9.5修改小节9.5.1绑定事件9.5.2调用api中的方法9.5.3定义修改小节的方法9.5.4完善saveOrUpdateVideo方法9.5.5测试

demo09-课程管理

课程大纲列表中有章节和小节,一个章节下面有多个小节,一个小节属于一个章节,也就是说,章节和小节是一对多的关系,这和我们前面做的课程分类列表一样

1.课程大纲列表后端

1.1创建两个实体类

在entity包下创建包chapter,然后在chapter包下分别创建章节、小节的实体类ChapterVo、VideoVo

//章节@Datapublic class ChapterVo {private String id;private String title;//小节private List<VideoVo> children = new ArrayList<>();}

//小节@Datapublic class VideoVo {private String id;private String title;}

1.2控制层

给控制器EduChapterController添加注解@CrossOrigin以解决跨域问题,再将该控制器中的@RequestMapping(“/eduservice/edu-chapter”)改为@RequestMapping(“/eduservice/chapter”)然后在该控制器中编写代码(不改请求路径当然也可以,我只是有强迫症)

@RestController@RequestMapping("/eduservice/chapter")@CrossOriginpublic class EduChapterController {@Autowiredprivate EduChapterService chapterService;//根据课程id查询并返回某一门课程下的所有章节、小节@GetMapping("getChapterVideo/{courseId}")public R getChapterVideo(@PathVariable String courseId) {List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);return R.ok().data("allChapterVideo", list);}}

1.3业务层

1.3.1业务层接口

在业务层的EduChapterService接口中定义抽象方法getChapterVideoByCourseId

public interface EduChapterService extends IService<EduChapter> {//根据课程id查询并返回某一门课程下的所有章节、小节List<ChapterVo> getChapterVideoByCourseId(String courseId);}

1.3.2业务层实现类

在实现类EduChapterServiceImpl中重写刚刚在业务层接口中定义的方法getChapterVideoByCourseId

@Servicepublic class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {@Autowiredprivate EduVideoService videoService;//根据课程id查询并返回某一门课程下的所有章节、小节@Overridepublic List<ChapterVo> getChapterVideoByCourseId(String courseId) {//1.根据课程id查询课程里面所有的章节QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();wrapperChapter.eq("course_id", courseId);List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);//2.根据课程id查询课程里面所有的小节QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();wrapperVideo.eq("course_id", courseId);List<EduVideo> eduVideoList = videoService.list(wrapperVideo);//3.创建list集合,用于最终封装数据List<ChapterVo> finalList = new ArrayList<>();//4.遍历查询出来的章节的list集合,进行封装for (int i = 0; i < eduChapterList.size(); i++) {//①得到eduChapterList中的每个EduChapter对象EduChapter eduChapter = eduChapterList.get(i);//②把eduChapter中我们需要的值获取出来,放到chapterVo中ChapterVo chapterVo = new ChapterVo();BeanUtils.copyProperties(eduChapter, chapterVo);//③把chapterVo放到最终list集合finalList.add(chapterVo);//④封装某一章节下的所有小节//创建list集合封装每个章节下的所有小节List<VideoVo> videoList = new ArrayList<>();//遍历查询出来的小节的list集合,进行封装for (int m = 0; m < eduVideoList.size(); m++) {//④.1:得到eduVideoList中的每个EduVideo对象EduVideo eduVideo = eduVideoList.get(m);//若eduVideo的chapter_id和章节的id相等,那么就进行封装if (eduVideo.getChapterId().equals(eduChapter.getId())) {//④.2:把eduVideo中我们需要的值获取出来,放到videoVo中VideoVo videoVo = new VideoVo();BeanUtils.copyProperties(eduVideo, videoVo);//④.3:将每个videoVo放到videoList中videoList.add(videoVo);}//④.4把某一章节下所有小节放到章节里面chapterVo.setChildren(videoList);}}return finalList;}}

解释一下截图中的第30和31行:我们现在是在Chapter中操作的,只能操作Chapter的数据库,肯定无法操作Video的数据库,想要在Chapter中操作Video的数据库(截图中第44行:videoService.list(wrapperVideo)就操作了Video的数据库)必须将Video注入进来,可以注入EduVideoMapper或EduVideoService或EduVideoServiceImpl,我这里选择注入EduVideoService

1.4测试

1.在数据库中执行如下sql命令向edu_chapter表和edu_video表插入数据

INSERT INTO `edu_chapter` VALUES ('2','1562267576652808193','第一章:好好学习',0,'-01-01 12:27:40','-01-01 12:55:30'),('1','1562267576652808193','第二章:天天向上',0,'-01-01 12:55:35','-01-01 12:27:40'),('6','1561333993918500866','第一章:摆烂',0,'-01-01 12:27:40','-10-09 08:32:47');INSERT INTO `edu_video` VALUES ('21','1562267576652808193','2','第一节:goodgood','','',0,0,0,0,'Empty',0,1,'-10-11 11:32:59','-10-11 11:57:38'),('22','1562267576652808193','2','第二节:study','','',0,0,0,0,'Empty',0,1,'-10-19 05:51:23','-10-19 05:51:33'),('11','1562267576652808193','1','第一节:dayday','','',0,0,0,0,'Empty',0,1,'-10-30 14:51:55','-10-30 14:51:55'),('12','1562267576652808193','1','第二节:up','','',0,0,0,0,'Empty',0,1,'-10-30 17:17:41','-10-30 17:17:41'),('555','1561333993918500866','6','第一节:开摆开摆','','',0,0,0,0,'Empty',0,1,'-10-30 17:17:41','-10-30 17:17:41');

2.传参数1562267576652808193然后点击"Try it out!"进行测试

3.可以看到测试成功

2.课程大纲列表前端

2.1在api中定义方法

在src–>api–>edu目录下创建chapter.js文件,并在该文件中定义方法调用后端接口

import request from '@/utils/request'export default {//1.根据课程id得到该课程下的所有章节、小节getAllChapterVideo(courseId) {return request({url: `/eduservice/chapter/getChapterVideo/${courseId}`,method: 'get'})}}

2.2调用api中的方法

1.想要在chapter.vue页面调用chapter.js中的方法,就需要先引入这个文件

import chapter from '@/api/edu/chapter'

2.接下来我们定义方法来调用api中的getAllChapterVideo方法

//根据课程id得到该课程下的所有章节、小节getChapterVideo() {chapter.getAllChapterVideo(id).then(response => {this.chapterVideoList = response.data.allChapterVideo})},

3.第2步的截图中第37行用到了数据模型chapterVideoList,所以我们要在data() {...}中定义出来这个数据模型

2.3初始化课程大纲列表

created() {...}中调用"2.2调用api中的方法"的第2步定义的getChapterVideo方法以初始化所有课程大纲列表

//根据课程id得到该课程下的所有章节、小节this.getChapterVideo()

2.4获取路径中的id值后初始化课程大纲列表

1.调用"2.2调用api中的方法"的第2步定义的getChapterVideo方法时需要传入参数,这个参数是课程id,但是我们目前还没有得到课程id,那我们从哪里才能得到课程id呢?

我们在"demo08-课程管理"的"4.10让前端添加后能得到课程id"的第4步我们可以知道:路径中的参数就是课程id,所以我们只要取到路径中的参数就是得到了课程id,具体操作和"demo05-讲师管理前端"的"10.3.3调用方法"根据id查询数据""类似

2.删去截图中红框圈起来的部分

3.在created() {...}中编写如下代码以完成课程大纲列表的初始化

//根据课程id得到该课程下的所有章节、小节if(this.$route.params && this.$route.params.id) {//获取路径中的id值this.courseId = this.$route.params.id//调用方法来得到某课程下的所有章节、小节this.getChapterVideo()}

4.第3步截图中的第35行用到了数据模型courseId,所以我们要在data() {...}中定义出来这个数据模型

5.调用getAllChapterVideo这个api时将courseId作为参数传过去

2.5让数据在页面显示

课程大纲列表的chapter.vue中的代码我们写成和以前做的课程分类列表一样是可以的,但我们做的课程分类列表的页面中的代码是人家给我们封装好的,我们基本上没怎么写代码。这里我们来用一种比较接近底层的方式来写课程大纲列表的chapter.vue页面

1.删除截图中方框圈起来的部分

2.将如下代码复制到chapter.vue页面中

<!-- 章节 --><ul class="chanpterList"><li v-for="chapter in chapterVideoList" :key="chapter.id"><p>{{ chapter.title }}</p><!-- 小节 --><ul class="chanpterList videoList"><li v-for="video in chapter.children" :key="video.id"><p>{{ video.title }}</p></li></ul></li></ul><div><el-button @click="previous">上一步</el-button><el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button></div>

截图中第15行的:key="chapter.id"和第21行的:key="video.id"都代表唯一标识

3.在chapter.vue页面的最后定义样式

<style scoped>.chanpterList{position: relative;list-style: none;margin: 0;padding: 0;}.chanpterList li{position: relative;}.chanpterList p{float: left;font-size: 20px;margin: 10px 0;padding: 10px;height: 70px;line-height: 50px;width: 100%;border: 1px solid #DDD;}.chanpterList .acts {float: right;font-size: 14px;}.videoList{padding-left: 50px;}.videoList p{float: left;font-size: 14px;margin: 10px 0;padding: 10px;height: 50px;line-height: 30px;width: 100%;border: 1px dotted #DDD;}</style>

2.6测试

在地址栏输入http://localhost:9528/#/course/chapter/1562267576652808193进行测试

3.修改课程基本信息后端

3.1数据回显的接口

在chapter.vue点击"上一步"后回到第一步,其实就是回到info.vue页面修改课程的基本信息,所以需要先做一个数据回显接口,也就是可以根据课程id查询课程基本信息

3.1.1控制层

在控制器EduCourseController中编写代码

//根据课程id查询课程基本信息@GetMapping("getCourseInfo/{courseId}")public R getCourseInfo(@PathVariable String courseId) {CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);return R.ok().data("courseInfoVo", courseInfoVo);}

3.1.2业务层接口

在业务层接口EduCourseService中定义抽象方法

//根据课程id查询课程基本信息CourseInfoVo getCourseInfo(String courseId);

3.1.3业务层实现类

在业务层实现类EduCourseServiceImpl中实现上一步定义的抽象方法

//根据课程id查询课程基本信息@Overridepublic CourseInfoVo getCourseInfo(String courseId) {//1.查询课程表EduCourse eduCourse = baseMapper.selectById(courseId);//2.将数据封装到CourseInfoVo对象中CourseInfoVo courseInfoVo = new CourseInfoVo();BeanUtils.copyProperties(eduCourse, courseInfoVo);//3.查询课程简介表EduCourseDescription courseDescription = courseDescriptionService.getById(courseId);//4.将数据封装到CourseInfoVo对象中courseInfoVo.setDescription(courseDescription.getDescription());return courseInfoVo;}

3.2修改课程基本信息的接口

3.2.1控制层

在控制器EduCourseController中编写代码

//修改课程基本信息@PostMapping("updateCourseInfo")public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {courseService.updateCourseInfo(courseInfoVo);return R.ok();}

3.2.2业务层接口

在业务层接口EduCourseService中定义抽象方法

3.2.3业务层实现类

在业务层实现类EduCourseServiceImpl中实现上一步定义的抽象方法

//修改课程基本信息@Overridepublic void updateCourseInfo(CourseInfoVo courseInfoVo) {//1.修改课程表EduCourse eduCourse = new EduCourse();BeanUtils.copyProperties(courseInfoVo, eduCourse);int update = baseMapper.updateById(eduCourse);if (update == 0) {throw new GuliException(20001, "修改课程表信息失败");}//2.修改课程简介表EduCourseDescription description = new EduCourseDescription();description.setId(courseInfoVo.getId()); //根据id修改,所以对象一定要有id值description.setDescription(courseInfoVo.getDescription());courseDescriptionService.updateById(description);}

4.修改课程基本信息前端

4.1在api中定义方法

在src–>api–>edu–>course.js中定义两个方法来分别调用后端的接口getCourseInfo、updateCourseInfo

//3.根据课程id获取课程基本信息getCourseInfoId(id) {return request({url: `/eduservice/course/getCourseInfo/${id}`,method: 'get'})},//4.修改课程基本信息updateCourseInfo(courseInfo) {return request({url: `/eduservice/course/updateCourseInfo`,method: 'post',data: courseInfo})}

4.2修改chapter.vue

将chapter.vue页面的previous方法中的this.$router.push({path: ‘/course/info/1’})改为this.$router.push({path: ‘/course/info/’+this.courseId})

顺便将next方法中的this.$router.push({path: ‘/course/publish/1’})改为this.$router.push({path: ‘/course/publish/’+this.courseId})

4.3在info.vue实现数据回显

4.3.1获取路径中的id值

1.在info.vue的created() {...}中添加如下代码

//获取路径中的id值if(this.$route.params && this.$route.params.id) {this.courseId = this.$route.params.id}

2.上一张截图中的第117行用了courseId数据模型,我们现在就去定义这个数据模型

4.3.2调用api中的方法

methods: {...}中定义方法调用api中的方法使得可以根据id查询课程基本信息

//根据课程id查询课程基本信息getInfo() {course.getCourseInfoId(this.courseId).then(response => {this.courseInfo = response.data.courseInfoVo})},

4.3.3调用根据id查询课程基本信息的方法

添加如下代码

//调用根据id查询课程基本信息的方法this.getInfo()

4.3.4测试

1.重启后端项目,然后点击"添加课程"路由,填写基本信息后点击"保存并下一步"

2.点击"上一步"

3.可以看到,除了二级课程分类显示的是课程分类id,其余数据都正常回显了

4.并且二级分类下拉列表没有数据,而讲师下拉列表和一级分类下拉列表有数据

4.3.5测试中发现的问题

1.从测试中我们知道,二级分类下拉列表没有数据,并且二级分类显示的是课程分类id而不是课程分类的名字

2.先解释一下为什么讲师下拉列表和一级分类下拉列表有数据:

①每次打开info.vue页面都会调用getListTeacher方法和getOneSubject方法

②这两个方法内部会分别得到讲师数据和一级课程分类数据,并将数据分别赋值给数据模型teacherList、subjectOneList

③然后框架就会分别遍历teacherList数组、subjectOneList数组

④最终结果就是"4.3.4测试"的第4步说的:讲师下拉列表和一级分类下拉列表有数据

3.那我们应该就知道了为什么二级分类下拉列表没有数据:

①我们给一级分类下拉列表和二级分类下拉列表做的是联动效果,只有触发change事件,才会调用subjectLevelOneChanged方法,才能将相应一级分类下的所有二级分类数据赋值给数据模型subjectTwoList

②可是当我们在"4.3.4测试"的第2步点击"上一步"回到info.vue页面时,并不会触发change事件,所以就不会调用subjectLevelOneChanged方法,所以数据模型subjectTwoList就是一个空数组

③那么就是遍历空数组subjectTwoList

④最终最终结果就是"4.3.4测试"的第4步说的:二级分类下拉列表没有数据

4.为什么讲师下拉列表和一级分类下拉列表显示的是讲师名字、一级分类名字,并且为什么显示的刚刚好就是我们"4.3.4测试"的第1步填写的讲师名字、一级分类名字(我这里只解释讲师下拉列表,一级分类下拉列表也是同理的):

①首先我们知道,讲师下拉列表和courseInfo.teacherId进行了双向绑定

②所以讲师下拉列表的值确实应该是讲师id。但是人家框架给我们做了封装:会将courseInfo.teacherId中存储的讲师id和从teacherList数组中遍历出来的所有讲师id依次进行比较,如果这两个id相同,就给id相同的option标签添加属性selected,底层大致是这样的:

<select>...<option value="...">string1010</option><option value="...">张三三三三三</option><option value="..." selected>张二二</option>...</select>

③所以不是显示讲师id,而是显示我填写的讲师名字,并且显示的刚刚好就是我在"4.3.4测试"的第1步填写的讲师名字

④上面说的是下拉列表的回显数据的底层,顺便也说下单选框和复选框回显数据时的底层吧:

<!-- 单选框: --><input type="radio" checked><!-- 复选框: --><input type="checkbox" checked>

5.为什么二级分类显示的是课程分类id而不是课程分类名字

①首先,二级分类下拉列表和courseInfo.subjectId进行了双向绑定

②三步走:

此时courseInfo.subjectId中存储的id是"4.3.4测试"的第1步选中的二级分类的id我们在"4.3.5测试中发现的问题"的第4步的②说过:会将courseInfo.subjectId中存储的二级分类id和从subjectTwoList数组中遍历出来的所有二级分类id依次进行比较,如果这两个id相同,就给id相同的option标签添加属性selected但是我们在"4.3.5测试中发现的问题"的第3步的②说过:subjectTwoList是一个空数组,这就意味着subjectTwoList数组中没有二级分类id能和courseInfo.subjectId中存储的二级分类id相同,所以不会显示中文的二级分类名字,而是显示"4.3.4测试"的第1步填写的二级分类的id

4.3.6初始化一级分类、二级分类、讲师列表

发现了问题接下来我们就来解决问题:

1.原来created() {...}中的代码如下

2.修改后created() {...}中的代码如下

//获取路径中的id值if(this.$route.params && this.$route.params.id) {//修改操作this.courseId = this.$route.params.id//调用根据id查询课程基本信息的方法this.getInfo()} else {//添加操作//初始化所有讲师this.getListTeacher()//初始化所有一级分类this.getOneSubject()}

我们这样做的目的是:路径中有id值就做修改操作,路径中没有id值就做添加操作

3.这样修改代码后做修改操作时数据模型中的subjectOneList、subjectTwoList、teacherList就都是空数组了,我们需要在getInfo方法中添加代码,使得做修改操作时可以给subjectOneList、subjectTwoList、teacherList这三个数组赋相应的值

//1.初始化一级分类和二级分类subject.getSubjectList().then(response => {//获取所有一级分类并赋值this.subjectOneList = response.data.list//遍历一级分类的数组for(var i = 0;i < this.subjectOneList.length;i++){//获取数组中每个一级分类var oneSubject = this.subjectOneList[i]//拿每个一级分类的id和数据模型courseInfo.subjectParentId比较是否相等if(oneSubject.id == this.courseInfo.subjectParentId) {//找到相等的那一个一级分类,获取到该一级分类下的所有二级分类并赋值this.subjectTwoList = oneSubject.children}}})//2.初始化所有讲师this.getListTeacher()

4.3.7测试

1.在地址栏输入http://localhost:9528/#/course/info/1562665646746079233进行测试(路经中的课程id1562665646746079233是我在"4.3.4测试"中添加的课程基本信息的id,可以去看"4.3.4测试"的第3步的截图中的地址栏)

2.可以看到此时数据回显完全成功

3.其实有问题:

在地址栏输入http://localhost:9528/#/course/info/xxx。xxx是数据库中的课程id,多找几个,多试几次,偶尔会出现这种现象:我在地址栏输入http://localhost:9528/#/course/info/id0001按回车后课程信息可以正常回显,然后我在地址栏输入http://localhost:9528/#/course/info/id0002按回车后该课程的其它数据都可以正常回显,但唯独课程简介显示的竟然是id0001的,此时必须按下回车刷新一下才可以正常回显id0002的所有课程信息。如果把课程简介的富文本编辑器改为普通的输入框("demo08-课程管理"的"4.15.5使用组件–添加富文本编辑器组件"是将普通输入框改为富文本编辑器,那么怎么把富文本编辑器改为普通输入框不用我多说了吧)就不会出现这个现象了。所以我猜测出现这种问题是富文本编辑器自身的问题想测试的话自行测试,但是别忘了测试之后把普通输入框改回富文本编辑器,原因就是:老师也用的富文本编辑框

4.3.8点击添加课程路由后清空数据

1.接着"4.3.7测试"继续做下去:我们点击"添加课程"路由,发现地址栏路径中不再有课程id,说明我们此时做的确实是添加操作,可是页面中仍有数据,这是因为点击"添加课程"路由后没有清空数据,和"demo05-讲师管理前端"的"10.8路由切换问题"遇到的问题是一样的

2.在created() {...}中使用代码this.courseInfo = {}用于清空表单,这行代码会清空courseInfo对象的属性,所以需要再添加一行代码this.courseInfo.cover = '/static/defaultcover.jpg'用来给courseInfo对象的cover属性赋值,以设置默认封面

3.在methods: {...}中定义方法init() {...},将created() {...}方法中的代码抽取出来放到init方法中,然后在created方法中调用init方法

4.添加如下代码

//监听watch: {$route(to, from){//路由变化方式,只要路由发生变化,该方法就会执行this.init()}},

5.自行测试,我测试时遇到了问题,我在"4.3.9渲染问题"会详细说

4.3.9渲染问题

1.我测试时遇到了问题:点击"添加课程"路由后,给课程选择一个封面并上传,但并没有显示我们上传的课程封面,而是显示默认课程封面

2.我们给上传封面成功调用的方法handleAvatarSuccess内部添加一行代码用于在控制台输出数据(添加的这行代码仅用于测试找问题,测试后记得将这行代码删掉)

3.点击"添加课程"路由,按F12打开开发者工具,点击"Console"菜单打开控制台

4.先不用管课程封面,将其它信息填写一下

5.然后在电脑选择一张照片作为封面上传,可以看到页面中显示的并不是我们上传的封面,而是默认封面;但控制台中输出的是我们上传的封面的url,而不是默认封面的地址/static/defaultcover.jpg

6.点击"保存并下一步"将数据存入数据库,去数据库中查看数据,可以看到,数据库中存的封面地址是我们上传的封面的url,而不是默认封面的地址/static/defaultcover.jpg

7.添加课程后点击"上一步"查看数据回显,可以看到,此时页面显示的是我们上传的封面而不再是默认封面

8.第5、6、7步的测试都说明了我们确实成功上传了照片作为封面并将该照片的url赋值给了courseInfo.cover,并最终将赋值后的courseInfo.cover存入数据库,可为什么第5步仍显示的是默认封面呢?这就涉及到数据渲染问题了,我不知道这是什么专业术语,我也不想知道,我已经在这个坑待了两天了,懒得管那么多了,我现在只想知道解决方法:

解决方法一:

给上传封面成功调用的方法handleAvatarSuccess内部添加一行代码this.$forceUpdate()用于强制刷新,让页面重新渲染

解决方法二:我们在"4.3.8点击添加课程路由后清空数据"的第2步添加了两行代码this.courseInfo = {}this.courseInfo.cover = '/static/defaultcover.jpg',现在我们将这两行代码删掉,改为this.courseInfo = {cover: '/static/defaultcover.jpg'}。为什么这样改我不知道,我也不想知道,还有,我用的是解决方法一

9.说一下为什么第4步我让先填写其它数据再管课程封面:我试过了:我们先修改课程封面,此时仍是默认封面,但是选择讲师下拉列表或一级课程分类下拉列表后就会触发渲染,显示我们上传的课程封面。我之所以让先填写其它数据再管课程封面就是为了让再现这种渲染问题

10./9/7:以前没问题的,但今天测试时选择二级分类后也出现了渲染问题,我的解决方法:

①给二级分类绑定事件(选择完二级分类后就会调用这个方法)

②编辑subjectLevelTwoChanged方法,方法内部强制刷新渲染

//解决渲染问题subjectLevelTwoChanged() {this.$forceUpdate()},

4.4修改课程基本信息

点击"保存并下一步"可能做的是修改,也可能做的是添加,所以我们要进行区分:

1.我们在"demo08-课程管理"的"4.8.3修改函数saveOrUpdate的内容"中给saveOrUpdate方法中编写的是添加操作的代码

2.定义方法addCourse,将saveOrUpdate方法中的代码原封不动地剪切粘贴到addCourse方法中

3.定义修改方法

//修改课程updateCourse() {course.updateCourseInfo(this.courseInfo).then(response => {//1.提示成功this.$message({type: 'success',message: '修改课程基本信息成功'})//2.跳转到第二步this.$router.push({path: '/course/chapter/' + this.courseId})})},

修改操作后不像添加操作那样通过response.data.courseId获取课程id。解决办法是:我们在"4.3.1获取路径中的id值"的截图中的第117行已经通过代码this.courseId = this.$route.params.id将路径中的课程id赋值给了courseId,所以可以我们这里用this.courseId

4.在saveOrUpdate方法中根据数据模型courseInfo.id是否为空来判断调用修改方法还是添加方法

saveOrUpdate() {//判断是添加操作还是修改操作if(!this.courseInfo.id) {//添加操作this.addCourse()} else {//修改操作this.updateCourse()}}

数据回显时在"4.3.2调用api中的方法"的截图中的第130行this.courseInfo = response.data.courseInfoVo将查询到的课程对象赋值给courseInfo,所以courseInfo对象中一定有课程id;而添加操作时id是在后端自动生成的,所以courseInfo对象中一定没有课程id。所以我们根据courseInfo对象中是否有id来进行判断当然,通过判断路径中是否有id值也是可以的,也就是说,截图中第241行的!this.courseInfo.id改为!this.$route.params.id也是可以的

5.自行进行测试,我的无论添加还是修改或是数据回显都没问题

5.章节前端(部分)

5.1添加按钮

我们在chapter.vue中添加"添加章节"的按钮

5.2添加弹框组件

1.填写章节信息时我们可以像添加讲师那样进入到表单中填写,但我们现在不用这种方式,有一种简单且常用的方式:点击"添加章节"按钮后弹出一个界面,我们在弹出的界面中填写章节信息,填写完毕点击"保存"就可以把数据添加到数据库中了

2.那么我们去element-ui中找一下这个组件,看一下它的代码,将代码根据需求进行修改

<el-dialog>实际上就是一个弹框组件@click="dialogFormVisible = true":visible.sync="dialogFormVisible"共同作用,目的是:点击"打开嵌套表单的 Dialog"时会将dialogFormVisible设为true,从而使弹框由不可见变为可见

3.老师已经将修改后的代码给我们了,我们直接用就可以了

①先将红框圈起来的部分删掉

②将下述代码复制到刚刚删除的位置

<!-- 章节 --><ul class="chanpterList"><liv-for="chapter in chapterVideoList":key="chapter.id"><p>{{ chapter.title }}<span class="acts"><el-button type="text">添加课时</el-button><el-button style="" type="text">编辑</el-button><el-button type="text">删除</el-button></span></p><!-- 视频 --><ul class="chanpterList videoList"><liv-for="video in chapter.children":key="video.id"><p>{{ video.title }}<span class="acts"><el-button type="text">编辑</el-button><el-button type="text">删除</el-button></span></p></li></ul></li></ul>

③将<el-dialog>标签添加到页面中(因为<el-dialog>是一个弹框组件,所以代码放在哪里都可以,只要是在<div>标签中就可以了)

<!-- 添加和修改章节表单 --><el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节"><el-form :model="chapter" label-width="120px"><el-form-item label="章节标题"><el-input v-model="chapter.title"/></el-form-item><el-form-item label="章节排序"><el-input-number v-model="chapter.sort" :min="0" controls-position="right"/></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogChapterFormVisible = false">取 消</el-button><el-button type="primary" @click="saveOrUpdate">确 定</el-button></div></el-dialog>

4.上张截图中52行用到了数据模型chapter,并且截图中第54和57行分别用到了chapter对象的title属性和sort属性;截图中第51行和第61行用到了数据模型dialogChapterFormVisible。

我们现在就去定义这两个数据模型,并且因为弹框默认是不可见的,所以给数据模型dialogChapterFormVisible赋初始值false

5.3给按钮绑定事件

给"添加章节"按钮添加点击事件,使得点击该按钮后给dialogChapterFormVisible赋值true,从而使章节弹框可见

6.章节后端

6.1添加章节

在控制器EduChapterController中编写添加章节的方法

//添加章节@PostMapping("addChapter")public R addChapter(@RequestBody EduChapter eduChapter) {chapterService.save(eduChapter);return R.ok();}

6.2修改章节

修改操作需要先根据章节id查询数据以在前端回显数据,再调用修改方法修改数据库中的数据,所以修改操作要求我们在控制器EduChapterController中编写两个方法

//根据章节id查询@GetMapping("getChapterInfo/{chapterId}")public R getChapterInfo(@PathVariable String chapterId) {EduChapter eduChapter = chapterService.getById(chapterId);return R.ok().data("chapter", eduChapter);}//修改章节@PostMapping("updateChapter")public R updateChapter(@RequestBody EduChapter eduChapter) {chapterService.updateById(eduChapter);return R.ok();}

6.3删除章节

6.3.1控制层

1.删除章节时有一个问题:

若该章节下没有小节那么直接删除该章节没有问题若该章节下有小节,那么直接删除该章节后数据库中仍会有该章节下的小节的数据,并且这些数据将成为垃圾数据

2.那么如果章节下有小节,怎么删除章节呢?实际开发中有两种思路:

删除章节时,把该章节下所有小节都删除删除章节时,进行判断,只有该章节下没有小节了,才可以删除该章节

3.我们这里使用刚刚说的第二种思路

在控制器EduChapterController中编写方法

//删除章节的方法@DeleteMapping("{chapterId}")public R deleteChapter(@PathVariable String chapterId) {boolean flag = chapterService.deleteChapter(chapterId);if (flag) {return R.ok();} else {return R.error();}}

6.3.2业务层接口

在业务层接口EduChapterService中编写抽象方法deleteChapter

//删除章节的方法void deleteChapter(String chapterId);

6.3.3业务层实现类

在业务层实现类EduChapterServiceImpl中实现刚刚编写的抽象方法

//删除章节的方法@Overridepublic boolean deleteChapter(String chapterId) {//根据章节id(chapterid)查询小节表(edu_video),若能查到数据,就不删除该章节QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();wrapper.eq("chapter_id", chapterId);int count = videoService.count(wrapper); //返回符合条件的数据的个数//判断if (count > 0) {//该章节下有小节,不删除该章节throw new GuliException(20001, "不删除");} else {//该章节下没有小节,删除该章节int result = baseMapper.deleteById(chapterId);return result > 0;}}

7.章节前端

7.1在api中定义方法

在chapter.js中分别定义如下方法来调用后端接口:添加章节、根据章节id查询章节、修改章节、删除章节

//2.添加章节addChapter(chapter) {return request({url: `/eduservice/chapter/addChapter`,method: 'post',data: chapter})},//3.根据章节id查询章节getChapter(chapterId) {return request({url: `/eduservice/chapter/getChapterInfo/${chapterId}`,method: 'get'})},//4.修改章节updateChapter(chapter) {return request({url: `/eduservice/chapter/updateChapter`,method: 'post',data: chapter})},//5.删除章节deleteChapter(chapterId) {return request({url: `/eduservice/chapter/${chapterId}`,method: 'delete'})}

7.2添加章节

7.2.1调用api中的方法

在"5.2添加弹框组件"的第3步的第③步的截图中第62行代码@click="saveOrUpdate"我们可以知道:点击弹框的"确定"按钮后会调用saveOrUpdate方法,下面我们来编写该方法

//添加章节saveOrUpdate() {//设置课程id到chapter对象里面this.chapter.courseId = this.courseIdchapter.addChapter(this.chapter).then(response => {//1.关闭弹框this.dialogChapterFormVisible = false//2.提示添加成功this.$message({type: 'success',message: '添加章节成功'})//3.刷新页面this.getChapterVideo()})},

截图中第95行的作用:数据表edu_chapter中的course_id字段我们设置的是非空字段,所以我们需要给数据模型chapter添加属性courseId

7.2.2给属性添加注解

给实体类EduChapter的gmtCreate属性和gmtModified属性添加@TableField注解以实现自动填充

7.2.3测试

1.重启后端服务器,在地址栏输入http://localhost:9528/#/course/chapter/1562267576652808193(1562267576652808193是课程id,去你自己的数据库随便找一个课程id就行了),点击"添加章节

2.填写完章节信息后点击"确定"

3.可以看到我们成功添加了这个章节

7.2.4bug

1.接着上面的步骤继续操作:我们再点击"添加章节",看到我们还没有填写章节信息,它自己就有章节信息

2.这是因为没有表单中有数据

3.解决方法是:每次弹出来这个界面,都清空表单中的数据

①原来点击"添加章节"后会将数据模型dialogChapterFormVisible赋值true使得可以显示弹框

②现在我们改变一下这段代码,使得点击"添加章节"时可以调用方法,这个方法我们就命名为openChapterDialog吧

③定义方法openChapterDialog,方法内部做两件事:给dialogChapterFormVisible赋值true、清空表单中的数据

//弹出添加章节的页面openChapterDialog() {//弹出界面this.dialogChapterFormVisible = true//清空表单数据this.chapter.title = ''this.chapter.sort = 0},

7.2.5完善saveOrUpdate方法

1.我们本来在saveOrUpdate方法中编写的是添加章节的代码

2.但实际上这个方法内部应该编写的是填写章节和修改章节的代码,下面我们来完善一下该方法

3.定义添加章节的方法addChapter,将saveOrUpdate方法中的代码原封不动的剪切粘贴过来

4.暂且在saveOrUpdate中编写如下代码调用添加章节的方法addChapter

5.自行进行测试

7.3修改章节

7.3.1给编辑按钮绑定事件

我们先给章节的"编辑"按钮添加绑定事件

截图中第25行的chapter.id是获取章节id并作为参数传给方法,以根据章节id去数据库查找章节信息并做数据回显

7.3.2调用api中的方法

调用api中的getChapter方法以实现数据回显

//修改章节信息时,我们需要先做数据回显openEditChapter(chapterId) {//先让弹框弹出来this.dialogChapterFormVisible = true//调用api中的方法实现数据回显chapter.getChapter(chapterId).then(response => {this.chapter = response.data.chapter})},

7.3.3定义修改章节的方法

//修改章节updateChapter() {chapter.updateChapter(this.chapter).then(response => {//1.关闭弹框this.dialogChapterFormVisible = false//2.提示修改成功this.$message({type: 'success',message: '修改章节成功'})//3.刷新页面this.getChapterVideo()})},

7.3.4完善saveOrUpdate方法

在saveOrUpdate方法中做判断,如果没有章节id说明此时做的是添加章节,如果有章节id说明此时做的是修改章节。完整的saveOrUpdate方法如下:

saveOrUpdate() {if (!this.chapter.id) {this.addChapter()} else {this.updateChapter()}},

7.3.5测试

1.在地址栏输入http://localhost:9528/#/course/chapter/1562267576652808193自行进行测试(1562267576652808193是课程id,去你自己的数据库随便找一个课程id就行了)

2.在测试中会遇到这样的一个bug:修改某个章节后再点击"添加章节"按钮,填写完信息后点击弹框中的"确定"会发现此时执行的修改操作,而不是添加操作,并且修改的是刚刚修改的章节

3.这是因为执行修改操作时我们做了数据回显,将章节id赋值给了chapter.id("7.3.2调用api中的方法"中截图的第100行this.chapter = response.data.chapter),而此时点击弹框中的"确认"进行我们认为的添加操作时,chapter.id是有值的,那么saveOrUpdate方法的!this.chapter.id判断结果就是false,所以就会调用修改方法而不是添加方法

3.解决方法是在修改操作中添加一行代码以使得chapter.id做布尔判断时判断结果为false

做布尔判断时除了undefined、null、false、0、NaN、""或’‘(空字符串)这六个值为false,其他值都视为 true。我试了null、’‘、false这三个,其中null和’'可以实现我们的需求,false不能实现,至于为什么不能实现我懒得深究,其余三个我没有试,想试的朋友自己试吧

7.4删除章节

7.4.1给删除按钮绑定事件

我们先给章节的"删除"按钮添加绑定事件

7.4.2调用api中的方法

此处确认框的代码借鉴"demo05-讲师管理前端"的"8.3在list.vue中定义删除方法"的第2步中的代码,我们复制过来稍加修改即可

//删除章节removeChapter(chapterId) {this.$confirm('此操作将永久删除章节记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {//点击确定会执行then方法//调用chapter.js中定义的删除方法chapter.deleteChapter(chapterId).then(response => {//删除成功//1.提示删除成功this.$message({type: 'success',message: '删除成功!'});//2.刷新页面this.getChapterVideo()}).catch(error => {}) //删除失败})},

7.4.3测试

在地址栏输入http://localhost:9528/#/course/chapter/1562267576652808193自行进行测试(1562267576652808193是课程id,去你自己的数据库随便找一个课程id就行了)

8.小节后端

8.1修改请求路径

我有强迫症,要和老师的请求路径一样,所以我将控制器EduVideoController的@RequestMapping("/eduservice/edu-video")中的edu-video改为video

8.2添加小节

在控制器EduVideoController上加注解@CrossOrigin以解决跨域问题,然后在该类中编写添加小节的方法

@RestController@RequestMapping("/eduservice/video")@CrossOriginpublic class EduVideoController {@Autowiredprivate EduVideoService videoService;//添加小节@PostMapping("addVideo")public R addVideo(@RequestBody EduVideo eduVideo) {videoService.save(eduVideo);return R.ok();}}

8.3删除小节

在控制器EduVideoController中编写删除小节的方法

//删除小节@DeleteMapping("{id}")public R deleteVideo(@PathVariable String id) {videoService.removeById(id);return R.ok();}

注意:删除小节的方法我们后面还要完善:小节下面还有视频,这种情况我们在"6.3.1控制层"的第2步说过,有两种方式可供选择:①删除小节时,把该小节下所有视频都删除、②删除小节时,进行判断,只有该小节下没有视频了,才可以删除该小节。

其一:我们"6.3删除章节"使用的是第二种方式,那我们删除小节时就用第一种方式吧:删除小节时,把该小节下所有视频都删除

其二:每个小节下对应且只对应一个视频,所以这里用第一种方式更好

8.4修改小节

修改操作需要先根据小节id查询数据以在前端回显数据,再调用修改方法修改数据库中的数据,所以修改操作要求我们在控制器EduVideoController中编写两个方法

//根据小节id查询@GetMapping("getVideoInfo/{videoId}")public R getVideoInfo(@PathVariable String videoId) {EduVideo eduVideo = videoService.getById(videoId);return R.ok().data("video", eduVideo);}//修改小节@PostMapping("updateVideo")public R updateVideo(@RequestBody EduVideo eduVideo) {videoService.updateById(eduVideo);return R.ok();}

9.小节前端

9.1添加小节弹框组件

1.这个弹框我们在"5.2添加弹框组件"的第3步的③已经使用过了,复制过来根据需求进行修改即可,老师也已经给我们准备好了

<!-- 添加和修改课时表单 --><el-dialog :visible.sync="dialogVideoFormVisible" title="添加课时"><el-form :model="video" label-width="120px"><el-form-item label="课时标题"><el-input v-model="video.title"/></el-form-item><el-form-item label="课时排序"><el-input-number v-model="video.sort" :min="0" controls-position="right"/></el-form-item><el-form-item label="是否免费"><el-radio-group v-model="video.free"><el-radio :label="true">免费</el-radio><el-radio :label="false">默认</el-radio></el-radio-group></el-form-item><el-form-item label="上传视频"><!-- TODO --></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogVideoFormVisible = false">取 消</el-button><el-button :disabled="saveVideoBtnDisabled" type="primary" @click="saveOrUpdateVideo">确 定</el-button></div></el-dialog>

2.截图中第67行和第86行用到了数据模型dialogVideoFormVisible;第68行用到了数据模型video,并且第70行、73行、76行分别用到了对象video中的属性title、sort、free。我们现在来定义这些数据模型

其中,video对象的videoSourceId属性目前还没看到过,但老师说了后面会用到,这里先定义出来

9.2在api中定义方法

在src–>api–>edu下定义video.js文件,并在里面编写方法调用后端接口

import request from '@/utils/request'export default {//1.添加小节addVideo(video) {return request({url: `/eduservice/video/addVideo`,method: 'post',data: video})},//2.删除小节deleteVideo(videoId) {return request({url: `/eduservice/video/${videoId}`,method: 'delete'})},//3.根据小节id查询小节getVideo(videoId) {return request({url: `/eduservice/video/getVideoInfo/${videoId}`,method: 'get'})},//4.修改小节updateVideo(video) {return request({url: `/eduservice/video/updateVideo`,method: 'post',data: video})}}

9.3添加小节

9.3.1引入js文件

在chapter.vue中引入刚刚创建的video.js文件

import video from '@/api/edu/video'

9.3.2给"添加课时"按钮绑定事件

9.3.3定义弹出添加小节页面的方法openVideo

//弹出添加小节的页面openVideo(chapterId) {//弹出界面this.dialogVideoFormVisible = true//设置章节idthis.video.chapterId = chapterId},

截图中第130行的作用:数据表edu_video中的chapter_id字段我们设置的是非空字段,所以我们需要给数据模型video添加属性chapterId

9.3.4调用api中的方法

1.定义addVideo方法调用api中添加小节的方法

//添加小节addVideo() {//设置课程idthis.video.courseId = this.courseIdvideo.addVideo(this.video).then(response => {//1.关闭弹框this.dialogVideoFormVisible = false//2.提示添加成功this.$message({type: 'success',message: '添加小节成功'})//3.刷新页面this.getChapterVideo()})},

截图中第135行的作用:数据表edu_video中的course_id字段我们设置的是非空字段,所以我们需要给数据模型video添加属性courseId

9.3.5编写saveOrUpdateVideo方法

在"9.1添加小节弹框组件"的第1步的截图中第87行代码@click="saveOrUpdateVideo"我们可以知道:点击弹框的"确定"按钮后会调用saveOrUpdateVideo方法,下面我们来编写该方法

saveOrUpdateVideo() {this.addVideo()},

9.3.6给属性添加注解

给实体类EduVideo的gmtCreate属性和gmtModified属性添加@TableField注解以实现自动填充

9.3.7测试

在地址栏输入http://localhost:9528/#/course/chapter/1562267576652808193自行进行测试

9.3.8bug

这个bug和"7.2.4bug"的一样,这里不多说了,解决方法:

在弹出添加小节页面的方法openVideo中清空表单数据

//清空表单数据this.video.title = ''this.video.sort = 0this.video.free = 0this.video.videoSourceId = ''

9.3.9编写错误

测试过程中有没有遇到这种情况:无论我选择的课时是免费还是收费,数据表edu_video中的is_free字段永远是0?

这是因为我们我们前端代码编写错误了:实体类EduVideo中的属性是isFree,但是我们给chapter.vue页面的video对象添加的是属性free而不是isFree,所以现在将下面三处的free改为isFree:

添加和修改课时表单的<el-radio-group v-model="video.free">中的free数据模型video中的freeopenVideo方法的this.video.free = 0中的free

将上面说的三处的free改为isFree后再测试就没问题了

9.4删除小节

9.4.1绑定事件

给"删除按钮"绑定事件

9.4.2调用api中的方法

此处确认框的代码借鉴"7.4.2调用api中的方法"中的代码,我们复制过来稍加修改即可

//删除小节removeVideo(videoId) {this.$confirm('此操作将永久删除小节记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {//点击确定会执行then方法//调用video.js中定义的删除方法video.deleteVideo(videoId).then(response => {//删除成功//1.提示删除成功this.$message({type: 'success',message: '删除成功!'});//2.刷新页面this.getChapterVideo()}).catch(error => {}) //删除失败})},

9.4.3测试

在地址栏输入http://localhost:9528/#/course/chapter/1562267576652808193自行进行测试

9.5修改小节

9.5.1绑定事件

给"编辑按钮"绑定事件

9.5.2调用api中的方法

调用api中的getVideo方法以实现数据回显

//修改小节信息时,我们需要先做数据回显openEditVideo(videoId) {//先让弹框弹出来this.dialogVideoFormVisible = true//调用api中的方法实现数据回显video.getVideo(videoId).then(response => {this.video = response.data.video})},

9.5.3定义修改小节的方法

//修改小节updateVideo() {video.updateVideo(this.video).then(response => {//1.关闭弹框this.dialogVideoFormVisible = false//2.提示修改成功this.$message({type: 'success',message: '修改小节成功'})//3.刷新页面this.getChapterVideo()//4.让video.id布尔判断为false,也就是清空video.idthis.video.id = null})},

9.5.4完善saveOrUpdateVideo方法

在saveOrUpdateVideo方法中做判断,如果没有小节id说明此时做的是添加小节,如果有小节id说明此时做的是修改小节。完整的saveOrUpdateVideo方法如下:

saveOrUpdateVideo() {if (!this.video.id) {this.addVideo()} else {this.updateVideo()}},

9.5.5测试

在地址栏输入http://localhost:9528/#/course/chapter/1562267576652808193自行进行测试

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。