43.首页课程功能

课程列表

 

课程详情

课程评论

一、课程列表

1、课程列表【后端】

1.1、在entity/frontvo文件夹下创建课程列表CourseQueryVo类

@ApiModel(value = "课程查询对象", description = "课程查询对象封装")
@Data
public class CourseQueryVo implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "课程名称")
    private String title;

    @ApiModelProperty(value = "讲师id")
    private String teacherId;

    @ApiModelProperty(value = "一级类别id")
    private String subjectParentId;

    @ApiModelProperty(value = "二级类别id")
    private String subjectId;

    @ApiModelProperty(value = "销量排序")
    private String buyCountSort;

    @ApiModelProperty(value = "最新时间排序")
    private String gmtCreateSort;

    @ApiModelProperty(value = "价格排序")
    private String priceSort;
}
View Code

1.2、在controller/front文件夹下创建CourseFrontController类

@RestController
@RequestMapping("/eduservice/coursefront")
@CrossOrigin
public class CourseFrontController {
    @Autowired
    private EduCourseService courseService;

    @Autowired
    private EduChapterService chapterService;

    //1 条件查询带分页查询课程
    @PostMapping("getCourseFrontList/{page}/{limit}")
    public R getCourseFrontList(@PathVariable long page,@PathVariable long limit,
                                 @RequestBody(required = false) CourseQueryVo courseQueryVo) {
        Page<EduCourse> pageCourse = new Page<>(page, limit);
        Map<String,Object> map = courseService.getCourseFrontList(pageCourse,courseQueryVo);

        //返回分页所有数据

        return R.ok().data(map);
    }
}
View Code

1.3、在EduCourseServiceImpl中实现getCourseFrontList方法

@Override
    public Map<String, Object> getCourseFrontList(Page<EduCourse> pageParam, CourseQueryVo courseQueryVo) {
        QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
        //判断条件值是否为空,不为空则拼接
        if(!StringUtils.isEmpty(courseQueryVo.getSubjectParentId())){//一级分类
            wrapper.eq("subject_parent_id",courseQueryVo.getSubjectParentId());
        }
        if(!StringUtils.isEmpty(courseQueryVo.getSubjectId())){//二级分类
            wrapper.eq("subject_id",courseQueryVo.getSubjectId());
        }
        if (!StringUtils.isEmpty(courseQueryVo.getBuyCountSort())) {//关注度排序
            wrapper.orderByDesc("buy_count");
        }

        if (!StringUtils.isEmpty(courseQueryVo.getGmtCreateSort())) {//最新排序
            wrapper.orderByDesc("gmt_create");
        }

        if (!StringUtils.isEmpty(courseQueryVo.getPriceSort())) {//价格排序
            wrapper.orderByDesc("price");
        }

        baseMapper.selectPage(pageParam,wrapper);
        List<EduCourse> records = pageParam.getRecords();
        long current = pageParam.getCurrent();
        long pages = pageParam.getPages();
        long size = pageParam.getSize();
        long total = pageParam.getTotal();
        boolean hasNext = pageParam.hasNext();
        boolean hasPrevious = pageParam.hasPrevious();

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("items", records);
        map.put("current", current);
        map.put("pages", pages);
        map.put("size", size);
        map.put("total", total);
        map.put("hasNext", hasNext);
        map.put("hasPrevious", hasPrevious);

        return map;
    }
View Code

2、课程列表【前端】

2.1、定义api

api/course.js

import request from '@/utils/request'
export default {
    getCourseList(page, limit, searchObj) {
        return request({
            url: `/eduservice/coursefront/getCourseFrontList/${page}/${limit}`,
            method: 'post',
            data: searchObj
        })
    },
    // 获取所有分类分类
    getAllSubject() {
        return request({
            url: `/eduservice/edu-subject/getAllSubject`,
            method: 'get'
        })
    }
}
View Code

2.2、页面调用接口

