> 文章列表 > 10 - 热播模块实现

10 - 热播模块实现

10 - 热播模块实现

热播模块实现

10-1:开篇

在本章节中我们将要完成【慕课热搜】中的最后一个模块【热播】。

【热播模块】分为两个页面:

  1. 热播列表
  2. 热播详情

对于【热播列表】而言,包含:

  1. 下拉刷新 + 上拉加载更多
  2. 视频播放(video 组件)

对于【热播详情】而言,包含:

  1. 视频播放(video 组件)
  2. 分页的弹幕列表
  3. 发表弹幕
  4. 点赞 + 收藏

所以说,对于【热播】模块而言,核心的点在于【视频播放】,也就是 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. 注册 mixinmixins: [MescrollMixin],data() {return {// 数据源videoList: [],size: 10,page: 1,// 是否为 initisInit: 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 数据的唯一方式是:提交 mutationsmutations: {/*** 保存视频对象到 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 数据到 vuexthis.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"><videoid="myVideo"class="video":src="videoData.play_url":danmu-list="danmuList"enable-danmudanmu-btncontrols/><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: ''}

只需要把该数据指定到 vuexvideo 模块下的 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-commitv-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);// 关闭 popthis.$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:热播详情 - 处理弹幕列表数据加载动画

当弹幕为空的时候,我们需要给用户一个提示。

以此,弹幕的状态可分为三种:

  1. 数据加载中
  2. 无弹幕数据
  3. 有弹幕数据

那么我们分别对这三种情况进行处理:

<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:总结

那么到这里我们整个的 热播 功能就全部完成了。

热播 功能的完成,也标记着我们整个 慕课热搜 业务功能全部搞定。

现在 慕课热搜 在微信小程序端运行的时候,我们可以发现无论是 功能 还是 业务 上,都是比较完善的。

但是,当我们把 慕课热搜 运行到浏览器端的时候,我们发现 应用会出现非常多的问题。

比如:

  1. hot 列表滚动,tabs 置顶效果消失
  2. 在火狐浏览器中,横线出现非常粗的滚动条
  3. 进行文章详情再返回,会出现 ui 错乱
  4. 文章详情无法展示
  5. 热播视频全部无法播放
  6. 登录功能无法使用

等等问题。

那么这些问题,我们怎么处理呢?

请看下一章:《多平台适配》

跨境电商