热播模块实现
10-1:开篇
在本章节中我们将要完成【慕课热搜】中的最后一个模块【热播】。
【热播模块】分为两个页面:
- 热播列表
- 热播详情
对于【热播列表】而言,包含:
- 下拉刷新 + 上拉加载更多
- 视频播放(
video
组件)
对于【热播详情】而言,包含:
- 视频播放(
video
组件) - 分页的弹幕列表
- 发表弹幕
- 点赞 + 收藏
所以说,对于【热播】模块而言,核心的点在于【视频播放】,也就是 video
组件的用法。
那么下面就让我们来开始【热播模块】的开发吧
10-2:热播列表 - 获取热播列表数据
定义接口:api/video
import request from '../utils/request';
/**
* 热播视频列表
*/
export function getHotVideoList(data) {
return request({
url: '/video/list',
data
});
}
在 hot-video
中获取数据:
<script>
import { getHotVideoList } from 'api/video';
export default {
data() {
return {
// 数据源
videoList: [],
size: 10,
page: 1
};
},
created() {
this.loadHotVideoList();
},
methods: {
/**
* 获取列表数据
*/
async loadHotVideoList() {
const { data: res } = await getHotVideoList({ page: this.page, size: this.size });
this.videoList = res.list;
console.log(this.videoList);
}
}
};
</script>
10-3:热播列表 - 渲染UI结构
hot-video
<template>
<view class="hot-video-container">
<block v-for="(item, index) in videoList" :key="index">
<hot-video-item :data="item" />
</block>
</view>
</template>
<style lang="scss" scoped>
.hot-video-container {
background-color: $uni-bg-color-grey;
}
</style>
hot-video-item
<template>
<view class="hot-video-item-container">
<view class="video-box">
<video id="myVideo" class="video" :src="data.play_url" enable-danmu danmu-btn controls />
</view>
<hot-video-info :data="data" />
</view>
</template>
<script>
export default {
props: {
data: {
type: Object,
required: true
}
},
data() {
return {};
}
};
</script>
<style lang="scss" scoped>
.hot-video-item-container {
margin-bottom: $uni-spacing-col-lg;
position: relative;
.video {
width: 100%;
height: 230px;
}
}
</style>
hot-video-info
<template>
<view class="hot-video-info-container">
<view class="video-title"> {{ data.title }} </view>
<view class="video-info">
<view class="author-box">
<image class="avatar" :src="data.poster_small" />
<text class="author-txt">{{ data.source_name }}</text>
</view>
<view class="barrage-box">
<uni-icons class="barrage-icon" type="videocam" />
<text class="barrage-num">{{ data.fmplaycnt }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'hot-video-info',
props: {
data: {
type: Object,
required: true
}
},
data() {
return {};
}
};
</script>
<style lang="scss">
.hot-video-info-container {
.video-title {
position: absolute;
top: $uni-spacing-col-big;
left: $uni-spacing-row-lg;
color: $uni-text-color-inverse;
font-size: $uni-font-size-lg;
}
.video-info {
display: flex;
justify-content: space-between;
background-color: $uni-bg-color;
padding: $uni-spacing-col-sm $uni-spacing-row-lg;
.author-box {
display: flex;
align-items: center;
.author-txt {
margin-left: $uni-spacing-row-sm;
font-size: $uni-font-size-base;
color: $uni-text-color;
font-weight: bold;
}
}
.barrage-box {
display: flex;
align-items: center;
.barrage-num {
margin-left: $uni-spacing-row-sm;
font-size: $uni-font-size-sm;
color: $uni-text-color;
}
}
}
}
</style>
10-4:热播列表 - 列表的下拉刷新与上拉加载
hot-video
: 在页面中使用 mescroll
比较简单
<template>
<view class="hot-video-container">
<!-- 1. 导入 mescroll-body -->
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback">
<block v-for="(item, index) in videoList" :key="index">
<hot-video-item :data="item" />
</block>
</mescroll-body>
</view>
</template>
<script>
// 2. 导入 mixin
import MescrollMixin from '@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js';
import { getHotVideoList } from 'api/video';
export default {
// 3. 注册 mixin
mixins: [MescrollMixin],
data() {
return {
// 数据源
videoList: [],
size: 10,
page: 1,
// 是否为 init
isInit: true,
// 实例
mescroll: null
};
},
created() {
// this.loadHotVideoList();
uni.showModal({
title: '提示',
content: '因浏览器对视频解析问题,具体呈现效果可能会存在差异!'
});
},
mounted() {
this.mescroll = this.$refs.mescrollRef.mescroll;
},
methods: {
/**
* 获取列表数据
*/
async loadHotVideoList() {
const { data: res } = await getHotVideoList({ page: this.page, size: this.size });
// 判断是否为第一页数据
if (this.page === 1) {
this.videoList = res.list;
} else {
this.videoList = [...this.videoList, ...res.list];
}
},
// 4. 实现回调方法
/**
* List 组件的首次加载
*/
async mescrollInit() {
await this.loadHotVideoList();
this.isInit = false;
// 结束 上拉加载 && 下拉刷新
this.mescroll.endSuccess();
},
/**
* 下拉刷新的回调
*/
async downCallback() {
if (this.isInit) return;
this.page = 1;
await this.loadHotVideoList();
// 结束 上拉加载 && 下拉刷新
this.mescroll.endSuccess();
},
/**
* 上拉加载的回调
*/
async upCallback() {
if (this.isInit) return;
this.page += 1;
await this.loadHotVideoList();
// 结束 上拉加载 && 下拉刷新
this.mescroll.endSuccess();
}
}
};
</script>
10-5:热播列表 - 点击进入详情页
创建新的分包页面 video-detail
为 hot-video-item
添加点击i事件:
<template>
<view class="hot-video-item-container">
...
<view @click="$emit('click')">
<hot-video-info :data="data" />
</view>
</view>
</template>
在 hot-video
中处理点击事件:
<template>
...
<hot-video-item :data="item" @click="onItemClick" />
...
</template>
<script>
export default {
methods: {
...
/**
* item 点击事件
*/
onItemClick() {
uni.navigateTo({
url: `/subpkg/pages/video-detail/video-detail`
});
}
}
};
</script>
10-6:热播详情 - 渲染详情页面的视频组件
因为在 Uniapp
中无法直接通过 navigateTo
方法,传递一个复杂的对象。
在为了不影响 简洁数据流 的前提下,我们通过 vuex
来保存当前用户点击的 video
数据。
创建 store/video
模块:
export default {
// 独立命名空间
namespaced: true,
// 通过 state 声明数据
state: () => ({
videoData: {}
}),
// 更改 state 数据的唯一方式是:提交 mutations
mutations: {
/**
* 保存视频对象到 vuex
*/
setVideoData(state, videoData) {
state.videoData = videoData;
}
}
};
在 store/index
中注册 video
模块:
import video from './modules/video';
const store = new Vuex.Store({
modules: {
video
}
});
监听 hot-video-item
中 info 的点击事件,对父组件传递事件:
<template>
<view class="hot-video-item-container">
...
<view @click="$emit('click', data)">
<hot-video-info :data="data" />
</view>
</view>
</template>
在 hot-video
页面中进行跳转:
<template>
...
<hot-video-item :data="item" @click="onItemClick" />
...
</template>
<script>
import { mapMutations } from 'vuex';
export default {
methods: {
...mapMutations('video', ['setVideoData']),
/**
* item 点击事件
*/
onItemClick(data) {
// 保存当前点击的 video 数据到 vuex
this.setVideoData(data);
// 进行页面跳转
uni.navigateTo({
url: `/subpkg/pages/video-detail/video-detail`
});
}
}
};
</script>
在 video-detail
中获取 video
数据,同时构建页面:
<template>
<view class="video-detail-container">
<view class="video-box">
<video id="myVideo" class="video" :src="videoData.play_url" enable-danmu danmu-btn controls />
<hot-video-info :data="videoData" />
</view>
</view>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {};
},
computed: {
...mapState('video', ['videoData'])
}
};
</script>
<style lang="scss" scoped>
.video-detail-container {
.video-box {
background-color: $uni-bg-color;
position: sticky;
top: 0;
z-index: 9;
.video {
width: 100%;
height: 230px;
}
}
}
</style>
10-7:热播详情 - 展示视频弹幕
想要展示视频弹幕,那么首先我们需要获取到 视频弹幕数据:
定义 api
请求接口:
/**
* 获取视频弹幕列表
*/
export function getVideoDanmuList(data) {
return request({
url: '/video/danmu',
data
});
}
在 video-detail
中调用该接口,并把获取到的数据通过 danmu-list
绑定到 video
组件中:
<template>
<view class="video-detail-container">
<view class="video-box">
<video
id="myVideo"
class="video"
:src="videoData.play_url"
:danmu-list="danmuList"
enable-danmu
danmu-btn
controls
/>
<hot-video-info :data="videoData" />
</view>
</view>
</template>
<script>
import { getVideoDanmuList } from 'api/video';
export default {
data() {
return {
// 弹幕数据源
danmuList: []
};
},
created() {
this.loadVideoDanmuList();
},
methods: {
/**
* 获取弹幕数据
*/
async loadVideoDanmuList() {
const { data: res } = await getVideoDanmuList({
videoId: this.videoData.id
});
this.danmuList = res.list;
}
}
};
</script>
注意:为了防止无法找到弹幕视频的情况,我们可以使用该数据来进行调试:
{
id: '17397196882089353352',
title: '治愈美少年?贺峻霖《蜂鸟》导演reaction!',
poster_small:
'https://tukuimg.bdstatic.com/processed/2cfa34e3dc199083960fdc0281edd472.jpeg@s_0,w_660,h_370,q_80',
poster_big:
'https://tukuimg.bdstatic.com/processed/2cfa34e3dc199083960fdc0281edd472.jpeg@s_0,w_660,h_370,q_80',
poster_pc:
'https://tukuimg.bdstatic.com/processed/2cfa34e3dc199083960fdc0281edd472.jpeg@s_0,w_660,h_370,q_80,f_webp',
source_name: '电视这个圈儿',
play_url:
'http://vd4.bdstatic.com/mda-mfay1nt2se8cd53g/cae_h264/1623555453965255937/mda-mfay1nt2se8cd53g.mp4?v_from_s=hba_haokan_4469',
duration: '06:37',
url: 'https://haokan.hao123.com/v?vid=17397196882089353352&pd=&context=',
show_tag: 0,
publish_time: '2021年06月13日',
is_pay_column: 0,
like: '163',
comment: '41',
playcnt: '3020',
fmplaycnt: '3020次播放',
fmplaycnt_2: '3020',
outstand_tag: ''
}
只需要把该数据指定到 vuex
的 video
模块下的 videoData
中,然后指定 编译模式到 video-detail
即可
10-8:热播详情 - 渲染全部弹幕模块
弹幕的数据直接使用 getVideoDanmuList
的接口即可,如果想要实现分页功能,可以使用 /video/comment/list
接口获取分页的评论数据。
video-detail
<template>
<view class="video-detail-container">
...
<!-- 弹幕模块 -->
<view class="danmu-box">
<!-- 弹幕列表 -->
<view class="comment-container">
<view class="all-comment-title">全部弹幕</view>
<view class="list">
<block v-for="(item, index) in danmuList" :key="index">
<article-comment-item :data="item" />
</block>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.video-detail-container {
...
.danmu-box {
border-top: $uni-spacing-col-sm solid $uni-bg-color-grey;
margin-bottom: 36px;
.comment-container {
padding: $uni-spacing-col-lg $uni-spacing-row-lg;
.all-comment-title {
font-size: $uni-font-size-lg;
font-weight: bold;
}
}
}
}
</style>
10-9:热播详情 - 渲染底部功能区
此处的 底部功能区 与 文章详情的底部功能区一样,所以可以复用 article-operate
组件:
video-detail
:
<template>
<view class="video-detail-container">
...
<!-- 底部功能区 -->
<article-operate
@commitClick="onCommit"
/>
<!-- 输入弹幕的popup -->
<uni-popup ref="popup" type="bottom" @change="onCommitPopupChange">
<article-comment-commit v-if="isShowCommit" />
</uni-popup>
</view>
</template>
<script>
...
export default {
...
methods: {
...
/**
* 发布弹幕点击事件
*/
onCommit() {
// 通过组件定义的ref调用uni-popup方法
this.$refs.popup.open();
},
/**
* 发布弹幕的 popup 切换事件
*/
onCommitPopupChange(e) {
// 修改对应的标记,当 popup 关闭时,为了动画平顺,进行延迟处理
if (e.show) {
this.isShowCommit = e.show;
} else {
setTimeout(() => {
this.isShowCommit = e.show;
}, 200);
}
}
}
};
</script>
功能区的 placeholder
与文章详情 不同 ,可以通过 props
指定:
article-operate
:
<template>
<view class="operate-container">
<!-- 输入框 -->
<view class="comment-box" @click="onCommitClick">
<my-search
:placeholderText="placeholder"
:config="{
height: 28,
backgroundColor: '#eeedf4',
icon: '/static/images/input-icon.png',
textColor: '#a6a5ab',
border: 'none'
}"
></my-search>
</view>
</view>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: 'article-operate',
props: {
...
placeholder: {
type: String,
default: '评论一句,前排打call...'
}
},
};
</script>
在 video-detail
中指定 placehloder
:
<!-- 底部功能区 -->
<article-operate
:placeholder="'发个弹幕,开心一下'"
...
10-10:热播详情 - 发布弹幕
video-detail:
<template>
<view class="video-detail-container">
...
<!-- 输入弹幕的popup -->
<uni-popup ref="popup" type="bottom" @change="onCommitPopupChange">
<article-comment-commit
v-if="isShowCommit"
:articleId="videoData.id"
@success="onSendDanmu"
/>
</uni-popup>
</view>
</template>
<script>
...
export default {
data() {
return {
...
// video 组件上下文
videoContext: null
};
},
...
onReady: function (res) {
// 获取 video 组件上下文
this.videoContext = uni.createVideoContext('myVideo');
},
methods: {
...
/**
* 弹幕发布成功之后的回调
*/
onSendDanmu(data) {
// 发送弹幕
this.videoContext.sendDanmu({
text: data.info.content,
color: '#00ff00'
});
// 添加弹幕到数据源
this.danmuList.unshift(data.info);
// 关闭 pop
this.$refs.popup.close();
// 关闭标记
this.isShowCommit = false;
// 提示用户
uni.showToast({
title: '发表成功'
});
}
}
};
</script>
10-11:热播详情 - 解决弹幕不显示的问题
原因:
弹幕之所以不显示,是因为我们修改了 danmuList
的数据源导致的,这个问题其实为 video
组件的 bug
。
解决方案:
深拷贝 danmuList
到一个新的数组,用于展示 弹幕列表
<template>
...
<block v-for="(item, index) in commentList" :key="index">
<article-comment-item :data="item" />
</block>
...
</template>
<script>
...
export default {
data() {
return {
// 弹幕数据源
danmuList: [],
// 列表数据源
commentList: [],
...
};
},
...
methods: {
/**
* 获取弹幕数据
*/
async loadVideoDanmuList() {
const { data: res } = await getVideoDanmuList({
videoId: this.videoData.id
});
this.danmuList = [...res.list];
this.commentList = [...res.list];
},
...
/**
* 弹幕发布成功之后的回调
*/
onSendBarrage(data) {
...
// 添加弹幕到数据源
this.commentList.unshift(data.info);
...
}
}
};
</script>
10-12:热播详情 - 定义弹幕的随机颜色值
在 utils
下创建一个新的 js
文件,用来定义 随机颜色方法:
utils/index.js
/**
* 返回随机色值
*/
export let getRandomColor = () => {
const rgb = [];
for (let i = 0; i < 3; ++i) {
let color = Math.floor(Math.random() * 256).toString(16);
color = color.length == 1 ? '0' + color : color;
rgb.push(color);
}
return '#' + rgb.join('');
};
在 video-detail
中使用该方法:
<script>
import { getRandomColor } from 'utils';
export default {
...
methods: {
/**
* 获取弹幕数据
*/
async loadVideoDanmuList() {
const { data: res } = await getVideoDanmuList({
videoId: this.videoData.id
});
// 定义随机颜色
res.list.forEach((item) => {
item.color = getRandomColor();
});
this.danmuList = [...res.list];
this.commentList = [...res.list];
},
...
/**
* 弹幕发布成功之后的回调
*/
onSendBarrage(data) {
// 发送弹幕
this.videoContext.sendDanmu({
text: data.info.content,
color: getRandomColor()
});
...
}
}
};
</script>
10-13:热播详情 - 处理弹幕列表数据加载动画
当弹幕为空的时候,我们需要给用户一个提示。
以此,弹幕的状态可分为三种:
- 数据加载中
- 无弹幕数据
- 有弹幕数据
那么我们分别对这三种情况进行处理:
<template>
<view class="danmu-box">
<!-- 加载动画 -->
<uni-load-more status="loading" v-if="isLoadingComment"></uni-load-more>
<!-- 无弹幕 -->
<empty-data v-else-if="commentList.length === 0"></empty-data>
<!-- 弹幕列表 -->
<view class="comment-container" v-else>
<view class="all-comment-title">全部弹幕</view>
<view class="list">
<block v-for="(item, index) in commentList" :key="index">
<article-comment-item :data="item" />
</block>
</view>
</view>
</view>
</template>
<script>
...
export default {
data() {
return {
...
// 弹幕列表数据加载中
isLoadingComment: true
};
},
methods: {
/**
* 获取弹幕数据
*/
async loadVideoDanmuList() {
...
this.isLoadingComment = false;
},
}
};
</script>
10-14:热播详情 - 点赞、收藏的实现思路
在前面我们让大家自己实现过 文章详情的 点赞和收藏 功能,并且在 video-detail
中我们复用了 收藏和点赞的组件,但是大家可以发现,此时 点赞与收藏 的功能无法在 video-detail
中进行使用。
出现这个问题的原因,是因为此时我们的 videoData
数据源,被保存到了 vuex
中。如果想要实现 点赞和收藏 的功能,那么需要在 监听到成功事件之后,修改 vuex
中的数据
video-detail
:
<template>
<article-operate
:placeholder="'发个弹幕,开心一下'"
:articleData="videoData"
@commitClick="onCommit"
@changePraise="onChangePraise"
@changeCollect="onChangeCollect"
...
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
methods: {
...mapMutations('video', ['setVideoData']),
/**
* 点赞处理回调
*/
onChangePraise(isPraise) {
this.setVideoData({ ...this.videoData, isPraise });
},
/**
* 收藏处理回调
*/
onChangeCollect(isCollect) {
this.setVideoData({ ...this.videoData, isCollect });
}
}
};
</script>
10-15:总结
那么到这里我们整个的 热播 功能就全部完成了。
热播 功能的完成,也标记着我们整个 慕课热搜 业务功能全部搞定。
现在 慕课热搜 在微信小程序端运行的时候,我们可以发现无论是 功能 还是 业务 上,都是比较完善的。
但是,当我们把 慕课热搜 运行到浏览器端的时候,我们发现 应用会出现非常多的问题。
比如:
hot
列表滚动,tabs
置顶效果消失- 在火狐浏览器中,横线出现非常粗的滚动条
- 进行文章详情再返回,会出现
ui
错乱 - 文章详情无法展示
- 热播视频全部无法播放
- 登录功能无法使用
等等问题。
那么这些问题,我们怎么处理呢?
请看下一章:《多平台适配》