pages/course/index.vue
<script>
import course from '@/api/course'
export default {

  data () {
    return {
      page:1,
      data:{},
      subjectNestedList: [], // 一级分类列表
      subSubjectList: [], // 二级分类列表
      searchObj: {}, // 查询表单对象
      oneIndex:-1,
      twoIndex:-1,
      buyCountSort:"",
      gmtCreateSort:"",
      priceSort:""
    }
  },

  //加载完渲染时
  created () {
    //获取课程第一页数据
    this.initCourse()

    //获取分类
    this.initSubject()
  },

  methods: {
    //1 查询课程第一页数据
    initCourse(){
      course.getCourseList(1, 8,this.searchObj).then(response => {
        this.data = response.data.data
      })
    },
    //2 查询所有一级分类
    initSubject(){
      course.getAllSubject().then(response => {
        this.subjectNestedList = response.data.data.list
      })
    },
    
    //4 点击一级分类,显示对应的二级分类,查询数据
    searchOne(subjectParentId, index) {
      
      //把传递过来的index值赋值给oneIndex,为了active样式生效
      this.oneIndex = index

      this.twoIndex = -1
      this.searchObj.subjectId = "";
      this.subSubjectList = [];

      //点击某个一级分类进行条件查询
      //把一级分类id值,赋值给searchObj
      this.searchObj.subjectParentId = subjectParentId;
      this.gotoPage(this.page)
      //拿着一级分类的id 跟所有的一级分类id进行比较
      for (let i = 0; i < this.subjectNestedList.length; i++) {
        //如果id相同 从一级分类里面获取对应的二级分类
        if (this.subjectNestedList[i].id === subjectParentId) {
          this.subSubjectList = this.subjectNestedList[i].children
        }
      }
    },

    //5 点击二级分类,查询数据
    searchTwo(subjectId, index) {
      //把传递过来的index值赋值给twoIndex,为了active样式生效
      this.twoIndex = index
      //把二级分类id值,赋值给searchObj
      this.searchObj.subjectId = subjectId;
      this.gotoPage(this.page)
    },
    //6 购买量查询排序
    searchBuyCount() {
      //设置对应变量值,为了样式生效
      this.buyCountSort = "1";
      this.gmtCreateSort = "";
      this.priceSort = "";
      //将值赋值到searchObj
      this.searchObj.buyCountSort = this.buyCountSort;
      this.searchObj.gmtCreateSort = this.gmtCreateSort;
      this.searchObj.priceSort = this.priceSort;
      this.gotoPage(this.page)
    },
    //7 更新时间查询排序
    searchGmtCreate() {
      debugger
      this.buyCountSort = "";
      this.gmtCreateSort = "1";
      this.priceSort = "";
      this.searchObj.buyCountSort = this.buyCountSort;
      this.searchObj.gmtCreateSort = this.gmtCreateSort;
      this.searchObj.priceSort = this.priceSort;
      this.gotoPage(this.page)
    },
    //8 价格查询排序
    searchPrice() {
      this.buyCountSort = "";
      this.gmtCreateSort = "";
      this.priceSort = "1";
      this.searchObj.buyCountSort = this.buyCountSort;
      this.searchObj.gmtCreateSort = this.gmtCreateSort;
      this.searchObj.priceSort = this.priceSort;
      this.gotoPage(this.page)
    },
    //3 分页切换的方法
    gotoPage(page) {
      this.page = page
      course.getCourseList(page, 8, this.searchObj).then(response => {
        this.data = response.data.data
      })
    }
  }
}
</script>
<style scoped>
  .active {
    background: #bdbdbd;
  }
  .hide {
    display: none;
  }
  .show {
    display: block;
  }
</style>
View Code

3、课程列表页面渲染

3.1、课程类别显示

<section class="c-s-dl">
  <dl>
    <dt>
      <span class="c-999 fsize14">课程类别</span>
    </dt>
    <dd class="c-s-dl-li">
      <ul class="clearfix">
        <li>
          <a title="全部" href="javascript:void(0);" @click="searchOne('')">全部</a>
        </li>
        <li v-for="(item,index) in subjectNestedList" v-bind:key="index" :class="{active:oneIndex==index}">
          <a :title="item.title" href="javascript:void(0);" @click="searchOne(item.id, index)">{{item.title}}</a>
        </li>
      </ul>
    </dd>
  </dl>
  <dl>
    <dt>
      <span class="c-999 fsize14"/>
    </dt>
    <dd class="c-s-dl-li">
      <ul class="clearfix">
        <li v-for="(item,index) in subSubjectList" v-bind:key="index" :class="{active:twoIndex==index}">
          <a :title="item.title" href="javascript:void(0);" @click="searchTwo(item.id, index)">{{item.title}}</a>
        </li>
      </ul>
    </dd>
  </dl>
  <div class="clear"/>
</section>
View Code

3.2、排序方式显示

<section class="fl">
  <ol class="js-tap clearfix">
    <li :class="{'current bg-orange':buyCountSort!=''}">
      <a title="销量" href="javascript:void(0);" @click="searchBuyCount()">销量
      <span :class="{hide:buyCountSort==''}"></span>
      </a>
    </li>
    <li :class="{'current bg-orange':gmtCreateSort!=''}">
      <a title="最新" href="javascript:void(0);" @click="searchGmtCreate()">最新
      <span :class="{hide:gmtCreateSort==''}"></span>
      </a>
    </li>
    <li :class="{'current bg-orange':priceSort!=''}">
      <a title="价格" href="javascript:void(0);" @click="searchPrice()">价格&nbsp;
        <span :class="{hide:priceSort==''}"></span>
      </a>
    </li>
  </ol>
</section>
View Code

3.3、无数据提示

添加:v-if="data.total==0"
<!-- /无数据提示 开始-->
<section class="no-data-wrap" v-if="data.total==0">
    <em class="icon30 no-data-ico">&nbsp;</em>
    <span class="c-666 fsize14 ml10 vam">没有相关数据,小编正在努力整理中...</span>
</section>
<!-- /无数据提示 结束-->
View Code

3.4、列表

<!-- 数据列表 开始-->
<article v-if="data.total>0" class="comm-course-list">
    <ul id="bna" class="of">
        <li v-for="item in data.items" :key="item.id">
            <div class="cc-l-wrap">
                <section class="course-img">
                    <img :src="item.cover" class="img-responsive" alt="听力口语">
                    <div class="cc-mask">
                        <a :href="'/course/'+item.id" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
                    </div>
                </section>
                <h3 class="hLh30 txtOf mt10">
                    <a :href="'/course/'+item.id" :title="item.title" class="course-title fsize18 c-333">{{ item.title }}</a>
                </h3>
                <section class="mt10 hLh20 of">
                    <span v-if="Number(item.price) === 0" class="fr jgTag bg-green">
                        <i class="c-fff fsize12 f-fA">免费</i>
                    </span>
                    <span class="fl jgAttr c-ccc f-fA">
                        <i class="c-999 f-fA">{{ item.viewCount }}人学习</i>
                        |
                        <i class="c-999 f-fA">9634评论</i>
                    </span>
                </section>
            </div>
        </li>
    </ul>
    <div class="clear"/>
</article>
<!-- /数据列表 结束-->
View Code

3.5、分页页面渲染

<div>
  <div class="paging">
    <!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
    <a
      :class="{undisable: !data.hasPrevious}"
      href="#"
      title="首页"
      @click.prevent="gotoPage(1)"></a>
    <a
      :class="{undisable: !data.hasPrevious}"
      href="#"
      title="前一页"
      @click.prevent="gotoPage(data.current-1)">&lt;</a>
    <a
      v-for="page in data.pages"
      :key="page"
      :class="{current: data.current == page, undisable: data.current == page}"
      :title="'第'+page+'页'"
      href="#"
      @click.prevent="gotoPage(page)">{{ page }}</a>
    <a
      :class="{undisable: !data.hasNext}"
      href="#"
      title="后一页"
      @click.prevent="gotoPage(data.current+1)">&gt;</a>
    <a
      :class="{undisable: !data.hasNext}"
      href="#"
      title="末页"
      @click.prevent="gotoPage(data.pages)"></a>
    <div class="clear"/>
  </div>
</div>
View Code

二、课程详情页

1、课程详情【后端】

在项目中很多时候需要把model转换成dto用于网站信息的展示,按前端的需要传递对象的数据,保证model对外是隐私的,例如密码之类的属性能很好地避免暴露在外,同时也会减小数据传输的体积。

1.1、在entity/frontvo文件夹下创建课程详情对象类CourseWebVo.java

@ApiModel(value="课程信息", description="网站课程详情页需要的相关字段")
@Data
public class CourseWebVo implements Serializable {

    private static final long serialVersionUID = 1L;

    private String id;

    @ApiModelProperty(value = "课程标题")
    private String title;

    @ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
    private BigDecimal price;

    @ApiModelProperty(value = "总课时")
    private Integer lessonNum;

    @ApiModelProperty(value = "课程封面图片路径")
    private String cover;

    @ApiModelProperty(value = "销售数量")
    private Long buyCount;

    @ApiModelProperty(value = "浏览数量")
    private Long viewCount;

    @ApiModelProperty(value = "课程简介")
    private String description;

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

    @ApiModelProperty(value = "讲师姓名")
    private String teacherName;

    @ApiModelProperty(value = "讲师资历,一句话说明讲师")
    private String intro;

    @ApiModelProperty(value = "讲师头像")
    private String avatar;

    @ApiModelProperty(value = "课程类别ID")
    private String subjectLevelOneId;

    @ApiModelProperty(value = "类别名称")
    private String subjectLevelOne;

    @ApiModelProperty(value = "课程类别ID")
    private String subjectLevelTwoId;

    @ApiModelProperty(value = "类别名称")
    private String subjectLevelTwo;

}
View Code

1.2、Mapper中关联查询课程和讲师信息

CourseMapper.java

CourseWebVo getBaseCourseInfo(String courseId);
View Code

CourseMapper.xml

    <!--sql语句:根据课程id查询课程详情-->
    <select id="getBaseCourseInfo" resultType="com.atguigu.eduservice.entity.frontvo.CourseWebVo">
      SELECT c.id,c.title,c.cover,
        CONVERT(c.price, DECIMAL(8,2)) AS price,
        c.lesson_num AS lessonNum,
        c.cover,
        c.buy_count AS buyCount,
        c.view_count AS viewCount,
        cd.description,

        t.id AS teacherId,
        t.name AS teacherName,
        t.intro,
        t.avatar,

        s1.id AS subjectLevelOneId,
        s1.title AS subjectLevelOne,
        s2.id AS subjectLevelTwoId,
        s2.title AS subjectLevelTwo

      FROM
        edu_course c
        LEFT JOIN edu_course_description cd ON c.id = cd.id
        LEFT JOIN edu_teacher t ON c.teacher_id = t.id
        LEFT JOIN edu_subject s1 ON c.subject_parent_id = s1.id
        LEFT JOIN edu_subject s2 ON c.subject_id = s2.id
      WHERE
        c.id = #{id}
    </select>
View Code

1.3、业务层获取数据并更新浏览量

CourseService接口实现
    @Override
    public CourseWebVo getBaseCourseInfo(String courseId) {
        return baseMapper.getBaseCourseInfo(courseId);
    }
View Code

1.4、接口层

CourseController
    //2 课程详情的方法
    @GetMapping("getCourseFrontInfo/{courseId}")
    public R getCourseFrontInfo(@PathVariable String courseId){
        //根据课程id,编写sql语句查询课程信息
        CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId);

        //根据课程id查询章节和小节
        List<ChapterVo> chapterVoList = chapterService.getChapterVideoByCourseId(courseId);

        return R.ok().data("courseWebVo",courseWebVo).data("chapterVoList",chapterVoList);
    }
View Code

2、课程详情【前端】

2.1、api/course.js

    //课程详情的方法
    getCourseInfo(courseId) {
        return request({
            url: `/eduservice/coursefront/getCourseFrontInfo/${courseId}`,
            method: 'get'
        })
    }
View Code

2.2、pages/course/_id.vue

<script>
import course from "@/api/course"
export default {
  asyncData({ params, error }) {
    return course.getCourseInfo(params.id).then(response => {
      //console.log(response);
      return { 
        course: response.data.data.courseWebVo,
        chapterList: response.data.data.chapterVoList
      }
    })
  }
}
</script>
View Code

3、页面模板

pages/course/_id.vue

3.1、template

<template>
  <div id="aCoursesList" class="bg-fa of">
    <!-- 课程详情 开始 -->
    <section class="container">

      <!-- 课程所属分类 开始 -->

      <!-- /课程所属分类 结束 -->

      <!-- 课程基本信息 开始 -->

      <!-- /课程基本信息 结束 -->

      <div class="mt20 c-infor-box">
        <article class="fl col-7">
          <section class="mr30">
            <div class="i-box">
              <div>
                <section id="c-i-tabTitle" class="c-infor-tabTitle c-tab-title">
                  <a name="c-i" class="current" title="课程详情">课程详情</a>
                </section>
              </div>
              <article class="ml10 mr10 pt20">

                <!-- 课程详情介绍 开始 -->

                <!-- /课程详情介绍 结束 -->

                <!-- 课程大纲 开始-->

                <!-- /课程大纲 结束 -->
              </article>
            </div>
          </section>
        </article>
        <aside class="fl col-3">
          <div class="i-box">
            <!-- 主讲讲师 开始-->

            <!-- /主讲讲师 结束 -->
          </div>
        </aside>
        <div class="clear"/>
      </div>
    </section>
    <!-- /课程详情 结束 -->
  </div>
</template>
View Code

3.2、课程所属分类

<!-- 课程所属分类 开始 -->
<section class="path-wrap txtOf hLh30">
    <a href="#" title class="c-999 fsize14">首页</a>
    \
    <a href="/course" title class="c-999 fsize14">课程列表</a>
    \
    <span class="c-333 fsize14">{{ course.subjectLevelOne }}</span>
    \
    <span class="c-333 fsize14">{{ course.subjectLevelTwo }}</span>
</section>
<!-- /课程所属分类 结束 -->
View Code

3.3、课程基本信息

