学习功能组件准备
整体分为上中下三部分,顶部为标题,中间为学习课程列表,底部为导航
那么我们就首先引入LayoutFooter组件
// learn/index.vue
<template>
<div class="learn">
<!-- 顶部功能 -->
<!-- 底部导航 -->
<layout-footer></layout-footer>
</div>
</template>
<script>
import LayoutFooter from '@/components/LayoutFooter'
export default {
name: 'Learn',
components: {
LayoutFooter
}
}
</script>
头部功能
设置NarBar进行标题显示就可以了
...
<!-- 顶部功能 -->
<van-nav-bar title="已购课程"></van-nav-bar>
...
课程列表公共组件
顶部列表与Course中使用的CourseContentList组件列表是非常类似的,我们可以把它封装成公共组件来减少工作量
Course显示的是所有的课程,而学习展示的则是用于已经购买的课程,数据不同,但是结构相同
这里将CourseContentList.vue作为公共组件移动到src/components 中
那么要注意:CourseContent中对于CourseContentList的引入路径要随之修改
// course/components/CourseContent.vue
...
import CourseContentList from '@/components/CourseContentList'
...
引入组件:
...
<!-- 课程列表 -->
<course-content-list></course-content-list>
...
<script>
...
import CourseContentList from '@/components/CourseContentList'
export default {
name: 'Learn',
components: {
...
CourseContentList
}
}
</script>
// 同理,要保持顶部和底部的距离,防止覆盖一部分内容
<style lang="scss" scoped>
.course-content-list {
top: 50px;
bottom: 50px;
}
</style>
接口抽离
课程列表组件初始设置时使用的是所有课程接口,如果我们改为其他的数据(已购课程),那么应当由父组件进行接口设置,再将其传入子组件,由子组件在适当的时机进行调用
修改我们的CourseContentList.vue
(+、-表示当前行代码我们应该去除还是新加)
<script>
// CourseContentList.vue
...
-import { getQueryCourses } from '@/services/course'
...
+props: {
+ // 用于请求数据的函数
+ fetchData: {
+ type: Function,
+ required: true
+ }
+},
...
async onRefresh () {
...
- // 重新请求数据
- const { data } = await getQueryCourses({
+ const { data } = await this.fetchData({
...
},
async onLoad () {
+ const { data } = await getQueryCourses({
- const { data } = await this.fetchData({
...
}
修改CourseContent.vue
// CourseContent.vue
...
<course-content-list
+ :fetchData="fetchData"
></course-content-list>
...
-import { getAllAds } from '@/services/course'
+import { getAllAds, getQueryCourses } from '@/services/course'
...
methods: {
// 传入请求
+ fetchData (options) {
+ return getQueryCourses(options)
+ },
...
学习组件处理
封装接口
这里我们使用已经获取已购课程接口:接口
// 获取已购课程信息
export const getPurchaseCourse = () => {
return request({
method: 'GET',
url: '/front/course/getPurchaseCourse'
})
}
引入到页面中,并给子组件发送请求方法名
数据绑定改进
由于所有课程接口和已购课程接口响应的数据格式不同,在进行数据绑定时需要进行检测
- 响应数据的格式
- 所有课程为:data.data.records
- 已购课程为:data.content
- 数据对应的键不同
- 课程名称不同
- 图片不同
- 已购课程没有价格相关数据
那么我们根据以上的条件可以进行代码的改进,比如加上||
判断,加上v-if控制显示
<van-cell
v-for="item in list"
:key="item.id">
<!-- 课程左侧图片 -->
<div>
<!-- 所有课程与已购课程图片数据属性名不同,检测使用 -->
<img :src="item.courseImgUrl || item.image">
</div>
<!-- 课程右侧信息 -->
<div class="course-info">
<!-- 名称检测 -->
<h3 v-text="item.courseName || item.name"></h3>
<p v-html="item.previewFirstField" class="course-preview"></p>
<!-- 如果为已购,无需再显示价格区域 -->
<p class="price-container" v-if="item.price">
<span class="course-discount">¥{{ item.discounts }}</span>
<s class="course-price">¥{{ item.price }}</s>
</p>
</div>
</van-cell>
响应数据格式检测:
// CourseContentList.js 响应数据格式检测
...
async onRefresh () {
...
// 如果存在数据,清空并课程数据,否则结束
if (data.data && data.data.records && data.data.records.length !== 0) {
this.list = data.data.records
+ } else if (data.content && data.content.length !== 0) {
+ this.list = data.content
}
...
},
async onLoad () {
...
// 检测,如果没有数据了,结束,如果有,保存
if (data.data && data.data.records && data.data.records.length !== 0) {
this.list.push(...data.data.records)
+ } else if (data.content && data.content.length !== 0) {
+ this.list.push(...data.content)
}
...
// 数据全部加载完成
+ if (data.data && data.data.records && data.data.records.length < 10) {
this.finished = true
+ } else if (data.content && data.content.length < 10) {
+ this.finished = true
}
}
}
...
课程详情
组件准备
创建src/views/course-info/index.vue,在组件中要通过props接收路径传参
<template>
<div class="course-info">课程内容的id:{{ courseId }}</div>
</template>
<script>
export default {
name: 'CourseInfo',
props: {
courseId: {
type: [String, Number],
required: true
}
}
}
</script>
<style lang="scss" scoped>
</style>
设置路由规则
// router/index.js
...
{
path: '/course-info/:courseId/',
name: 'course-info',
component: () => import(/* webpackChunkName: 'course-info' */'@/views/course/info'),
props: true
},
...
设置跳转
// course/components/CourseContentList.vue
...
<van-cell
v-for="item in list"
...
@click="$router.push({
name: 'course-info',
params: {
courseId: item.id
}
})"
>
...
接口封装
使用接口:获取课程详情接口
// 获取课程详情信息
export const getCourseById = params => {
return request({
method: 'GET',
url: '/front/course/getCourseById',
params
})
}
引入到页面,并且发送请求验证没有问题
// course-info/index.vue
...
<script>
import { getCourseById } from '@/services/course'
export default {
...
data () {
return {
// 课程信息
course: {}
}
},
created () {
this.loadCourse()
},
methods: {
async loadCourse () {
const { data } = await getCourseById({
courseId: this.courseId
})
this.course = data.content
console.log(data)
}
}
}
</script>
<style lang="scss" scoped></style>
主体内容区域处理
整体采用Vant的cell单元格处理
// course-info/index.vue
<template>
<div class="course-info">
<van-cell-group>
<!-- 课程图片 -->
<van-cell class="course-img"></van-cell>
<!-- 课程描述 -->
<van-cell class="course-desctription"></van-cell>
<!-- 课程详细内容 -->
<van-cell class="course-detail"></van-cell>
</van-cell-group>
</div>
</template>
顶部图片和课程信息,外加修改具体的样式
// course-info/index.vue
...
<!-- 课程图片 -->
<van-cell class="course-img">
<img :src="course.courseImgUrl" alt="">
</van-cell>
<!-- 课程描述 -->
<van-cell class="course-desctription">
<!-- 课程名称 -->
<h2 v-text="course.courseName"></h2>
<!-- 课程概述 -->
<p v-text="course.previewFirstField"></p>
<!-- 课程销售信息 -->
<div class="course-saleInfo">
<p class="course-price">
<span class="discounts">¥{{ course.discounts }} </span>
<span>¥{{ course.price }}</span>
</p>
<span class="tag">{{ course.sales }}人已购</span>
<span class="tag">每周三、五更新</span>
</div>
</van-cell>
...
<style lang="scss" scoped>
.van-cell {
padding: 0;
}
.course-img {
height: 280px;
}
.course-desctription {
padding: 10px 20px;
height: 150px;
}
.course-desctription h2 {
padding: 0;
}
.course-saleInfo {
display: flex;
}
.course-price {
flex: 1;
margin: 0;
}
.course-price .discounts {
color: #ff7452;
font-size: 24px;
font-weight: 700;
}
.course-saleInfo .tag{
line-height: 15px;
background: #f8f9fa;
border-radius: 2px;
padding: 7px 8px;
font-size: 12px;
font-weight: 700;
color: #666;
margin-left: 10px;
}
</style>
主体选项卡
选项卡使用vant的Tab标签页组件
设置到页面中
<van-tabs v-model="active">
<van-tab title="标签 1">内容 1</van-tab>
<van-tab title="标签 2">内容 2</van-tab>
<van-tab title="标签 3">内容 3</van-tab>
<van-tab title="标签 4">内容 4</van-tab>
</van-tabs>
课程详情
设置详情部分数据
<van-tab title="详情">
<!-- 课程详情信息在后台是通过富文本编辑器设置的 -->
<!-- 内容为html文本 -->
<div v-html="course.courseDescription"></div>
</van-tab>
当详情内容过于长的时候,将选项卡部分固定,可以使用Tab组件的粘性定位功能(scrollspy表示滚动导航,美化效果拉满)
// course-info/index.vue
<van-tabs sticky scrollspy>...</van-tabs>
章节列表处理
课程内容要显示课程的章节和课时信息
封装接口
接口为获取课程章节:地址
封装到course.js中
// course.js
...
// 获取课程章节
export const getSectionAndLesson = params => {
return request({
method: 'GET',
url: '/front/course/session/getSectionAndLesson',
params
})
}
封装章节的组件
Vant没有提供与需求相似的用于显示章节和课时的组件,我们可以自行封装一手
准备组件文件,CourseSectionAndLesson用于单个章节与内部课时展示
<template>
<div class="course-section-and-lesson">章节列表</div>
</template>
<script>
export default {
name: 'CourseSectionAndLesson',
props: {
sectionData: {
type: Object,
required: true
}
}
}
</script>
<style lang="scss" scoped>
</style>
引入该组件
// course-info/index.vue
...
<van-tab title="内容">
<course-section-and-lesson></course-section-and-lesson>
</van-tab>
...
<script>
import CourseSectionAndLesson from './components/CourseSectionAndLesson'
import { getCourseById, getSectionAndLesson } from '@/services/course'
...
components: {
CourseSectionAndLesson
},
...
data () {
return {
...
// 章节信息
sections: {}
}
},
created () {
...
this.loadSection()
},
...
methods: {
async loadSection () {
// 请求数据
const { data } = await getSectionAndLesson({
courseId: this.courseId
})
this.sections = data.content.courseSectionList
console.log(data)
},
...
</script>
遍历sections,同时从父组件中传递数据给章节组件
<van-tab title="内容">
<course-section-and-lesson
v-for="item in sections"
:key="item.id"
:section-data="item">
</course-section-and-lesson>
</van-tab>
章节组件布局处理
<template>
<div class="section-and-lesson">
<!-- 章节 -->
<h2 class="section" v-text="sectionData.sectionName"></h2>
<!-- 课时 -->
<p
v-for="item in sectionData.courseLessons"
:key="item.id"
class="lesson"
>
<!-- 课时标题 -->
<span v-text="item.theme"></span>
<!-- 课时图标,使用 Vant 的 icon 图标组件 -->
<van-icon v-if="item.canPlay" name="play-circle" size="20" />
<van-icon v-else name="lock" size="20" />
</p>
</div>
</template>
...
<style lang="scss" scoped>
.section-and-lesson {
padding: 0 20px;
}
// 让课时标题与图标两端显示
.lesson {
display: flex;
justify-content: space-between;
}
</style>
底部支付功能
布局处理
整体采用vant的Tabbar组件,也可自行设置元素进行固定定位处理
// course-info/index.vue
<template>
<div class="course-info">
<!-- 如果已购,去除底部支付区域并设置主体内容区域占满屏幕 -->
<van-cell-group :style="styleOptions">
...
</van-cell-group>
<!-- 底部支付功能 -->
<van-tabbar v-if="!course.isBuy">
<div class="price">
<span v-text="course.discountsTag"></span>
<span class="discounts">¥{{ course.discounts }}</span>
<span>¥{{ course.price }}</span>
</div>
<van-button
type="primary"
>立即购买</van-button>
</van-tabbar>
</div>
</template>
...
data () {
return {
...
// 样式信息
styleOptions: {}
}
},
...
async loadCourse () {
...
if (data.content.isBuy) {
this.styleOptions.bottom = 0
}
}
...
<style lang="scss" scoped>
...
// 修改 discounts 选择器范围,让顶部与底部均可使用
.discounts {
color: #ff7452;
font-size: 24px;
font-weight: 700;
}
...
// 添加底部导航后设置
.van-cell-group {
position: fixed;
// 预留底部支付区域高度
width: 100%;
top: 0;
bottom: 50px;
overflow-y: auto;
}
// 调整内部文字位置
.van-tabbar {
line-height: 50px;
// 设置 padding 后元素超出窗口
padding: 0 20px;
// 设置 box-sizing
box-sizing: border-box;
display: flex;
// 内部元素左右显示
justify-content: space-between;
// 内容居中
align-items: center;
}
span {
font-size: 14px;
}
// 尺寸调整
.van-button {
width: 50%;
height: 80%;
}
</style>
如果已经购买了该课程,就无需显示这个功能了
- 显示隐藏控制
- 主体内容区域位置处理
// course-info/index.vue
<template>
<div class="course-info">
<!-- 如果已购,去除底部支付区域并设置主体内容区域占满屏幕 -->
<van-cell-group :style="styleOptions">
...
</van-cell-group>
<!-- 底部支付功能 -->
<van-tabbar v-if="!course.isBuy">
...
</van-tabbar>
</div>
</template>
...
data () {
return {
...
// 样式信息
styleOptions: {}
}
},
...
async loadCourse () {
...
if (data.content.isBuy) {
this.styleOptions.bottom = 0
}
}
...
视频播放组件
组件准备
当点击某个可播放的课时时,需要进行视频播放,设置视频组件用于播放视频
- 设置导航用于返回上一页
// course-info/video.vue
<template>
<div class="course-video">
<!-- 导航 -->
<van-nav-bar
title="视频"
left-text="返回"
@click-left="$router.go(-1)"
/>
</div>
</template>
<script>
export default {
name: 'CourseVideo'
}
</script>
<style lang="scss" scoped></style>
设置路由:
// router/index.js
...
// 视频页
{
path: '/lesson-video/:lessonId/',
name: 'lesson-video',
component: () => import(/* webpackChunkName: 'lesson-video' */'@/views/course-info/video'),
props: true
},
...
点击课时时,如果可以播放,那么就跳转该视频页,并传递ID课时
// CourseSection.vue
...
<p
...
@click="handleClick(item)"
>
...
<script>
methods: {
handleClick (lessonInfo) {
if (lessonInfo.canPlay) {
this.$router.push({
name: 'lesson-video',
params: {
lessonId: lessonInfo.id
}
})
}
}
}
...
video.vue接收lessonId用于请求视频数据
// course-info/video.vue
...
props: {
lessonId: {
type: [String, Number],
required: true
}
},
...
接口封装
需要使用以下的接口
- 根据fileId获取阿里云对应的视频播放信息:接口
// course.js
...
// 根据fileId获取阿里云对应的视频播放信息
export const getVideoInfo = params => {
return request({
method: 'GET',
url: '/front/course/media/videoPlayInfo',
params
})
}
引入,请求
// video.vue
...
import { getVideoInfo } from '@/services/course'
...
created () {
this.loadVideo()
},
methods: {
async loadVideo () {
const { data } = await getVideoInfo({
lessonId: this.lessonId
})
console.log(data)
}
}
阿里云视频点播
播放需要使用阿里云的视频播放功能
在public/index.html中引入文件:
- css文件:
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.9.3/skins/default/aliplayer-min.css" />
- js文件:
<script src="https://g.alicdn.com/de/prismplayer/2.9.3/aliplayer-h5-min.js"></script>
可使用在线配置获取创建实例代码
- 选择playauth播放方式即可
具体的代码
<template>
<div class="course-video">
...
<!-- 设置视频容器 -->
<div id="video-container"></div>
</div>
</template>
<script>
// 引入接口,请求视频播放需要的 vid 与 playAuth
import { getVideoInfo } from '@/services/course'
export default {
...
created () {
this.loadVideo()
},
methods: {
async loadVideo () {
const { data } = await getVideoInfo({
lessonId: this.lessonId
})
// 初始化播放器
const player = new window.Aliplayer({
// 视频容器 ID
id: 'video-container',
// 视频 ID
vid: data.content.fileId,
// 播放凭证
playauth: data.content.playAuth,
qualitySort: 'asc',
format: 'mp4',
mediaType: 'video',
width: '100%',
// 高度调整
height: '100%',
autoplay: true,
isLive: false,
rePlay: false,
playsinline: true,
preload: true,
controlBarVisibility: 'hover',
useH5Prism: true
}, function (player) {
console.log('The player is created')
})
console.log(player)
}
}
}
</script>
<style lang="scss" scoped>
.course-video {
width: 100%;
height: 210px;
}
#video-container {
width: 100%;
height: auto;
}
</style>
小知识:手机和电脑链接到同一个无线网络下,可以使用手机访问IP地址:8080端口访问项目,可以调试项目
完成