學(xué)習(xí)功能組件準(zhǔn)備
整體分為上中下三部分甩鳄,頂部為標(biāo)題逞度,中間為學(xué)習(xí)課程列表,底部為導(dǎo)航
那么我們就首先引入LayoutFooter組件
// learn/index.vue
<template>
<div class="learn">
<!-- 頂部功能 -->
<!-- 底部導(dǎo)航 -->
<layout-footer></layout-footer>
</div>
</template>
<script>
import LayoutFooter from '@/components/LayoutFooter'
export default {
name: 'Learn',
components: {
LayoutFooter
}
}
</script>
頭部功能
設(shè)置NarBar進(jìn)行標(biāo)題顯示就可以了
...
<!-- 頂部功能 -->
<van-nav-bar title="已購課程"></van-nav-bar>
...
課程列表公共組件
頂部列表與Course中使用的CourseContentList組件列表是非常類似的妙啃,我們可以把它封裝成公共組件來減少工作量
Course顯示的是所有的課程档泽,而學(xué)習(xí)展示的則是用于已經(jīng)購買的課程,數(shù)據(jù)不同揖赴,但是結(jié)構(gòu)相同
這里將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>
// 同理馆匿,要保持頂部和底部的距離,防止覆蓋一部分內(nèi)容
<style lang="scss" scoped>
.course-content-list {
top: 50px;
bottom: 50px;
}
</style>
接口抽離
課程列表組件初始設(shè)置時使用的是所有課程接口燥滑,如果我們改為其他的數(shù)據(jù)(已購課程)渐北,那么應(yīng)當(dāng)由父組件進(jìn)行接口設(shè)置,再將其傳入子組件铭拧,由子組件在適當(dāng)?shù)臅r機(jī)進(jìn)行調(diào)用
修改我們的CourseContentList.vue
(+赃蛛、-表示當(dāng)前行代碼我們應(yīng)該去除還是新加)
<script>
// CourseContentList.vue
...
-import { getQueryCourses } from '@/services/course'
...
+props: {
+ // 用于請求數(shù)據(jù)的函數(shù)
+ fetchData: {
+ type: Function,
+ required: true
+ }
+},
...
async onRefresh () {
...
- // 重新請求數(shù)據(jù)
- 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)
+ },
...
學(xué)習(xí)組件處理
封裝接口
這里我們使用已經(jīng)獲取已購課程接口:接口
// 獲取已購課程信息
export const getPurchaseCourse = () => {
return request({
method: 'GET',
url: '/front/course/getPurchaseCourse'
})
}
引入到頁面中,并給子組件發(fā)送請求方法名
數(shù)據(jù)綁定改進(jìn)
由于所有課程接口和已購課程接口響應(yīng)的數(shù)據(jù)格式不同搀菩,在進(jìn)行數(shù)據(jù)綁定時需要進(jìn)行檢測
- 響應(yīng)數(shù)據(jù)的格式
- 所有課程為:data.data.records
- 已購課程為:data.content
- 數(shù)據(jù)對應(yīng)的鍵不同
- 課程名稱不同
- 圖片不同
- 已購課程沒有價格相關(guān)數(shù)據(jù)
那么我們根據(jù)以上的條件可以進(jìn)行代碼的改進(jìn)焊虏,比如加上||
判斷,加上v-if控制顯示
<van-cell
v-for="item in list"
:key="item.id">
<!-- 課程左側(cè)圖片 -->
<div>
<!-- 所有課程與已購課程圖片數(shù)據(jù)屬性名不同秕磷,檢測使用 -->
<img :src="item.courseImgUrl || item.image">
</div>
<!-- 課程右側(cè)信息 -->
<div class="course-info">
<!-- 名稱檢測 -->
<h3 v-text="item.courseName || item.name"></h3>
<p v-html="item.previewFirstField" class="course-preview"></p>
<!-- 如果為已購,無需再顯示價格區(qū)域 -->
<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>
響應(yīng)數(shù)據(jù)格式檢測:
// CourseContentList.js 響應(yīng)數(shù)據(jù)格式檢測
...
async onRefresh () {
...
// 如果存在數(shù)據(jù)炼团,清空并課程數(shù)據(jù)澎嚣,否則結(jié)束
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 () {
...
// 檢測疏尿,如果沒有數(shù)據(jù)了,結(jié)束易桃,如果有褥琐,保存
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)
}
...
// 數(shù)據(jù)全部加載完成
+ 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
}
}
}
...
課程詳情
組件準(zhǔn)備
創(chuàng)建src/views/course-info/index.vue,在組件中要通過props接收路徑傳參
<template>
<div class="course-info">課程內(nèi)容的id:{{ courseId }}</div>
</template>
<script>
export default {
name: 'CourseInfo',
props: {
courseId: {
type: [String, Number],
required: true
}
}
}
</script>
<style lang="scss" scoped>
</style>
設(shè)置路由規(guī)則
// router/index.js
...
{
path: '/course-info/:courseId/',
name: 'course-info',
component: () => import(/* webpackChunkName: 'course-info' */'@/views/course/info'),
props: true
},
...
設(shè)置跳轉(zhuǎn)
// 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
})
}
引入到頁面晤郑,并且發(fā)送請求驗(yàn)證沒有問題
// 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>
主體內(nèi)容區(qū)域處理
整體采用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>
<!-- 課程詳細(xì)內(nèi)容 -->
<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標(biāo)簽頁組件
設(shè)置到頁面中
<van-tabs v-model="active">
<van-tab title="標(biāo)簽 1">內(nèi)容 1</van-tab>
<van-tab title="標(biāo)簽 2">內(nèi)容 2</van-tab>
<van-tab title="標(biāo)簽 3">內(nèi)容 3</van-tab>
<van-tab title="標(biāo)簽 4">內(nèi)容 4</van-tab>
</van-tabs>
課程詳情
設(shè)置詳情部分?jǐn)?shù)據(jù)
<van-tab title="詳情">
<!-- 課程詳情信息在后臺是通過富文本編輯器設(shè)置的 -->
<!-- 內(nèi)容為html文本 -->
<div v-html="course.courseDescription"></div>
</van-tab>
當(dāng)詳情內(nèi)容過于長的時候造寝,將選項卡部分固定磕洪,可以使用Tab組件的粘性定位功能(scrollspy表示滾動導(dǎo)航,美化效果拉滿)
// course-info/index.vue
<van-tabs sticky scrollspy>...</van-tabs>
章節(jié)列表處理
課程內(nèi)容要顯示課程的章節(jié)和課時信息
封裝接口
接口為獲取課程章節(jié):地址
封裝到course.js中
// course.js
...
// 獲取課程章節(jié)
export const getSectionAndLesson = params => {
return request({
method: 'GET',
url: '/front/course/session/getSectionAndLesson',
params
})
}
封裝章節(jié)的組件
Vant沒有提供與需求相似的用于顯示章節(jié)和課時的組件诫龙,我們可以自行封裝一手
準(zhǔn)備組件文件析显,CourseSectionAndLesson用于單個章節(jié)與內(nèi)部課時展示
<template>
<div class="course-section-and-lesson">章節(jié)列表</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="內(nèi)容">
<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 {
...
// 章節(jié)信息
sections: {}
}
},
created () {
...
this.loadSection()
},
...
methods: {
async loadSection () {
// 請求數(shù)據(jù)
const { data } = await getSectionAndLesson({
courseId: this.courseId
})
this.sections = data.content.courseSectionList
console.log(data)
},
...
</script>
遍歷sections,同時從父組件中傳遞數(shù)據(jù)給章節(jié)組件
<van-tab title="內(nèi)容">
<course-section-and-lesson
v-for="item in sections"
:key="item.id"
:section-data="item">
</course-section-and-lesson>
</van-tab>
章節(jié)組件布局處理
<template>
<div class="section-and-lesson">
<!-- 章節(jié) -->
<h2 class="section" v-text="sectionData.sectionName"></h2>
<!-- 課時 -->
<p
v-for="item in sectionData.courseLessons"
:key="item.id"
class="lesson"
>
<!-- 課時標(biāo)題 -->
<span v-text="item.theme"></span>
<!-- 課時圖標(biāo)签赃,使用 Vant 的 icon 圖標(biāo)組件 -->
<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;
}
// 讓課時標(biāo)題與圖標(biāo)兩端顯示
.lesson {
display: flex;
justify-content: space-between;
}
</style>
底部支付功能
布局處理
整體采用vant的Tabbar組件谷异,也可自行設(shè)置元素進(jìn)行固定定位處理
// course-info/index.vue
<template>
<div class="course-info">
<!-- 如果已購,去除底部支付區(qū)域并設(shè)置主體內(nèi)容區(qū)域占滿屏幕 -->
<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;
}
...
// 添加底部導(dǎo)航后設(shè)置
.van-cell-group {
position: fixed;
// 預(yù)留底部支付區(qū)域高度
width: 100%;
top: 0;
bottom: 50px;
overflow-y: auto;
}
// 調(diào)整內(nèi)部文字位置
.van-tabbar {
line-height: 50px;
// 設(shè)置 padding 后元素超出窗口
padding: 0 20px;
// 設(shè)置 box-sizing
box-sizing: border-box;
display: flex;
// 內(nèi)部元素左右顯示
justify-content: space-between;
// 內(nèi)容居中
align-items: center;
}
span {
font-size: 14px;
}
// 尺寸調(diào)整
.van-button {
width: 50%;
height: 80%;
}
</style>
如果已經(jīng)購買了該課程歹嘹,就無需顯示這個功能了
- 顯示隱藏控制
- 主體內(nèi)容區(qū)域位置處理
// course-info/index.vue
<template>
<div class="course-info">
<!-- 如果已購,去除底部支付區(qū)域并設(shè)置主體內(nèi)容區(qū)域占滿屏幕 -->
<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
}
}
...
視頻播放組件
組件準(zhǔn)備
當(dāng)點(diǎn)擊某個可播放的課時時孔庭,需要進(jìn)行視頻播放尺上,設(shè)置視頻組件用于播放視頻
- 設(shè)置導(dǎo)航用于返回上一頁
// course-info/video.vue
<template>
<div class="course-video">
<!-- 導(dǎo)航 -->
<van-nav-bar
title="視頻"
left-text="返回"
@click-left="$router.go(-1)"
/>
</div>
</template>
<script>
export default {
name: 'CourseVideo'
}
</script>
<style lang="scss" scoped></style>
設(shè)置路由:
// router/index.js
...
// 視頻頁
{
path: '/lesson-video/:lessonId/',
name: 'lesson-video',
component: () => import(/* webpackChunkName: 'lesson-video' */'@/views/course-info/video'),
props: true
},
...
點(diǎn)擊課時時,如果可以播放史飞,那么就跳轉(zhuǎn)該視頻頁尖昏,并傳遞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用于請求視頻數(shù)據(jù)
// course-info/video.vue
...
props: {
lessonId: {
type: [String, Number],
required: true
}
},
...
接口封裝
需要使用以下的接口
- 根據(jù)fileId獲取阿里云對應(yīng)的視頻播放信息:接口
// course.js
...
// 根據(jù)fileId獲取阿里云對應(yīng)的視頻播放信息
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)
}
}
阿里云視頻點(diǎn)播
播放需要使用阿里云的視頻播放功能
在public/index.html中引入文件:
- css文件:
<link rel="stylesheet" />
- js文件:
<script src="https://g.alicdn.com/de/prismplayer/2.9.3/aliplayer-h5-min.js"></script>
可使用在線配置獲取創(chuàng)建實(shí)例代碼
- 選擇playauth播放方式即可
具體的代碼
<template>
<div class="course-video">
...
<!-- 設(shè)置視頻容器 -->
<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%',
// 高度調(diào)整
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>
小知識:手機(jī)和電腦鏈接到同一個無線網(wǎng)絡(luò)下抽诉,可以使用手機(jī)訪問IP地址:8080端口訪問項目,可以調(diào)試項目
完成