适配小程序的下滑上滑播放视频组件
提示:总感觉自己写好一点,我这里没有封装组件可以自己封装一下就可以了
文章目录
前言
随着时间流失,我的故事很多,当时人生很短,有一天需要开发一个视频组件,公司的测试妹妹说,我之前是用的别人组件,这里bug,哪里bug, 感觉他非常的讨厌我一样,我像下定了某种决心,我自己弄一个,也是经过了一个下午,也是弄出来了,我自信满满的给测试妹子看,那测试妹子惊呆了,直接就是这眼神,这小眼神,感觉要吃了我一样,时间很快,到了中午,阳光像照进了我心里,暖暖的,当然主要是有妹子陪我吃饭,哈哈哈,这妹子从现在开始就特别崇拜我,慢慢的就过去了,一天有一天,到了放假时间了,她居然约我吃饭,我也是勉强的答应了,我从别人哪里打听居然是富婆,哎呀我去,这下赚了啊,然后我们正常发展中,吃饭吃饭,等去玩了,吃完饭我们在散步,突然,~秀的声,她居然来亲我,我也是没有拒绝我也勉勉强强的配合了,那一时候我们就开始,走到了一次。哈哈哈哈,我从此走上了巅峰。
往下看。。。。。
我和你说哦,都是我想出来的。哈哈哈,假的,还是开始正题吧。
一、代码
html
<template>
<view class="video-container" :style="{ height: screenHeight + 'px' }">
<!-- 视频列表 -->
<swiper class="video-swiper" :vertical="true" :circular="false" :current="currentIndex" @change="onSwiperChange"
:duration="300" :style="{ height: screenHeight + 'px' }" @animationfinish="onAnimationFinish">
<swiper-item v-for="(video, index) in videoList" :key="getVideoKey(video, index)" class="video-item">
<view class="video-wrapper">
<!-- 视频播放器 -->
<video :id="'video-' + index" :src="video.videoUrl" :poster="getVideoPoster(video.videoUrl)"
:autoplay="false" :loop="false" :controls="false" :muted="isMuted" :show-play-btn="false"
:show-center-play-btn="false" :enable-play-gesture="true" :enable-progress-gesture="false"
object-fit="cover" class="video-player" @play="onVideoPlay(index)" @pause="onVideoPause(index)"
@ended="onVideoEnded(index)" @error="onVideoError(index, $event)"
@loadedmetadata="onVideoLoaded(index)" @timeupdate="onVideoTimeUpdate(index, $event)"
@waiting="onVideoWaiting(index)" @stalled="onVideoStalled(index)">
</video>
<!-- 视频封面(播放前显示) -->
<view v-if="!video.isPlaying || video.loadFailed" class="video-poster" @tap="playVideo(index)">
<image :src="getVideoPoster(video.videoUrl)" mode="aspectFill" class="poster-image"></image>
<view class="play-icon-wrapper">
<u-icon name="play-right-fill" size="60" color="#fff"></u-icon>
</view>
<!-- 加载失败提示 -->
<view v-if="video.loadFailed" class="load-failed-tips">
<text class="failed-text">视频加载失败,点击重试</text>
</view>
</view>
<!-- 加载状态 -->
<view v-if="video.loading && !video.loadFailed" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 边界提示 -->
<view v-if="showBoundaryTips" class="boundary-tips">
<text v-if="isFirstVideo && isSwipingUp" class="tip-text">已经是第一个视频了</text>
<text v-if="isLastVideo && isSwipingDown" class="tip-text">没有更多视频了</text>
</view>
<!-- 暂停按钮 -->
<view v-if="video.isPaused" class="pause-overlay" @tap="togglePlay(index)">
<view class="pause-icon-wrapper">
<u-icon name="pause" size="70" color="#fff"></u-icon>
</view>
</view>
<!-- 底部信息栏 -->
<view class="video-info">
<!-- 用户信息 -->
<view class="user-info">
<image :src="video.avatar" class="user-avatar"></image>
<view class="user-name">{{ video.nickname }}</view>
<button class="follow-btn" @tap="followUser(video.userId)">关注</button>
</view>
<!-- 视频描述 -->
<view class="video-description">
<text class="description-text">{{ video.videoContent }}</text>
</view>
<!-- 标签 -->
<view v-if="video.tags" class="tags-container">
<text v-for="tag in parseTags(video.tags)" :key="tag" class="tag-item"
@tap="searchByTag(tag)">
#{{ tag }}
</text>
</view>
<!-- 位置信息 -->
<view v-if="video.location" class="location-info">
<u-icon name="map-fill" size="16" color="#fff"></u-icon>
<text>{{ video.location }}</text>
</view>
<!-- 发布时间 -->
<view class="publish-time">
<u-icon name="calendar" size="16" color="rgba(255, 255, 255, 0.6)"></u-icon>
<text>{{ formatRelativeTime(video.createdAt) }}</text>
</view>
</view>
<!-- 右侧互动按钮 -->
<view class="interaction-sidebar">
<!-- 头像 -->
<view class="sidebar-avatar" @tap="goUserProfile(video.userId)">
<image :src="video.avatar" class="avatar-image" mode="aspectFill"></image>
<view class="follow-animation">
<u-icon name="plus" size="12" color="#ff2442"></u-icon>
</view>
</view>
<!-- 点赞 -->
<view class="interaction-btn" @click.tap="() =>likeVideo(video, index)">
<u-icon :name="video._liked ? 'heart-fill' : 'heart'" size="28"
:color="video._liked ? '#ff2442' : '#fff'" class="btn-icon">
</u-icon>
<text class="btn-count">{{ video.likeCount || 0 }}</text>
<!-- 点赞动画 -->
<view v-if="video.showLikeAnimation" class="like-animation">
<u-icon name="heart-fill" size="80" color="#ff2442"></u-icon>
</view>
</view>
<!-- 评论 -->
<view class="interaction-btn" @tap="commentVideo(video)">
<u-icon name="chat" size="28" color="#fff" class="btn-icon"></u-icon>
<text class="btn-count">{{ video.commentCount || 0 }}</text>
</view>
<!-- 收藏 -->
<view class="interaction-btn" @click.tap="() => collectVideo(video, index)">
<u-icon :name="video._collected ? 'star-fill' : 'star'" size="28"
:color="video._collected ? '#ff9500' : '#fff'" class="btn-icon">
</u-icon>
<text class="btn-count">{{ video.collectCount || 0 }}</text>
</view>
<view class="interaction-btn" @tap="toggleMute">
<u-icon v-if="isMuted" name="volume-off" size="24" color="#fff"></u-icon>
<u-icon v-else name="volume" size="24" color="#fff"></u-icon>
</view>
<!-- 分享 -->
<view class="interaction-btn" @tap="shareVideo(video)">
<u-icon name="share" size="28" color="#fff" class="btn-icon"></u-icon>
<text class="btn-count">{{ video.shareCount || 0 }}</text>
</view>
<!-- 加载更多提示 -->
<view v-if="index === videoList.length - 1 && loadingMore" class="load-more-indicator">
<view class="loading-dots">
<u-icon name="loading" size="20" color="#fff"></u-icon>
</view>
</view>
<!-- 已到底部提示 -->
<view v-if="index === videoList.length - 1 && isEndOfList" class="end-of-list">
<u-icon name="more-dot-fill" size="20" color="rgba(255, 255, 255, 0.6)"></u-icon>
</view>
</view>
<!-- 进度条 -->
<view class="progress-container">
<view class="progress-bar">
<view class="progress-current" :style="{ width: video.progress + '%' }"></view>
</view>
</view>
<!-- 返回按钮 -->
<view class="back-btn" @tap="goBack">
<u-icon name="arrow-left" size="24" color="#fff"></u-icon>
</view>
</view>
</swiper-item>
</swiper>
<!-- 评论弹窗 -->
<u-popup :show="showComment" mode="bottom" round="20" @close="closeComment">
<view class="comment-popup">
<view class="comment-header">
<text class="comment-title">评论 {{ currentVideo.commentCount || 0 }}</text>
<view class="close-btn" @tap="closeComment">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<scroll-view class="comment-list" scroll-y>
<!-- 评论列表 -->
<view v-for="comment in comments" :key="comment.id" class="comment-item">
<image :src="comment.avatar" class="comment-avatar"></image>
<view class="comment-content">
<text class="comment-author">{{ comment.nickname }}</text>
<text class="comment-text">{{ comment.content }}</text>
<view class="comment-meta">
<text>{{ formatRelativeTime(comment.createdAt) }}</text>
<view class="comment-actions">
<view class="comment-like" @tap="likeComment(comment)">
<u-icon :name="comment.liked ? 'thumb-up-fill' : 'thumb-up'" size="16"
:color="comment.liked ? '#ff2442' : '#999'">
</u-icon>
<text>{{ comment.likeCount || 0 }}</text>
</view>
<text class="reply-btn" @tap="replyComment(comment)">回复</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="comment-input-area">
<input type="text" v-model="commentInput" placeholder="写下你的评论..." class="comment-input"
@confirm="sendComment">
<button class="send-btn" @tap="sendComment">
<u-icon
name="https://dm-materials.oss-cn-guangzhou.aliyuncs.com/KHCFDC%E5%8F%91%E9%80%8120251215100911.png"
size="20" color="#fff"></u-icon>
<text>发送</text>
</button>
</view>
</view>
</u-popup>
<u-popup :show="loginOrNot" mode="center" round="10" @close="loginOrNot = false" :safeAreaInsetBottom="false">
<view
style="width: 600rpx; padding: 40rpx 30rpx 30rpx; background: #fff; border-radius: 20rpx; position: relative;">
<!-- 标题 -->
<view
style="font-size: 36rpx; font-weight: bold; text-align: center; margin-bottom: 30rpx; color: #333;">
请登录</view>
<!-- 内容区域 -->
<view style="margin-bottom: 40rpx;">
<view
style="font-size: 30rpx; color: #333; text-align: center; margin-bottom: 30rpx; font-weight: 500;">
「云居库」------ 家具设计与美学灵感社区</view>
<view style="padding: 0 20rpx;">
<view
style="font-size: 26rpx; color: #666; line-height: 1.8; padding: 8rpx 0; position: relative; padding-left: 20rpx;">
🔍 一站解锁设计核心资源</view>
<view
style="font-size: 26rpx; color: #666; line-height: 1.8; padding: 8rpx 0; position: relative; padding-left: 20rpx;">
✅ 国外奢品家具图鉴 | 原创设计首发</view>
<view
style="font-size: 26rpx; color: #666; line-height: 1.8; padding: 8rpx 0; position: relative; padding-left: 20rpx;">
✅ 巢居美学指南 | 治愈系小物推荐</view>
<view
style="font-size: 26rpx; color: #666; line-height: 1.8; padding: 8rpx 0; position: relative; padding-left: 20rpx;">
✅ 设计师专属社区 | 国外品牌源文件直下</view>
<view
style="font-size: 26rpx; color: #666; line-height: 1.8; padding: 8rpx 0; position: relative; padding-left: 20rpx;">
✨ 在这里,找到设计的灵感与质感,立即登录,开启你的云居库之旅~</view>
</view>
</view>
<!-- 登录按钮 -->
<view style="margin: 20rpx 0 30rpx;">
<u-button open-type="getPhoneNumber" @getphonenumber="handleGetPhoneNumber" text="手机号登录"
color="#007FFF" shape="circle" size="normal"></u-button>
</view>
<!-- 底部叉掉按钮 -->
<button @click="gb()"
style="position: relative; bottom: -90px; left: 0%; z-index: 9999; background: #fff; border: none; border-radius: 50%; width: 24px; height: 25px; font-size: 16px; display: flex; align-items: center; justify-content: center; cursor: pointer;color: #000;">
×
</button>
</view>
</u-popup>
</view>
</template>
<script>
import {
getToken,
getUserId,
setToken,
setUserId
} from "@/utils/data";
import {
authweixin,
likeArticle,
collectArticle,
checkUserCollectionStatus,
getVideoNeighbors
} from "@/api/index_api.js";
export default {
data() {
return {
loginOrNot: false,
screenHeight: 0,
currentIndex: 0,
isMuted: false,
videoList: [],
loadingMore: false,
showComment: false,
currentVideo: {},
comments: [],
commentInput: '',
videoContexts: {},
progressTimers: {},
// 初始视频ID(从首页传入)
initialVideoId: null,
initialVideoData: null,
// 边界处理相关
isEndOfList: false, // 是否已经到底
noMoreVideos: false, // 是否没有更多视频
showBoundaryTips: false, // 是否显示边界提示
isSwipingUp: false, // 是否向上滑动
isSwipingDown: false, // 是否向下滑动
lastSwipeDirection: null, // 最后滑动方向
swipeTimer: null, // 滑动提示计时器
// 简化:不再缓存视频ID,每次滑动都重新查询
loadingNeighbors: false, // 是否正在加载相邻视频
neighborCache: new Map(), // 缓存已查询过的视频ID对应的相邻数据
// 当前正在播放的视频索引
currentPlayingIndex: -1,
// 防止重复播放
isSwitchingVideo: false,
// 视频监控定时器
videoMonitorTimers: {},
// 视频加载重试次数
videoRetryCount: {},
// 预加载视频索引
preloadIndex: -1
}
},
computed: {
// 是否是第一个视频
isFirstVideo() {
return this.currentIndex === 0;
},
// 是否是最后一个视频
isLastVideo() {
return this.currentIndex === this.videoList.length - 1;
},
// 当前视频
currentVideoData() {
return this.videoList[this.currentIndex] || {};
}
},
onLoad(options) {
// 获取屏幕高度
const systemInfo = uni.getSystemInfoSync();
this.screenHeight = systemInfo.windowHeight;
// 获取传入的视频ID
if (options.videoId) {
this.initialVideoId = parseInt(options.videoId);
}
// 如果有完整的视频数据
if (options.videoData) {
try {
this.initialVideoData = JSON.parse(decodeURIComponent(options.videoData));
} catch (e) {
console.error('解析视频数据失败', e);
}
}
// 初始化数据
this.initVideos();
},
onReady() {
// 页面准备好后自动播放第一个视频
// this.$nextTick(() => {
// if (this.videoList.length > 0) {
// this.playVideo(0);
// }
// });
this.$nextTick(() => {
// 关键修复:增加延迟确保video组件完全渲染
setTimeout(() => {
if (this.videoList.length > 0) {
this.playVideo(0);
}
}, 500);
});
},
onUnload() {
// 页面卸载时停止所有定时器和视频
this.stopAllVideos();
this.clearAllTimers();
clearTimeout(this.swipeTimer);
// 清除监控定时器
this.clearAllMonitorTimers();
},
onHide() {
// 页面隐藏时暂停当前视频
if (this.videoList[this.currentIndex]) {
this.pauseVideo(this.currentIndex);
}
},
onShow() {
// 页面显示时恢复播放当前视频
if (this.videoList[this.currentIndex] && !this.videoList[this.currentIndex].loadFailed) {
this.playVideo(this.currentIndex);
}
},
methods: {
// 生成唯一的key
getVideoKey(video, index) {
return video.videoId ? `video-${video.videoId}-${index}` : `video-index-${index}`;
},
// 初始化视频列表 - 简化逻辑
async initVideos() {
try {
if (this.initialVideoId) {
// 直接加载当前视频及其相邻视频
await this.loadCurrentVideoData(this.initialVideoId);
} else if (this.initialVideoData) {
// 如果有初始视频数据,先添加到列表
const video = this.createVideoObject(this.initialVideoData, true);
this.videoList.push(video);
this.currentIndex = 0;
// 然后加载相邻视频
await this.loadCurrentVideoData(video.videoId);
} else {
// 没有初始数据
uni.showToast({
title: '暂无视频数据',
icon: 'none'
});
}
} catch (error) {
console.error('初始化视频失败', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
}
},
// 加载当前视频的数据
async loadCurrentVideoData(videoId) {
try {
const response = await getVideoNeighbors(videoId);
if (!response) {
console.warn('API返回数据格式错误:', response);
return;
}
const {
shang,
cu,
xia
} = response;
// 缓存这次查询的结果
this.neighborCache.set(videoId, response);
// 清空视频列表
this.videoList = [];
// 添加上一个视频(如果存在)
if (shang && shang.videoId) {
const prevVideo = this.createVideoObject(shang);
this.videoList.push(prevVideo);
console.log('添加上一个视频:', shang.videoId);
}
// 添加当前视频(如果存在)
if (cu && cu.videoId) {
const currentVideo = this.createVideoObject(cu);
this.videoList.push(currentVideo);
this.currentIndex = this.videoList.length - 1;
console.log('添加当前视频:', cu.videoId);
// 如果有初始数据,合并用户交互状态
if (this.initialVideoData && cu.videoId === this.initialVideoData.id) {
currentVideo._liked = this.initialVideoData._liked || false;
currentVideo._collected = this.initialVideoData._collected || false;
}
}
// 添加下一个视频(如果存在)
if (xia && xia.videoId) {
const nextVideo = this.createVideoObject(xia);
this.videoList.push(nextVideo);
console.log('添加下一个视频:', xia.videoId);
this.noMoreVideos = false;
this.isEndOfList = false;
} else {
// 没有下一个视频
this.noMoreVideos = true;
this.isEndOfList = true;
console.log('这是最后一个视频');
}
console.log('初始化后视频列表:', this.videoList.map(v => v.videoId));
} catch (error) {
console.error('加载视频数据失败', error);
uni.showToast({
title: '加载失败,请稍后重试',
icon: 'none'
});
}
},
// 创建视频对象
createVideoObject(videoData) {
return {
videoId: videoData.videoId || videoData.id,
videoTitle: videoData.videoTitle || videoData.title,
videoContent: videoData.videoContent || videoData.content,
videoUrl: videoData.videoUrl,
location: videoData.location,
userId: videoData.userId,
nickname: videoData.nickname,
avatar: videoData.avatar,
viewCount: videoData.viewCount || 0,
likeCount: videoData.likeCount || 0,
collectCount: videoData.collectCount || 0,
commentCount: videoData.commentCount || 0,
shareCount: videoData.shareCount || 0,
tags: videoData.tags,
createdAt: videoData.createdAt,
// 前端状态
isPlaying: false,
isPaused: false,
loading: false,
loadFailed: false, // 新增:加载失败状态
progress: 0,
_liked: false,
_collected: false,
showLikeAnimation: false,
lastUpdateTime: 0, // 新增:最后更新时间,用于监控视频是否卡住
bufferState: 'normal', // 缓冲状态:normal/waiting/stalled
duration: 0, // 视频时长(属性,不是方法)
currentTime: 0 // 当前播放时间
};
},
async onSwiperChange(e) {
if (this.isSwitchingVideo) {
console.log('正在切换视频中,跳过');
return;
}
this.isSwitchingVideo = true;
const oldIndex = this.currentIndex;
const newIndex = e.detail.current;
// 判断滑动方向
this.lastSwipeDirection = newIndex > oldIndex ? 'down' : 'up';
this.isSwipingDown = this.lastSwipeDirection === 'down';
this.isSwipingUp = this.lastSwipeDirection === 'up';
console.log('滑动切换:', {
from: oldIndex,
to: newIndex,
direction: this.lastSwipeDirection,
totalVideos: this.videoList.length
});
// 停止旧视频的播放(彻底停止,释放资源)
if (this.videoList[oldIndex]) {
this.stopVideo(oldIndex, false); // 完全停止旧视频
// 额外清理:删除旧视频上下文
if (this.videoContexts[oldIndex]) {
delete this.videoContexts[oldIndex];
}
}
// 更新当前索引
this.currentIndex = newIndex;
// 关键修复:使用nextTick确保DOM更新后再创建视频上下文
this.$nextTick(() => {
// 增加延迟兼容小程序渲染机制
setTimeout(() => {
this.playVideo(newIndex);
// 预加载下一个视频
this.preloadNextVideo(newIndex);
}, 100);
});
// 检查是否需要加载更多视频
await this.checkAndLoadMoreVideos(newIndex);
// 重置切换状态(缩短延迟)
setTimeout(() => {
this.isSwitchingVideo = false;
}, 300);
},
// 预加载下一个视频(修复prefetch不存在的问题)
preloadNextVideo(currentIndex) {
// 取消之前的预加载
if (this.preloadIndex !== -1 && this.preloadIndex !== currentIndex) {
this.stopVideo(this.preloadIndex, false);
this.preloadIndex = -1;
}
// 预加载下一个视频(向下滑动)
if (currentIndex < this.videoList.length - 1) {
const nextIndex = currentIndex + 1;
this.preloadIndex = nextIndex;
setTimeout(() => {
if (this.videoList[nextIndex] && !this.videoList[nextIndex].isPlaying) {
try {
const videoContext = uni.createVideoContext(`video-${nextIndex}`, this);
if (videoContext) {
// 微信小程序中没有prefetch方法,改用提前创建上下文
this.videoContexts[nextIndex] = videoContext;
console.log('预加载视频上下文创建完成:', nextIndex);
}
} catch (e) {
console.warn('预加载视频失败:', nextIndex, e);
}
}
}, 500);
}
},
// 检查并加载更多视频
async checkAndLoadMoreVideos(currentIndex) {
const currentVideo = this.videoList[currentIndex];
if (!currentVideo) return;
// 如果向上滑动到第一个视频,尝试加载前面的视频
if (currentIndex === 0 && this.isSwipingUp) {
await this.loadPreviousVideo(currentVideo.videoId);
// 加载完成后立即播放当前视频(修复向上滑动后不自动播放的问题)
this.$nextTick(() => {
setTimeout(() => {
this.playVideo(this.currentIndex);
}, 200);
});
}
// 如果向下滑动到最后一个视频,尝试加载后面的视频
if (currentIndex === this.videoList.length - 1 && this.isSwipingDown) {
await this.loadNextVideo(currentVideo.videoId);
}
},
// 加载前面的视频(向上滑动时)
async loadPreviousVideo(videoId) {
if (this.loadingNeighbors || this.loadingMore) {
console.log('正在加载中,跳过');
return;
}
console.log('尝试加载前面的视频:', videoId);
try {
this.loadingNeighbors = true;
// 先检查缓存
if (this.neighborCache.has(videoId)) {
const cachedData = this.neighborCache.get(videoId);
const {
shang
} = cachedData;
if (shang && shang.videoId) {
// 检查是否已经在列表中
const exists = this.videoList.some(v => v.videoId === shang.videoId);
if (!exists) {
const prevVideo = this.createVideoObject(shang);
this.videoList.unshift(prevVideo);
// 更新当前索引,因为数组前面插入了新元素
this.currentIndex += 1;
console.log('从缓存添加前面视频:', shang.videoId);
return;
}
}
}
// 缓存中没有或需要重新查询
const response = await getVideoNeighbors(videoId);
if (!response) {
console.warn('加载前面视频返回数据格式错误');
return;
}
// 缓存结果
this.neighborCache.set(videoId, response);
const {
shang
} = response;
if (shang && shang.videoId) {
// 检查是否已经在列表中
const exists = this.videoList.some(v => v.videoId === shang.videoId);
if (!exists) {
const prevVideo = this.createVideoObject(shang);
this.videoList.unshift(prevVideo);
// 更新当前索引,因为数组前面插入了新元素
this.currentIndex += 1;
console.log('从API添加前面视频:', shang.videoId);
}
} else {
// 没有前面的视频了
this.showBoundaryTip('已经是第一个视频了');
}
} catch (error) {
console.error('加载前面视频失败', error);
} finally {
this.loadingNeighbors = false;
}
},
// 加载后面的视频(向下滑动时)
async loadNextVideo(videoId) {
if (this.loadingNeighbors || this.loadingMore) {
console.log('正在加载中,跳过');
return;
}
console.log('尝试加载后面的视频:', videoId);
// 如果已经到底了,显示提示
if (this.isEndOfList) {
this.showBoundaryTip('没有更多视频了');
return;
}
try {
this.loadingNeighbors = true;
// 先检查缓存
if (this.neighborCache.has(videoId)) {
const cachedData = this.neighborCache.get(videoId);
const {
xia
} = cachedData;
if (xia && xia.videoId) {
// 检查是否已经在列表中
const exists = this.videoList.some(v => v.videoId === xia.videoId);
if (!exists) {
const nextVideo = this.createVideoObject(xia);
this.videoList.push(nextVideo);
console.log('从缓存添加后面视频:', xia.videoId);
return;
}
} else {
// 缓存中没有下一个视频,说明到底了
this.isEndOfList = true;
this.showBoundaryTip('没有更多视频了');
return;
}
}
// 缓存中没有或需要重新查询
const response = await getVideoNeighbors(videoId);
if (!response) {
console.warn('加载后面视频返回数据格式错误');
return;
}
// 缓存结果
this.neighborCache.set(videoId, response);
const {
xia
} = response;
if (xia && xia.videoId) {
// 检查是否已经在列表中
const exists = this.videoList.some(v => v.videoId === xia.videoId);
if (!exists) {
const nextVideo = this.createVideoObject(xia);
this.videoList.push(nextVideo);
console.log('从API添加后面视频:', xia.videoId);
this.isEndOfList = false;
}
} else {
// 没有后面的视频了
this.isEndOfList = true;
this.showBoundaryTip('没有更多视频了');
}
} catch (error) {
console.error('加载后面视频失败', error);
this.isEndOfList = true;
} finally {
this.loadingNeighbors = false;
}
},
// 显示边界提示
showBoundaryTip(message) {
this.showBoundaryTips = true;
console.log('边界提示:', message);
// 2秒后隐藏提示
clearTimeout(this.swipeTimer);
this.swipeTimer = setTimeout(() => {
this.showBoundaryTips = false;
}, 2000);
},
// 滑动动画结束
onAnimationFinish() {
// 动画结束后确保当前视频播放
setTimeout(() => {
this.playVideo(this.currentIndex);
}, 100);
this.isSwipingUp = false;
this.isSwipingDown = false;
},
playVideo(index) {
if (index < 0 || index >= this.videoList.length) {
console.warn('播放视频索引越界:', index);
return;
}
const video = this.videoList[index];
if (!video) return;
// 如果已经在播放,跳过
if (video.isPlaying && !video.isPaused) {
console.log('视频已经在播放中,跳过,索引:', index);
return;
}
console.log('播放视频,索引:', index, '视频ID:', video.videoId);
// 清除加载失败状态
if (video.loadFailed) {
video.loadFailed = false;
this.videoRetryCount[index] = 0; // 重置重试次数
}
// 停止其他所有视频(彻底停止)
this.videoList.forEach((v, i) => {
if (i !== index && (v.isPlaying || v.isPaused)) {
this.stopVideo(i, false); // 完全停止其他视频
}
});
// 设置状态
video.loading = true;
video.isPlaying = false;
video.isPaused = false;
video.bufferState = 'normal';
// 关键修复:增加多次重试逻辑 + 兼容小程序自动播放限制
const playWithRetry = (retryTimes = 0) => {
try {
// 先销毁旧的上下文
if (this.videoContexts[index]) {
delete this.videoContexts[index];
}
// 重新创建video context
const videoContext = uni.createVideoContext(`video-${index}`, this);
if (!videoContext) {
throw new Error('无法创建VideoContext');
}
this.videoContexts[index] = videoContext;
// 设置静音状态(小程序静音视频更容易自动播放)
videoContext.muted = this.isMuted;
// 重置视频播放位置
videoContext.seek(0);
// 关键修复:多次尝试播放,兼容小程序渲染延迟
const attemptPlay = () => {
try {
videoContext.play();
console.log('播放命令已发送,索引:', index, '重试次数:', retryTimes);
// 立即检查播放状态
setTimeout(() => {
if (!video.isPlaying && video.loading) {
if (retryTimes < 3) {
console.log('播放失败,重试...', index, retryTimes + 1);
playWithRetry(retryTimes + 1);
} else {
console.warn('多次播放失败,标记为加载失败:', index);
video.loading = false;
video.loadFailed = true;
}
}
}, 500);
} catch (e) {
if (retryTimes < 3) {
setTimeout(() => playWithRetry(retryTimes + 1), 300);
} else {
video.loading = false;
video.loadFailed = true;
}
}
};
attemptPlay();
} catch (error) {
console.error('视频准备失败:', error);
if (retryTimes < 3) {
setTimeout(() => playWithRetry(retryTimes + 1), 500);
} else {
video.loading = false;
video.loadFailed = true;
}
}
};
// 启动重试播放
playWithRetry();
// 记录播放次数
this.recordVideoView(video.videoId);
},
// 重试播放视频
retryPlayVideo(index) {
const video = this.videoList[index];
if (!video || video.loadFailed) return;
console.log('重试播放视频,索引:', index, '重试次数:', this.videoRetryCount[index] || 0);
// 限制重试次数
if (this.videoRetryCount[index] && this.videoRetryCount[index] >= 2) {
video.loading = false;
video.loadFailed = true;
return;
}
// 增加重试次数
this.videoRetryCount[index] = (this.videoRetryCount[index] || 0) + 1;
// 重新播放
if (this.videoContexts[index]) {
try {
this.videoContexts[index].seek(0);
setTimeout(() => {
this.videoContexts[index].play();
}, 500);
} catch (e) {
console.error('重试播放失败:', e);
video.loading = false;
video.loadFailed = true;
}
}
},
// 开始视频卡顿监控
startVideoMonitor(index) {
// 清除旧的监控器
this.stopVideoMonitor(index);
// 开始新的监控
this.videoMonitorTimers[index] = setInterval(() => {
const video = this.videoList[index];
if (video && video.isPlaying && !video.isPaused) {
const now = Date.now();
if (video.lastUpdateTime && (now - video.lastUpdateTime > 5000)) {
// 超过5秒没有更新,认为视频卡住了
console.warn('视频可能卡住了,索引:', index);
this.handleVideoStuck(index);
}
}
}, 2000); // 监控频率降低到2秒一次
},
// 停止视频监控
stopVideoMonitor(index) {
if (this.videoMonitorTimers[index]) {
clearInterval(this.videoMonitorTimers[index]);
delete this.videoMonitorTimers[index];
}
},
// 清除所有监控定时器
clearAllMonitorTimers() {
Object.keys(this.videoMonitorTimers).forEach(index => {
clearInterval(this.videoMonitorTimers[index]);
});
this.videoMonitorTimers = {};
},
// 处理视频卡住
handleVideoStuck(index) {
const video = this.videoList[index];
if (!video || !video.isPlaying) return;
console.log('尝试恢复卡住的视频,索引:', index);
// 强制重置并重新播放
if (this.videoContexts[index]) {
try {
// 暂停并重置
this.videoContexts[index].pause();
this.videoContexts[index].seek(video.progress > 0 ? video.progress / 100 * video.duration : 0);
// 延迟播放
setTimeout(() => {
this.videoContexts[index].play();
video.lastUpdateTime = Date.now();
video.bufferState = 'normal';
}, 1000);
} catch (error) {
console.error('恢复视频失败:', error);
video.loadFailed = true;
video.isPlaying = false;
}
}
},
// 视频加载完成事件(修复duration获取方式)
onVideoLoaded(index) {
console.log('视频元数据加载完成,索引:', index);
const video = this.videoList[index];
if (video) {
video.loading = false;
video.lastUpdateTime = Date.now();
// 微信小程序中通过wx.createVideoContext获取的duration是属性,不是方法
// 这里改为从事件中获取duration
if (this.videoContexts[index]) {
// 兼容处理:尝试获取duration
try {
// 对于微信小程序,duration是通过video组件的bindtimeupdate事件获取的
// 这里先标记,等待timeupdate事件
video.duration = video.duration || 0;
} catch (e) {
console.warn('获取视频时长失败:', e);
}
}
}
},
// 视频缓冲等待事件
onVideoWaiting(index) {
console.log('视频缓冲中,索引:', index);
const video = this.videoList[index];
if (video) {
video.bufferState = 'waiting';
}
},
// 视频加载停滞事件
onVideoStalled(index) {
console.log('视频加载停滞,索引:', index);
const video = this.videoList[index];
if (video) {
video.bufferState = 'stalled';
// 尝试恢复
setTimeout(() => {
this.handleVideoStuck(index);
}, 3000);
}
},
// 视频时间更新事件(修复duration获取)
onVideoTimeUpdate(index, event) {
const video = this.videoList[index];
if (video && video.isPlaying) {
video.lastUpdateTime = Date.now();
video.bufferState = 'normal';
// 计算真实进度
if (event && event.detail) {
// 从事件中获取时长和当前时间(修复核心问题)
video.duration = event.detail.duration || video.duration;
video.currentTime = event.detail.currentTime || video.currentTime;
const progress = (video.currentTime / video.duration) * 100;
video.progress = Math.min(progress, 100);
}
}
},
// 停止视频(修复版本)
stopVideo(index, pauseOnly = false) {
if (index < 0 || index >= this.videoList.length) return;
const video = this.videoList[index];
if (!video) return;
console.log(pauseOnly ? '暂停视频' : '停止视频', '索引:', index);
// 停止video context
if (this.videoContexts[index]) {
try {
if (pauseOnly) {
// 只是暂停
this.videoContexts[index].pause();
} else {
// 完全停止并重置
this.videoContexts[index].pause();
this.videoContexts[index].seek(0);
}
} catch (error) {
console.error('停止视频上下文失败', error);
}
}
// 停止视频监控
this.stopVideoMonitor(index);
// 更新状态
video.isPlaying = false;
video.isPaused = pauseOnly; // 如果是暂停,则isPaused为true
video.loading = false;
video.bufferState = 'normal';
if (!pauseOnly) {
video.progress = 0; // 重置进度
video.currentTime = 0;
// 保留duration以便下次播放
video.loadFailed = false; // 重置加载失败状态
// 重置重试次数
if (this.videoRetryCount[index]) {
delete this.videoRetryCount[index];
}
}
// 停止进度条计时器
this.stopProgressTimer(index);
// 如果这是当前播放的视频,更新记录
if (index === this.currentPlayingIndex) {
this.currentPlayingIndex = -1;
}
},
// 暂停视频(只是暂停,不重置)
pauseVideo(index) {
this.stopVideo(index, true);
},
// 停止所有视频
stopAllVideos() {
// 停止所有视频上下文
Object.keys(this.videoContexts).forEach(index => {
const context = this.videoContexts[index];
if (context) {
try {
context.pause();
context.seek(0);
} catch (error) {
console.error('停止视频失败:', error);
}
}
});
// 清空上下文对象
this.videoContexts = {};
this.videoRetryCount = {};
this.preloadIndex = -1;
// 重置所有视频状态
this.videoList.forEach(video => {
video.isPlaying = false;
video.isPaused = false;
video.progress = 0;
video.loading = false;
video.loadFailed = false;
video.bufferState = 'normal';
video.currentTime = 0;
});
// 重置当前播放索引
this.currentPlayingIndex = -1;
// 清除所有定时器
this.clearAllTimers();
this.clearAllMonitorTimers();
},
// 切换播放状态
togglePlay(index) {
if (!this.videoList[index]) return;
const video = this.videoList[index];
// 如果视频加载失败,点击重新加载
if (video.loadFailed) {
console.log('重新加载失败的视频,索引:', index);
this.playVideo(index);
return;
}
if (video.isPaused) {
// 继续播放
this.playVideo(index);
} else {
// 暂停
this.pauseVideo(index);
}
},
// 开始进度条计时器
startProgressTimer(index) {
// 清除旧的计时器
this.stopProgressTimer(index);
// 开始新的计时器(作为备用,实际使用视频的timeupdate事件)
this.progressTimers[index] = setInterval(() => {
const video = this.videoList[index];
if (video && video.isPlaying && !video.isPaused && !video.loadFailed && video.bufferState ===
'normal') {
// 如果视频没有卡住,更新进度
const now = Date.now();
if (!video.lastUpdateTime || (now - video.lastUpdateTime < 5000)) {
// 只在视频正常播放时更新进度
if (video.duration && video.currentTime) {
const progress = (video.currentTime / video.duration) * 100;
video.progress = Math.min(progress, 100);
}
// 如果进度到达100%,触发结束事件
if (video.progress >= 100) {
this.onVideoEnded(index);
}
}
}
}, 500); // 降低更新频率,减少性能消耗
},
// 停止进度条计时器
stopProgressTimer(index) {
if (this.progressTimers[index]) {
clearInterval(this.progressTimers[index]);
delete this.progressTimers[index];
}
},
// 清除所有计时器
clearAllTimers() {
Object.keys(this.progressTimers).forEach(index => {
clearInterval(this.progressTimers[index]);
});
this.progressTimers = {};
},
// 视频播放事件
onVideoPlay(index) {
console.log('视频开始播放,索引:', index);
const video = this.videoList[index];
if (video) {
video.isPlaying = true;
video.isPaused = false;
video.loading = false;
video.loadFailed = false;
this.currentPlayingIndex = index;
video.lastUpdateTime = Date.now();
// 启动视频监控
this.startVideoMonitor(index);
// 启动进度条计时器
this.startProgressTimer(index);
// 确保其他视频都真正停止
this.videoList.forEach((v, i) => {
if (i !== index && v.isPlaying) {
this.stopVideo(i, false);
}
});
}
},
// 视频暂停事件
onVideoPause(index) {
console.log('视频暂停,索引:', index);
const video = this.videoList[index];
if (video) {
video.isPaused = true;
video.isPlaying = false;
// 停止监控
this.stopVideoMonitor(index);
this.stopProgressTimer(index);
// 如果不是当前显示的视频,完全停止
if (index !== this.currentIndex) {
this.stopVideo(index, false);
}
}
},
// 视频结束事件
onVideoEnded(index) {
console.log('视频播放结束,索引:', index);
// 完全停止当前视频
this.stopVideo(index, false);
// 自动播放下一个
if (index < this.videoList.length - 1) {
console.log('自动播放下一个视频,新索引:', index + 1);
setTimeout(() => {
this.currentIndex = index + 1;
this.playVideo(index + 1);
}, 500);
} else {
// 如果是最后一个视频,显示结束提示
uni.showToast({
title: '没有更多视频了!',
icon: 'none'
});
}
},
// 视频错误事件
onVideoError(index, event) {
console.error('视频加载失败,索引:', index, '错误信息:', event);
const video = this.videoList[index];
if (video) {
video.loading = false;
video.isPlaying = false;
video.bufferState = 'normal';
// 增加重试逻辑
if (!this.videoRetryCount[index] || this.videoRetryCount[index] < 2) {
this.retryPlayVideo(index);
} else {
video.loadFailed = true;
this.currentPlayingIndex = -1;
// 延迟显示错误提示,避免频繁提示
setTimeout(() => {
if (video.loadFailed) {
uni.showToast({
title: '视频加载失败',
icon: 'none'
});
}
}, 500);
}
}
},
// 切换静音
toggleMute() {
this.isMuted = !this.isMuted;
console.log('切换静音状态:', this.isMuted);
// 更新所有视频的静音状态
Object.values(this.videoContexts).forEach(context => {
if (context) {
context.muted = this.isMuted;
}
});
},
// 点赞视频
async likeVideo(video, index) {
try {
// 1. 先通过索引获取 videoList 中的原对象(关键:操作响应式数组中的原对象)
const targetVideo = this.videoList[index];
if (!targetVideo) return;
// 前端立即响应
console.log("点赞对象", targetVideo);
const wasLiked = targetVideo._liked;
const oldLikeCount = targetVideo.likeCount || 0;
// 2. 使用 $set 确保响应式更新(解决页面不刷新核心问题)
this.$set(targetVideo, '_liked', !wasLiked);
const newLikeCount = wasLiked ? oldLikeCount - 1 : oldLikeCount + 1;
this.$set(targetVideo, 'likeCount', newLikeCount);
// 显示点赞动画
if (!wasLiked) {
this.$set(targetVideo, 'showLikeAnimation', true);
setTimeout(() => {
this.$set(targetVideo, 'showLikeAnimation', false);
}, 800);
}
// 调用API
const query = {
increment: !wasLiked
};
const response = await likeArticle(query, targetVideo.videoId);
// 3. 接口返回后,用后端数据更新(同样用 $set 保证响应式)
if (response && response.likeCount !== undefined) { // 改为更通用的判断
// 匹配 videoList 中对应 videoId 的视频并更新(兼容列表顺序变化)
const videoIndex = this.videoList.findIndex(v => v.videoId === targetVideo.videoId);
if (videoIndex !== -1) {
this.$set(this.videoList[videoIndex], 'likeCount', response.likeCount);
// 重新计算点赞状态(避免后端数据和前端临时值不一致)
const finalLiked = wasLiked ?
response.likeCount < oldLikeCount :
response.likeCount > oldLikeCount;
this.$set(this.videoList[videoIndex], '_liked', finalLiked);
console.log("点赞成功,更新后数据:", this.videoList[videoIndex]);
this.$forceUpdate();
}
}
} catch (error) {
console.error('点赞失败', error);
// 4. 接口失败时回滚前端状态(提升用户体验)
const targetVideo = this.videoList[index];
if (targetVideo) {
this.$set(targetVideo, '_liked', !targetVideo._liked);
const oldLikeCount = targetVideo.likeCount || 0;
this.$set(targetVideo, 'likeCount', targetVideo._liked ? oldLikeCount + 1 : oldLikeCount - 1);
}
uni.showToast({
title: '点赞失败,请重试',
icon: 'none'
});
}
},
// 收藏视频
async collectVideo(video, index) {
try {
const isLogin = uni.getStorageSync('isLogin');
if (!isLogin && getToken() == "") {
this.loginOrNot = true
console.log("点击视频");
return
}
// 1. 通过索引获取 videoList 中的原对象(确保操作响应式数据)
const targetVideo = this.videoList[index];
if (!targetVideo) return;
// 前端立即响应
console.log("收藏对象", targetVideo);
const wasCollected = targetVideo._collected;
const oldCollectCount = targetVideo.collectCount || 0;
// 2. 使用 $set 确保响应式更新(核心修复:解决页面不刷新)
this.$set(targetVideo, '_collected', !wasCollected);
const newCollectCount = wasCollected ? oldCollectCount - 1 : oldCollectCount + 1;
this.$set(targetVideo, 'collectCount', newCollectCount);
// 调用API
const query = {
increment: !wasCollected
};
const response = await collectArticle(query, targetVideo.videoId);
// 3. 接口返回后,按后端结果修正状态(同样用 $set 保证响应式)
if (response && response.whetherToCollect !== undefined) {
// 匹配 videoList 中对应 videoId 的视频(兼容列表顺序变化)
const videoIndex = this.videoList.findIndex(v => v.videoId === targetVideo.videoId);
if (videoIndex !== -1) {
if (!response.whetherToCollect) {
// 操作失败,回滚状态(响应式更新)
this.$set(this.videoList[videoIndex], '_collected', wasCollected);
this.$set(this.videoList[videoIndex], 'collectCount', oldCollectCount);
console.log("收藏操作失败,已回滚状态", this.videoList[videoIndex]);
} else {
// 操作成功,同步后端返回的收藏数(如果有)
if (response.collectCount !== undefined) {
this.$set(this.videoList[videoIndex], 'collectCount', response.collectCount);
}
this.$forceUpdate();
}
}
}
} catch (error) {
console.error('收藏失败', error);
// 4. 接口调用异常时,回滚前端状态(提升用户体验)
const targetVideo = this.videoList[index];
if (targetVideo) {
const wasCollected = targetVideo._collected;
const oldCollectCount = targetVideo.collectCount || 0;
// 回滚响应式数据
this.$set(targetVideo, '_collected', !wasCollected);
this.$set(targetVideo, 'collectCount', wasCollected ? oldCollectCount + 1 : oldCollectCount -
1);
}
// 提示用户
uni.showToast({
title: '收藏失败,请重试',
icon: 'none'
});
}
},
// 评论视频
commentVideo(video) {
this.currentVideo = video;
this.showComment = true;
// 这里可以加载评论列表
},
// 关闭评论
closeComment() {
this.showComment = false;
this.commentInput = '';
},
// 发送评论
sendComment() {
if (!this.commentInput.trim()) return;
// 这里调用发送评论的API
console.log('发送评论:', this.commentInput);
// 模拟添加评论
this.comments.unshift({
id: Date.now(),
nickname: '当前用户',
avatar: 'https://randomuser.me/api/portraits/men/1.jpg',
content: this.commentInput,
createdAt: new Date().toISOString(),
likeCount: 0,
liked: false
});
// 更新评论数
this.currentVideo.commentCount = (this.currentVideo.commentCount || 0) + 1;
// 清空输入框
this.commentInput = '';
// 隐藏键盘
uni.hideKeyboard();
},
// 分享视频
shareVideo(video) {
uni.showShareMenu({
withShareTicket: true,
success: () => {
console.log('分享成功');
// 更新分享数
video.shareCount = (video.shareCount || 0) + 1;
}
});
},
// 关注用户
followUser(userId) {
uni.showToast({
title: '关注成功',
icon: 'success'
});
},
// 跳转到用户主页
goUserProfile(userId) {
uni.navigateTo({
url: `/pages/user/profile?userId=${userId}`
});
},
// 返回上一页
goBack() {
uni.navigateBack();
},
// 记录视频观看
recordVideoView(videoId) {
// 这里可以调用API记录观看次数
console.log('记录观看视频:', videoId);
},
// 获取视频封面
getVideoPoster(videoUrl) {
if (!videoUrl) return '/static/images/video-poster.jpg';
// 如果是OSS视频,获取第一帧
if (videoUrl.includes('oss-cn-')) {
const separator = videoUrl.includes('?') ? '&' : '?';
return `${videoUrl}${separator}x-oss-process=video/snapshot,t_0,m_fast`;
}
return '/static/images/video-poster.jpg';
},
// 解析标签
parseTags(tagsString) {
if (!tagsString) return [];
return tagsString.split(',').filter(tag => tag.trim());
},
// 按标签搜索
searchByTag(tag) {
uni.showToast({
title: `搜索标签: ${tag}`,
icon: 'none'
});
},
// 格式化相对时间
formatRelativeTime(timeStr) {
if (!timeStr) return '';
const createTime = new Date(timeStr.replace(/-/g, '/'));
const now = new Date();
const diffInSeconds = Math.floor((now - createTime) / 1000);
if (diffInSeconds < 60) {
return '刚刚';
}
const diffInMinutes = Math.floor(diffInSeconds / 60);
if (diffInMinutes < 60) {
return `${diffInMinutes}分钟前`;
}
const diffInHours = Math.floor(diffInMinutes / 60);
if (diffInHours < 24) {
return `${diffInHours}小时前`;
}
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays < 7) {
return `${diffInDays}天前`;
}
const diffInWeeks = Math.floor(diffInDays / 7);
if (diffInWeeks < 4) {
return `${diffInWeeks}周前`;
}
const diffInMonths = Math.floor(diffInDays / 30);
if (diffInMonths < 12) {
return `${diffInMonths}个月前`;
}
const diffInYears = Math.floor(diffInDays / 365);
return `${diffInYears}年前`;
},
// 显示音乐信息
// showMusicInfo(video) {
// uni.showToast({
// title: '音乐: 视频原声',
// icon: 'none'
// });
// },
// 点赞评论
likeComment(comment) {
comment.liked = !comment.liked;
comment.likeCount = comment.liked ? (comment.likeCount || 0) + 1 : Math.max(0, (comment.likeCount || 0) -
1);
},
// 回复评论
replyComment(comment) {
this.commentInput = `回复 @${comment.nickname}: `;
uni.pageScrollTo({
scrollTop: 1000,
duration: 300
});
},
async handleGetPhoneNumber(e) {
let that = this
if (e.detail.code == undefined) {
that.visible = 'unlogin';
return
}
uni.showLoading({
icon: 'loading',
title: "正在登陆",
mask: true,
duration: 10000
});
that.visible = 'unlogin';
uni.login({
async success(res) {
try {
let body = {
"phoneCode": e.detail.code,
"loginCode": res.code,
"role": 5,
"parentId": 0
}
let login = await authweixin(body);
console.log("== 登陆信息 ==", login);
uni.hideLoading();
setToken(login.accessToken)
setUserId(login.userId)
that.loginOrNot = false
const systemInfo = uni.getSystemInfoSync();
that.screenHeight = systemInfo.screenHeight;
that.$forceUpdate();
} catch (e) {
console.log(e)
} finally {
uni.hideLoading();
}
}
})
},
gb() {
this.loginOrNot = false
},
}
}
</script>
<style lang="scss" scoped>
/* 新增样式 */
.load-failed-tips {
position: absolute;
bottom: 150rpx;
left: 0;
right: 0;
text-align: center;
z-index: 5;
}
.failed-text {
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 28rpx;
padding: 15rpx 30rpx;
border-radius: 30rpx;
}
.loading-text {
color: #fff;
font-size: 24rpx;
margin-top: 15rpx;
}
/* 其他样式保持不变 */
.video-container {
width: 100%;
background-color: #000;
position: relative;
overflow: hidden;
}
.video-swiper {
width: 100%;
}
.video-item {
width: 100%;
height: 100%;
}
.video-wrapper {
position: relative;
width: 100%;
height: 100%;
background-color: #000;
}
.video-player {
width: 100%;
height: 100%;
}
.video-poster {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.poster-image {
width: 100%;
height: 100%;
}
.play-icon-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120rpx;
height: 120rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.loading-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 6rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.boundary-tips {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 20;
background: rgba(0, 0, 0, 0.7);
padding: 20rpx 40rpx;
border-radius: 40rpx;
animation: fadeInOut 2s ease;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
20% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
80% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
}
.tip-text {
color: #fff;
font-size: 28rpx;
}
.pause-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
}
.pause-icon-wrapper {
width: 140rpx;
height: 140rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.mute-btn {
position: absolute;
top: 60rpx;
right: 30rpx;
width: 60rpx;
height: 60rpx;
background: rgba(0, 0, 0, 0.4);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.video-info {
position: absolute;
bottom: 120rpx;
left: 20rpx;
right: 140rpx;
z-index: 5;
color: #fff;
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 2rpx solid #fff;
margin-right: 20rpx;
}
.user-name {
font-size: 32rpx;
font-weight: 600;
flex: 1;
}
.follow-btn {
background: transparent;
border: 2rpx solid #fff;
color: #fff;
font-size: 24rpx;
padding: 8rpx 24rpx;
border-radius: 20rpx;
line-height: 1;
}
.follow-btn::after {
border: none;
}
.video-description {
font-size: 28rpx;
line-height: 1.5;
margin-bottom: 15rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-bottom: 15rpx;
}
.tag-item {
background: rgba(0, 0, 0, 0.4);
color: #fff;
font-size: 24rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
}
.location-info {
display: flex;
align-items: center;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 10rpx;
gap: 8rpx;
}
.publish-time {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.6);
}
.interaction-sidebar {
position: absolute;
right: 20rpx;
bottom: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
z-index: 5;
gap: 30rpx;
}
.sidebar-avatar {
position: relative;
width: 100rpx;
height: 100rpx;
margin-bottom: 10rpx;
}
.avatar-image {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
border: 4rpx solid #fff;
}
.follow-animation {
position: absolute;
bottom: -4rpx;
right: -4rpx;
width: 28rpx;
height: 28rpx;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.interaction-btn {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
}
.btn-icon {
transition: transform 0.3s;
}
.btn-count {
font-size: 24rpx;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
}
.like-animation {
position: absolute;
top: -30rpx;
left: 50%;
transform: translateX(-50%);
animation: floatUp 0.8s ease-out forwards;
z-index: 10;
}
@keyframes floatUp {
0% {
transform: translateX(-50%) scale(0.5);
opacity: 1;
}
100% {
transform: translateX(-50%) translateY(-100rpx) scale(1.2);
opacity: 0;
}
}
.music-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
}
.music-dot {
position: absolute;
top: -4rpx;
right: -4rpx;
width: 16rpx;
height: 16rpx;
background: #ff2442;
border-radius: 50%;
animation: musicBeat 1s infinite alternate;
}
@keyframes musicBeat {
from {
transform: scale(0.8);
}
to {
transform: scale(1.2);
}
}
.load-more-indicator {
margin-top: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-dots {
display: flex;
gap: 10rpx;
margin-bottom: 10rpx;
}
.end-of-list {
margin-top: 30rpx;
text-align: center;
}
.progress-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 4rpx;
background: rgba(255, 255, 255, 0.2);
z-index: 10;
}
.progress-bar {
width: 100%;
height: 100%;
background: transparent;
}
.progress-current {
height: 100%;
background: #fff;
transition: width 0.1s linear;
}
.back-btn {
position: absolute;
top: 60rpx;
left: 30rpx;
width: 60rpx;
height: 60rpx;
background: rgba(0, 0, 0, 0.4);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
/* 评论弹窗样式 */
.comment-popup {
background: #fff;
border-radius: 40rpx 40rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
}
.comment-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.comment-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.comment-list {
max-height: 800rpx;
padding: 20rpx 30rpx;
}
.comment-item {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.comment-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
}
.comment-content {
flex: 1;
display: flex;
flex-direction: column;
}
.comment-author {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.comment-text {
font-size: 28rpx;
color: #333;
line-height: 1.5;
margin-bottom: 12rpx;
}
.comment-meta {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #999;
}
.comment-actions {
display: flex;
align-items: center;
gap: 30rpx;
}
.comment-like {
display: flex;
align-items: center;
gap: 8rpx;
}
.reply-btn {
color: #666;
}
.comment-input-area {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-top: 2rpx solid #f0f0f0;
background: #fff;
}
.comment-input {
flex: 1;
height: 80rpx;
padding: 0 30rpx;
background: #f5f5f5;
border-radius: 40rpx;
font-size: 28rpx;
}
.send-btn {
margin-left: 20rpx;
background: #9D7A53;
color: #fff;
font-size: 28rpx;
padding: 0 40rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
gap: 8rpx;
}
.send-btn::after {
border: none;
}
</style>
总结
直接复制就可用。接口改成自己的
{
"code": 0,
"data": {
"shang": {
"videoId": 24,
"videoTitle": "快来看吧",
"videoContent": "人生很短,但是我的故事缺很长",
"videoUrl": "https://linda-video-doc.oss-cn-guangzhou.aliyuncs.com/2GDjHz4588b8dc154f23253ccecb39da2cc120251213110830.mp4",
"location": null,
"userId": 7,
"nickname": "Hannah",
"avatar": "https://randomuser.me/api/portraits/women/17.jpg",
"viewCount": 0,
"likeCount": 0,
"collectCount": 0,
"commentCount": 0,
"shareCount": 0,
"tags": "视频,家具",
"createdAt": "2025-12-13 11:09:08"
},
"cu": {
"videoId": 25,
"videoTitle": "看看这场景非常的奈斯",
"videoContent": "快咯,如果是你的房子你几点回家",
"videoUrl": "https://linda-video-doc.oss-cn-guangzhou.aliyuncs.com/MUvsqOHe64c757a58d7472cc4fe2dbd9ab2c520251213110918.mp4",
"location": null,
"userId": 7,
"nickname": "Hannah",
"avatar": "https://randomuser.me/api/portraits/women/17.jpg",
"viewCount": 0,
"likeCount": 3,
"collectCount": 1,
"commentCount": 0,
"shareCount": 0,
"tags": "视频,家具,沙发,场景",
"createdAt": "2025-12-13 11:10:24"
},
"xia": {
"videoId": 26,
"videoTitle": "开来咯",
"videoContent": "时间大厦及回答哦i哦让你",
"videoUrl": "https://linda-video-doc.oss-cn-guangzhou.aliyuncs.com/AsNL8f0b2d646234d71a4def747fffee69cf520251213111052.mp4",
"location": null,
"userId": 7,
"nickname": "Hannah",
"avatar": "https://randor.me/api/portraits/women/17.jpg",
"viewCount": 0,
"likeCount": 21,
"collectCount": 1,
"commentCount": 0,
"shareCount": 0,
"tags": "视频,家具",
"createdAt": "2025-12-13 11:11:20"
}
},
"msg": ""
}