一、课程添加功能
概览
课程添加的步骤
课程相关表的关系
后端实现
1、代码生成器
只修改表名即可,依次填入:“edu_course”, “edu_course_description”, “edu_chapter”, “edu_video”
生成完成后,
- 删除EduCourseDescriptionController类,因为简介在课程里面去做就行了。
- 在所有entity包下的实体类中,创建和更新时间字段处,添加下面的注解:
2、创建vo类封装表单提交的数据
在entity.vo包下编写:
@Data
public class CourseInfoVo {
@ApiModelProperty(value = "课程ID")
private String id;
@ApiModelProperty(value = "课程讲师ID")
private String teacherId;
@ApiModelProperty(value = "课程专业ID")
private String subjectId;
@ApiModelProperty(value = "课程标题")
private String title;
@ApiModelProperty(value = "课程价格,0代表免费")
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "课程简介")
private String description;
}
3、controller 层
@Api(description = "添加课程信息")
@RestController
@RequestMapping("/eduservice/course")
@CrossOrigin
public class EduCourseController {
@Autowired
private EduCourseService courseService;
@ApiOperation("添加课程信息")
@PostMapping("addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
// 返回添加之后课程id,为了后面添加大纲使用
String courseId = courseService.saveCourseInfo(courseInfoVo);
return R.ok().data("courseId", courseId);
}
}
4、service 层
(1)service
public interface EduCourseService extends IService<EduCourse> {
// 添加课程信息
String saveCourseInfo(CourseInfoVo courseInfoVo);
}
(2)serviceImpl
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
//注入课程描述的service
@Autowired
EduCourseDescriptionService courseDescriptionService;
@Override
public String saveCourseInfo(CourseInfoVo courseInfoVo) {
//1、向课程表添加课程基本信息,将courseInfoVo值转换到eduCourse中
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int insert = baseMapper.insert(eduCourse);//影响行数
if (insert <= 0) {//添加失败
throw new GuliException(20001, "添加课程失败");
}
//添加成功,获取添加之后的课程ID
String cid = eduCourse.getId();
//2、向课程描述表中添加信息
EduCourseDescription courseDescription = new EduCourseDescription();
courseDescription.setDescription(courseInfoVo.getDescription());
//手动设置描述ID就是课程ID,确保一对一的关系
//同时确保EduCourseDescription类中的ID注解为 @TableId(value = "id", type = IdType.INPUT)
courseDescription.setId(cid);
courseDescriptionService.save(courseDescription);
//3、返回课程ID
return cid;
}
}
(3)同时需要修改EduCourseDescription类的主键生成策略
input 代表手动设置而非自动生成。
5、测试
- 先修改表结构 edu_course 表结构,至少让 subject_id 和 subject_parent_id 字段可以为 null,teacher_id 和 cover 为 null 是为了后面
- 测试时候不添加 id,因为是自动生成的。
然后启动项目:访问:http://localhost:8001/swagger-ui.html,传入一些基本的数据如下:
点击try it out!,成功:
数据库中的表数据添加成功:
前端实现
1、添加路由
router/index.js添加:
{
path: '/course',
component: Layout,
redirect: '/course/list',
name: '课程管理',
meta: { title: '课程管理', icon: 'example' },
children: [
{
path: 'list',
name: '课程列表',
component: () => import('@/views/edu/course/list'),
meta: { title: '课程列表', icon: 'table' }
},
{
path: 'info',
name: '添加课程',
component: () => import('@/views/edu/course/info'),
meta: { title: '添加课程', icon: 'tree' }
},
{
path: 'info/:id',
name: 'EduCourseInfoEdit',
component: () => import('@/views/edu/course/info'),
meta: { title: '编辑课程基本信息', noCache: true },
hidden: true
},
{
path: 'chapter/:id',
name: 'EduCourseChapterEdit',
component: () => import('@/views/edu/course/chapter'), meta: { title: '编辑课程大纲', noCache: true },
hidden: true
},
{
path: 'publish/:id',
name: 'EduCoursePublishEdit',
component: () => import('@/views/edu/course/publish'), meta: { title: '发布课程', noCache: true },
hidden: true
}
]
}
2、添加Vue组件
views/edu目录下新建course目录,然后建立info.vue,chapter.vue,list.vue,publish.vue
在 static目录下放置一张 01.jpg 的图片作为课程的默认封面,例如下面的图片:
下面是info.vue的代码:
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps
:active="1"
process-status="wait"
align-center
style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="最终发布"/>
</el-steps>
<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.subjectParentId"
placeholder="一级分类"
@change="subjectLevelOneChanged">
<el-option
v-for="subject in subjectOneList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>
<!-- 二级分类 -->
<el-select
v-model="courseInfo.subjectId"
placeholder="二级分类">
<el-option
v-for="subject in subjectTwoList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>
</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="总课时">
<el-input-number
:min="0"
v-model="courseInfo.lessonNum"
controls-position="right"
placeholder="请填写课程的总课时数"/>
</el-form-item>
<!-- 课程简介 -->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
<!-- 课程封面 -->
<el-form-item label="课程封面">
<el-upload
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:action="BASE_API+'/eduoss/fileoss'"
class="avatar-uploader">
<img :src="courseInfo.cover">
</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-item>
<el-button
:disabled="saveBtnDisabled"
type="primary"
@click="saveOrUpdate">保存并下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import course from '@/api/edu/course'
import subject from '@/api/edu/subject'
import Tinymce from '@/components/Tinymce' // 引入组件
export default {
// 声明组件
components: { Tinymce },
data() {
return {
saveBtnDisabled: false,
courseInfo: {
title: '',
subjectId: '', // 二级分类id
subjectParentId: '', // 一级分类id
teacherId: '',
lessonNum: 0,
description: '',
cover: '/static/01.jpg',
price: 0
},
courseId: '',
BASE_API: process.env.BASE_API, // 接口API地址
teacherList: [], // 用于封装所有讲师
subjectOneList: [], // 一级分类
subjectTwoList: [] // 二级分类
}
},
watch: {
$route(to, from) { // 路由变化方式,路由发生变化就会执行
this.init()
}
},
created() {
this.init()
},
methods: {
// 将判断过程封装为init方法
init() {
// 路径中如果有id值则做回显操作,否则执行添加操作
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
// 调用根据课程id查询信息的方法
this.getInfo()
} else {
// 初始化所有讲师
this.getListTeacher()
// 初始化一级分类
this.getOneSubject()
// 清空表单
// this.courseInfo = {}
}
},
// 根据课程id查询信息
getInfo() {
course
.getCourseInfoById(this.courseId)
.then(response => {
this.courseInfo = response.data.courseInfoVo
// 1、查询所以分类
subject
.getSubjectList()
.then(response => {
// 2、获取一级分类
this.subjectOneList = response.data.list
// 3、遍历比较
for (var i = 0; i < this.subjectOneList.length; i++) {
var oneSubject = this.subjectOneList[i]
if (this.courseInfo.subjectParentId === oneSubject.id) {
// 获取二级分类
this.subjectTwoList = oneSubject.children
}
}
})
// 初始化所有讲师
this.getListTeacher()
})
},
// 上传封面成功调用的方法
handleAvatarSuccess(res, file) {
this.courseInfo.cover = res.data.url
},
// 上传之前调用的方法
beforeAvatarUpload(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
},
// 点击某个一级分类,触发change,显示对应二级分类
subjectLevelOneChanged(value) {
// value就是一级分类id值
// 遍历所有的分类,包含一级和二级
for (var i = 0; i < this.subjectOneList.length; i++) {
// 每个一级分类
var oneSubject = this.subjectOneList[i]
// 判断所有一级分类id和点击的一级分类id是否一样
if (value === oneSubject.id) {
// 从一级分类获取里面所有的二级分类
this.subjectTwoList = oneSubject.children
// 把二级分类id值清空
this.courseInfo.subjectId = ''
}
}
},
// 查询所有一级分类
getOneSubject() {
subject
.getSubjectList()
.then(response => {
this.subjectOneList = response.data.list
})
},
// 查询所有讲师
getListTeacher() {
course
.getListTeacher()
.then(response => {
this.teacherList = response.data.items
})
},
// 添加课程
addCourse() {
course
.addCourseInfo(this.courseInfo)
.then(response => {
// 提示
this.$message({
type: 'success',
message: '添加课程信息成功!'
})
// 跳转到第二步
this.$router.push({ path: '/course/chapter/' + response.data.courseId })
})
},
// 修改课程
updateCourse() {
course
.updateCourseInfo(this.courseInfo)
.then(response => {
// 提示
this.$message({
type: 'success',
message: '修改课程信息成功!'
})
// 跳转
this.$router.push({ path: '/course/chapter/' + this.courseId })
})
},
saveOrUpdate() {
// 判断添加还是修改
if (!this.courseInfo.id) {
this.addCourse()
} else {
this.updateCourse()
}
}
}
}
</script>
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
富文本编辑器 Tinymce
Tinymce是一个传统 JavaScript 插件,默认不能用于 Vue.js 因此需要做一些特殊的整合步骤。
1、组件初始化
(1)复制脚本库
- src/components 目录添加:Tinymce
- static 目录添加:tinymce4.7.5
资料下载:富文本编辑器组件.rar
(2)配置html变量
在 /build/webpack.dev.conf.js 中添加配置,使在html页面中可是使用这里定义的BASE_URL变量:
templateParameters: {
BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}
(3)引入js脚本
在guli-admin-1010/index.html 中引入js脚本
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
<script src=<%= BASE_URL %>/tinymce4.7.5/langs/zh_CN.js></script>
2、使用组件
为了让 Tinymce 能用于 Vue.js 项目,vue-element-admin-master 对 Tinymce 进行了封装,下面我们将它引入到我们的课程信息页面。
(1)引入和声明组件
前面课程信息组件 views/edu/course/info.vue 中已经引入了 Tinymce:
(2)组件模版
同样已经提供好了:
(3)组件样式
在info.vue文件的最后添加如下代码,调整上传图片按钮的高度,同样已经提供好了:
(4)bug修复
entity/vo 目录下的 CourseInfoVo 类,由于自动生成器的bug,需要手动添加 subjectParentId 字段:
二、课程大纲列表功能
后端实现
1、创建实体类
在entity.vo包下创建实体类:
@Data
public class ChapterVo {
private String id;
private String title;
// 表示小节
private List<VideoVo> children = new ArrayList<>();
}
@Data
public class VideoVo {
private String id;
private String title;
}
2、controller 层
EduChapterController
@Api(description = "课程大纲")
@RestController
@RequestMapping("/eduservice/chapter")
@CrossOrigin
public class EduChapterController {
@Autowired
private EduChapterService chapterService;
/**
* 1、根据课程id查询课程大纲列表
*/
@ApiOperation(value = "课程大纲列表")
@GetMapping("getChapterVideo/{courseId}")
public R getChapterVideo(@PathVariable String courseId) {
List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("allChapterVideo", list);
}
/**
* 2、根据章节id查询
*/
@ApiOperation(value = "根据章节id查询")
@GetMapping("getChapterInfo/{chapterId}")
public R getChapterInfo(@PathVariable String chapterId) {
EduChapter eduChapter = chapterService.getById(chapterId);
return R.ok().data("chapter", eduChapter);
}
/**
* 3、添加章节
*/
@ApiOperation(value = "添加章节")
@PostMapping("addChapter")
public R addChapter(@RequestBody EduChapter eduChapter) {
chapterService.save(eduChapter);
return R.ok();
}
/**
* 4、修改章节
*/
@ApiOperation(value = "修改章节")
@PostMapping("updateChapter")
public R updateChapter(@RequestBody EduChapter eduChapter) {
chapterService.updateById(eduChapter);
return R.ok();
}
/**
* 5、删除章节
*/
@ApiOperation(value = "删除章节")
@DeleteMapping("delete/{chapterId}")
public R deleteChapter(@PathVariable String chapterId) {
boolean flag = chapterService.deleteChapter(chapterId);
if (flag) {
return R.ok();
}
return R.error();
}
}
EduVideoController
@RestController
@RequestMapping("/eduservice/video")
@CrossOrigin
public class EduVideoController {
@Autowired
private EduVideoService videoService;
/**
* 1、添加小节
*/
@PostMapping("addVideo")
public R addVideo(@RequestBody EduVideo eduVideo) {
videoService.save(eduVideo);
return R.ok();
}
/**
* 2、删除小节
* 后面这个方法需要完善,删除小节的时候,同时把视频也删除
*/
@DeleteMapping("delete/{id}")
public R deleteVideo(@PathVariable String id) {
videoService.removeById(id);
return R.ok();
}
}
EduCourseController回显方法:
@ApiOperation("根据课程id查询课程基本信息")
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId) {
CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);
return R.ok().data("courseInfoVo", courseInfoVo);
}
@ApiOperation("修改课程信息")
@PostMapping("updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
courseService.updateCourseInfo(courseInfoVo);
return R.ok();
}
3、service 层
(1)service接口
public interface EduChapterService extends IService<EduChapter> {
List<ChapterVo> getChapterVideoByCourseId(String courseId);
boolean deleteChapter(String chapterId);
}
EduCourseService回显:
public interface EduCourseService extends IService<EduCourse> {
// 添加课程信息
String saveCourseInfo(CourseInfoVo courseInfoVo);
//获取课程信息
CourseInfoVo getCourseInfo(String courseId);
//修改课程信息
void updateCourseInfo(CourseInfoVo courseInfoVo);
}
(2)实现类
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
@Autowired
private EduVideoService videoService;//注入小节service
//课程大纲列表,根据课程id进行查询
@Override
public 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);
//创建list集合,用于最终封装数据
List<ChapterVo> finalList = new ArrayList<>();
//3 遍历查询章节list集合进行封装
//遍历查询章节list集合
for (int i = 0; i < eduChapterList.size(); i++) {
//每个章节
EduChapter eduChapter = eduChapterList.get(i);
//eduChapter对象值复制到ChapterVo里面
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(eduChapter, chapterVo);
//把chapterVo放到最终list集合
finalList.add(chapterVo);
//创建集合,用于封装章节的小节
List<VideoVo> videoList = new ArrayList<>();
//4 遍历查询小节list集合,进行封装
for (int m = 0; m < eduVideoList.size(); m++) {
//得到每个小节
EduVideo eduVideo = eduVideoList.get(m);
//判断:小节里面chapterid和章节里面id是否一样
if (eduVideo.getChapterId().equals(eduChapter.getId())) {
//进行封装
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(eduVideo, videoVo);
//放到小节封装集合
videoList.add(videoVo);
}
}
//把封装之后小节list集合,放到章节对象里面
chapterVo.setChildren(videoList);
}
return finalList;
}
}
EduCourseServiceImpl回显方法:
// 根据课程id查询课程基本信息
@Override
public CourseInfoVo getCourseInfo(String courseId) {
// 1、查询课程表
EduCourse eduCourse = baseMapper.selectById(courseId);
CourseInfoVo courseInfoVo = new CourseInfoVo();
BeanUtils.copyProperties(eduCourse, courseInfoVo);
// 2、查询课程描述表
EduCourseDescription courseDescription = courseDescriptionService.getById(courseId);
courseInfoVo.setDescription(courseDescription.getDescription());
return courseInfoVo;
}
// 修改课程信息
@Override
public void updateCourseInfo(CourseInfoVo courseInfoVo) {
// 1、修改课程表
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
boolean update = updateById(eduCourse);
if(!update) {
throw new GuliException(20001,"修改课程信息失败");
}
// 2、修改描述表
EduCourseDescription description = new EduCourseDescription();
description.setId(courseInfoVo.getId());
description.setDescription(courseInfoVo.getDescription());
courseDescriptionService.updateById(description);
}
前端实现
1、js
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 添加章节
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/delete/${chapterId}`,
method: 'delete'
})
}
}
创建 course.js:
import request from '@/utils/request'
export default {
// 1 添加课程信息
addCourseInfo(courseInfo) {
return request({
url: '/eduservice/course/addCourseInfo',
method: 'post',
data: courseInfo
})
},
// 2 查询所有讲师
getListTeacher() {
return request({
url: '/eduservice/teacher/findAll',
method: 'get'
})
},
// 3 根据课程id查询课程基本信息
getCourseInfoById(courseId) {
return request({
url: `/eduservice/course/getCourseInfo/${courseId}`,
method: 'get'
})
},
// 4 修改课程信息
updateCourseInfo(courseInfo) {
return request({
url: '/eduservice/course/updateCourseInfo',
method: 'post',
data: courseInfo
})
}
}
创建 video.js:
import request from '@/utils/request'
export default {
// 添加小节
addVideo(video) {
return request({
url: '/eduservice/video/addVideo',
method: 'post',
data: video
})
},
// 删除小节
deleteVideo(id) {
return request({
url: `/eduservice/video/delete/${id}`,
method: 'delete'
})
}
}
2、vue 组件
edu/course/chapter.vue 文件:
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="最终发布"/>
</el-steps>
<el-button type="text" @click="openChapterDialog()">添加章节</el-button>
<!-- 章节 -->
<ul class="chapterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button style="" type="text" @click="openVideo(chapter.id)">添加小节</el-button>
<el-button style="" type="text" @click="openEditChatper(chapter.id)">编辑</el-button>
<el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
</span>
</p>
<!-- 小节 -->
<ul class="chapterList videoList">
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
<span class="acts">
<el-button style="" type="text">编辑</el-button>
<el-button type="text" @click="removeVideo(video.id)">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</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>
<!-- 添加和修改课时表单 -->
<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.isFree">
<el-radio :label="true">免费</el-radio>
<el-radio :label="false">默认</el-radio>
</el-radio-group>
</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>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
import video from '@/api/edu/video'
export default {
data() {
return {
saveBtnDisabled: false,
courseId: '', // 课程id
chapterVideoList: [],
chapter: { // 封装章节数据
title: '',
sort: 0
},
video: { // 封装小节数据
title: '',
sort: 0,
isFree: false,
videoSourceId: ''
},
dialogChapterFormVisible: false, // 章节弹框
dialogVideoFormVisible: false // 小节弹框
}
},
created() {
// 获取路由的id值
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
// 根据课程id查询章节和小节
this.getChapterVideo()
}
},
methods: {
// ======================小节操作======================
//删除小节
removeVideo(id) {
this.$confirm('此操作将删除小节, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { //点击确定,执行then方法
//调用删除的方法
video.deleteVideo(id)
.then(response => {//删除成功
//提示信息
this.$message({
type: 'success',
message: '删除小节成功!'
});
//刷新页面
this.getChapterVideo()
})
}) //点击取消,执行catch方法
},
//修改小节弹框数据回显
openEditVideo(videoId) {
//弹框
this.dialogVideoFormVisible = true
//调用接口
video.getVideo(videoId)
.then(response => {
this.video = response.data.video
})
},
//添加小节弹框的方法
openVideo(chapterId) {
//弹框
this.dialogVideoFormVisible = true
//清空数据
this.video.id = ''
this.video.title = ''
this.video.sort = 0
this.video.isFree = false
this.video.videoSourceId = '',
// this.video.videoOriginalName = ''//视频点播功能需要打开这里的注释
//设置章节id
this.video.chapterId = chapterId
},
//添加小节
addVideo() {
//设置课程id
this.video.courseId = this.courseId
video.addVideo(this.video)
.then(response => {
//关闭弹框
this.dialogVideoFormVisible = false
//提示
this.$message({
type: 'success',
message: '添加小节成功!'
});
//刷新页面
this.getChapterVideo()
})
},
//修改小节
updateVideo() {
//设置课程id
this.video.courseId = this.courseId
video.updateVideo(this.video)
.then(response => {
//关闭弹框
this.dialogVideoFormVisible = false
//提示
this.$message({
type: 'success',
message: '修改小节成功!'
});
//刷新页面
this.getChapterVideo()
})
},
saveOrUpdateVideo() {
console.log("小节id"+this.video.id)
if (!this.video.id) {
this.addVideo()
} else {
this.updateVideo()
}
// this.video.id = ''
},
// ======================章节操作======================
//删除章节
removeChapter(chapterId) {
this.$confirm('此操作将删除章节, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { //点击确定,执行then方法
//调用删除的方法
chapter.deleteChapter(chapterId)
.then(response => {//删除成功
//提示信息
this.$message({
type: 'success',
message: '删除成功!'
});
//刷新页面
this.getChapterVideo()
})
}) //点击取消,执行catch方法
},
//修改章节弹框数据回显
openEditChatper(chapterId) {
//弹框
this.dialogChapterFormVisible = true
//调用接口
chapter.getChapter(chapterId)
.then(response => {
this.chapter = response.data.chapter
})
},
//弹出添加章节页面
openChapterDialog() {
//弹框
this.dialogChapterFormVisible = true
//表单数据清空
this.chapter.title = ''
this.chapter.sort = ''
},
//添加章节
addChapter() {
//设置课程id到chapter对象里面
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter)
.then(response => {
//关闭弹框
this.dialogChapterFormVisible = false
//提示
this.$message({
type: 'success',
message: '添加章节成功!'
});
//刷新页面
this.getChapterVideo()
})
},
//修改章节的方法
updateChapter() {
chapter.updateChapter(this.chapter)
.then(response => {
//关闭弹框
this.dialogChapterFormVisible = false
//提示
this.$message({
type: 'success',
message: '修改章节成功!'
});
//刷新页面
this.getChapterVideo()
})
},
saveOrUpdate() {
if (!this.chapter.id) {
this.addChapter()
} else {
this.updateChapter()
}
},
//根据课程id查询章节和小节
getChapterVideo() {
chapter.getAllChapterVideo(this.courseId)
.then(response => {
this.chapterVideoList = response.data.allChapterVideo
})
},
previous() {
// 跳转到上一步
this.$router.push({ path: '/course/info/' + this.courseId })
},
next() {
// 跳转到第二步
this.$router.push({ path: '/course/publish/' + this.courseId })
}
}
}
</script>
<style scoped>
.chapterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chapterList li{
position: relative;
}
.chapterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chapterList .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>
三、课程最终发布功能
后端实现
1、定义vo
CoursePublishVo
@Data
public class CoursePublishVo {
private String id;
private String title;
private String cover;
private Integer lessonNum;
private String subjectLevelOne;
private String subjectLevelTwo;
private String teacherName;
private String price;// 只用于显示
}
2、mapper 层
EduCourseMapper
public interface EduCourseMapper extends BaseMapper<EduCourse> {
CoursePublishVo getPublishCourseInfo(String courseId);
}
xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jyunkai.eduservice.mapper.EduCourseMapper">
<!--sql语句,根据课程id查询课程确认信息-->
<select id="getPublishCourseInfo" resultType="org.jyunkai.eduservice.entity.vo.CoursePublishVo">
SELECT
ec.id,
ec.title,
ec.price,
ec.lesson_num AS lessonNum,
ec.cover,
et.name AS teacherName,
es1.title AS subjectLevelOne,
es2.title AS subjectLevelTwo
FROM
edu_course ec
LEFT JOIN edu_course_description ecd ON ec.id = ecd.id
LEFT JOIN edu_teacher et ON ec.teacher_id = et.id
LEFT JOIN edu_subject es1 ON ec.subject_parent_id = es1.id
LEFT JOIN edu_subject es2 ON ec.subject_id = es2.id
WHERE
ec.id = #{courseId}
</select>
</mapper>
service模块的pom文件添加:
其中:两个代表多层目录,一个代表一层目录
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
application.properties添加:
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/eduservice/mapper/xml/*.xml
3、controller层
EduCourseController添加方法:
@ApiOperation("根据课程id查询课程确认信息")
@GetMapping("getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id) {
CoursePublishVo coursePublishVo = courseService.publishCourseInfo(id);
return R.ok().data("publishCourse", coursePublishVo);
}
@PostMapping("publishCourse/{id}")
public R publishCourse(@PathVariable String id) {
EduCourse eduCourse = new EduCourse();
eduCourse.setId(id);
eduCourse.setStatus("Normal");
courseService.updateById(eduCourse);
return R.ok();
}
4、service 层
EduCourseService 添加方法:
// 根据课程id查询课程确认信息
CoursePublishVo publishCourseInfo(String id);
EduCourseServiceImpl 添加方法:
// 根据课程id查询课程确认信息
@Override
public CoursePublishVo publishCourseInfo(String id) {
return baseMapper.getPublishCourseInfo(id);
}
前端实现
1、js
course.js添加方法
// 5 课程确认信息显示
getPublishCourseInfo(id) {
return request({
url: `/eduservice/course/getPublishCourseInfo/${id}`,
method: 'get'
})
},
// 6 课程最终发布
publishCourse(id) {
return request({
url: `/eduservice/course/publishCourse/${id}`,
method: 'post'
})
}
2、vue
publish.vue
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps
:active="3"
process-status="wait"
align-center
style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="发布课程"/>
</el-steps>
<div class="ccInfo">
<img :src="coursePublish.cover">
<div class="main">
<h2>
{{ coursePublish.title }}
</h2>
<p class="gray">
<span>
共{{ coursePublish.lessonNum }}课时
</span>
</p>
<p>
<span>
所属分类:{{ coursePublish.subjectLevelOne }} — {{ coursePublish.subjectLevelTwo }}
</span>
</p>
<p>
课程讲师:{{ coursePublish.teacherName }}
</p>
<h3 class="red">
¥{{ coursePublish.price }}
</h3>
</div>
</div>
<div>
<el-button @click="previous">返回修改</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="publish">发布课程</el-button>
</div>
</div>
</template>
<script>
import course from '@/api/edu/course'
export default {
data() {
return {
saveBtnDisabled: false,
courseId: '',
coursePublish: {}
}
},
created() {
// 获取路由课程id值
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
// 调用接口方法根据课程id查询
this.getCoursePublishById()
}
},
methods: {
// 根据课程id查询
getCoursePublishById() {
course
.getPublishCourseInfo(this.courseId)
.then(response => {
this.coursePublish = response.data.publishCourse
})
},
previous() {
this.$route.push({ path: '/course/chapter/1' })
},
publish() {
course
.publishCourse(this.courseId)
.then(response => {
// 提示信息
this.$message({
type: 'success',
message: '课程发布成功!'
})
// 跳转课程列表页面
this.$route.push({ path: '/course/list' })
})
}
}
}
</script>
<style scoped>
.ccInfo {
background: #f5f5f5;
padding: 20px;
overflow: hidden;
border: 1px dashed #DDD;
margin-bottom: 40px;
position: relative;
}
.ccInfo img {
background: #d6d6d6;
width: 500px;
height: 278px;
display: block;
float: left;
border: none;
}
.ccInfo .main {
margin-left: 520px;
}
.ccInfo .main h2 {
font-size: 28px;
margin-bottom: 30px;
line-height: 1;
font-weight: normal;
}
.ccInfo .main p {
margin-bottom: 10px;
word-wrap: break-word;
line-height: 24px;
max-height: 48px;
overflow: hidden;
}
.ccInfo .main p {
margin-bottom: 10px;
word-wrap: break-word;
line-height: 24px;
max-height: 48px;
overflow: hidden;
}
.ccInfo .main h3 {
left: 540px;
bottom: 20px;
line-height: 1;
font-size: 28px;
color: #d32f24;
font-weight: normal;
position: absolute;
}
</style>
四、课程列表功能
后端
EduCourseController添加方法:
// 课程列表 基本实现
// TODO 完善条件查询带分页
@ApiOperation("课程列表")
@GetMapping
public R getCourseList() {
List<EduCourse> list = courseService.list(null);
return R.ok().data("list", list);
}
// 删除课程
@ApiOperation("删除课程")
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId) {
courseService.removeCourse(courseId);
return R.ok();
}
EduCourseServiceImpl 添加:
// 注入章节小节的service
@Autowired
private EduVideoService videoService;
@Autowired
private EduChapterService chapterService;
// 删除课程
@Override
public void removeCourse(String courseId) {
// 1、根据课程id删除小节
videoService.removeVideoByCourseId(courseId);
// 2、根据课程id删除章节
chapterService.removeChapterByCourseId(courseId);
// 3、根据课程id删除描述
courseDescriptionService.removeById(courseId);
// 4、根据课程id删除课程本身
int result = baseMapper.deleteById(courseId);
if (result == 0) {
throw new GuliException(20001, "删除失败");
}
}
EduChapterServiceImpl 添加:
// 根据课程id删除章节
@Override
public void removeChapterByCourseId(String courseId) {
QueryWrapper<EduChapter> wrapper = new QueryWrapper<>();
wrapper.eq("course_id", courseId);
baseMapper.delete(wrapper);
}
EduVideoServiceImpl 添加:
// 根据课程id删除小节
// TODO 删除小节的时候,还要删除对应的视频
@Override
public void removeVideoByCourseId(String courseId) {
QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
wrapper.eq("course_id", courseId);
baseMapper.delete(wrapper);
}
前端
course.js
// TODO 7 课程列表
getListCourse() {
return request({
url: '/eduservice/course',
method: 'get'
})
}
edu/course/list.vue
<template>
<div class="app-container">
课程列表
<!--查询表单-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item>
<el-input v-model="courseQuery.title" placeholder="课程名称"/>
</el-form-item>
<el-form-item>
<el-select v-model="courseQuery.status" clearable placeholder="课程状态">
<el-option value="Normal" label="已发布"/>
<el-option value="Draft" label="未发布"/>
</el-select>
</el-form-item>
<el-button type="primary" icon="el-icon-search" @click="getList()">查询</el-button>
<el-button type="default" @click="resetData()">清空</el-button>
</el-form>
<!-- 表格 -->
<el-table
:data="list"
border
fit
highlight-current-row>
<el-table-column
label="序号"
width="70"
align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="title" label="课程名称" width="80" />
<el-table-column label="课程状态" width="80">
<template slot-scope="scope">
{{ scope.row.status==='Normal'?'已发布':'未发布' }}
</template>
</el-table-column>
<el-table-column prop="lessonNum" label="课时数" />
<el-table-column prop="gmtCreate" label="添加时间" width="160"/>
<el-table-column prop="viewCount" label="浏览数量" width="60" />
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<router-link :to="'/teacher/edit/'+scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit">编辑课程基本信息</el-button>
</router-link>
<router-link :to="'/teacher/edit/'+scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit">编辑课程大纲息</el-button>
</router-link>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeDataById(scope.row.id)">删除课程信息</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
:current-page="page"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center;"
layout="total, prev, pager, next, jumper"
@current-change="getList"
/>
</div>
</template>
<script>
import course from '@/api/edu/course'
export default {
data() { // 定义变量和初始值
return {
list: null, // 查询之后接口返回集合
page: 1, // 当前页
limit: 10, // 每页记录数
total: 0, // 总记录数
courseQuery: {} // 条件封装对象
}
},
created() { // 页面渲染之前执行,一般调用methods定义的方法
// 调用
this.getList()
},
methods: {// 创建具体的方法,调用teacher.js定义的方法
// 讲师列表的方法
getList() {
course.getListCourse()
.then(response => { // 请求成功
// response接口返回的数据
this.list = response.data.list
})
},
resetData() { // 清空的方法
// 表单输入项数据清空
this.courseQuery = {}
// 查询所有讲师数据
this.getList()
}
}
}
</script>
最终测试
启动前后端项目: