一、课程列表
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.jsimport 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()">价格 <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"> </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)"><</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)">></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.vue3.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 }} </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> </p> <aside> <span class="c-fff f-fM">购买数</span> <br> <h6 class="c-fff f-fM mt10">{{ course.buyCount }}</h6> </aside> </li> <li> <p> </p> <aside> <span class="c-fff f-fM">课时数</span> <br> <h6 class="c-fff f-fM mt10">{{ course.lessonNum }}</h6> </aside> </li> <li> <p> </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"> </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、播放组件相关文档
集成文档:https://help.aliyun.com/document_detail/51991.html?spm=a2c4g.11186623.2.39.478e192b8VSdEn 在线配置:https://player.alicdn.com/aliplayer/setting/setting.html 功能展示:https://player.alicdn.com/aliplayer/presentation/index.html2.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, // 播放器类型:html5View Code