谷粒学院(十)课程管理模块 | 课程大纲列表 | 二级联动 | 富文本编辑器

一、课程发布分析

1、发布流程图

image-20220222215116462

image-20220222215148525

2、数据表对应

20201015201549687

3、课程相关表的关系

image-20220222215449355

二、课程管理–添加课程后端

1、使用代码生成器生成相关课程的代码

image-20220222095006534

2、细节问题

(1)创建vo实体类用户表单数据封装 vo:ViewObject:用于后台向前台显示所创建的实体

(2)把表单提交过来的数据添加到数据库 ==>向两张表添加数据:课程表 和 课程描述表

(3)把讲师和分类使用下拉列表显示 课程分类 做成二级联动

3、创建vo实体类

@ApiModel(value = "课程基本信息", description = "编辑课程基本信息的表单对象")
@Data
public class CourseInfoVo {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "课程ID")
    private String id;

    @ApiModelProperty(value = "课程讲师ID")
    private String teacherId;

    @ApiModelProperty(value = "课程专业ID")
    private String subjectId;

    @ApiModelProperty(value = "课程专业父级ID")
    private String subjectParentId;

    @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;

}

4、编写Controller类

@Api(description = "课程管理")
@RestController
@RequestMapping("/eduservice/course")
@CrossOrigin
public class EduCourseController {

    @Autowired
    private EduCourseService courseService;

    @ApiOperation("添加课程基本信息")
    @PostMapping("saveCourseInfo")
    public R saveCourseInfo(@ApiParam(name = "CourseInfoForm", value = "课程基本信息", required = true)
            @RequestBody CourseInfoVo courseInfoVo){
        //返回课程id,为了跳转到课程大纲使用
        String courseId = courseService.saveCourseInfo(courseInfoVo);
        return R.ok().data("courseId", courseId);
    }

}

5、编写Service类

@Service
@Transactional //开启事务管理
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {

    @Autowired
    private EduCourseDescriptionService courseDescriptionService;

    //添加课程基本信息
    @Override
    public String saveCourseInfo(CourseInfoVo courseInfoVo) {
        //1.添加课程基本信息信息到课程表
        EduCourse eduCourse = new EduCourse();
        BeanUtils.copyProperties(courseInfoVo,eduCourse);
        int insert = this.baseMapper.insert(eduCourse);
        if(insert==0){
            throw new GuLiException(20001, "添加课程失败!");
        }

        //2.添加课程简介信息到简介表
        String id = eduCourse.getId();//获得添加之后的课程id
        EduCourseDescription eduCourseDescription = new EduCourseDescription();
        BeanUtils.copyProperties(courseInfoVo,eduCourseDescription);
        eduCourseDescription.setId(id);//因为两张表是一对一关系,所以id也应该相同
        boolean save = courseDescriptionService.save(eduCourseDescription);
        if(save==false){
            throw new GuLiException(20001, "添加课程失败!");
        }
        return id;
    }
}

注意问题:

(1)课程和描述是一对一关系,添加之后,id应该是相同的。所以描述的id要填入课程的id.
EduCourseDescription类中id 属性@TableId(value = "id", type = IdType.INPUT)

(2)EduCourse类和EduCourseDescription类中的时间需要自动填充@TableField(fill = FieldFill.INSERT)

6、启动Swagger测试

三、课程管理–添加课程前端(填写课程基本信息)

1、添加课程管理路由

api/router/index.js