<!-- 课程基本信息 开始 -->
<div>
    <article class="c-v-pic-wrap" style="height: 357px;">
        <section id="videoPlay" class="p-h-video-box">
            <img :src="course.cover" :alt="course.title" class="dis c-v-pic">
        </section>
    </article>
    <aside class="c-attr-wrap">
        <section class="ml20 mr15">
            <h2 class="hLh30 txtOf mt15">
                <span class="c-fff fsize24">{{ course.title }}</span>
            </h2>
            <section class="c-attr-jg">
                <span class="c-fff">价格:</span>
                <b class="c-yellow" style="font-size:24px;">¥{{ course.price }}</b>
            </section>
            <section class="c-attr-mt c-attr-undis">
                <span class="c-fff fsize14">主讲: {{ course.teacherName }}&nbsp;&nbsp;&nbsp;</span>
            </section>
            <section class="c-attr-mt of">
                <span class="ml10 vam">
                    <em class="icon18 scIcon"/>
                    <a class="c-fff vam" title="收藏" href="#" >收藏</a>
                </span>
            </section>
            <section class="c-attr-mt">
                <a href="#" title="立即观看" class="comm-btn c-btn-3">立即观看</a>
            </section>
        </section>
    </aside>
    <aside class="thr-attr-box">
        <ol class="thr-attr-ol clearfix">
            <li>
                <p>&nbsp;</p>
                <aside>
                    <span class="c-fff f-fM">购买数</span>
                    <br>
                    <h6 class="c-fff f-fM mt10">{{ course.buyCount }}</h6>
                </aside>
            </li>
            <li>
                <p>&nbsp;</p>
                <aside>
                    <span class="c-fff f-fM">课时数</span>
                    <br>
                    <h6 class="c-fff f-fM mt10">{{ course.lessonNum }}</h6>
                </aside>
            </li>
            <li>
                <p>&nbsp;</p>
                <aside>
                    <span class="c-fff f-fM">浏览数</span>
                    <br>
                    <h6 class="c-fff f-fM mt10">{{ course.viewCount }}</h6>
                </aside>
            </li>
        </ol>
    </aside>
    <div class="clear"/>
</div>
<!-- /课程基本信息 结束 -->
View Code

3.4、课程详情介绍

<!-- 课程详情介绍 开始 -->
<div>
    <h6 class="c-i-content c-infor-title">
        <span>课程介绍</span>
    </h6>
    <div class="course-txt-body-wrap">
        <section class="course-txt-body">
            <!-- 将内容中的html翻译过来 -->
            <p v-html="course.description">{{ course.description }}</p>
        </section>
    </div>
</div>
<!-- /课程详情介绍 结束 -->
View Code

3.5、课程大纲

<!-- 课程大纲 开始-->
<div class="mt50">
    <h6 class="c-g-content c-infor-title">
        <span>课程大纲</span>
    </h6>
    <section class="mt20">
        <div class="lh-menu-wrap">
            <menu id="lh-menu" class="lh-menu mt10 mr10">
                <ul>
                    <!-- 课程章节目录 -->
                    <li v-for="chapter in chapterList" :key="chapter.id" class="lh-menu-stair">
                        <a :title="chapter.title" href="javascript: void(0)" class="current-1">
                            <em class="lh-menu-i-1 icon18 mr10"/>{{ chapter.title }}
                        </a>
                        <ol class="lh-menu-ol" style="display: block;">
                            <li v-for="video in chapter.children" :key="video.id" class="lh-menu-second ml30">
                                <a href="#" title>
                                    <span v-if="video.free === true" class="fr">
                                        <i class="free-icon vam mr10">免费试听</i>
                                    </span>
                                    <em class="lh-menu-i-2 icon16 mr5">&nbsp;</em>{{ video.title }}
                                </a>
                            </li>
                        </ol>
                    </li>
                </ul>
            </menu>
        </div>
    </section>
    <!-- /课程大纲 结束 -->
View Code

3.6、主讲讲师

<!-- 主讲讲师 开始-->
<div>
    <section class="c-infor-tabTitle c-tab-title">
        <a title href="javascript:void(0)">主讲讲师</a>
    </section>
    <section class="stud-act-list">
        <ul style="height: auto;">
            <li>
                <div class="u-face">
                    <a :href="'/teacher/'+course.teacherId" target="_blank">
                        <img :src="course.avatar" width="50" height="50" alt>
                    </a>
                </div>
                <section class="hLh30 txtOf">
                    <a :href="'/teacher/'+course.teacherId" class="c-333 fsize16 fl" target="_blank">{{ course.teacherName }}</a>
                </section>
                <section class="hLh20 txtOf">
                    <span class="c-999">{{ course.intro }}</span>
                </section>
            </li>
        </ul>
    </section>
</div>
<!-- /主讲讲师 结束 -->
View Code

三、小节视频播放功能

1、后端获取播放凭证

1.1、VideoController

