最近需求说要做个类似抖音那种视频的,我二话不说就用了swiper-view组件,但是效果不太理想,后面改用css属性
先放效果图:
<template>
<view class="video-scroll-container" @touchstart="handleTouchStart" @touchend="handleTouchEnd"
@touchmove="handleTouchMove">
<view class="video-list"
:style="{ transform: 'translateY(' + -currentIndex * windowHeight + 'px)', transition: isSwiping ? 'transform 0.3s ease' : 'none' }">
<view v-for="(video, index) in videoList" :key="video.id" class="video-item"
:style="{ height: windowHeight + 'px' }">
<view class="video-wrapper" @click="handleVideoClick(index)">
<!-- 视频 -->
<video :poster="video?.cover" :controls="false" :id="'video' + index" :src="video.video_src" :show-play-btn="false"
:enable-progress-gesture="false" :autoplay="true" @play="onPlay(video,index)"
@ended="onEnded(index)" @pause="onPause(index)" @timeupdate="onTimeUpdate($event,index)"
class="fullscreen-video" :ref="(el) => setVideoRef(el, index)"></video>
<!-- 圆形进度条 -->
<view class="progress-wrap">
<!-- <circle-progress :percent="video.progressPercent || 0" :stroke-width="6"
stroke-color="#fff" /> -->
<CircleProgress :percent="video.progressPercent || 0" :stroke-width="6" stroke-color="#fff">
{{ video.remainingTime }}
</CircleProgress>
</view>
<!-- 控制按钮 -->
<view v-if="isControlVisible" class="control-btn" @click.stop="togglePlay(index)">
<cover-view>
<image style="width: 116rpx;height: 116rpx;" src="/static/images/icon/play.png"
mode="aspectFit">
</image>
</cover-view>
</view>
<view class="fixed">
<cover-view>
<view class="flex flex_dir_c flex_items_c" @click.stop="toDrap('like',video)">
<image src="/static/images/icon/img1.png" mode=""></image>
<view class="txt">
{{video?.like_num}}
</view>
</view>
<view class="flex flex_dir_c flex_items_c" @click.stop="toDrap('email',video)">
<image src="/static/images/icon/img2.png" mode=""></image>
<view class="txt">
{{video?.comment_num}}
</view>
</view>
<view class="flex flex_dir_c flex_items_c" @click.stop="toDrap('share',video)">
<image src="/static/images/icon/img3.png" mode=""></image>
<view class="txt">
{{video?.share_num}}
</view>
</view>
<view class="flex flex_dir_c flex_items_c" @click.stop="toDrap('send',video)">
<image src="/static/images/icon/img4.png" mode=""></image>
<view class="txt">
发布
</view>
</view>
</cover-view>
</view>
<!-- 发布人信息 -->
<view class="publisher-info">
<cover-view>
<!-- <text>{{ video.publisher }}</text> -->
<view class="Info">
<view class="good-box" v-if="video?.goods !== null" @click.stop="toDetail(video)">
<image :src="video?.goods?.thumb" class="good-img" mode=""></image>
<view class="text-wrap">
<view class="desc">
{{video?.goods?.title}}
</view>
<view class="money">
<view class="price">
<text class="fs_24">¥</text>{{video?.goods?.price}}
</view>
<view class="old-price">
¥{{video?.goods?.market_price}}
</view>
</view>
</view>
<image class="carImg" src="/static/images/icon/vCar.png" mode=""></image>
</view>
<view class="video-detail-box">
<view class="good-and-title">
<view class="video-title">
{{video?.title}}
</view>
</view>
<view class="detail-box iphoneXStyle flex flex_items_c ">
<view class="flex flex_items_c flex_just_sb" style="width: 100%;">
<view class="left-user-info flex flex_items_c">
<image class="avatar" :src="video?.member?.avatar_image" mode="">
</image>
<view class="user flex flex_dir_c">
<text
class="name">{{video?.member?.nickname || video?.member?.username}}</text>
<text class="num">共{{video?.member_video_num || 0}}个分享视频</text>
</view>
</view>
<view class="center-follow">
<view class="btn-follow"
:class="[video?.is_follow=== 0?'caBtn':'acBtn']"
@click.stop="toEditFollow(video)">
{{video?.is_follow=== 0?'关注':'已关注'}}
</view>
</view>
</view>
<!-- <view class="right-icon flex flex_items_c">
<view class="flex flex_dir_c flex_items_c rightIcon"
@click.stop="toDrap('share',video)">
<uni-icons type="redo" color="#fff" size="20"></uni-icons>
<view class="">
{{video?.share_num}}
</view>
</view>
<view class="flex flex_dir_c flex_items_c rightIcon"
@click.stop="toDrap('like',video)">
<uni-icons v-if="video?.like_num == 0" type="heart" color="#fff"
size="20"></uni-icons>
<uni-icons v-else type="heart-filled" color="red" size="20"></uni-icons>
<view class="">
{{video?.like_num}}
</view>
</view>
<view class="flex flex_dir_c flex_items_c rightIcon"
@click.stop="toDrap('email',video)">
<uni-icons type="email" color="#fff" size="20"></uni-icons>
<view class="">
{{video?.comment_num}}
</view>
</view>
</view> -->
</view>
</view>
</view>
</cover-view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
onHide,
onReachBottom
} from "@dcloudio/uni-app"
import {
ref,
onMounted,
getCurrentInstance,
computed,
watch,
onUnmounted
} from 'vue'
import {
wxShare
} from "@/utils/share.js";
import {
getEnvironment,
} from '@/utils/utils.js';
import CircleProgress from '@/components/circle.vue'
const {
proxy
} = getCurrentInstance()
const props = defineProps({
videoList: {
type: Array,
required: true,
default: () => []
},
// id: {
// type: [String || Number],
// // required: true,
// default: ''
// }
})
import {
getBasicSet,
getVideoList,
addRecord,
getlogViewTime,
getvideoLike,
getCommentLike,
createComment,
getSupport,
getEditFollow
} from '@/api/customerCenter.js'
const playVId = ref({})
const currentIndex = ref(0)
const playingIndex = ref(-1)
const videoRefs = ref([])
const startY = ref(0)
const endY = ref(0)
const isPausedManually = ref(false);
const isSwiping = ref(false)
const windowHeight = ref(0)
const videoId = ref('')
const content = ref('')
const playIcon = ref('https://yourdomain.com/icon/play.png')
const pauseIcon = ref('https://yourdomain.com/icon/pause.png')
const videoTime = ref('')
const durateTime = ref('')
const isControlVisible = ref(false);
let controlTimeout = null;
const duration = ref(0); // 视频总时长(秒)
const currentTime = ref(0); // 当前播放时间(秒)
const CommentList = ref([])
const showShare = ref(false)
const emit = defineEmits(['openPop', 'openShare', 'send']);
// 剩余时间计算
const remainingTime = computed(() => {
return Math.max(duration.value - currentTime.value, 0);
});
const previousIndex = ref(-1);
const popup = ref(null)
const popup1 = ref(null)
// 显示控制按钮并设置2秒后隐藏
const showControl = () => {
isControlVisible.value = true;
// if (playingIndex.value !== -1 && !isPausedManually.value) {
// if (controlTimeout) clearTimeout(controlTimeout);
// controlTimeout = setTimeout(() => {
// isControlVisible.value = false;
// }, 2000);
// }
};
// watch(props.id, (newValue) => {
// console.log('变化', props.id);
// })
function handleVideoClick(index) {
const videoContext = uni.createVideoContext(`video${index}`, proxy);
if (!videoContext) {
console.error('视频上下文不存在', index);
return;
}
if (playingIndex.value === index) {
// 当前视频正在播放 → 执行暂停
videoContext.pause();
playingIndex.value = -1;
isPausedManually.value = true;
// 强制显示控制按钮
isControlVisible.value = true;
} else {
// 暂停之前正在播放的视频(如果有的话)
if (playingIndex.value !== -1) {
const prevContext = uni.createVideoContext(`video${playingIndex.value}`, proxy);
if (prevContext) {
prevContext.pause();
}
}
// 播放当前视频
videoContext.play();
playingIndex.value = index;
isPausedManually.value = false;
// 隐藏控制按钮
isControlVisible.value = false;
}
}
onReachBottom(() => {
console.log('触底');
})
function shareHandler(val) {
console.log('val', val);
let link = location.href;
let shareInfo = {
title: val?.title,
desc: 'AI雷达视频',
link,
imgUrl: val?.cover,
}
if ([1, 2].includes(getEnvironment())) {
wxShare(shareInfo).then(res => {
console.log(res);
emit('openShare')
})
} else {
uni.setClipboardData({
data: link,
success: function() {}
});
}
}
function toDrap(type, e) {
videoId.value = e.id
switch (type) {
case 'like':
getLick(e)
break;
case 'email':
// 无法在本页面加弹窗会导致滚动视频也滚动恶心
emit('openPop', e.id)
// popup.value.open()
// getCommentList(e.id)
break;
case 'share':
shareHandler(e)
// showShare.value = true
break;
case 'send':
emit('send')
break;
default:
break;
}
}
function toDetail(e) {
uni.navigateTo({
url: `/pages/goods/index?id=${e?.goods?.id}&vid=${e?.id}`
})
}
async function getCommentList() {
const {
result,
data,
msg
} = await getCommentLike({
video_id: videoId.value
})
if (result === 1) {
CommentList.value = data?.list?.data
} else {
uni.showToast({
title: msg,
icon: 'none'
})
}
}
onUnmounted(() => {
pauseAllVideos()
})
function toComment() {
content.value = ''
popup1.value.open()
}
async function popLike(e) {
const {
result,
data,
msg
} = await getSupport({
comment_id: e.id
})
if (result == 1) {
if (e.support_num > 0) {
e.is_support = 0
e.support_num -= 1
} else {
e.is_support = 1
e.support_num += 1
}
} else {
uni.showToast({
title: msg,
icon: 'none'
})
}
}
async function getLick(e) {
const {
result,
data,
msg
} = await getvideoLike({
video_goods_id: e.id
})
if (result == 1) {
if (e.like_num > 0) {
e.like_num -= 1
} else {
e.like_num += 1
}
} else {
uni.showToast({
title: msg,
icon: 'none'
})
}
}
// 鼠标离开时立即隐藏
const hideControl = () => {
isControlVisible.value = false;
if (controlTimeout) clearTimeout(controlTimeout);
};
async function toEditFollow(item) {
const {
result,
data,
msg
} = await getEditFollow({
member_id: item.uid,
follow_type: item.is_follow === 0 ? 1 : 2
})
if (result == 1) {
if (item.is_follow === 0) {
item.is_follow = 1
} else {
item.is_follow = 0
}
} else {
uni.showToast({
title: msg,
icon: 'none'
})
}
}
// 播完继续
// const onEnded = (index) => {
// const videoContext = uni.createVideoContext(`video${index}`, proxy);
// if (playingIndex.value === index) {
// videoContext.play(); // 自动重新播放
// }
// };
// const onEnded = (index) => {
// setTimeout(() => {
// playVideo(index);
// }, 500); // 0.5秒后重新播放
// };
const onEnded = (index) => {
if (playingIndex.value === index) {
const videoContext = uni.createVideoContext(`video${index}`, proxy);
videoContext.play(); // 只重新播放当前视频
}
};
// 获取窗口高度
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
windowHeight.value = res.windowHeight
}
})
const videoContext = uni.createVideoContext('video0', proxy)
console.log('Video context created:', videoContext)
props.videoList.filter(oid => oid == props.id)
})
// 设置 video ref
const setVideoRef = (el, index) => {
videoRefs.value[index] = el
}
// 开始触摸
const handleTouchStart = (e) => {
console.log('开始', e.touches[0].pageY);
// getLogViewTime(playVId?.value)
// getRecord(playVId?.value?.id)
// e.preventDefault();
startY.value = e.touches[0].pageY
isSwiping.value = false;
}
const handleTouchMove = (e) => {
const currentY = e.touches[0].pageY;
if (Math.abs(currentY - startY.value) > 10) {
isSwiping.value = true; // 超过阈值认为是滑动
e.preventDefault(); // 此时开始阻止默认行为
}
}
// 结束触摸
const handleTouchEnd = (e) => {
endY.value = e.changedTouches[0].pageY;
if (!isSwiping.value) {
console.log("这是一个点击事件");
// 这里可以触发额外的点击处理逻辑
return;
}
const diff = endY.value - startY.value;
if (diff > 0 && currentIndex.value > 0) {
currentIndex.value--;
} else if (diff < 0 && currentIndex.value < props.videoList.length - 1) {
currentIndex.value++;
}
// 防止超出范围
if (currentIndex.value >= props.videoList.length) {
currentIndex.value = props.videoList.length - 1;
}
setTimeout(() => {
isSwiping.value = false;
console.log('currentIndex.value', currentIndex.value);
console.log('previousIndex.value', previousIndex.value);
if (currentIndex.value !== previousIndex.value) {
getLogViewTime(props.videoList[currentIndex.value])
getRecord(props.videoList[currentIndex.value]?.id);
}
pauseOtherVideos(currentIndex.value);
playCurrentVideo(currentIndex.value);
}, 300);
};
// 优化 关闭页面自动停止所有播放
function pauseAllVideos() {
videoRefs.value.forEach((video, idx) => {
if (video) {
const videoContext = uni.createVideoContext(`video${idx}`, proxy);
videoContext.pause();
}
});
console.log('停止所有播放');
playingIndex.value = -1; // 更新播放索引状态
isPausedManually.value = true; // 更新手动暂停状态
}
const pauseOtherVideos = (currentIdx) => {
// videoRefs.value.forEach((video, idx) => {
// if (idx !== currentIdx && video) {
// const videoContext = uni.createVideoContext(`video${idx}`, proxy);
// videoContext.pause();
// }
// });
videoRefs.value.forEach((video, idx) => {
if (idx !== currentIdx && video) {
const videoContext = uni.createVideoContext(`video${idx}`, proxy);
videoContext.pause();
}
});
};
// 播放指定视频
const playCurrentVideo = (index) => {
previousIndex.value = currentIndex.value;
const videoContext = uni.createVideoContext(`video${index}`, proxy);
if (videoContext) {
videoContext.play();
playingIndex.value = index;
isPausedManually.value = false;
}
};
// 停止所有视频
// const stopAllVideos = () => {
// videoRefs.value.forEach((video, idx) => {
// if (video && idx !== currentIndex.value) {
// const videoContext = uni.createVideoContext(`video${idx}`, proxy);
// console.log('dadadadad', videoContext);
// videoContext.pause();
// }
// });
// };
// 播放/暂停切换
// const togglePlay = (index) => {
// const videoContext = uni.createVideoContext(`video${index}`, proxy);
// if (playingIndex.value === index) {
// videoContext.pause();
// playingIndex.value = -1;
// isPausedManually.value = true;
// } else {
// stopAllVideos();
// playVideo(index); // 使用统一播放方法
// }
// };
const togglePlay = (index) => {
const videoContext = uni.createVideoContext(`video${index}`, proxy);
if (!videoContext) {
console.error('无法获取视频上下文', index);
return;
}
if (playingIndex.value === index) {
console.log('暂停');
// 当前视频正在播放 → 执行暂停
videoContext.pause();
playingIndex.value = -1;
isPausedManually.value = true;
// 强制显示控制按钮
isControlVisible.value = true;
} else {
console.log('播放');
// 不再暂停其他视频,只播放当前视频
videoContext.play();
playingIndex.value = index;
isPausedManually.value = false;
// 播放时隐藏控制按钮
isControlVisible.value = false;
}
};
async function createCommem() {
const {
result,
data,
msg
} = await createComment({
content: content.value,
video_id: videoId.value
})
if (result === 1) {
uni.showToast({
title: msg,
icon: 'none'
})
popup1.value.close()
getCommentList()
} else {
uni.showToast({
title: msg,
icon: 'none'
})
}
}
const onTimeUpdate = (event, index) => {
const duration = event.detail.duration;
const currentTime = event.detail.currentTime;
// 边界检查:确保时间是有效数值
if (typeof duration !== 'number' || typeof currentTime !== 'number' || isNaN(duration) || isNaN(
currentTime)) {
return;
}
// 更新进度条百分比
if (duration > 0) {
props.videoList[index].progressPercent = (currentTime / duration) * 100;
}
// 计算剩余时间并格式化为 MM:SS
const remainingSeconds = Math.max(0, duration - currentTime);
const minutes = Math.floor(remainingSeconds / 60)
.toString()
.padStart(2, '0');
const seconds = Math.floor(remainingSeconds % 60)
.toString()
.padStart(2, '0');
props.videoList[index].remainingTime = `${minutes}:${seconds}`;
// 视频开始播放时重置手动暂停状态
if (currentTime === 0 && playingIndex.value === index) {
isPausedManually.value = false;
}
};
const formatTime = (seconds) => {
const totalSeconds = Math.floor(seconds);
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
const secs = (totalSeconds % 60).toString().padStart(2, '0');
return `${minutes}:${secs}`;
};
// 播放事件处理
const onPlay = (e, index) => {
playVId.value = e
playingIndex.value = index
isControlVisible.value = false;
}
async function getLogViewTime(e) {
const {
result,
data,
msg
} = await getlogViewTime({
video_id: e?.id,
video_length: parseInt(e.remainingTime?.split(":")[1], 10)
})
}
async function getRecord(e) {
const {
result,
data,
msg
} = await addRecord({
video_id: e
})
if (result != 1) {
uni.showToast({
title: msg,
icon: 'none'
})
}
}
// 暂停事件处理
const onPause = (index) => {
if (playingIndex.value === index) {
playingIndex.value = -1;
isPausedManually.value = true;
// 强制显示控制按钮
isControlVisible.value = true;
}
};
const playVideo = (index) => {
const videoContext = uni.createVideoContext(`video${index}`, proxy);
if (videoContext) {
videoContext.play();
playingIndex.value = index;
isPausedManually.value = false;
}
};
</script>
<style scoped lang="scss">
.video-scroll-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
// touch-action: pan-y;
touch-action: none;
}
.video-list {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
.video-item {
width: 100%;
position: relative;
overflow: hidden;
}
.video-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.fullscreen-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.progress-wrap {
position: absolute;
top: 120rpx;
right: 30rpx;
z-index: 99;
}
.control-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 98;
opacity: 0.7;
transition: opacity 0.3s ease;
}
.control-btn:hover {
opacity: 1;
}
.fixed {
position: absolute;
right: 0;
z-index: 999;
top: 50%;
right: 24rpx;
image {
width: 80rpx;
height: 80rpx;
}
.txt {
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
text-align: left;
font-style: normal;
text-transform: none;
}
}
.publisher-info {
position: absolute;
bottom: 30rpx;
// left: 30rpx;
color: #fff;
font-size: 28rpx;
width: 100%;
z-index: 99;
}
.good-box {
display: flex;
z-index: 10;
// background: rgba(0, 0, 0, .4);
width: 464rpx;
height: 136rpx;
background: transparent;
border-radius: 8rpx 8rpx 8rpx 8rpx;
border: 2rpx solid rgba(255, 255, 255, 0.4);
padding: 8rpx;
position: absolute;
left: 24rpx;
bottom: 180rpx;
justify-content: space-between;
align-items: center;
.good-img {
margin-right: 0.35rem;
width: 120rpx;
height: 120rpx;
flex-shrink: 0;
}
.text-wrap {
padding-right: 20rpx;
flex: 1;
display: flex;
flex-direction: column;
text-align: left;
max-width: 10rem;
min-width: 5rem;
font-weight: 700;
justify-content: space-evenly;
.desc {
height: 40rpx;
font-weight: 500;
font-size: 32rpx;
line-height: 40rpx;
text-align: left;
font-style: normal;
text-transform: none;
color: #fff;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.money {
display: flex;
word-break: break-all;
flex-wrap: wrap;
padding-top: 20rpx;
.price {
// font-size: 13px;
line-height: 13px;
color: #f15353;
margin-right: 0.5rem;
}
.old-price {
font-weight: 400;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
text-align: left;
font-style: normal;
text-transform: none;
text-decoration: line-through;
}
}
}
.carImg {
width: 56rpx;
height: 56rpx;
}
}
.video-detail-box {
position: absolute;
z-index: 99;
left: 0;
bottom: -40rpx;
width: 100%;
color: #fff;
// padding-top: 2rem;
.good-and-title {
left: 0;
bottom: 6.875rem;
z-index: 99;
display: flex;
justify-content: space-between;
align-items: center;
.video-title {
z-index: 10;
width: 18rem;
padding: 0.375rem 0 0 1rem;
text-align: left;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-clamp: 2;
font-weight: 500;
font-size: 36rpx;
line-height: 40rpx;
}
}
.detail-box {
justify-content: space-between;
background-color: #232323;
padding: 40rpx 24rpx;
}
.iphoneXStyle {
// padding-bottom: 20px;
}
.left-user-info {
width: 9rem;
display: flex;
text-align: left;
}
.avatar {
margin-right: 0.5rem;
width: 88rpx;
height: 88rpx;
border: 2rpx solid #FFFFFF;
border-radius: 50%;
background: #fff;
}
.name {
font-weight: 500;
font-size: 28rpx;
color: #FFFFFF;
line-height: 40rpx;
text-align: left;
font-style: normal;
text-transform: none;
}
.num {
opacity: 0.6;
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
line-height: 40rpx;
text-align: left;
font-style: normal;
text-transform: none;
}
.center-follow {
padding-left: 1rem;
}
.btn-follow {
display: flex;
justify-content: center;
width: 128rpx;
height: 64rpx;
border-radius: 32rpx;
line-height: 64rpx;
}
.rightIcon {
padding-left: 1rem;
}
}
.caBtn {
opacity: 0.5;
background: #555500;
}
.acBtn {
background-color: #033A89;
// opacity: 0.1;
}
.cont {
// height: 200px;
padding: 0.375rem 0;
position: relative;
.title {
text-align: center;
}
.input-C {
position: absolute;
bottom: 0;
width: 92%;
padding-left: 0.875rem;
.inpitBox {
border: none;
margin: 0.875rem auto;
padding: 0.5rem;
background: #eaeaea;
border-radius: 2rem;
display: block;
}
}
.listC {
padding: 10rpx 0 100rpx;
height: calc(100vh - 520rpx);
overflow: auto;
.info {
padding: 10rpx 0;
font-size: 28rpx;
}
.item {
font-size: 24rpx;
padding: 0 0.875rem;
.infoC {
font-size: 24rpx;
.imageC {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
margin-right: 0.72rem;
}
.blueC {
color: #ff6260;
}
}
}
}
}
.con1 {
padding: 1rem;
}
.custom-input {
flex: 1;
width: 18.13rem;
background-color: #f5f5f5;
border-radius: 0.5rem;
padding: 0.5rem;
}
.pop1Txt {
color: #999;
width: 66rpx;
margin: 0 0.5rem;
height: 1.88rem;
font-size: .94rem;
line-height: 1.88rem;
color: #0072e8;
}
</style>
基于css属性会超出视频个数范围,所以要加入这个:
if (currentIndex.value >= props.videoList.length) {
currentIndex.value = props.videoList.length - 1;
}
代码很多不需要,懒得去了,大家可以基于自己需要作为组件传入特定的视频数组就行了