{
        path: '/course',
        component: Layout, //布局
        redirect: '/course/table',
        name: 'CourseAdd',
        meta: { title: '课程管理', icon: 'form' },
        children: [{
                path: 'list',
                name: 'EduCourseList',
                component: () =>
                    import ('@/views/edu/course/list'),
                meta: { title: '课程列表', icon: 'table' }
            },
            {
                path: 'info',
                name: 'EduCourseInfo',
                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组件

image-20220222221214743

3、整合步骤条组件实现页面跳转

参考:http://element-cn.eleme.io/#/zh-CN/component/steps

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="saveOrUpdate">保存并下一步</el-button>
        </el-form-item>
    </el-form>
  </div>
</template>
<script>
export default {
    data() {
        return {
            saveBtnDisabled:false	// 保存按钮是否禁用
        }   
    },
    created() {

    },
    methods:{
        saveOrUpdate() {
            //跳转到第二步
            this.$router.push({path:'/course/chapter/1'})
        }
    }
}
</script>

2、课程大纲页面—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() {
    
  },

  methods: {
    previous() {
      this.$router.push({ path: '/edu/course/info/1' })
    },

    next() {
      //跳转到第二步
      this.$router.push({ path: '/edu/course/publish/1' })
    }
  }
}
</script>

3、课程发布页面—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: '/course/chapter/1' })
    },

    publish() {
      console.log('publish')
      this.$router.push({ path: '/course/list' })
    }
  }
}
</script>

4、定义api

api/edu/course.js

import request from '@/utils/request'

export default {
    //1.添加课程信息
    saveCourseInfo(courseInfo) {
        return request({
            url: `/eduservice/course/saveCourseInfo`,
            method: 'post',
            data: courseInfo
        })
    }
}

5、添加课程表单

src/views/edu/info.vue

<el-form label-width="120px">

    <el-form-item label="课程标题">
        <el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写"/>
    </el-form-item>

    <!-- 所属分类 TODO -->

    <!-- 课程讲师 TODO -->
    
    
    <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="课程简介">
      <el-input v-model="courseInfo.description" placeholder=" "/>
   </el-form-item>

    <!-- 课程封面 TODO -->
 
    <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>


<script>
import course from '@/api/edu/course'
export default {
    data() {
        return {
            saveBtnDisabled:false,
            courseInfo:{
                title: '',
                subjectId: '',//二级分类id
                subjectParentId:'',//一级分类id
                teacherId: '',
                lessonNum: 0,
                description: '',
                cover: '',
                price: 0
            },
        }   
    },
    created() {

    },
    methods:{
         //提交课程信息
        saveOrUpdate(){
          course.saveCourseInfo(this.courseInfo)
          .then(response=>{
              this.$message({
                type:'success',
                message:'保存成功!'
              })
              //跳转到课程大纲页面
              this.$router.push({path:'/course/chapter/'+response.data.courseId})
          }).catch(response=>{
            this.$message({
                type:'error',
                message:'保存失败!'
              })
          })
            
        }
    }
}
</script>

**主意:**TODO是为了后续进行完善,一个项目都是从简单到复杂,功能一步步变繁多的.

7、讲师下拉列表

(1)组件模板

 <!-- 课程讲师 -->
        <el-form-item label="课程讲师">
          <el-select
          filterable
            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>

(2)定义api

api/edu/teacher.js

//查询所有讲师
getTeacherList() {
    return request({
        url: `/eduservice/teacher/findAll`,
        method: 'get'
    })
}

组件中引入teacher的api

import teacher from '@/api/edu/teacher'

(3)组件js代码

定义data

teacherList: [] // 讲师列表

表单初始化时获取讲师列表

created() {
    this.getTeacherList()
},
methods: {
    //....
    //查询所有讲师
    getTeacherList(){
        course.getTeacherList().then(response=>{
            this.teacherList = response.data.items
        })
    }    
}

(4)页面效果展示

image-20220227092309004

8、课程分类多级联动

1、两个下拉列表实现

思路分析:使用多个下拉列表来达到多级联动效果,前面的被触发,后面的下拉列表被赋值填充.

(1)组件数据定义—定义data中

subjectOneList:[],//一级分类
subjectTwoList:[]//二级分类

(2)组件模板

<el-form-item label="课程分类">
  <el-select
    filterable
    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 filterable 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>

(3)组件脚本

created() {
    //初始化一级分类
    this.getOneSubject()
},
method:{
	//....
	//查询所有的一级分类
    getOneSubject() {
        subject.getSubjectList()
            .then(response => {
                this.subjectOneList = response.data.list
            })
    },
    点击某个一级分类,触发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 = ''
    }
}

(1)页面效果

image-20220227095210387

2、使用级联组件

(1)组件模板