service-vod微服务中创建 VideoController.java
controller中创建 getPlayAuth 接口方法
//根据视频id获取视频凭证
    @GetMapping("GetPlayAuth/{id}")
    public R getPlayAuth(@PathVariable String id){
        try{
            //创建初始化对象
            DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
            //创建获取凭证request和response对象
            GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
            //向request设置视频id
            request.setVideoId(id);
            //调用方法得到凭证
            GetVideoPlayAuthResponse response = client.getAcsResponse(request);
            String playAuth = response.getPlayAuth();
            return R.ok().data("playAuth",playAuth);
        }catch (Exception e){
            throw new GuliException(20001,"获取视频凭证失败");
        }
    }
View Code

2.1、VideoVo

2、前端播放器整合

2.1、点击播放超链接

course/_id.vue
修改课时目录超链接
<a
   :href="'/player/'+video.videoSourceId"
   :title="video.title"
   target="_blank">
View Code

2.2、layout

因为播放器的布局和其他页面的基本布局不一致,因此创建新的布局容器 layouts/video.vue
<template>
  <div class="guli-player">
    <div class="head">
      <a href="#" title="谷粒学院">
        <img class="logo" src="~/assets/img/logo.png" lt="谷粒学院">
    </a></div>
    <div class="body">
      <div class="content"><nuxt/></div>
    </div>
  </div>
</template>
<script>
export default {}
</script>

<style>
html,body{
  height:100%;
}
</style>

<style scoped>
.head {
  height: 50px;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
}

.head .logo{
  height: 50px;
  margin-left: 10px;
}

.body {
  position: absolute;
  top: 50px;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
}
</style>
View Code

2.3、api

创建api模块 api/vod.js,从后端获取播放凭证
import request from '@/utils/request'

export default {

  getPlayAuth(vid) {
    return request({
      url: `/eduvod/video/GetPlayAuth/${vid}`,
      method: 'get'
    })
  }

}
View Code

2.4、播放组件相关文档

2.5、创建播放页面

创建 pages/player/_vid.vue
(1)引入播放器js库和css样式
<template>
  <div>

    <!-- 阿里云视频播放器样式 -->
    <link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.1/skins/default/aliplayer-min.css" >
    <!-- 阿里云视频播放器脚本 -->
    <script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/de/prismplayer/2.8.1/aliplayer-min.js" />

    <!-- 定义播放器dom -->
    <div id="J_prismPlayer" class="prism-player" />
  </div>
</template>
View Code
(2)获取播放凭证
<script>
import vod from '@/api/vod'
export default { 
    layout: 'video',//应用video布局
    asyncData({ params, error }) {
        //params.vid为地址栏的视频id,vid为动态文件名
        return vod.getPlayAuth(params.vid).then(response => {
            // console.log(response.data.data)
            return {
            vid: params.vid,
            playAuth: response.data.data.playAuth
            }
        })
    }
}
</script>
View Code

(3)创建播放器

 /**
 * 页面渲染完成时:此时js脚本已加载,Aliplayer已定义,可以使用
 * 如果在created生命周期函数中使用,Aliplayer is not defined错误
 */
mounted() {
    
    new Aliplayer({
        id: 'J_prismPlayer',
        vid: this.vid, // 视频id
        playauth: this.playAuth, // 播放凭证
        encryptType: '1', // 如果播放加密视频,则需设置encryptType=1,非加密视频无需设置此项
        width: '100%',
        height: '500px'
    }, function(player) {
        console.log('播放器创建成功')
    })
}
View Code

4)其他常见的可选配置

// 以下可选设置
cover: 'http://guli.shop/photo/banner/1525939573202.jpg', // 封面
qualitySort: 'asc', // 清晰度排序

mediaType: 'video', // 返回音频还是视频
autoplay: false, // 自动播放
isLive: false, // 直播
rePlay: false, // 循环播放
preload: true,
controlBarVisibility: 'hover', // 控制条的显示方式:鼠标悬停
useH5Prism: true, // 播放器类型:html5
View Code

 四、课程评论功能

1、数据库设计

