项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59
文章目录
demo16-讲师显示、课程显示
1.讲师分页查询后端
1.1控制层
在service_edu模块的front包下创建控制器TeacherFrontController
@RestController
@RequestMapping("/eduservice/teacherfront")
@CrossOrigin
public class TeacherFrontController {
@Autowired
private EduTeacherService teacherService;
//1.分页查询讲师
@PostMapping("getTeacherFrontList/{page}/{limit}")
public R getTeacherFrontList(@PathVariable long page, @PathVariable long limit) {
Page<EduTeacher> pageTeacher = new Page<>(page, limit);
Map<String, Object> map = teacherService.getTeacherFrontList(pageTeacher);
return R.ok().data(map);
}
}
- 前面在做讲师分页查询时前端用的是element-ui实现的,比较简单,只需要传一些参数就行了,所以"demo03-后台讲师管理模块"的"7.3控制层编写方法"中编写的分页查询方法只需要返回total(总记录数)和rows(该页数据的list集合)。这里我们不使用element-ui了(当然,不是不能使用,而是讲另一种做法),我们使用比较接近底层的方式,这就要求我们需要返回分页的所有数据(当前页、每页记录数、总页数、总记录数…)
- 我们给前端返回分页所有数据的方式是:在service层做分页,将数据放到map集合中并返回给控制层,控制层再将分页所有数据返回给前端
1.2业务层接口
在业务层接口EduTeacherService中定义抽象方法
//1.分页查询讲师
Map<String, Object> getTeacherFrontList(Page<EduTeacher> pageTeacher);
1.3业务层实现类
在业务层实现类EduTeacherServiceImpl中实现上一步定义的抽象方法
//1.分页查询讲师
@Override
public Map<String, Object> getTeacherFrontList(Page<EduTeacher> pageParam) {
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
baseMapper.selectPage(pageParam, wrapper); //执行后会将分页数据封装到pageParam
//获取分页所有数据
List<EduTeacher> records = pageParam.getRecords(); //该页数据的list集合
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集合中
HashMap<String, Object> map = new HashMap<>();
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;
}
1.4测试
重启服务,使用swagger进行测试
2.讲师分页查询前端
2.1在api中定义方法
在api目录下创建teacher.js文件,定义方法来调用后端分页查询讲师的接口
import request from '@/utils/request'
export default {
//分页查询讲师
getTeacherList(page, limit) {
return request({
url: `/eduservice/teacherfront/getTeacherFrontList/${
page}/${
limit}`,
method: 'post'
})
}
}
2.2调用api中的方法
1.在index.vue页面引入teacher.js文件
import teacherApi from '@/api/teacher'
2.我们之前的做法是:在data() {return {...}}
中定义数据模型、在methods: {...}
中定义方法、在created() {...}
中进行初始化渲染。
这里我们换一种方式,我们在teacher–>index.vue的export default {...}
中编写如下代码:
//异步调用
asyncData({
params, error }) {
return teacherApi.getTeacherList(1, 8)
.then(response => {
//得到后端返回的map集合
return {
data: response.data.data }
})
}
- 截图中第199行的asyncData方法表示异步调用:我们在访问index.vue页面时asyncData方法中编写的方法并不会马上调用,当访问之后才会调用。
- created和asyncData的区别:created中的代码是我们一加载页面就会执行;而asyncData中的代码并不是我们一加载页面就执行,而是加载完页面后才会执行,这就叫异步调用
- 异步调用的特点:asyncData方法中的代码只会执行一次
- 截图中第199行的第二个参数error是调用过程中的错误信息;第一个参数params就相当与之前的this.$route.params,也就是以前如果我们想要获取路径中id需要使用this.$route.params.id,现在我们使用params.id就可以了(当然,使用this.$route.params.id也是可以的,但我们肯定喜欢用简化的params.id呀)
- 截图中第200行为什么只查询8条数据:为了让页面样式美观,我们固定查询8条讲师记录
- 截图中第203行:赋值时我们以前使用的是
data() { return { data: {} }}
和this.data = response.data.data
,我们这里使用的是return { data: response.data.data }
,这种方式底层实际上就是先定义一个数据模型data,然后给其赋值。两种方式都可以,第二种更简化一些 - 一个坑:截图中第200行的
return teacherApi.getTeacherList(1, 8)
一定要写在同一行,不要一行写return
,然后下一行写teacherApi.getTeacherList(1, 8)
,否则会出问题的
2.3讲师列表页面渲染
1.当没有从数据库中查询到一条数据,就在页面显示"没有相关数据,小编正在努力整理中…"
在index.vue中添加代码
v-if="data.total==0"
v-if="data.total>0"
2.将下图中红框圈起来的部分删掉
3.将下述代码复制粘贴到刚刚删除的位置
<li v-for="teacher in data.items" :key="teacher.id">
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" :title="teacher.name" target="_blank">
<img :src="teacher.avatar" :alt="teacher.name">
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a href="/teacher/1" :title="teacher.name" target="_blank" class="fsize18 c-666">{
{teacher.name}}</a>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999">{
{teacher.intro}}</span>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">{
{teacher.career}}</p>
</div>
</section>
</li>
4.点击首页的"名师"菜单查看效果
2.4添加分页功能
2.4.1定义分页方法
1.我们在"2.2调用api中的方法"的第2步说过,asyncData异步调用中的代码只会执行一次,也就是说只会调用一次后端接口查询一次讲师数据,想要点击页面中的分页数字(2、3、4…)看其它讲师数据是无法实现的,也就是说无法实现分页切换
2.所以我们还是要在methods: {...}
中定义方法来实现分页
methods: {
//分页切换
gotoPage(page) {
//参数是页码数
teacherApi.getTeacherList(page, 8)
.then(response => {
this.data = response.data.data
})
}
}
截图中第83行:我们在"2.2调用api中的方法"的第2步说过,return { data: response.data.data }
就是定义数据模型data并给其赋值,也就是说页面中此时是有data数据模型的,所以这里不再需要定义数据模型data,直接使用this.data = response.data.data
进行赋值即可
2.4.2分页页面渲染
1.删除下图中红框圈起来的部分
2.将下面的分页代码复制粘贴到刚刚删除的位置
代码是老师提供给我们的,我们以后使用时将这段代码复制过来根据需求进行修改即可
<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>
- 截图中第53行的
:class="{undisable: !data.hasPrevious}"
是用了css样式,当此时data.hasPrevious判断为false,也就是在第一页时,就不能点击"首页"按钮 - 截图中第56行的
@click.prevent="gotoPage(1)"
我们在前面说过,是阻止标签默认的行为,因为"首页"是在a标签中的,所以本来点击"首页"后是应该发生页面跳转的,但是此时不会发生跳转而是执行方法gotoPage(1)
2.4.3测试
保存修改后,自行测试,我的没问题
3.讲师详情后端
3.1后端需从数据库查什么
- 根据讲师id查询讲师基本信息
- 根据讲师id查询讲师所讲课程
因为用到了课程表,所以需要先在控制器TeacherFrontController中注入EduCourseService
@Autowired
private EduCourseService courseService;
3.2控制层
在控制器TeacherFrontController中编写代码
//2.讲师详情
@GetMapping("getTeacherFrontInfo/{teacherId}")
public R getTeacherFrontInfo(@PathVariable String teacherId) {
//1.根据讲师id查询讲师基本信息
EduTeacher eduTeacher = teacherService.getById(teacherId);
//2.根据讲师id查询所讲课程
QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
wrapper.eq("teacher_id", teacherId);
List<EduCourse> courseList = courseService.list(wrapper);
return R.ok().data("teacher", eduTeacher).data("courseList", courseList);
}
4.讲师详情前端
4.1修改页面
在teacher目录的index.vue页面将下图中方框圈起来的这两处都改为:href="'/teacher/'+teacher.id"
4.2在api中定义方法
在teacher.js中定义方法调用后端查询讲师详情的接口
//讲师详情
getTeacherInfo(id) {
return request({
url: `/eduservice/teacherfront/getTeacherFrontInfo/${
id}`,
method: 'get'
})
}
4.3调用api中的方法
1.我们在"demo13-搭建前台环境、首页数据显示"的"4.2动态路由"说过,课程详情是动态路由,动态路由创建的文件是以下划线开头的vue文件,参数名为下划线后边的文件名,如_id.vue
。
所以编写代码要在teacher目录下的_id.vue
文件中编写代码
2.在_id.vue中引入teacher.js文件
import teacherApi from '@/api/teacher'
3.在_id.vue页面的export default {...}
中编写代码
//异步调用
asyncData({
params, error }) {
return teacherApi.getTeacherInfo(params.id)
.then(response => {
return {
teacher: response.data.data.teacher,
courseList: response.data.data.courseList
}
})
}
截图中第117行的params.id
:我们在"2.2调用api中的方法"的第2步说过,params.id
是用来获取路由中的id值,等价于this.$route.params.id
,用哪一种都可以。使用params.id
时要注意,因为文件名是_id.vue
,所以要使用params.id
;假如文件名是_vvvvid.vue
那么就要使用params.vvvvid
4.4讲师详情页面渲染
1.将_id.vue页面的下图中红框圈起来的部分删掉
2.将下述代码复制粘贴到上一步删除的位置
<li v-for="course in courseList" :key="course.id">
<div class="cc-l-wrap">
<section class="course-img">
<img :src="course.cover" class="img-responsive" >
<div class="cc-mask">
<a href="#" title="开始学习" target="_blank" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="#" :title="course.title" target="_blank" class="course-title fsize18 c-333">{
{course.title}}</a>
</h3>
</div>
</li>
3.将_id.vue中的代码按照下图进行修改
4.5测试
保存修改进行测试,我的没问题
5.课程列表后端
5.1创建vo类
我们想要在课程列表页面实现:根据一级分类、二级分类、销量、最新时间、价格这些条件来展示课程,所以需要创建vo类来封装这些条件数据
在service_edu模块的entity包下创建包frontvo专门用来放一些前台的vo类,然后在frontvo包下创建vo类CourseFrontVo
@Data
public class CourseFrontVo {
@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;
}
5.2控制层
在front包下创建控制器CourseFrontController
@RestController
@RequestMapping("/eduservice/coursefront")
@CrossOrigin
public class CourseFrontController {
@Autowired
private EduCourseService courseService;
//1.课程条件查询带分页
@PostMapping("getFrontCourseList/{page}/{limit}")
public R getFrontCourseList(@PathVariable long page,
@PathVariable long limit,
@RequestBody(required = false) CourseFrontVo courseFrontVo) {
Page<EduCourse> pageCourse = new Page<>(page, limit);
Map<String, Object> map = courseService.getCourseFrontList(pageCourse, courseFrontVo);
return R.ok().data(map);
}
}
截图中第25行的required = false
:我们在"demo03-后台讲师管理模块"的"7.5改进"说过,条件对象可以为空,所以这里需要给@RequestBody注解添加属性required = false
。如果不加这个属性,条件对象为空时就会报错
5.3业务层接口
在业务层接口EduCourseService中定义抽象方法
//课程条件查询带分页
Map<String, Object> getCourseFrontList(Page<EduCourse> pageCourse, CourseFrontVo courseFrontVo);
5.4业务层实现类
在业务层实现类EduCourseServiceImpl中实现上一步定义的抽象方法
//课程条件查询带分页
@Override
public Map<String, Object> getCourseFrontList(Page<EduCourse> pageParam, CourseFrontVo courseFrontVo) {
QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
//判断条件值是否为空,不为空就拼接条件
if (!StringUtils.isEmpty(courseFrontVo.getSubjectParentId())) {
//一级分类
wrapper.eq("subject_parent_id", courseFrontVo.getSubjectParentId());
}
if (!StringUtils.isEmpty(courseFrontVo.getSubjectId())) {
//二级分类
wrapper.eq("subject_id", courseFrontVo.getSubjectId());
}
if (!StringUtils.isEmpty(courseFrontVo.getBuyCountSort())) {
//关注度排序
wrapper.orderByDesc("buy_count");
}
if (!StringUtils.isEmpty(courseFrontVo.getGmtCreateSort())) {
//最新时间排序
wrapper.orderByDesc("gmt_create");
}
if (!StringUtils.isEmpty(courseFrontVo.getPriceSort())) {
//价格排序
wrapper.orderByDesc("price");
}
baseMapper.selectPage(pageParam, wrapper);
//获取分页所有数据
List<EduCourse> records = pageParam.getRecords(); //该页数据的list集合
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集合中
HashMap<String, Object> map = new HashMap<>();
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;
}
条件查询中的销量排序、最新时间排序、价格排序与一级分类、二级分类是不同的:一级分类、二级分类是前端点击某个分类后会将该一级分类、二级分类的id传给后端,可以拿着这个分类id去数据库中查找;但是销量排序、最新时间排序、价格排序显然无法这样做,我们的做法是:在页面点击"关注度"或"最新"或"价格"时前端会给后端传一个值,这个值是多少无所谓,主要是通过这个值让后端判断是否需要做销量排序或最新时间排序或价格排序
6.课程列表前端
6.1在api中定义方法
1.在页面点击一级分类后,会根据一级分类id得到对应的二级分类,具体的实现方法我们在"demo08-课程管理"的"4.13.3定义change事件方法"的第3步就已经遇见过了:先从后端查询到所有一级二级分类,然后根据页面中点击的是哪个一级分类,在前端进行遍历操作,从而将该一级分类下的所有二级分类显示出来。查询得到所有一级分类的接口我们在"demo07-课程分类管理"的"7.3控制层"就已经编写过了(控制器EduSubjectController中的getAllSubject方法),拿来直接用就行了
2.在api目录下创建course.js文件
import request from '@/utils/request'
export default {
//1.课程条件查询带分页
getCourseList(page, limit, searchObj) {
return request({
url: `/eduservice/coursefront/getFrontCourseList/${
page}/${
limit}`,
method: 'post',
data: searchObj
})
},
//2.查询所有一级二级分类
getAllSubject() {
return request({
url: `/eduservice/subject/getAllSubject`,
method: 'get'
})
}
}
6.2调用api中的方法
1.在course目录下的index.vue文件中引入course.js文件
import courseApi from '@/api/course'
2.在export default {...}
中编写代码调用api中的方法
data() {
return {
page:1, //当前页
data:{
}, //课程列表
subjectNestedList: [], // 一级分类列表
subSubjectList: [], // 二级分类列表
searchObj: {
}, // 查询表单对象
oneIndex:-1,
twoIndex:-1,
buyCountSort:"",
gmtCreateSort:"",
priceSort:""
}
},
created() {
//查询第一页数据
this.initCourseFirst()
//一级分类显示
this.initSubject()
},
methods: {
//1.查询第一页数据
initCourseFirst() {
courseApi.getCourseList(1, 8, this.searchObj)
.then(response => {
this.data = response.data.data
})
},
//2.查询所有一级分类(一级分类中包含有二级分类)
initSubject() {
courseApi.getAllSubject()
.then(response => {
this.subjectNestedList = response.data.data.list
})
},
//3.分页切换的方法
gotoPage(page) {
//参数是页码数
courseApi.getCourseList(page, 8, this.searchObj)
.then(response => {
this.data = response.data.data
})
}
}
- 截图中第322行的
data:{}
:老师说了封装课程列表的数据模型data应该是列表而不是对象(因为后端返回的是列表),也就是说这里不应该是data:{}
而应该是data:[]
,但后来老师说了,用data:{}
也行,说封装为对象后后面我们也能把它变为数组 - 截图中第326行
oneIndex:-1
的作用:选中某个一级分类后会改变这个一级分类的样式。同理,twoIndex:-1
的作用是选中某个二级分类后会改变这个二级分类的样式 - 截图中第328、329、330行的buyCountSort、gmtCreateSort、priceSort也有改变样式的作用,还有另一个作用我们在"5.4业务层实现类"说过了:给后端传值,后端通过判断是否传值来确定是否排序,做什么排序
6.3一级分类渲染
1.将index.vue页面中下图红框圈起来的部分删掉
2.将下述代码复制粘贴到刚刚删除的位置
<li v-for="(item,index) in subjectNestedList" :key="index">
<a :title="item.title" href="#">{
{item.title}}</a>
</li>
老师说因为点击一级分类时会改变样式,为了方便我们这里需要用索引index(这个index的具体作用在后面的"6.8.3改变点击的一级分类的样式")
6.4二级分类渲染
1.将index.vue页面中下图红框圈起来的部分删掉
2.将下述代码复制粘贴到刚刚删除的位置
<li v-for="(item,index) in subSubjectList" :key="index">
<a :title="item.title" href="#">{
{item.title}}</a>
</li>
6.5课程列表渲染
1.在index.vue页面添加如下代码
v-if="data.total==0"
v-if="data.total>0"
2.将index.vue页面中下图红框圈起来的部分删掉
3.将下述代码复制粘贴到刚刚删除的位置
<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="item.title">
<div class="cc-mask">
<a href="/course/1" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a href="/course/1" :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">9634人学习</i>
|
<i class="c-999 f-fA">9634评论</i>
</span>
</section>
</div>
</li>
截图中第85行的Number(item.price) === 0
:因为数据库中price字段是decimal类型的,所以需要先Number(item.price)
将其转为数字,才可以和0进行判断
6.6测试
保存修改后去页面中看效果(别忘了重启后端)
6.7分页页面渲染
1.将index.vue页面中下图红框圈起来的部分删掉
2.将下述代码复制粘贴到刚刚删除的位置
<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>
6.8一级二级分类联动
6.8.1给一级分类绑定事件
添加如下代码
@click="searchOne(item.id,index)"
给searchOne方法传的第二个参数index目前用不到,后面改变点击的一级分类的样式时会用到(在后面的"6.8.3改变点击的一级分类的样式")
6.8.2定义一级分类的方法
//4.点击某个一级分类,查询对应二级分类
searchOne(subjectParentId, index) {
//1.点击某个一级分类,就需要进行查询
//1.1将一级分类的id赋值给searchObj
this.searchObj.subjectParentId = subjectParentId
//1.1从后端查询数据
this.gotoPage(1)
//2.拿着页面中点击的一级分类的id与所有一级分类id进行比较
//如果两者id相同,就从该一级分类中获取到所有二级分类
for (let i=0;i<this.subjectNestedList.length;i++) {
//2.1获取每个一级分类
var oneSubject = this.subjectNestedList[i]
//2.2比较id是否相同
if (subjectParentId == oneSubject.id) {
//2.3从该一级分类中获取所有二级分类
this.subSubjectList = oneSubject.children
}
}
}
6.8.3改变点击的一级分类的样式
1.将下述代码复制粘贴到index.vue中
<style scoped>
.active {
background: #bdbdbd;
}
.hide {
display: none;
}
.show {
display: block;
}
</style>
2.在index.vue中添加如下代码就可以实现点击某一级分类后会改变该一级分类的样式
:class="{active:oneIndex==index}"
这行代码的作用是:当oneIndex和index值相等时就会使用active样式改变背景色
3.所以我们需要在searchOne方法中添加代码,使得可以让oneIndex和index值相等
//把传递过来的index值赋值给数据模型oneIndex,使得可以改变点击的一级分类的样式
this.oneIndex = index
//为了避免bug.我们清空一些值
this.twoIndex = -1
this.searchObj.subjectId = ""
this.subSubjectList = []
6.8.4给二级分类绑定事件、添加样式
在图中两处分别添加如下代码
:class="{active:twoIndex==index}"
@click="searchTwo(item.id,index)"
6.8.5定义二级分类的方法
//5.点击某个二级分类,进行相应的查询
searchTwo(subjectId, index) {
//1.给数据模型twoIndex赋值使得可以显示样式
this.twoIndex = index
//2.点击某个二级分类,就需要进行查询
//2.1将二级分类的id赋值给searchObj
this.searchObj.subjectId = subjectId
//2.2从后端查询数据
this.gotoPage(1)
}
6.9销量或最新或价格排序
1.将index.vue页面中下图红框圈起来的部分删掉
2.将下述代码复制粘贴到刚刚删除的位置
<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>
截图中第50行的:class="{'current bg-orange':buyCountSort!=''}"
:判断buyCountSort是否有值,若有值就让样式生效
3.定义方法
//6.根据销量排序
searchBuyCount() {
//1.给数据模型buyCountSort设值,其余两个数据模型设为空
this.buyCountSort = "1"
this.gmtCreateSort = ""
this.priceSort = ""
//2.把值赋给searchObj
this.searchObj.buyCountSort = this.buyCountSort
this.searchObj.gmtCreateSort = this.gmtCreateSort
this.searchObj.priceSort = this.priceSort
//3.调用方法来查询
this.gotoPage(1)
},
//7.根据最新排序
searchGmtCreate() {
//1.给数据模型gmtCreateSort设值,其余两个数据模型设为空
this.buyCountSort = ""
this.gmtCreateSort = "1"
this.priceSort = ""
//2.把值赋给searchObj
this.searchObj.buyCountSort = this.buyCountSort
this.searchObj.gmtCreateSort = this.gmtCreateSort
this.searchObj.priceSort = this.priceSort
//3.调用方法来查询
this.gotoPage(1)
},
//8.根据价格排序
searchPrice() {
//1.给数据模型priceSort设值,其余两个数据模型设为空
this.buyCountSort = ""
this.gmtCreateSort = ""
this.priceSort = "1"
//2.把值赋给searchObj
this.searchObj.buyCountSort = this.buyCountSort
this.searchObj.gmtCreateSort = this.gmtCreateSort
this.searchObj.priceSort = this.priceSort
//3.调用方法来查询
this.gotoPage(1)
}
截图中第230行为什么给数据模型buyCountSort赋值为1?我们在"5.4业务层实现类"说过了,这个值是多少无所谓,只要有值,后端就会做对应的查询
7.课程详情后端
7.1分析
1.查询课程详情需要查询课程基本信息、对应讲师、课程简介,这种情况和"demo10-课程管理"的"1.1分析"说的一样,涉及的数据表太多,建议我们通过手写sql语句来实现
2.后端需要两个接口:
- 编写sql语句,根据课程id查询课程信息
- 课程基本信息
- 课程分类
- 课程简介
- 所属讲师
- 根据课程id查询章节和小节(我们在"demo09-课程管理"的"1.3.2业务层实现类"已经在业务层实现类EduChapterServiceImpl中编写过这个方法了,无需重复编写)
7.2创建vo类
在frontvo包下创建vo类CourseWebVo
@Data
public class CourseWebVo {
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;
}
7.3控制层
1.因为我们需要用到EduChapterService接口的getChapterVideoByCourseId方法,所以需要在控制器CourseFrontController中注入EduChapterService
@Autowired
private EduChapterService chapterService;
2.在控制器CourseFrontController中编写方法
//2.查询课程详情
@GetMapping("getFrontCourseInfo/{courseId}")
public R getFrontCourseInfo(@PathVariable String courseId) {
//根据课程id查询课程信息(手写sql语句来实现)
CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId);
//根据课程id查询课程章节和小节
List<ChapterVo> chapterVideoList = chapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("courseWebVo", courseWebVo).data("chapterVideoList", chapterVideoList);
}
7.4业务层接口
在业务层接口EduCourseService中定义抽象方法
//根据课程id查询课程信息(手写sql语句来实现)
CourseWebVo getBaseCourseInfo(String courseId);
7.5业务层实现类
在业务层实现类EduCourseServiceImpl中实现上一步定义的抽象方法
//根据课程id查询课程信息(手写sql语句来实现)
@Override
public CourseWebVo getBaseCourseInfo(String courseId) {
return baseMapper.getBaseCourseInfo(courseId);
}
7.6持久层
7.6.1在mapper中定义方法
在EduCourseMapper中定义"根据课程id查询课程信息"的抽象方法
//根据课程id查询课程信息
CourseWebVo getBaseCourseInfo(String courseId);
7.6.2编写映射
在EduCourseMapper.xml中编写刚刚定义的抽象方法的映射
<!--sql语句:根据课程id查询课程信息-->
<select id="getBaseCourseInfo" resultType="com.atguigu.eduservice.entity.frontvo.CourseWebVo">
SELECT ec.id,ec.title,ec.cover,ec.lesson_num AS lessonNum,ec.price,
ec.buy_count AS buyCount,ec.view_count AS viewCount,
ecd.description,
et.id AS teacherId,et.name AS teacherName,et.intro,et.avatar,
es1.title AS subjectLevelOne,es1.id AS subjectLevelOneId,
es2.title AS subjectLevelTwo,es2.id AS subjectLevelTwoId
FROM edu_course ec LEFT OUTER JOIN edu_subject es1 ON ec.subject_parent_id=es1.id
LEFT OUTER JOIN edu_subject es2 ON ec.subject_id=es2.id
LEFT OUTER JOIN edu_teacher et ON ec.teacher_id=et.id
LEFT OUTER JOIN edu_course_description ecd ON ec.id=ecd.id
WHERE ec.id=#{courseId}
</select>
8.课程详情前端
8.1修改页面
在course目录的index.vue页面将下图中方框圈起来的这两处都改为:href="'/course/'+item.id"
8.2在api中定义方法
在course.js中定义方法调用后端查询课程详情的接口
//3.查询课程详情
getCourseInfo(id) {
return request({
url: `/eduservice/coursefront/getFrontCourseInfo/${
id}`,
method: 'get'
})
}
8.3调用api中的方法
1.在_id.vue中引入course.js文件
import courseApi from '@/api/course'
2.在_id.vue页面的export default {...}
中编写代码
我们可以用原始的方式:在data() {return {...}}
中定义数据模型、在methods: {...}
中定义方法、在created() {...}
中进行初始化渲染。也可以使用异步调用方式。我们这里使用异步调用方式,因为使用原始方式的话我们还要在created() {...}
中编写代码获取路由中的id值,而使用异步调用方式的话我们直接使用params.id就可以获取路由中的id值
//异步调用
asyncData({
params, error }) {
return courseApi.getCourseInfo(params.id)
.then(response => {
return {
courseWebVo: response.data.data.courseWebVo,
chapterVideoList: response.data.data.chapterVideoList
}
})
}
8.4课程详情页面渲染
1.将_id.vue中的代码按照下图进行修改
2.将_id.vue页面的下图中红框圈起来的部分删掉
3.将下述代码复制粘贴到上一步删除的位置
<li class="lh-menu-stair" v-for="chapter in chapterVideoList" :key="chapter.id">
<a href="javascript: void(0)" :title="chapter.title" class="current-1">
<em class="lh-menu-i-1 icon18 mr10"></em>{
{chapter.title}}
</a>
<ol class="lh-menu-ol" style="display: block;">
<li class="lh-menu-second ml30" v-for="video in chapter.children" :key="video.id">
<a href="#" title>
<span 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>
4.测试结果如下,可以发现其他都正常,但是课程简介竟然带着html标签
5.解决方法是给<p>标签添加一个属性使得可以翻译html标签
v-html="courseWebVo.description"
6.此时课程简介就能正常显示了
9.阿里云视频播放器测试
播放视频有两种方式:播放地址播放、播放凭证播放。推荐使用播放凭证播放,因为实际开发中我们的视频都是加密的,无法使用播放地址播放。下面我们来演示使用播放凭证播放:
1.在工作区vue1010下创建一个文件夹aliyunplayer,这该文件夹下创建文件01.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<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"></script>
</head>
<body>
<div class="prism-player" id="J_prismPlayer"></div>
<script>
var player = new Aliplayer({
id: 'J_prismPlayer',
width: '100%',
autoplay: false,
cover: 'https://img-blog.csdnimg.cn/065732aef50d4927b21f80886772de7d.png#pic_center',
//根据播放凭证播放
encryptType:'1',
vid : '52f90c17ab8541169c4464c374200a79', //视频id
playauth : 'eyJTZWN1cml0eVRva2VuIjoiQ0FJU2h3TjFxNkZ0NUIyeWZTaklyNWZETTR6OHArcFlockRTZUhiN3J6TU1OUFYyMm9PWXFEejJJSDFFZm5sb0FPa2FzUHd6bFd0UjZmd2Vsck1xRnNRZkh4ZVpQSk1odnN3UG9WbjRKcExGc3QySjZyOEpqc1ZtZzkxUTEwYXBzdlhKYXNEVkVmbDJFNVhFTWlJUi8wMGU2TC8rY2lyWXBUWEhWYlNDbFo5Z2FQa09Rd0M4ZGtBb0xkeEtKd3hrMnQxNFVtWFdPYVNDUHdMU2htUEJMVXhtdldnR2wyUnp1NHV5M3ZPZDVoZlpwMXI4eE80YXhlTDBQb1AyVjgxbExacGxlc3FwM0k0U2M3YmFnaFpVNGdscjhxbHg3c3BCNVN5Vmt0eVdHVWhKL3phTElvaXQ3TnBqZmlCMGVvUUFQb3BGcC9YNmp2QWF3UExVbTliWXhncGhCOFIrWGo3RFpZYXV4N0d6ZW9XVE84MCthS3p3TmxuVXo5bUxMZU9WaVE0L1ptOEJQdzQ0RUxoSWFGMElVRXh6Rm1xQ2QvWDRvZ3lRTzE3eUdwTG9pdjltamNCSHFIeno1c2VQS2xTMVJMR1U3RDBWSUpkVWJUbHphazVNalRTNEsvTllLMUFkS0FvNFhlcVBNYXgzYlFGRHI1M3ZzVGJiWHpaYjBtcHR1UG56ZDFRSUNGZk1sRWVVR29BQkI5LzhZb0xPVThYc2djVjVzamYvdDVzQ3J6d1FhMFNkWGE0c3hVNkFDb05Rbis2SUNyN0hOZVZkeEZRVmk5UEtwVERWZ2VRaDRldDB2RkxlYnVTRnlCdmlkaU5McWY1dmxwdmNrTkpLckxSUU5DU1VseVZSc25nM0dPbnRoNmZMUGF3SWpNK003TjlnVStubkpFMkJDS3pvNnVqb25adHBBYXZKck0zWlYrZz0iLCJBdXRoSW5mbyI6IntcIkNJXCI6XCIrekJ2RVBaT2E2T0cwZnpweGNWQTNqRWtBUHMzbWpUc3V1TlpBcm0zeTBSVHhIQTlHNHAwT1hFM2NJd1VGQWxHXCIsXCJDYWxsZXJcIjpcIjl4V3p5R2NXRXZoSldHWVh2VC9UUWdNcU5TZ08zcE1aSjdlOVE1U3pEUk09XCIsXCJFeHBpcmVUaW1lXCI6XCIyMDIyLTA5LTE2VDA3OjQ3OjU3WlwiLFwiTWVkaWFJZFwiOlwiNTJmOTBjMTdhYjg1NDExNjljNDQ2NGMzNzQyMDBhNzlcIixcIlNpZ25hdHVyZVwiOlwiK0VNZ05hYVdEVURVeGR1QzR6MGI5a0RENEdvPVwifSIsIlZpZGVvTWV0YSI6eyJTdGF0dXMiOiJOb3JtYWwiLCJWaWRlb0lkIjoiNTJmOTBjMTdhYjg1NDExNjljNDQ2NGMzNzQyMDBhNzkiLCJUaXRsZSI6IjYgLSBXaGF0IElmIEkgV2FudCB0byBNb3ZlIEZhc3RlciIsIkNvdmVyVVJMIjoiaHR0cDovL291dGluLTE2ZjU0MDI0MmI1MTExZWRiYTRjMDAxNjNlMWM4ZGJhLm9zcy1jbi1zaGFuZ2hhaS5hbGl5dW5jcy5jb20vNTJmOTBjMTdhYjg1NDExNjljNDQ2NGMzNzQyMDBhNzkvc25hcHNob3RzL2ExYWJlZTliYjhjMTQ4YTc4MmEzYzA3NGQ1MTU3YzA5LTAwMDAxLmpwZz9FeHBpcmVzPTE2NjMzMTc5NzcmT1NTQWNjZXNzS2V5SWQ9TFRBSTNEa3h0c2JVeU5ZViZTaWduYXR1cmU9Z2RHa1RucEFkcXZHbSUyQkFOeGswQjJiY1VydlElM0QiLCJEdXJhdGlvbiI6MTYuMjc2N30sIkFjY2Vzc0tleUlkIjoiU1RTLk5Udng2SEo1eTFyOXpQSkszWTh6WjVGM0oiLCJBY2Nlc3NLZXlTZWNyZXQiOiJCRVpnSkM3ajNTdXZmRUt3SmVnODE2QjJSY3ExNGRnWjRSb0dCSm4xODRYSCIsIlJlZ2lvbiI6ImNuLXNoYW5naGFpIiwiQ3VzdG9tZXJJZCI6MTUzMjIzNTkwOTgwMDgyMX0='
},function(player){
console.log('播放器创建好了。')
});
</script>
</body>
</html>
-
截图中第8行和第9行分别是引入脚本和css文件,这步在整合阿里云视频播放器时必不可少
-
截图中第17行的
autoplay: false
表示进入页面后不会自动播放视频 -
截图中第18行的
cover: 'https://img-blog.csdnimg.cn/065732aef50d4927b21f80886772de7d.png#pic_center'
:如果设置的是进入页面后不自动播放视频,此时我们选择一个图片作为视频封面 -
截图中第20行的
encryptType:'1'
:如果播放加密视频,则必须添加这行代码,如果播放非加密视频是否添加这行代码都可以 -
截图中第22行的playauth是视频凭证。根据视频id获取凭证的方法我们在service_vod模块的测试类TestVod中已经编写过了,我们将setVideoId方法的参数改为视频id并执行测试方法即可得到视频凭证,执行后得到我这个视频的凭证是eyJTZWN1cml0eVRva2VuI…后面太长了我就不写了
2.在空白处右键选择"Open with Live Server"去浏览器中看效果
3.可以看到不会自动播放,并且显示的视频封面就是我们设置的封面,而且视频可以正常播放(我测试时遇到的问题:执行测试方法获取凭证后第一次可以使用,但是过一会儿后就不能使用了,必须要重新执行测试方法获取新的凭证才能播放)
10.整合阿里云视频播放器后端
在service_vod模块的控制器VodController中编写方法
//根据视频id获取视频凭证
@GetMapping("getPlayAuth/{id}")
public R getPlayAuth(@PathVariable String id) {
try {
//1.创建初始化对象
DefaultAcsClient client =InitVodClient.initVodClient(
ConstantVodUtils.ACCESS_KEY_ID,
ConstantVodUtils.ACCESS_KEY_SECRET);
//2.创建获取视频凭证的request和response
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
//3.向request对象里面设置视频id(加密视频id、没有加密视频的id都是可以的)
request.setVideoId(id);
//4.调用初始化对象里面的方法,获取视频信息
GetVideoPlayAuthResponse response = client.getAcsResponse(request);
//5.从获取到的视频信息中取视频凭证的信息
String playAuth = response.getPlayAuth();
return R.ok().data("playAuth", playAuth);
} catch (Exception e) {
throw new GuliException(20001, "获取视频凭证失败");
}
}
11.整合阿里云视频播放器前端
11.1创建播放页面
1.我们想要实现点击某个小节视频后可以在一个新页面播放视频,这里用到了动态路由,所以我们在pages目录下创建文件夹player,然后在player目录下创建页面_vid.vue
<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>
2.将course目录下的_id.vue页面中下图红框圈起来的部分改为:href="'/player/'+video.videoSourceId"
3.第2步中使用video.videoSourceId得到视频的id,所以我们需要给vo类VideoVo(在service_edu模块,entity–>chapter–>VideoVo)添加属性videoSourceId
11.2创建布局页面
因为播放器的布局和其他页面的基本布局不一致,因此我们在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>
11.3在api中定义方法
在api目录下创建vod.js文件,调用后端"根据视频id获取凭证"的接口
import request from '@/utils/request'
export default {
//根据视频id获取凭证
getPlayAuth(vid) {
return request({
url: `/eduvod/video/getPlayAuth/${
vid}`,
method: 'get'
})
}
}
11.4调用api中的方法(获取播放凭证)
在_vid.vue页面中编写代码
<script>
import vod from '@/api/vod'
export default {
layout: 'video', //应用video布局
//异步调用
asyncData({ params, error }) {
return vod.getPlayAuth(params.vid)
.then(response => {
return {
vid: params.vid,
playAuth: response.data.data.playAuth
}
})
}
}
</script>
- 截图中第19行的
params.vid
:我们页面名是_vid.vue,所以使用params.vid获取路由中的参数 - 截图中第22行的
vid: params.vid
:因为后面播放视频时需要用到视频id,所以这里给数据模型vid赋上视频id
11.5创建播放器
1.因为我们获取视频凭证时用的是异步调用的方式,我们在"2.2调用api中的方法"的第2步说过,刚进入页面时还不会执行asyncData中的代码,只有发送请求后才会执行asyncData中的代码,从而获取到视频id和视频凭证,这两个数据我们创建播放器时会用到,所以为了确保创建播放器时已经获取到了这两个数据,我们需要将创建播放器的代码写到mounted方法中(我们在"demo04-前端技术"的"3.10实例生命周期"说过,mounted方法是在页面渲染之后执行的)
2.在_vid.vue中添加如下代码
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('播放器创建成功')
})
}
截图中第29行的id: 'J_prismPlayer'
:这里的id需要和"11.1创建播放页面"的第1步的截图的第10行的id对应
11.6测试
1.保存前端修改并重启后端,效果如下,可以看到视频可以正常播放,目前功能已经实现
2.但是是在同一个标签页播放的,如果我们想要它在新的标签页播放,就需要给course目录下的_id.vue页面的a标签添加属性target="_blank"
3.保存修改,再次去浏览器中看效果,可以看到此时是在新的标签页播放
11.7其他常见的可选配置
我们在"11.5创建播放器"编写的代码都是创建播放器时的必选配置,下面是一些可选配置,我们也添加过来,看看效果
// 以下可选设置
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
12.修改一个小bug
为了让课程封面尺寸合适,我们在course目录下的_id.vue页面添加如下代码
height="357px"
这样的话课程封面的尺寸就好看多了