<el-form-item label="课程分类">
    <el-cascader
    filterable 
    v-model="subject"
    :options="subjectList"
    :props="subProps"
    @change="handleChange">
    </el-cascader>
</el-form-item>

(2)模板数据

courseInfo:{
    //...
    subjectId: '',//一级分类id
    subjectParentId:'',//二级分类id
    //...
},
subjectList:[],//课程分类列表
subProps:{//模板数据
    value:'id',
    label:'title',
    children:'children'
 },
subject:[],//存放级联列表的id数组

(3)组件js脚本

 handleChange(){//参数为value数据,如果不需要可以不写.
     console.log(this.subject);
     this.courseInfo.subjectId = this.subject[1] //将对应的一二级分类id放入到courseInfo中
     this.courseInfo.subjectParentId = this.subject[0]
 }

(4)页面效果

image-20220227095537948

9、课程封面

参考 http://element-cn.eleme.io/#/zh-CN/component/upload 用户头像上传

1、定义data数据模板

courseInfo:{
  	//....
    cover: 'https://guli-photos.oss-cn-hangzhou.aliyuncs.com/course.jpg',//默认头像
    //....
},
BASE_API: process.env.BASE_API // 接口API地址

2、组件模板

<el-form-item label="课程封面">

    <el-upload
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeAvatarUpload"
    :action="BASE_API+'/ossService/file/upload'"
    class="avatar-uploader">
    <img :src="courseInfo.cover">      
     </el-upload>

</el-form-item>

3、结果回调

handleAvatarSuccess(res,file){//头像上传成功后
    this.courseInfo.cover = res.data.url
},
beforeAvatarUpload(file){//上传头像之前进行照片校验
    const isPic = (file.type === 'image/jpeg' || file.type === 'image/jpg' ||  file.type === 'image/png')
    const isLt2M = file.size / 1024 / 1024 < 2

    if (!isPic) {
        this.$message.error('上传头像图片只能是 JPG/JPEG/PNG 格式!')
    }
    if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!')
    }
    return isPic && isLt2M
}

10、富文本编辑器

1、Tinymce可视化编辑器

参考:
https://panjiachen.gitee.io/vue-element-admin/#/components/tinymce
https://panjiachen.gitee.io/vue-element-admin/#/example/create

2、组件初始化

(1)复制脚本库

将脚本库复制到项目的static目录下(在vue-element-admin-master的static路径下)

20201019105034329

(2)配置html变量

在 guli-admin/build/webpack.dev.conf.js 中添加配置,使在html页面中可是使用这里定义的BASE_URL变量

new HtmlWebpackPlugin({
  //.....
  templateParameters: {
    BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
  }
})

(3)引入js脚本

在guli-admin/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>
3、组件引入

(1)引入组件

info.vue中引入 Tinymce

import Tinymce from '@/components/Tinymce'

export default {
  components: { Tinymce },
  ......
}

(2)组件模板

<!-- 课程简介-->
<el-form-item label="课程简介">
   <tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>

(3)组件样式

<style scoped>
.tinymce-container {
  line-height: 29px;
}
</style>

(4)组件效果图

image-20220227101951811

11、课程基本信息页面效果展示

show

四、课程大纲列表 - 后端开发

1、创建两个实体类,章节和小节

ChapterVo

@ApiModel(value = "章节封装类",description = "章节封装类")
@Data
public class ChapterVo {

    private String id;

    private String title;

    //表示小节
    private List<VideoVo> children = new ArrayList<>();
}

VideoVo

@ApiModel(value = "小节封装类",description = "小节封装类")
@Data
public class VideoVo {

    private String id;

    private String title;

}

2、编写Controller

@Api(description = "课程大纲")
@RestController
@RequestMapping("/eduservice/chapter")
@CrossOrigin
public class EduChapterController {

    @Autowired
    private EduChapterService chapterService;

    //课程大纲列表,根据课程id进行查询
    @ApiOperation("根据课程id查询章节和小节")
    @GetMapping("getChapterVideo/{courseId}")
    public R getChapterVideo(@PathVariable String courseId){
        List<ChapterVo>  chapterVoList = chapterService.getChapterVideo(courseId);
        return R.ok().data("list",chapterVoList);
    }
}

