313 lines
8.5 KiB
Vue
313 lines
8.5 KiB
Vue
![]() |
<template>
|
|||
|
<swiper
|
|||
|
class="m-tiktok-video-swiper"
|
|||
|
circular
|
|||
|
@change="swiperChange"
|
|||
|
:current="state.current"
|
|||
|
:vertical="true"
|
|||
|
duration="300"
|
|||
|
>
|
|||
|
<swiper-item v-for="(item, index) in state.displaySwiperList" :key="index">
|
|||
|
<view class="swiper-item" @click="(e) => handleClick(index, e)">
|
|||
|
<video
|
|||
|
:id="`video__${index}`"
|
|||
|
:controls="controls"
|
|||
|
:autoplay="false"
|
|||
|
:loop="loop"
|
|||
|
@ended="ended"
|
|||
|
@controlstoggle="controlstoggle"
|
|||
|
@play="onPlay"
|
|||
|
@error="emits('error')"
|
|||
|
class="m-tiktok-video-player"
|
|||
|
:src="item.src || item.explainUrl"
|
|||
|
v-if="index === 0 || !state.isFirstLoad"
|
|||
|
></video>
|
|||
|
<view class="cover-triangle" v-if="pause"></view>
|
|||
|
<image
|
|||
|
v-if="item.poster && state.displayIndex != index"
|
|||
|
:src="item.poster"
|
|||
|
class="m-tiktok-video-poster"
|
|||
|
mode="aspectFit"
|
|||
|
></image>
|
|||
|
<slot :item="item"></slot>
|
|||
|
</view>
|
|||
|
</swiper-item>
|
|||
|
</swiper>
|
|||
|
</template>
|
|||
|
|
|||
|
<script setup>
|
|||
|
import { ref, reactive, getCurrentInstance, watch, nextTick } from 'vue';
|
|||
|
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
|||
|
const _this = getCurrentInstance();
|
|||
|
const emits = defineEmits(['play', 'error', 'loadMore', 'change', 'controlstoggle', 'click', 'ended']);
|
|||
|
|
|||
|
const lastTapTime = ref(0);
|
|||
|
const pause = ref(false);
|
|||
|
const props = defineProps({
|
|||
|
/**
|
|||
|
* 视频列表
|
|||
|
*/
|
|||
|
videoList: {
|
|||
|
type: Array,
|
|||
|
default: () => [],
|
|||
|
},
|
|||
|
/**
|
|||
|
* 是否循环播放一个视频
|
|||
|
*/
|
|||
|
loop: {
|
|||
|
type: Boolean,
|
|||
|
default: true,
|
|||
|
},
|
|||
|
/**
|
|||
|
* 显示原生控制栏
|
|||
|
*/
|
|||
|
controls: {
|
|||
|
type: Boolean,
|
|||
|
default: true,
|
|||
|
},
|
|||
|
/**
|
|||
|
* 是否自动播放
|
|||
|
*/
|
|||
|
autoplay: {
|
|||
|
type: Boolean,
|
|||
|
default: true,
|
|||
|
},
|
|||
|
/**
|
|||
|
* 是否自动滚动播放
|
|||
|
*/
|
|||
|
autoChange: {
|
|||
|
type: Boolean,
|
|||
|
default: false,
|
|||
|
},
|
|||
|
/**
|
|||
|
* 滚动加载阈值(即播放到剩余多少个之后触发加载更多
|
|||
|
*/
|
|||
|
loadMoreOffsetCount: {
|
|||
|
type: Number,
|
|||
|
default: 2,
|
|||
|
},
|
|||
|
/**
|
|||
|
* 暂停 1 单机暂停; 2 双击暂停; 0关闭
|
|||
|
*/
|
|||
|
pauseType: {
|
|||
|
type: Number,
|
|||
|
default: 2,
|
|||
|
},
|
|||
|
});
|
|||
|
|
|||
|
const state = reactive({
|
|||
|
originList: [], // 源数据
|
|||
|
displaySwiperList: [], // swiper需要的数据
|
|||
|
displayIndex: 0, // 用于显示swiper的真正的下标数值只有:0,1,2。
|
|||
|
originIndex: 0, // 记录源数据的下标
|
|||
|
current: 0,
|
|||
|
oid: 0,
|
|||
|
showControls: '',
|
|||
|
toggleShow: true, // 显示面板
|
|||
|
videoContexts: [],
|
|||
|
isFirstLoad: true,
|
|||
|
});
|
|||
|
|
|||
|
const initVideoContexts = () => {
|
|||
|
state.videoContexts = [
|
|||
|
uni.createVideoContext('video__0', _this),
|
|||
|
uni.createVideoContext('video__1', _this),
|
|||
|
uni.createVideoContext('video__2', _this),
|
|||
|
];
|
|||
|
};
|
|||
|
|
|||
|
const onPlay = (e) => {
|
|||
|
emits('play', e);
|
|||
|
};
|
|||
|
|
|||
|
const setVideoRef = (el, index) => {
|
|||
|
if (el) {
|
|||
|
videoRefs.value[index] = el;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
function handleClick(index, e) {
|
|||
|
const now = Date.now();
|
|||
|
switch (props.pauseType) {
|
|||
|
case 1:
|
|||
|
if (pause.value) {
|
|||
|
state.videoContexts[index].play();
|
|||
|
pause.value = false;
|
|||
|
} else {
|
|||
|
state.videoContexts[index].pause();
|
|||
|
pause.value = true;
|
|||
|
}
|
|||
|
break;
|
|||
|
case 2:
|
|||
|
if (now - lastTapTime.value < 300) {
|
|||
|
if (pause.value) {
|
|||
|
state.videoContexts[index].play();
|
|||
|
pause.value = false;
|
|||
|
} else {
|
|||
|
state.videoContexts[index].pause();
|
|||
|
pause.value = true;
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
lastTapTime.value = now;
|
|||
|
state.toggleShow = !state.toggleShow;
|
|||
|
emits('click', e);
|
|||
|
}
|
|||
|
function ended() {
|
|||
|
// 自动切换下一个视频
|
|||
|
if (props.autoChange) {
|
|||
|
if (state.displayIndex < 2) {
|
|||
|
state.current = state.displayIndex + 1;
|
|||
|
} else {
|
|||
|
state.current = 0;
|
|||
|
}
|
|||
|
}
|
|||
|
emits('ended');
|
|||
|
}
|
|||
|
/**
|
|||
|
* 初始一个显示的swiper数据
|
|||
|
* @originIndex 从源数据的哪个开始显示默认0,如从其他页面跳转进来,要显示第n个,这个参数就是他的下标
|
|||
|
*/
|
|||
|
function initSwiperData(originIndex = state.originIndex) {
|
|||
|
const originListLength = state.originList.length; // 源数据长度
|
|||
|
const displayList = [];
|
|||
|
displayList[state.displayIndex] = state.originList[originIndex];
|
|||
|
displayList[state.displayIndex - 1 == -1 ? 2 : state.displayIndex - 1] =
|
|||
|
state.originList[originIndex - 1 == -1 ? originListLength - 1 : originIndex - 1];
|
|||
|
displayList[state.displayIndex + 1 == 3 ? 0 : state.displayIndex + 1] =
|
|||
|
state.originList[originIndex + 1 == originListLength ? 0 : originIndex + 1];
|
|||
|
state.displaySwiperList = displayList;
|
|||
|
|
|||
|
if (state.oid >= state.originList.length) {
|
|||
|
state.oid = 0;
|
|||
|
}
|
|||
|
if (state.oid < 0) {
|
|||
|
state.oid = state.originList.length - 1;
|
|||
|
}
|
|||
|
// 暂停所有视频
|
|||
|
state.videoContexts.map((item) => item?.stop());
|
|||
|
setTimeout(() => {
|
|||
|
// 当前视频
|
|||
|
if (props.autoplay) {
|
|||
|
uni.createVideoContext(`video__${state.displayIndex}`, _this).play();
|
|||
|
}
|
|||
|
}, 500);
|
|||
|
// 数据改变
|
|||
|
emits('change', {
|
|||
|
index: originIndex,
|
|||
|
detail: state.originList[originIndex],
|
|||
|
});
|
|||
|
// 加载更多
|
|||
|
var pCount = state.originList.length - props.loadMoreOffsetCount;
|
|||
|
if (originIndex == pCount) {
|
|||
|
emits('loadMore');
|
|||
|
}
|
|||
|
}
|
|||
|
/**
|
|||
|
* swiper滑动时候
|
|||
|
*/
|
|||
|
function swiperChange(event) {
|
|||
|
const { current } = event.detail;
|
|||
|
state.isFirstLoad = false;
|
|||
|
const originListLength = state.originList.length; // 源数据长度
|
|||
|
// 向后滚动
|
|||
|
if (state.displayIndex - current == 2 || state.displayIndex - current == -1) {
|
|||
|
state.originIndex = state.originIndex + 1 == originListLength ? 0 : state.originIndex + 1;
|
|||
|
state.displayIndex = state.displayIndex + 1 == 3 ? 0 : state.displayIndex + 1;
|
|||
|
state.oid = state.originIndex - 1;
|
|||
|
initSwiperData(state.originIndex);
|
|||
|
}
|
|||
|
// 如果两者的差为-2或者1则是向前滑动
|
|||
|
else if (state.displayIndex - current == -2 || state.displayIndex - current == 1) {
|
|||
|
state.originIndex = state.originIndex - 1 == -1 ? originListLength - 1 : state.originIndex - 1;
|
|||
|
state.displayIndex = state.displayIndex - 1 == -1 ? 2 : state.displayIndex - 1;
|
|||
|
state.oid = state.originIndex + 1;
|
|||
|
initSwiperData(state.originIndex);
|
|||
|
}
|
|||
|
state.toggleShow = true;
|
|||
|
}
|
|||
|
|
|||
|
function controlstoggle(e) {
|
|||
|
state.showControls = e.detail.show;
|
|||
|
emits('controlstoggle', e);
|
|||
|
}
|
|||
|
|
|||
|
watch(
|
|||
|
() => props.videoList,
|
|||
|
() => {
|
|||
|
if (props.videoList?.length) {
|
|||
|
state.originList = props.videoList;
|
|||
|
if (state.isFirstLoad || !state.videoContexts?.length) {
|
|||
|
initSwiperData();
|
|||
|
initVideoContexts();
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
{
|
|||
|
immediate: true,
|
|||
|
}
|
|||
|
);
|
|||
|
|
|||
|
let loadTimer = null;
|
|||
|
onLoad(() => {
|
|||
|
// 为了首次只加载一条视频(提高首次加载性能),延迟加载后续视频
|
|||
|
loadTimer = setTimeout(() => {
|
|||
|
state.isFirstLoad = false;
|
|||
|
clearTimeout(loadTimer);
|
|||
|
}, 5000);
|
|||
|
});
|
|||
|
|
|||
|
onUnload(() => {
|
|||
|
clearTimeout(loadTimer);
|
|||
|
});
|
|||
|
|
|||
|
defineExpose({
|
|||
|
initSwiperData,
|
|||
|
});
|
|||
|
</script>
|
|||
|
|
|||
|
<style lang="stylus" scoped>
|
|||
|
.m-tiktok-video-swiper,
|
|||
|
.m-tiktok-video-player {
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
background-color: #000;
|
|||
|
}
|
|||
|
.m-tiktok-video-swiper {
|
|||
|
.swiper-item {
|
|||
|
position: relative;
|
|||
|
height: 100%;
|
|||
|
}
|
|||
|
.m-tiktok-video-poster {
|
|||
|
background-color: #000;
|
|||
|
position: absolute;
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.cover-triangle{
|
|||
|
position: absolute;
|
|||
|
left: 50%;
|
|||
|
top: 50%;
|
|||
|
transform: translate(-50%, -50%)
|
|||
|
width: 132rpx
|
|||
|
height: 132rpx
|
|||
|
border-radius: 50%;
|
|||
|
background: rgba(0,0,0,0.3)
|
|||
|
}
|
|||
|
.cover-triangle::after {
|
|||
|
content: '';
|
|||
|
position: absolute;
|
|||
|
top: 50%;
|
|||
|
left: 50%;
|
|||
|
transform: translate(-40%, -50%) rotate(90deg);
|
|||
|
width: 0;
|
|||
|
height: 0;
|
|||
|
border-left: 40rpx solid transparent;
|
|||
|
border-right: 40rpx solid transparent;
|
|||
|
border-bottom: 60rpx solid #fff;
|
|||
|
}
|
|||
|
</style>
|