CREATE TABLE `edu_comment` (
  `id` char(19) NOT NULL COMMENT '讲师ID',
  `course_id` varchar(19) NOT NULL DEFAULT '' COMMENT '课程id',
  `teacher_id` char(19) NOT NULL DEFAULT '' COMMENT '讲师id',
  `member_id` varchar(19) NOT NULL DEFAULT '' COMMENT '会员id',
  `nickname` varchar(50) DEFAULT NULL COMMENT '会员昵称',
  `avatar` varchar(255) DEFAULT NULL COMMENT '会员头像',
  `content` varchar(500) DEFAULT NULL COMMENT '评论内容',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_course_id` (`course_id`),
  KEY `idx_teacher_id` (`teacher_id`),
  KEY `idx_member_id` (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论';
View Code

2、后端接口代码

2.1、在service-edu模块,使用mp生成课程评论代码

2.2、在通用模块com.atguigu.commonutils.vo文件夹下创建UcenterMember实体类

用于service-ucenter模块返回用户id,用户昵称和用户头像等信息。

@Data
public class UcenterMember {

    @ApiModelProperty(value = "会员id")
    private String id;

    @ApiModelProperty(value = "昵称")
    private String nickname;

    @ApiModelProperty(value = "用户头像")
    private String avatar;

}
View Code

2.3、在service-ucenter模块,创建接口

实现用户id获取用户信息,返回用户信息对象
@ApiOperation(value = "根据用户id获取用户信息")
    @GetMapping("getInfoUc/{id}")
    public com.atguigu.commonutils.vo.UcenterMember getInfoById(@PathVariable String id){
        try {
            UcenterMember ucenterMember = memberService.getById(id);
            com.atguigu.commonutils.vo.UcenterMember member = new com.atguigu.commonutils.vo.UcenterMember();
            BeanUtils.copyProperties(ucenterMember,member);
            return member;
        }catch (Exception e){
            e.printStackTrace();
            throw new GuliException(20001,"error");
        }
    }
View Code

2.4、创建课程评论controller

(1)在service-edu模块创建Ucenterclient接口,实现微服务调用
@FeignClient(name = "service-ucenter",fallback = UcenterClientImpl.class)//调用的服务器名称
@Component
public interface UcenterClient {
    //根据token获取登录信息
    @GetMapping("/educenter/member/getInfoUc/{id}")
    public UcenterMemberPay getInfoById(@PathVariable("id") String id);
}
View Code

(2)在service-edu模块创建UcenterclientImpl实现类

@Component
public class UcenterClientImpl implements UcenterClient{

    @Override
    public UcenterMemberPay getInfoById(String id) {
        return null;
    }
}
View Code

(3)创建评论列表和添加评论接口

 在front文件夹下创建CommentFrontController类,编写接口
@RestController
@RequestMapping("/eduservice/commentfront")
@CrossOrigin
public class CommentFrontController {

    @Autowired
    private EduCommentService commentService;
    
    @Autowired
    private UcenterClient ucenterClient;

    @ApiOperation(value = "评论分页")
    @GetMapping("getCommentFrontList/{page}/{limit}/{courseId}")
    public R getTeacherFrontList(@PathVariable long page, @PathVariable long limit, @PathVariable String courseId) {
        Page<EduComment> pageComment = new Page<>(page, limit);
        Map<String,Object> map = commentService.getCommentFrontList(pageComment,courseId);

        //返回分页所有数据
        return R.ok().data("map",map);
    }

    @ApiOperation(value = "添加评论")
    @PostMapping("addComment")
    public R addComment(@RequestBody EduComment comment, HttpServletRequest request) {
        String memberId = JwtUtils.getMemberIdByJwtToken(request);
        if(StringUtils.isEmpty(memberId)) {
            return R.error().code(28004).message("请登录");
        }
        comment.setMemberId(memberId);

        UcenterMemberPay ucenterInfo = ucenterClient.getInfoById(memberId);

        comment.setNickname(ucenterInfo.getNickname());
        comment.setAvatar(ucenterInfo.getAvatar());

        commentService.save(comment);
        return R.ok().success(true);
    }
}
View Code

(4)EduCommentServiceImpl实现getCommentFrontList方法

@Override
    public Map<String, Object> getCommentFrontList(Page<EduComment> pageParam,String courseId) {
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("course_id",courseId);
        baseMapper.selectPage(pageParam,wrapper);

        List<EduComment> records = pageParam.getRecords();
        long current = pageParam.getCurrent();
        long pages = pageParam.getPages();
        long size = pageParam.getSize();
        long total = pageParam.getTotal();

        boolean hasNext = pageParam.hasNext();//是否有下一页
        boolean hasPrevious = pageParam.hasPrevious();//是否有上一页


        //把分页数据获取出来,放到map集合
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("items", records);
        map.put("current", current);
        map.put("pages", pages);
        map.put("size", size);
        map.put("total", total);
        map.put("hasNext", hasNext);
        map.put("hasPrevious", hasPrevious);

        //返回map
        return map;
    }
View Code

3、课程评论前端整合

3.1、在api创建commonedu.js

import request from '@/utils/request'

export default {

  getPageList(page, limit, courseId) {
    return request({
      url: `/eduservice/commentfront/getCommentFrontList/${page}/${limit}/${courseId}`,
      method: 'get',
      //params: {courseId}
    })
  },
  addComment(comment) {
    return request({
      url: `/eduservice/commentfront/addComment`,
      method: 'post',
      data: comment
    })
  }
}
View Code

3.2、在课程详情页面,调用方法 _id.vue

<script>
import course from "@/api/course"
import comment from "@/api/common"
export default {
  asyncData({ params, error }) {
    return course.getCourseInfo(params.id).then(response => {
      //console.log(response);
      return { 
        course: response.data.data.courseWebVo,
        chapterList: response.data.data.chapterVoList,
        courseId: params.id
      }
    })
  },
  data() {
    return {
      data:{},
      page:1,
      limit:4,
      total:10,
      comment:{
        content:'',
        courseId:''
      },
      course:{},
      chapterList:[],
      isbuyCourse:false
    }
  },
  created() {
    this.initCourseInfo()
    this.initComment()
  },
  methods:{
    //获取课程详情
    initCourseInfo() {
      course.getCourseInfo(this.courseId)
            .then(response => {
              this.course=response.data.data.courseWebVo
              this.chapterList=response.data.data.chapterList
              this.isbuyCourse=response.data.data.isbuyCourse
            })
    },

    initComment(){
       comment.getPageList(this.page, this.limit, this.courseId).then(response => {
          //  console.log(response.data.data)
           this.data = response.data.data.map
       })
    },
    addComment(){
        this.comment.courseId = this.courseId
        this.comment.teacherId = this.course.teacherId
        comment.addComment(this.comment).then(response => {
            if(response.data.success){
                this.comment.content = ''
                this.initComment()
            }
        })
    },
    gotoPage(page){
          comment.getPageList(page, this.limit,this.courseId).then(response => {
              this.data = response.data.data.map
          })
      }
  }
}
</script>
View Code

3.3、在课程详情页面 _id.vue显示评论

<div class="mt50 commentHtml"><div>
      <h6 class="c-c-content c-infor-title" id="i-art-comment">
        <span class="commentTitle">课程评论</span>
      </h6>
      <section class="lh-bj-list pr mt20 replyhtml">
        <ul>
          <li class="unBr">
            <aside class="noter-pic">
              <img width="50" height="50" class="picImg" src="~/assets/img/avatar-boy.gif">
              </aside>
            <div class="of">
              <section class="n-reply-wrap">
                <fieldset>
                  <textarea name="" v-model="comment.content" placeholder="输入您要评论的文字" id="commentContent"></textarea>
                </fieldset>
                <p class="of mt5 tar pl10 pr10">
                  <span class="fl "><tt class="c-red commentContentmeg" style="display: none;"></tt></span>
                  <input type="button" @click="addComment()" value="回复" class="lh-reply-btn">
                </p>
              </section>
            </div>
          </li>
        </ul>
      </section>
      <section class="">
          <section class="question-list lh-bj-list pr">
            <ul class="pr10">
              <li v-for="(comment,index) in data.items" v-bind:key="index">
                  <aside class="noter-pic">
                    <img width="50" height="50" class="picImg" :src="comment.avatar">
                    </aside>
                  <div class="of">
                    <span class="fl"> 
                    <font class="fsize12 c-blue"> 
                      {{comment.nickname}}</font>
                    <font class="fsize12 c-999 ml5">评论:</font></span>
                  </div>
                  <div class="noter-txt mt5">
                    <p>{{comment.content}}</p>
                  </div>
                  <div class="of mt5">
                    <span class="fr"><font class="fsize12 c-999 ml5">{{comment.gmtCreate}}</font></span>
                  </div>
                </li>
              
              </ul>
          </section>
        </section>
        
        <!-- 公共分页 开始 -->
        <div class="paging">
            <!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
            <a
            :class="{undisable: !data.hasPrevious}"
            href="#"
            title="首页"
            @click.prevent="gotoPage(1)"></a>
            <a
            :class="{undisable: !data.hasPrevious}"
            href="#"
            title="前一页"
            @click.prevent="gotoPage(data.current-1)">&lt;</a>
            <a
            v-for="page in data.pages"
            :key="page"
            :class="{current: data.current == page, undisable: data.current == page}"
            :title="'第'+page+'页'"
            href="#"
            @click.prevent="gotoPage(page)">{{ page }}</a>
            <a
            :class="{undisable: !data.hasNext}"
            href="#"
            title="后一页"
            @click.prevent="gotoPage(data.current+1)">&gt;</a>
            <a
            :class="{undisable: !data.hasNext}"
            href="#"
            title="末页"
            @click.prevent="gotoPage(data.pages)"></a>
            <div class="clear"/>
        </div>
        <!-- 公共分页 结束 -->
      </div>
    </div>
View Code

 

posted @ 2021-04-15 15:44  记录人生  阅读(151)  评论(0编辑  收藏  举报