3、编写Service

@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {

    @Autowired
    private EduVideoService videoService;

    //根据课程id查询课程大纲
    @Override
    public List <ChapterVo> getChapterVideo(String courseId) {
        //1.查询所有章节
        QueryWrapper <EduChapter> chapterWrapper = new QueryWrapper <>();
        chapterWrapper.eq("course_id",courseId);
        List <EduChapter> chapterList = this.baseMapper.selectList(chapterWrapper);

        //2.查询所有的小节
        QueryWrapper <EduVideo> videoWrapper = new QueryWrapper <>();
        videoWrapper.eq("course_id",courseId);
        List <EduVideo> videoList = videoService.list(videoWrapper);

        //定义最终的数据类型
        List <ChapterVo> finalChapterList = new ArrayList <ChapterVo>();

        //3.遍历章节集合,将其封装到List <ChapterVo>
        for (EduChapter chapter : chapterList) {
            ChapterVo chapterVo = new ChapterVo();
            BeanUtils.copyProperties(chapter,chapterVo);
            finalChapterList.add(chapterVo);//将其放入结果集

            List <VideoVo> children = new ArrayList <>();
            //4.遍历小节集合,将其封装到对应的章节中
            for (EduVideo video : videoList) {
                if(video.getChapterId().equals(chapter.getId())){//如果当前小节输入当前章节,则加入该章节
                    VideoVo videoVo = new VideoVo();
                    BeanUtils.copyProperties(video,videoVo);
                    children.add(videoVo);
                }
            }
            chapterVo.setChildren(children);//将章节的对应小节设置进去
        }

        return finalChapterList;
    }
}

4、使用Swagger进行测试

五、课程大纲列表 – 前端开发

1、页面模板

<!-- 章节 -->
<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" @click="openEditVideo(video.id)">编辑</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>

2、页面样式

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

3、定义api

import request from '@/utils/request'

export default {
    //根据课程id查询章节和小节
    getChapterVideo(courseId) {
        return request({
            url: `/eduservice/chapter/getChapterVideo/${courseId}`,
            method: 'get'
        })
    }
}

4、页面调用

在views/edu/course/chapter.vue 中

created() {
  //获取路由中的id值
  if(this.$route.params && this.$route.params.id) {
    this.courseId = this.$route.params.id
    //调用 根据课程id查询章节和小节
    this.getChapterVideo()
  }
},
methods:{
    //根据课程id查询章节和小节
    getChapterVideo() {
      chapter.getChapterVideo(this.courseId)
        .then(response =>{
            this.chapterVideoList = response.data.allChapterVideo
        })
    },
    //......
}

5、效果展示

image-20220227121754004

六、修改课程基本信息(从大纲返回到课程基本信息页面)

流程:点击上一步的时候,回到第一步页面,把课程基本信息数据回显。修改数据后点击保存实现更新数据效果。

1、后端Controller类

//根据课程id查询课程信息
@ApiOperation("查询课程信息")
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId){//@PathVariable可以省略嘛???
    CourseInfoVo courseInfoVo =  courseService.getCourseInfo(courseId);
    return R.ok().data("courseInfoVo",courseInfoVo);
}

//根据课程id修改课程信息
@ApiOperation("修改课程信息")
@PutMapping("updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
    courseService.updateCourseInfo(courseInfoVo);
    return R.ok();
}

2、后端Service类

//通过id查询课程信息
@Override
public CourseInfoVo getCourseInfo(String courseId) {
    //1.查询课程信息
    EduCourse course = this.baseMapper.selectById(courseId);
    CourseInfoVo courseInfoVo = new CourseInfoVo();
    BeanUtils.copyProperties(course,courseInfoVo);
    //2.查询课程描述信息
    EduCourseDescription courseDescription = courseDescriptionService.getById(courseId);
    courseInfoVo.setDescription(courseDescription.getDescription());

    return courseInfoVo;
}

