谷粒学院——第八章、课程管理

一、课程添加功能

概览

课程添加的步骤

04-课程发布流程的说明.png

课程相关表的关系

05-课程相关表关系.png

后端实现

1、代码生成器

只修改表名即可,依次填入:“edu_course”, “edu_course_description”, “edu_chapter”, “edu_video”
image.png
生成完成后,

  • 删除EduCourseDescriptionController类,因为简介在课程里面去做就行了。
  • 在所有entity包下的实体类中,创建和更新时间字段处,添加下面的注解:

image.png

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 代表手动设置而非自动生成。
image.png

5、测试

  • 先修改表结构 edu_course 表结构,至少让 subject_id 和 subject_parent_id 字段可以为 null,teacher_id 和 cover 为 null 是为了后面
  • 测试时候不添加 id,因为是自动生成的。

image.png
然后启动项目:访问:http://localhost:8001/swagger-ui.html,传入一些基本的数据如下:
image.png
点击try it out!,成功:
image.png
数据库中的表数据添加成功:
image.png

前端实现

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 的图片作为课程的默认封面,例如下面的图片:
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
}

image.png

(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>

image.png

2、使用组件

为了让 Tinymce 能用于 Vue.js 项目,vue-element-admin-master 对 Tinymce 进行了封装,下面我们将它引入到我们的课程信息页面。

(1)引入和声明组件

前面课程信息组件 views/edu/course/info.vue 中已经引入了 Tinymce:
image.png

(2)组件模版

同样已经提供好了:
image.png

(3)组件样式

在info.vue文件的最后添加如下代码,调整上传图片按钮的高度,同样已经提供好了:
image.png

(4)bug修复

entity/vo 目录下的 CourseInfoVo 类,由于自动生成器的bug,需要手动添加 subjectParentId 字段:
image.png

二、课程大纲列表功能

后端实现

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>

最终测试

启动前后端项目:
image.png
image.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肉丝不切片

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值