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. 注册 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: ''
    }

只需要把该数据指定到 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-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:热播详情 - 处理弹幕列表数据加载动画

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

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

  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. 登录功能无法使用

等等问题。

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值