//修改课程信息
@Override
public void updateCourseInfo(CourseInfoVo courseInfoVo) {
    //1.修改课程表
    EduCourse course = new EduCourse();
    BeanUtils.copyProperties(courseInfoVo,course);
    int update1 = this.baseMapper.updateById(course);
    if(update1==0){
        throw new GuLiException(20001, "修改失败!");
    }
    //2.修改课程描述表
    EduCourseDescription courseDescription = new EduCourseDescription();
    BeanUtils.copyProperties(courseInfoVo,courseDescription);
    boolean flag = courseDescriptionService.updateById(courseDescription);
    if(!flag){
        throw new GuLiException(20001, "修改失败!");
    }
}

3、定义前端两个接口

api/edu/course.js

//根据id查询课程信息
getCourseInfo(courseId) {
    return request({
        url: `/eduservice/course/getCourseInfo/${courseId}`,
        method: 'get'
    })
},
//根据id修改课程信息
updateCourseInfo(courseInfoVo) {
    return request({
        url: `/eduservice/course/updateCourseInfo`,
        method: 'put',
        data: courseInfoVo
    })
}

4、修改/edu/course/chapter.vue中路径

image-20220227105003984

5、在info.vue页面实现数据回显

created() {
    //获取路由id值,回显数据
    if(this.$route.params && this.$route.params.id) {
        this.courseId = this.$route.params.id
        //调用根据id查询课程的方法
        this.getCourseInfo()
    }else{
        //初始化courseInfo
        this.courseInfo = {cover: 'https://guli-photos.oss-cn-hangzhou.aliyuncs.com/course.jpg'}//先清空...
        //初始化一级分类
        this.getOneSubject()
    } 
    //初始化讲师列表
    this.getListTeacher()
},
methods:{
	//根据课程id查询,回显数据
	getCourseInfo(){
	    course.getCourseInfo(this.courseId)
	        .then(response => {
	            //在courseInfo课程基本信息,包含一级分类id和二级分类id
	            this.courseInfo = response.data.courseInfoVo
            	//需要补充代码的地方...
	        })
	}
	//....
}

注意这里我们在运行的时候发现二级分类下拉框没有数据,因为subjectTwoList数组为空,所以只显示查询出来的id

解决办法就是:因为在courseInfo课程基本信息,包含一级分类id和二级分类id,所以先查询所有一级分类,然后遍历所有一级分类,比较当前courseInfo里面一级分类id 和 所有的一级分类id,如果相同则遍历一级分类下面的二级分类

subject.getAllSubject().then(response=>{
    this.subjectOneList = response.data.list
    for(var i = 0;i < this.subjectOneList.length;i++){
        if(this.subjectOneList[i].id==this.courseInfo.subjectParentId){{
            this.subjectTwoList = this.subjectOneList[i].children
        }}
    }
});

此时启动服务测试,页面所以数据都可正常回显。

6、更新数据 js

这里我们对之前写的加以修改,把添加和修改方法单独提出来写

addCourseInfo(){//添加课程基本信息
    course.saveCourseInfo(this.courseInfo)
        .then(response=>{
        this.$message({
            type:'success',
            message:'课程添加成功!'
        })
        //跳转到课程大纲页面
        this.$router.push({path:'/course/chapter/'+response.data.courseId})
    }).catch(response=>{
        this.$message({
            type:'error',
            message:'课程添加失败!'
        })
    })
},
updateCourseInfo(){//修改课程基本信息
    course.updateCourseInfo(this.courseInfo).then(response=>{
        this.$message({
            type:'success',
            message:'课程修改成功!'
        })
        //跳转到课程大纲页面
        this.$router.push({path:'/course/chapter/'+this.courseId})
    }).catch(response=>{
        this.$message({
            type:'error',
            message:'课程修改失败!'
        })
    })
},
//提交课程信息
saveOrUpdate(){
    if(!this.courseInfo.id){
        // console.log("添加课程");
        this.addCourseInfo()
    }else{
        // console.log("修改课程");
        this.updateCourseInfo()
    }        
},

7、解决从修改页面跳转到添加页面,页面数据保留的问题

添加一个路径监听,路径发生变化就执行一次初始化方法

