目录
1.视频组件 video-base 编写
- 效果展示【缩略图】:
- 效果展示【点击后的全屏效果】:
组件名称:TVideoBase
为了播放各种格式的视频(.m3u8、.mp4...),此组件调用了 video.js 库- 安装 video.js:npm install --save video.js
- 在组件中,应该导入这个库:
import videojs from 'video.js'; import 'video.js/dist/video-js.css';
- 可供接受的参数(props):
/** * t-video-base * @desc 视频 * @param {string} [themeStyle] - 主题风格 * @param {Object} [data] - 数据 * @param {Object} [cStyle] - 自定义样式 * @example * <t-video-base></t-video-base> */ props: { // 数据 data: { type: Object, default: () => ({}), }, // 自定义样式 cStyle: { type: Object, default: () => ({ wrapper: {}, }), }, },
- 组件模板:
<template> <div class="t-video-base"> <!-- 封面容器 + 播放按钮 --> <div class="video" :style="{ ...cStyle.wrapper, background:`url(${data.coverSrc}) 0% 0% / 100% 100% no-repeat`}" @click="openViewer" > <!-- 播放按钮 --> <div class="video-play"> <i class="el-icon-caret-right" /> </div> </div> <!-- 全屏状态 - 容器 --> <div v-if="showViewer" id="t-video-viewer" class="video-viewer" > <div class="video-viewer__box"> <!-- 全屏状态 - 顶部标题 --> <div class="video-viewer__name"> {{ data.videoName }} </div> <!-- 全屏状态 - 关闭按钮 --> <div class="video-viewer__close" @click="closeViewer" > <i class="el-icon-close" /> </div> <!-- 全屏状态 - 视频播放标签 --> <video ref="realtimeVideo" class="video-js video-viewer__video" :src="data.videoSrc" controls="controls" autoplay > 浏览器不支持视频标签 </video> </div> </div> </div> </template>
- 该模板包含了两个部分: 缩略图模块 + 全屏模块
- 缩略图模块:
- 通过动态绑定样式,设置用户传入的背景图,作为缩略封面
- :style="{ ...cStyle.wrapper, background:`url(${data.coverSrc}) 0% 0% / 100% 100% no-repeat`}"
- 通过给播放按钮绑定 click事件,实现全屏模块的展示 @click="openViewer"
- 全屏模块:
- 首先通过 v-if="showViewer" 确定 全屏模块是否展示,此全屏模块容器A应该设置 id,id="t-video-viewer"
- 接着在上述容器A中,再设置一个容器B,用于包裹 视频全屏状态下的:标题,关闭按钮,video标签
- 全屏的标题,根据用户传入的内容进行设置
- 全屏的关闭按钮绑定 click事件,实现全屏模块的隐藏 @click="closeViewer"
- 全屏的视频标签,根据用户传入的视频地址,绑定 src,为了获取这个视频标签的实例对象,需要给该标签绑定 ref,也就是 ref="realtimeVideo"
在 setup() 中写逻辑(video.js 库操作):- 声明各种变量:
setup(props) { const state = reactive({ // video标签 dom元素 实例 realtimeVideo: {} as HTMLElement, // video.js 这个库的实例(播放器) player: null, }); // 是否全屏显示 const showViewer = ref(false); // videojs index.html引入 或者 import引入 const video = (window as any).videojs || videojs; }
- 注意:
- 如果项目中存在 video.js,一般会被放在 public/index.html 下进行引入,此时组件内如果需要使用 video.js,就可以通过 const video = (window as any).videojs 导入
- 如果项目中未安装 video.js,而是采用 npm 安装的形式,则就相当于最上面说的,通过 import 导入,在 setup() 中获取方法:const video = videojs;
- 获取 <video></video> 这种 dom元素实例 的方法:
- 在标签上添加 ref 属性:ref="realtimeVideo"
- 在 setup() 中声明与 ref 相同名的响应式变量:const state = reactive({ realtimeVideo: {} as HTMLElement })
- 两者一定要同名,尽可能声明类型 xxx as HTMLElement
打开全屏播放视频【重要】:/** * 打开大图浏览 */ function openViewer() { showViewer.value = true; // 将全屏模块放出来 // 将组件挂载到body标签里 nextTick(() => { const body = document.querySelector('body'); if (body.append) { body.append(document.getElementById('t-video-viewer')); } else { body.appendChild(document.getElementById('t-video-viewer')); } // 如果用户传入了 videojs 的自定义配置 if (props.data.videojs) { console.log('onMounted realtimeVideo', state.realtimeVideo); state.player = video(state.realtimeVideo, props.data.videojs, () => { console.log('on ready'); }); console.log('onMounted player', state.player); } }); }
- 第一步是将全屏模块放出来,也就是满足全屏模块的 v-if 条件
- 因为 v-if 改变之后,dom会进行重新渲染,也就是说,全屏模式的相关dom是后出现的,这时如果进行数据交互,应该放在 nextTick(() => {}) 函数中(个人理解,如果错了请指正 >_<)
- 在 nextTick 函数中,先获取 body dom元素,然后在 body 中追加全屏模块 body.append(document.getElementById('t-video-viewer'));
- 如果用户传入了 videojs 的自定义配置,就把最初声明的 响应式播放器变量 player 进行赋值,也就是调用 video() 方法,传入 video标签的ref 及 用户传入的 videojs 配置
关闭全屏播放视频:/** * 关闭大图浏览 */ function closeViewer() { showViewer.value = false; }
- onBeforeUnmount(() => {}) 组件卸载前执行的操作
onBeforeUnmount(() => { if (state.player) { (state.player as any).dispose(); } });
- 组件卸载之前,如果还有播放器,应该进行 销毁(调用 video.js 中的 dispose())
默认样式设置:.t-video-base { // 缩略图外容器 .video { position: relative; width: 100%; height: 119px; border-radius: 7px; box-shadow: 0 0 20px rgba(0, 0, 0,.8) inset; overflow: hidden; cursor: pointer; display: flex; justify-content: center; align-items: center; transition: all .5s; &:hover { box-shadow: 0 0 20px 8px rgba(0, 0, 0,.8) inset; } // 播放按钮 &-play { width: 32px; height: 32px; background: rgba(9, 16, 41, .7); border-radius: 50%; color: #fff; font-size: 18px; display: flex; justify-content: center; align-items: center; } } } // 全屏模块外容器 .video-viewer { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 9000; display: flex; justify-content: center; align-items: center; background: rgba(0, 0, 0, .5); // 关闭按钮 &__close { position: absolute; z-index: 2; top: 15px; right: 15px; width: 24px; height: 24px; font-size: 24px; line-height: 24px; color: #eee; cursor: pointer; } &__close:hover { color: #fff; } // 视频名称 &__name { position: absolute; z-index: 2; left: 18px; top: 12px; font-size: 16px; color: #eee; line-height: 24px; } // 名字和关闭按钮的外容器 &__box { position: relative; width: 90%; border-radius: 6px; overflow: hidden; display: flex; align-items: center; justify-content: center; } &__box::after { // 欸【?】 content: ' '; display: block; position: absolute; top: 0; bottom: 0; z-index: 1; width: 100%; height: 100%; box-shadow: 0 28px 50px -3px rgba(0, 0, 0, 0.85) inset; pointer-events: none; } // 视频标签 &__video { margin: 0; width: 100%; height: 100%; outline: none; } }
- 这里的关键点,是 pointer-events: none;
2.视频组件 video-base 使用
- 导入组件及样式:
- 一种是在 main.ts 中全局引入,另一种是在 单独的 .vue文件中按需引入
// main.ts import TVideoBase from "@p-base/video-base"; import '@p-base/video-base/dist/cjs/index.css'; createApp(App).use(TVideoBase); // xx.vue import TVideoBase from "@p-base/video-base"; export default defineComponent({ components: { TVideoBase, }, })
使用标题组件:<!-- 轮播视频 --> <div v-if="videoData"> <swiper :pagination="{ clickable: true }" :autoplay="true"> <swiper-slide v-for="(item, index) in videoData" :key="index" class="t-mb16"> <t-video-base :data="item" :c-style="{ wrapper:{ width:'100%', height: '240px' } }" /> </swiper-slide> </swiper> </div> <div v-if="!videoData">暂无内容!</div> // 视频封面 const videoCover = require('@/assets/images/icons/video-cover.png').default; setup() { const state = reactive({ player: [] as any[], // video.js 相关 - 播放器 }); // 视频数据 const videoData: any = ref([]); /** * 判断视频格式 */ const checkTV = (str: string) => { let type; const index = str.lastIndexOf('.'); const last = str.substr(index); if (last === '.MP4' || last === '.mp4') { type = 'video/mp4'; } else if (last === '.m3u8' || last === '.M3U8') { type = 'application/x-mpegURL'; } return type; }; /** * 获取视频信息 * @params code 点位编码 */ const getAnimalDetail = (code: string | number) => { const params = { code, // 点位编码 videoPeriods: state.numberPeriodsVideo, // 视频期数 }; getAnimalDetailJson(params).then((data) => { const res: any = data.data; if (res && res.code === 200) { // 视频信息 videoData.value = res.data.data.videoList.map((item: any) => ({ // 用户自行传入 videojs 的配置 videojs: { aspectRatio: '16:7', height: 300, autoplay: true, controls: false, sources: [{ src: item.url, type: checkTV(item.url), // 判断视频格式 }], }, videoName: '物种详情', desc: '这是物种详情的视频', coverSrc: videoCover, })); } }); }; return { ...toRefs(state), videoData, }; }
- 注意:
- 如果仅仅设置了 videoSrc,视频会采用 videoSrc 的源文件
- 如果同时设置了 videoSrc 及 videojs,视频会优先采用 videojs 中的配置项
02-23
12-19
1528
01-04
2156
02-02
356
10-13
2244