课程管理模块
1. 需求分析
2. 配置路由
{
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: 'EduCourseInfoEdit',
component: () => import('@/views/edu/course/chapter'),
meta: { title: '编辑课程大纲', noCache: 'true' },
hidden: true
},
{
path: 'publish/:id',
name: 'EduCourseInfoEdit',
component: () => import('@/views/edu/course/publish'),
meta: { title: '发布课程', noCache: 'true' },
hidden: true
}
]
},
1. 创建路由对应的页面
- 课程信息页面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>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">保存并下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('info created')
},
methods: {
next() {
console.log('next')
this.$router.push({ path: '/edu/course/chapter/1' })
}
}
}
</script>
- 课程大纲页面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-form label-width="120px">
<el-form-item>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('chapter created')
},
methods: {
previous() {
console.log('previous')
this.$router.push({ path: '/edu/course/info/1' })
},
next() {
console.log('next')
this.$router.push({ path: '/edu/course/publish/1' })
}
}
}
</script>
- 课程发布页面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>
<el-form label-width="120px">
<el-form-item>
<el-button @click="previous">返回修改</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="publish">发布课程</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('publish created')
},
methods: {
previous() {
console.log('previous')
this.$router.push({ path: '/edu/course/chapter/1' })
},
publish() {
console.log('publish')
this.$router.push({ path: '/edu/course/list' })
}
}
}
</script>
2. 启动测试发现可以来回跳转
3. 编写三个页面
1. 页面一后端代码
- 根据表educhapter educourse educoursedescription eduvideo生成对应的entity....文件
- 修改EduCourseDescription的主键生成策略,改为input手动输入,并修改创建时间和修改时间
- 在对应的controller中创建方法
@RestController
@RequestMapping("/eduservice/course")
@CrossOrigin
public class EduCourseController {
@Autowired
private EduCourseService courseService;
//添加课程基本信息的方法
@PostMapping("addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
//返回添加之后的id ,为了前端添加使用
String id = courseService.saveCourseInfo(courseInfoVo);
return R.ok().data("courseId",id);
}
}
- 定义业务层方法
//添加课程基本信息的方法
String saveCourseInfo(CourseInfoVo courseInfoVo);
- 定义实现类中的方法
package com.sli.eduservice.service.impl;
import com.sli.eduservice.entity.EduCourse;
import com.sli.eduservice.entity.EduCourseDescription;
import com.sli.eduservice.entity.vo.CourseInfoVo;
import com.sli.eduservice.mapper.EduCourseMapper;
import com.sli.eduservice.service.EduCourseDescriptionService;
import com.sli.eduservice.service.EduCourseService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sli.servicebase.exceptionhandler.GuliException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 课程 服务实现类
* </p>
*
* @author sli
* @since 2021-10-21
*/
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
//课程描述的注入
@Autowired
private EduCourseDescriptionService courseDescriptionService;
//添加课程基本信息的方法
@Override
public String saveCourseInfo(CourseInfoVo courseInfoVo) {
//1. 向课程表中添加课程基本信息
//CourseInfoVo转换成eduCourse对象
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo,eduCourse);//将courseInfoVo中的值放入eduCourse中
int insert = baseMapper.insert(eduCourse);
if (insert <= 0){
//添加失败
throw new GuliException(20001,"添加课程信息失败");
}
//3.因为测试完之后发现两个生成的id不相同,所以需要设置id
//获取添加之后的id---->4
String cid = eduCourse.getId();
//2. 向课程简介表添加课程信息
EduCourseDescription courseDescription = new EduCourseDescription();
courseDescription.setDescription(courseInfoVo.getDescription());
//4. 设置描述的id就是课程的id
courseDescription.setId(cid);
courseDescriptionService.save(courseDescription);
return cid;
}
}
- swagger测试
2. 页面一前端的实现
- 定义api
import request from '@/utils/request'
export default {
//1 添加课程信息功能
/*2.ajax:
url就是待载入的页面地址(controller请求)
data就是待发送的key/value参数(键值对)
success约等于then(回调函数)
success中的data约等于response
*/
addCourseInfo(courseInfo) {
return request({
url: '/eduservice/course/addCourseInfo',
method: 'post',
data:courseInfo
})
},
//查询所有的讲师
getListTeacher(){
return request({
url: '/eduservice/teacher/findAll',
method: 'get',
})
}
}
- 以下的实现(级联显示,还包含了下拉显示所有的讲师)
- 将课程简介加入Tinymce可视化插件
3.1. 将脚本分别复制到common和static目录下面
3.2. 在guli-admin/build/webpack.dev.conf.js下面添加配置
new HtmlWebpackPlugin({
......,
templateParameters: {
BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}
})
3.3. 在/index.html中引入js脚本
<body>
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
<script src=<%= BASE_URL %>/tinymce4.7.5/langs/zh_CN.js></script>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
3.4 在要使用的页面引入组件
- 页面的完整实现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>
<!-- 所属分类 TODO -->
<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="二级分类" @change="subjectLevelOneChanged">
<el-option
v-for="subject in subjectTwoList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>
</el-form-item>
<!-- 课程讲师 TODO -->
<!-- 课程讲师 -->
<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>
<!-- 课程简介 TODO -->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
<!-- 课程封面 TODO -->
<!-- 课程封面-->
<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,
subjectParentId:'',//一级分类id
subjectId:''//二级分类id
},
//封装所有讲师的数据
teacherList:[],
subjectOneList:[],//一级分类
subjectTwoList:[],//二级分类
BASE_API: process.env.BASE_API//接口api地址
};
},
created() {
//初始化所有讲师
this.getListTeacher()
//初始化一级分类
this.getOneSubject()
},
methods: {
//上传封面成功调用的方法
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值
//遍历所有的分类(里面包含所有的一级和二级,然后比较value和一级的值来判断二级是哪个)
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
})
},
saveOrUpdate() {
course.addCourseInfo(this.courseInfo)
.then(response => {
//提示
this.$message({
type: 'success',
message: '添加课程信息成功!'
});
//跳转到第二步
this.$router.push({path:'/course/chapter/'+response.data.courseId})
})
}
},
};
</script>
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
3. 启动测试-->成功
4. 页面二后端代码编写
以上是实现了添加课程的代码,现在点击上一步要回显页面,还有增加章节的CRUD代码和小节的CRUD代码
1. 回显数据
EduChapterController
//根据课程id查询课程基本信息
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId) {
CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);
return R.ok().data("courseInfoVo",courseInfoVo);
}
EduCourseService
CourseInfoVo getCourseInfo(String courseId);
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;
}
注意:数据库的edu_course中is_delete键有问题,记得修改
2. 章节的CRUD
需要先创建两个对应的vo类chapter.ChapterVo和chapter.Video
EduChapterController
@Autowired
private EduChapterService chapterService;
//课程大纲列表,根据课程id进行查询
@GetMapping("getChapterVideo/{courseId}")
public R getChapterVideo(@PathVariable String courseId){
List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("allChapterVideo",list);
}
//添加章节
@PostMapping("addChapter")
public R addChapter(@RequestBody EduChapter eduChapter){
chapterService.save(eduChapter);
return R.ok();
}
//根据章节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();
}
//删除
@DeleteMapping("{chapterId}")
public R deleteChapter(@PathVariable String chapterId){
boolean flag = chapterService.deleteChapter(chapterId);
if (flag) {
return R.ok();
}else {
return R.error();
}
}
EduChapterService
List<ChapterVo> getChapterVideoByCourseId(String courseId);
boolean deleteChapter(String chapterId);
EduChapterServiceImpl
@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<>();
wrapperChapter.eq("course_id",courseId);
List<EduVideo> eduVideoList = videoService.list(wrapperVideo);
//3. 遍历查询的list集合进行封装
//创建list用于封装
List<ChapterVo> finalList = new ArrayList<>();
//遍历查询到的章节集合
for (int i = 0; i < eduChapterList.size(); i++) {
EduChapter eduChapter = eduChapterList.get(i);
//将eduChapter对象的值复制到ChapterVo中
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(eduChapter,chapterVo);
finalList.add(chapterVo);
//创建集合,用于封装章节中的小节
List<VideoVo> videoList = new ArrayList<>();
//4. 遍历查询小节的list集合,进行封装
for (int m = 0; m < eduVideoList.size(); m++) {
//得到每个小结
EduVideo eduVideo = eduVideoList.get(m);
//判断:小节里面chapter id 和章节中的id是否一样
if (eduVideo.getChapterId().equals(eduChapter.getId())){
//进行封装
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(eduVideo,videoVo);
//放入小节封装的集合中
videoList.add(videoVo);
}
}
//将封装之后的小结list集合封装到章节中
chapterVo.setChildren(videoList);
}
return finalList;
}
//删除章节的方法
@Override
public boolean deleteChapter(String chapterId) {
//根据chapterId查询小节表,如果查到,不删
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;
}
}
小节的CRUD
EduVideoController
@Autowired
private EduVideoService videoService;
//添加
@PostMapping("addVideo")
public R addVideo(@RequestBody EduVideo eduVideo){
videoService.save(eduVideo);
return R.ok();
}
//删除
// TODO 后面需要完善:删除小节的时候同时把里面的视屏删除
@DeleteMapping("{id}")
public R deleteVideo(@PathVariable String id){
videoService.removeById(id);
return R.ok();
}
5. 页面二前端代码实现
编写前端代码记得编写api
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="chanpterList">
<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="chanpterList 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.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>
</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,
free: 0,
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方法
},
//添加小节弹框的方法
openVideo(chapterId) {
//弹框
this.dialogVideoFormVisible = true
//设置章节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()
})
},
saveOrUpdateVideo() {
this.addVideo()
},
//==============================章节操作====================================
//删除章节
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 = 0
},
//添加章节
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>
.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>
点击查看代码
<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>
<!-- 所属分类 TODO -->
<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>
<!-- 课程讲师 TODO -->
<!-- 课程讲师 -->
<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>
<!-- 课程简介 TODO -->
<!-- 课程简介-->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
<!-- 课程封面 TODO -->
<!-- 课程封面-->
<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:[]//二级分类
}
},
created() {
//获取路由id值
if(this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
//调用根据id查询课程的方法
this.getInfo()
} else {
//初始化所有讲师
this.getListTeacher()
//初始化一级分类
this.getOneSubject()
}
},
methods:{
//根据课程id查询
getInfo() {
course.getCourseInfoId(this.courseId)
.then(response => {
//在courseInfo课程基本信息,包含 一级分类id 和 二级分类id
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]
//比较当前courseInfo里面一级分类id和所有的一级分类id
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>
6.页面三后端代码编写
最终发布页面的实现需要回显前面两个的数据
select ec.id,ec.title,ec.price,ec.lesson_num,ec.cover,
et.name as teacherName,
es1.title as subjectLevelOne,
es2.title as subjectLevelTwo
from edu_course ec left outer join edu_course_description ecd on ec.id=ecd.id
left outer join edu_teacher et on ec.teacher_id=et.id
left outer join edu_subject es1 on ec.subject_parent_id=es1.id
left outer join edu_subject es2 on ec.subject_parent_id=es2.id
where ec.id=#{courseId}
- 编写vo类
@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;
}
- 项目中编写mapper接口
EduCourseMapper
//根据课程id查询课程基本信息的方法
public CoursePublishVo getPublishCourseInfo(String courseId);
- 编写xml文件(EduCourseMapper.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="com.sli.eduservice.mapper.EduCourseMapper">
<!--根据课程id查询课程的确认信息,使用了左连接,查询了四张表中的内容-->
<select id="getPublishCourseInfo" resultType="com.sli.eduservice.entity.vo.CoursePublishVo">
select ec.id,ec.title,ec.price,ec.lesson_num,ec.cover,
et.name as teacherName,
es1.title as subjectLevelOne,
es2.title as subjectLevelTwo
from edu_course ec left outer join edu_course_description ecd on ec.id=ecd.id
left outer join edu_teacher et on ec.teacher_id=et.id
left outer join edu_subject es1 on ec.subject_parent_id=es1.id
left outer join edu_subject es2 on ec.subject_parent_id=es2.id
where ec.id=#{courseId}
</select>
</mapper>
- 业务层在controller中
//根据课程id查询课程确认信息
@GetMapping("getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id){
CoursePublishVo coursePublishVo = courseService.publishCourseInfo(id);
return R.ok().data("publishCourse",coursePublishVo);
}
- service中
CoursePublishVo publishCourseInfo(String id);
- service实现类中调用
//根据课程id查询课程确认信息
@Override
public CoursePublishVo publishCourseInfo(String id) {
//调用写好的mapper
CoursePublishVo publishCourseInfo = baseMapper.getPublishCourseInfo(id);
return publishCourseInfo;
}
- 测试出现问题
问题:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.sli.eduservice.mapper.EduCourseMapper.getPublishCourseInfo
问题分析:dao层编译之后只有class文件,没有mapper.xml,因为maven工程在默认情况下将src/main/java目录下面的所有资源文件是不发布到target目录下面的.
解决方案:在pom.xml中配置如下的代码,并且在application.properties中加入如下的代码
<build>
<resources>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filterling>false</filterling>
</resources>
</build>
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/sli/eduservice/mapper/xml/*.xml
重新打包target会出现xml文件
8. 课程最终发布需要修改状态(status)将Draft改为Normal
EduCourseController.java
//课程最终发布
//需要修改课程的状态(未发布为default.已发布为normal)
@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();
}
7. 页面三前端的实现
src.spi.edu.course.js
//根据课程id查询课程基本信息
getCourseInfoId(id) {
return request({
url: '/eduservice/course/getCourseInfo/'+id,
method: 'get'
})
},
//修改课程信息
updateCourseInfo(courseInfo) {
return request({
url: '/eduservice/course/updateCourseInfo',
method: 'post',
data: courseInfo
})
},
//课程确认信息的回显
getPublihCourseInfo(id) {
return request({
url: '/eduservice/course/getPublishCourseInfo/'+id,
method: 'get'
})
},
//课程最终发布
publishCourse(id) {
return request({
url: '/eduservice/course/publishCourse/'+id,
method: 'post'
})
},
edu.list.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.getCoursePublishId()
}
},
methods: {
//根据课程id查询
getCoursePublishId() {
course.getPublihCourseInfo(this.courseId)
.then(response => {
this.coursePublish = response.data.publishCourse
})
},
previous() {
console.log('previous')
this.$router.push({ path: '/course/chapter/1' })
},
publish() {
course.publishCourse(this.courseId)
.then(response => {
//提示
this.$message({
type: 'success',
message: '课程发布成功!'
});
//跳转课程列表页面
this.$router.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>
</script>
8. 页面四后端代码
类似于讲师列表的那个页面,先实现删除的那个按钮和条件分页查询
EduCourseController.java
//课程列表
//条件分页查询
@PostMapping("/pageCourseCondition/{current}/{limit}")
public R pageCourseCondition(@PathVariable long current,
@PathVariable long limit,
@RequestBody(required = false)CourseQuery courseQuery){
//创建一个page对象
Page<EduCourse> pageCourse = new Page<>(current, limit);
//构建wrapper条件
QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
//多条件组合查询,判断条件值是否为空
String title = courseQuery.getTitle();
String status = courseQuery.getStatus();
if (!StringUtils.isEmpty(title)){
wrapper.like("title",title);
}
if (!StringUtils.isEmpty(status)){
wrapper.eq("status",status);
}
//排序,使前端添加之后第一个显示
wrapper.orderByDesc("gmt_create");
//调用方法,实现条件查询分页
courseService.page(pageCourse,wrapper);
long total = pageCourse.getTotal();//总记录数
List<EduCourse> records = pageCourse.getRecords();//数据的list集合
return R.ok().data("total",total).data("rows",records);
}
//删除课程
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId){
courseService.removeCourse(courseId);
return R.ok();
}
实体类CourseQuery(也就是前端的条件)
@Data
public class CourseQuery {
private static final long serialVersionUID = 1L;
private String title;//课程名称,模糊查询
private String status;//默认发布状态 normal为发布 draft为未发布
}
EduCourseService接口
void removeCourse(String courseId);
EduCourseServiceImpl实现类
//删除课程
@Override
public void removeCourse(String courseId) {
//1 根据课程id删除小节
eduVideoService.removeVideoByCourseId(courseId);
//2 根据课程id删除章节
chapterService.removeChapterByCourseId(courseId);
//3 根据课程id删除描述
courseDescriptionService.removeById(courseId);
//4 根据课程id删除课程本身
baseMapper.deleteById(courseId);
}
- 根据课程id删除小节 EduVideoService接口
void removeVideoByCourseId(String courseId);
- 对应的实现类EduVideoServiceImpl
//课程最终发布
//需要修改课程状态
@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();
}
//1. 根据课程id删除小节
//TODO 删除小节需要删除对应的视屏文件
@Override
public void removeVideoByCourseId(String courseId) {
QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
wrapper.eq("course_id",courseId);
baseMapper.delete(wrapper);
}
- 根据课程id删除章节 EduChapterService接口
//2. 根据课程id删除章节
void removeChapterByCourseId(String courseId);
- 对应的实现类EduChapterServiceImpl
//2. 根据课程id删除章节
@Override
public void removeChapterByCourseId(String courseId) {
QueryWrapper<EduChapter> wrapper = new QueryWrapper<>();
wrapper.eq("course_id",courseId);
baseMapper.delete(wrapper);
}
- 根据课程id删除描述 因为删除描述外键就是courseId
- 根据课程id删除课程本身 因为删除本身也是courseId
9. 页面四前端页面的实现
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="text" size="mini" icon="el-icon-edit">编辑课程基本信息</el-button>
</router-link>
<router-link :to="'/teacher/edit/'+scope.row.id">
<el-button type="text" 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(){//在页面渲染之前执行
this.getList()
},
methods:{//创建具体的方法,调用teacher.js定义的方法
//讲师列表
getList(page = 1){
this.page = page
course.getCourseListPage(this.page,this.limit,this.courseQuery)
.then(response =>{
this.list = response.data.rows
this.total = response.data.total
})
.catch(error =>{
console.log(error)
})
},
resetData(){//清空的方法
//表单输入项数据清空
this.courseQuery = {}
//查询所有的讲师数据
this.getList()
},
removeDataById(id){
this.$confirm('此操作将永久删除该课程, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { //点击确定 then方法
//调用删除的方法
course.deleteCourse(id)
.then(response => {//删除成功
//提示信息
this.$message({
type: 'success',
message: '删除成功!'
});
//回到刷新列表页面
this.getList()
})
})
}
}
}
</script>
sec.edu.course.js
//删除课程
deleteCourse(id){
return request({
url: '/eduservice/course/' + id,
method: 'delete'
})
},
//课程列表(条件查询带分页)
//current:当前页,limit每页记录数,courseQuery条件对象
getCourseListPage(current,limit,courseQuery){
return request({
// url: 'eduservice/teacher/pageTeacherCondition/' + current + "/" + limit,
url: `/eduservice/course/pageCourseCondition/${current}/${limit}`,
method: 'post',
data: courseQuery
})
},
未解决问题:按条件查询的时候的课程状态好像不能使用:value=只能是数字
待解决:课程视频的上传
标签:11,return,String,courseId,course,课程,课程管理,模块,id From: https://www.cnblogs.com/sli1f/p/15682407.html