created() {
      this.init()
},
watch:{//监听
    $route(to,from){//当路由路径发生变化时,方法就会被执行
        this.init()
    }
},
methods: {
    init(){//初始化方法封装
        if(this.$route.params && this.$route.params.id){//如果是数据回显
            this.courseId = this.$route.params.id
            //查询课程信息
            this.getCourseInfo()
        }else{//如果是课程添加
            this.courseInfo = {cover: 'https://guli-photos.oss-cn-hangzhou.aliyuncs.com/course.jpg'}//先清空...
            this.getOneSubject()//查询所有一级分类
        }
        // this.getAllSubject()//方法二:级联下拉菜单
        this.getTeacherList()
    }
}

8、启动服务测试即可

七、课程章节添加、修改、删除 --后端开发

1、编写Controller类

//查询章节
@GetMapping("getChapter/{id}")
@ApiOperation("查询章节通过id")
public R getChapter(@PathVariable String id){
    EduChapter chapter = chapterService.getById(id);
    return R.ok().data("chapter",chapter);
}

//添加章节
@PostMapping("addChapter")
@ApiOperation("添加章节")
public R addChapter(@RequestBody EduChapter chapter){
    boolean save = chapterService.save(chapter);
    if(save){
        return R.ok();
    }else{
        return R.error();
    }
}

//修改章节
@ApiOperation("修改章节")
@PutMapping("updateChapter")
public R updateChapter(@RequestBody EduChapter chapter){
    boolean update = chapterService.updateById(chapter);
    if(update){
        return R.ok();
    }else{
        return R.error();
    }
}

//删除章节
@DeleteMapping("delChapter/{id}")
public R delChapter(@PathVariable String id){
    Boolean flag = chapterService.delChapter(id);
    if (flag){
        return R.ok();
    }else{
        return R.error();
    }
}

注意:在实体类中的时间加上@TableField(fill = FieldFill.INSERT) 等注解

2、编写Service类

//删除章节
@Override
public Boolean delChapter(String chapterId) {
    //1.先查询该章节下是否有小节
    QueryWrapper <EduVideo> wrapper = new QueryWrapper <>();
    wrapper.eq("chapter_id", chapterId);
    int count = videoService.count(wrapper);
    //2.如果有则 无法删除
    if(count>0){
        throw new GuLiException(20001,"清先删除小节,再删除章节!");
    }else{
        //如果没有则进行删除..
        int delete = this.baseMapper.deleteById(chapterId);
        return delete > 0;
    }
}

注意:这里我们要对删除进行判断

  • 如果章节里面没有小节,直接删除。
  • 如果章节里面有小节,如何删除?
    第一种: 删除章节的时候,把章节里面所有小节都删除。
    第二种: 如果删除章节下面有小节,不让进行删除。

我们现在对于删除章节使用的是第一种,后面对于删除课程采用的是第二种(删除课程->删除课程描述->删除对应的章节->删除对应的小节)

八、课程章节添加、修改、删除 – 前端开发

1、定义api接口

//根据章节id查询章节.
getChapter(chapterId) {
    return request({
        url: `/eduservice/chapter/getChapter/${chapterId}`,
        method: 'get'
    })
},
//根据id更新章节
updateChapter(chapter) {
    return request({
        url: `/eduservice/chapter/updateChapter`,
        method: 'put',
        data: chapter
    })
},
//根据id删除章节
delChapter(id) {
    return request({
        url: `/eduservice/chapter/delChapter/${id}`,
        method: 'delete'
    })
},
//增加章节
addChapter(chapter) {
    return request({
        url: `/eduservice/chapter/addChapter`,
        method: 'post',
        data: chapter
    })
}

2、添加章节按钮

<el-button type="text" @click="openChapterDialog()">添加章节</el-button>

3、添加弹出框表单

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

4.编写js

data() {
    return {
        //....
        courseId:'',//课程id
        chapterVideoList:[],
        chapter:{//封装章节数据
			title: '',
	        sort: 0
        },
        dialogChapterFormVisible:false//章节弹框
    }
},
methods:{
    //删除章节
    removeChapter(chapterId){
            this.$confirm('此操作将永久删除章节记录,是否继续?','提示',{
            confirmButtonText:'确定',
            cancelButtonText:'取消',
            type:'warning'
        }).then(()=>{//点击确定
            chapter.delChapter(chapterId).then((response)=>{
            this.$message({
              type:'success',
              message:'删除成功!' 
             }) 
            //回到列表页面
            this.getAllChapterVideo()
          }).catch((response)=>{
             this.$message({
                  type: 'error',
                  message: response.data.message
              })
          })
        })
        .catch(() => { // 失败
          this.$message({
                type: 'info',
                message: '取消删除!'
            })
          this.getAllChapterVideo()
        } 
      )
    },
	//弹出添加章节表单
	openChapterDialog(){
          this.dialogChapterFormVisible = true
          this.chapter = {title: '',sort: 0}
    },
    //数据回显
    editChapter(id){
        this.dialogChapterFormVisible = true
        chapter.getChapter(id).then(response=>{
            this.chapter = response.data.chapter
        })
    },
    //编辑章节
    updateChapter(){
        chapter.updateChapter(this.chapter).then(response=>{
           //给与提示
            this.$message({
                type:'success',
                message:'章节编辑成功!'
            })
            //关闭弹框
            this.dialogChapterFormVisible = false
            this.getAllChapterVideo()//查询所有章节/小节,刷新页面
          }).catch(response=>{
            //给与提示
            this.$message({
              type:'error',
              message:'章节编辑失败!'
            })
              //关闭弹框
            this.dialogChapterFormVisible = false
            this.getAllChapterVideo()
          })
           
    },
    //增加章节
    addChapter(){
        this.chapter.courseId = this.courseId
        chapter.addChapter(this.chapter).then(response=>{
            this.dialogChapterFormVisible = false
            //给与提示
            this.$message({
                type:'success',
                message:'章节添加成功!'
            })
            //刷新页面
            this.getAllChapterVideo()//这些公共语句即使类似也必须放在then里面,不然逻辑就会混乱...
        }).catch(response=>{
            this.dialogChapterFormVisible = false
            //给与提示
            this.$message({
                type:'error',
                message:'章节添加失败!'
            })
            //刷新页面
            this.getAllChapterVideo()
        })
          
    },
    //增加和修改
    saveOrUpdate(){
        if(!this.chapter.id || this.chapter.id==""){
            this.addChapter()
        }else{
            this.updateChapter()
        }
    },
	//......
}

注意:启动服务运行出现403错误。

  • 第一种是Controller类中跨域注解没有写@CrossOrigin

  • 第二种是访问路径写的不对。

九、课程小节添加、修改、删除 --后端开发

1、编写Controller类

@Api(description = "小节管理")
@RestController
@RequestMapping("/eduservice/video")
@CrossOrigin
public class EduVideoController {

    @Autowired
    private EduVideoService videoService;

    //增加小节
    @ApiOperation("添加小节")
    @PostMapping("addVideo")
    public R addVideo(@RequestBody EduVideo video){
        boolean flag = videoService.save(video);
        if(flag){
            return R.ok();
        }else{
            return R.error();
        }
    }


    //修改小节
    @ApiOperation("修改小节")
    @PutMapping("updateVideo")
    public R updateVideo(@RequestBody EduVideo video){
        boolean flag = videoService.updateById(video);
        if(flag){
            return R.ok();
        }else{
            return R.error();
        }
    }

    //删除小节TODD 后面这个方法需要完善:删除小节时候,同时要把里面的视频删除
    @ApiOperation("删除小节")
    @DeleteMapping("delVideo/{id}")
    public R delVideo(@PathVariable String id){
        boolean flag = videoService.removeById(id);
        if(flag){
            return R.ok();
        }else{
            return R.error();
        }
    }
    
    //查询小节
    @ApiOperation("查询小节")
    @GetMapping("getVideo/{id}")
    public R getVideo(@PathVariable String id){
        EduVideo video = videoService.getById(id);
        return R.ok().data("video",video);
    }
}

注意:在实体类中的时间加上@TableField(fill = FieldFill.INSERT)等注解

十、课程小节添加、修改、删除 – 前端开发

1、定义api接口

import request from '@/utils/request'

export default {
    //添加小节
    addVideo(video) {
        return request({
            url: `/eduservice/video/addVideo`,
            method: 'post',
            data: video
        })
    },
    //修改小节
    updateVideo(video) {
        return request({
            url: `/eduservice/video/updateVideo`,
            method: 'put',
            data: video
        })
    },
    //删除小节
    delVideo(id) {
        return request({
            url: `/eduservice/video/delVideo/${id}`,
            method: 'delete'
        })
    },
    //查询小节通过id
    getVideo(id) {
        return request({
            url: `/eduservice/video/getVideo/${id}`,
            method: 'get'
        })
    },
}

2、添加修改页面

<!-- 添加和修改课时表单 -->
<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>

3、引入包

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

4、初始数据定义

data() {
    return {
        //....
        video: {//封装小节数据
            title: '',
            sort: 0,
            isFree: 0,
            videoSourceId: '',//视频id(后面用到)
            videoOriginalName:''//视频名称(后面用到)
        },
        dialogVideoFormVisible:false//小节弹框
    }
},

5、编写增删改查的js

//当点击更新/添加中的确认按钮时
saveOrUpdateVideo(){
    if(!this.video.id){
        this.addVideo()
    }else{
        this.updateVideo()
    }
},
//当点击添加小节
openVideoDialog(chapterId){
    this.dialogVideoFormVisible = true
    this.chapterId = chapterId
    this.fileList = []
    this.video ={// 课时对象  //打开之前先清空
        title: '',
        sort: 0,
        isFree: 0,
        videoSourceId: '',
        videoOriginalName:''
    }
},
//添加小节
addVideo(){
    this.video.courseId = this.courseId
    this.video.chapterId = this.chapterId
    video.addVideo(this.video).then(response=>{
        this.dialogVideoFormVisible = false
        //给与提示
        this.$message({
            type:'success',
            message:'小节添加成功!'
        })
        //刷新页面
        this.getAllChapterVideo()//这些公共语句即使类似也必须放在then里面,不然逻辑就会混乱...
    }).catch(response=>{
        this.dialogVideoFormVisible = false
        //给与提示
        this.$message({
            type:'error',
            message:'小节添加失败!'
        })
        //刷新页面
        this.getAllChapterVideo()//这些公共语句即使类似也必须放在then里面,不然逻辑就会混乱...
    })
},
//编辑小节前的数据回显
editVideo(id){
    this.openVideoDialog(id)//进行初始化
    this.dialogVideoFormVisible = true
    video.getVideo(id).then(response=>{
        //普通书籍回显
        this.video = response.data.video
        //进行视频回显
        if(this.video.videoOriginalName!=""){
            this.fileList = [{name:this.video.videoOriginalName}]
        }
    })
},
//编辑小节
updateVideo(){
    video.updateVideo(this.video).then(response=>{
        //给与提示
        this.$message({
            type:'success',
            message:'小节编辑成功!'
        })
        //关闭弹框
        this.dialogVideoFormVisible = false
        this.getAllChapterVideo()
    }).catch(response=>{
        //给与提示
        this.$message({
            type:'error',
            message:'小节编辑成功!'
        })
        //关闭弹框
        this.dialogVideoFormVisible = false
        this.getAllChapterVideo()
    })
},
//删除小节
delVideo(id){
    this.$confirm('此操作将永久删除小节记录,是否继续?','提示',{
        confirmButtonText:'确定',
        cancelButtonText:'取消',
        type:'warning'
    }).then(()=>{//点击确定
        video.delVideo(id).then((response)=>{
            this.$message({
                type:'success',
                message:'删除成功!' 
            }) 
            //回到列表页面
            this.getAllChapterVideo()
        }).catch(()=>{
            this.$message({
                type:'error',
                message:'删除失败!' 
            }) 
        })
    }).catch(()=>{
        this.$message({
            type:'info',
            message:'取消删除!' 
        }) 
    })
},
//...

6、启动服务测试即可


如果有收获!!! 希望老铁们来个三连,点赞、收藏、转发。
创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱编程的大李子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值