<template>
|
<view class="video-container">
|
<!-- 视频列表 -->
|
<swiper
|
class="video-swiper"
|
vertical
|
circular
|
:current="currentIndex"
|
@change="onSwiperChange"
|
>
|
<swiper-item v-for="(item, index) in videoList" :key="item.id">
|
<view style="width: 100%;height: 100%;" v-if="item.videoContentType === 'video'">
|
<!-- 播放按钮(仅当视频暂停时显示) -->
|
<view
|
class="play-icon"
|
@click="togglePlay(index)"
|
v-if="!currentVideoIsPlaying"
|
>
|
<image src="/static/video/play.png" style="width: 45px;height: 45px" mode="aspectFit"></image>
|
</view>
|
<video
|
:id="'video'+index"
|
:ref="'video'+index"
|
:src="item.videoUrl"
|
:autoplay="currentIndex === index"
|
:controls="false"
|
:loop="true"
|
:object-fit="item.objectFit"
|
:enable-progress-gesture="false"
|
class="video-item"
|
@play="onPlay(item.id, index)"
|
@pause="onPause(index)"
|
@ended="onEnded(index)"
|
@click="togglePlay(index)"
|
@timeupdate="onTimeUpdate($event)"
|
@loadedmetadata="onLoadedMetadata($event)"
|
|
></video>
|
<!-- 自定义控制条 -->
|
<view
|
@touchstart="handleTouchStart"
|
@touchmove="handleTouchMove"
|
@touchend="handleTouchEnd"
|
class="container">
|
<!-- 进度条 - 整个区域可拖动 -->
|
<view class="process-warp" :style="{ opacity: showProcess ? 1 : 0 }">
|
<!-- 显示当前进度 -->
|
<view class="progress-text">{{ hasPlayTime }}/{{formartDuration}}</view>
|
<view
|
class="progress-bar"
|
id="progressBar"
|
>
|
|
<!-- 已填充部分 -->
|
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
|
</view>
|
</view>
|
</view>
|
</view>
|
<view style="width: 100%; height: 100%;" v-else-if="item.videoContentType === 'img'">
|
<uni-swiper-dot
|
:info="item.imgs"
|
:current="currentImgIndex"
|
mode="round"
|
style="width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;"
|
:dots-styles="{width: 24, bottom: 24,selectedBackgroundColor: 'green', backgroundColor: 'gray'}"
|
>
|
<swiper class="swiper-box" @change="imgChange" :autoplay="true" :interval="3000">
|
<swiper-item v-for="img in item.imgs" :key="img">
|
<view class="swiper-item">
|
<!-- 调整 image 样式,使其居中且按比例缩放 -->
|
<image
|
:src="img"
|
mode="aspectFit"
|
style="width: 100%; height: 100%; display: block; margin: 0 auto;"
|
></image>
|
</view>
|
</swiper-item>
|
</swiper>
|
</uni-swiper-dot>
|
</view>
|
|
|
<!-- 悬挂商品链接层 -->
|
<view class="goods-link-warp" v-if="item.goodsList.length > 0">
|
<view class="goods-link">
|
<swiper @change="goodsChange" :autoplay="true" :interval="4000" style="height: 120rpx;">
|
<swiper-item v-for="goods in item.goodsList" :key="goods.goodsId">
|
<view class="goods-container" @click="jumpToPay(item.id)">
|
<!-- 商品图片 -->
|
<image class="goods-image" :src="goods.thumbnail" mode="aspectFill"></image>
|
|
<!-- 商品信息 -->
|
<view class="goods-info">
|
<text class="goods-name">{{goods.goodsName}}</text>
|
<view class="price-section">
|
<text class="current-price">¥{{goods.price}}</text>
|
<text class="original-price" v-if="goods.originalPrice">¥{{goods.originalPrice}}</text>
|
</view>
|
</view>
|
</view>
|
</swiper-item>
|
</swiper>
|
</view>
|
</view>
|
|
|
<!-- 视频信息层 -->
|
<view class="video-info">
|
<view>
|
<text class="video-author">@{{item.authorName}}</text>
|
</view>
|
<view style="width: 100%;word-wrap: break-word;white-space: normal;overflow-wrap: break-word;">
|
<text class="video-title">{{item.title}}</text>
|
<text class="video-tag" v-for="(tag, index) in item.tagList" :key="tag.id">#{{tag.tagName}}</text>
|
</view>
|
</view>
|
|
<!-- 右侧互动按钮 -->
|
<view class="action-buttons">
|
<view class="avatar-container">
|
<image class="avatar" @click="jumpToHomePage(item.authorId)" :src="item.authorAvatar" mode="aspectFill"></image>
|
<!-- 关注图标 - 使用绝对定位 -->
|
<view v-if="!item.subscribeThisAuthor" class="follow-icon" @click="subscribeAuth(index, item.authorId)">
|
<text class="iconfont"></text>
|
</view>
|
</view>
|
<view class="action-item" @click="toggleCollect(item, index)">
|
<text class="iconfont" v-if="item.collected"></text>
|
<text class="iconfont" v-else></text>
|
<text style="font-size: 10px;font-weight: lighter;">{{item.collectNum}}</text>
|
</view>
|
<view class="action-item" @click="showComments(item)">
|
<text class="iconfont"></text>
|
<text style="font-size: 10px;font-weight: lighter;">{{item.commentNum}}</text>
|
</view>
|
</view>
|
|
</swiper-item>
|
</swiper>
|
|
<!-- 评论弹窗 -->
|
<uni-popup ref="commentPopup" type="bottom" :is-mask-click="true" @maskClick="closeCommentPopup">
|
<view class="comment-popup">
|
<view class="popup-header">
|
<text class="popup-title" v-if="!commentForm.replyId">评论({{commentsTotal}})</text>
|
<view class="reply-title" v-else>
|
<text>回复 @{{commentForm.replyUserNickname}}</text>
|
<text class="cancel-reply" @click="cancelReply">取消</text>
|
</view>
|
<text class="iconfont close-icon" @click="closeCommentPopup"></text>
|
</view>
|
|
<scroll-view class="comment-list" scroll-y :show-scrollbar="false" @scrolltolower="getCommentPage">
|
<view v-if="commentLoading" class="loading">
|
<uni-load-more status="loading"></uni-load-more>
|
</view>
|
|
<view v-else-if="comments.length === 0" class="empty">
|
暂无评论,快来发表第一条评论吧~
|
</view>
|
|
<view v-else class="comment-item" v-for="(comment, index) in comments" :key="comment.id">
|
<view style="display: flex;">
|
<image class="comment-avatar" :src="comment.userAvatar || '/static/default-avatar.png'"></image>
|
<view class="comment-content">
|
<text class="nickname">{{comment.userNickname}}</text>
|
<text class="content">{{comment.commentContent}}</text>
|
<view style="position: relative;">
|
<text class="time">{{formatTime(comment.createTime)}}</text>
|
<text @click="openReply(comment)" class="reply-btu time">回复</text>
|
<text v-if="!comment.hasThumbsUp" class="thumbs-up time iconfont" @click="thubmsUp(comment.id, index, null)"><text v-show="comment.thumbsUpNum > 0" class="thumbs-num">{{comment.thumbsUpNum}}</text></text>
|
<text v-else class="thumbs-up time iconfont" @click="cancelThumbsUp(comment.id, index, null)"><text v-show="comment.thumbsUpNum > 0" class="thumbs-num">{{comment.thumbsUpNum}}</text></text>
|
</view>
|
</view>
|
</view>
|
<!-- 回复列表 -->
|
<view class="reply-list" v-if="comment.replies && comment.replies.length > 0">
|
<view class="reply-item" v-for="(reply, replyIndex) in comment.replies" :key="reply.id">
|
<view class="reply-content">
|
<view style="display: flex;">
|
<image class="comment-reply-avatar" :src="reply.replyUserAvatar || '/static/default-avatar.png'"></image>
|
<text class="nickname">{{reply.userNickname}}</text>
|
<text v-if="reply.replyUserId && reply.masterCommentId !== reply.replyId" class="reply-to"><text style="margin-right: 10rpx;font-size: 28rpx;" class="iconfont"></text>{{reply.replyUserNickname}}</text>
|
</view>
|
<text class="content">{{reply.commentContent}}</text>
|
<view class="reply-footer">
|
<text class="time">{{formatTime(reply.createTime)}}</text>
|
<text @click="openReply(comment, reply)" class="reply-btu time">回复</text>
|
<text v-if="!reply.hasThumbsUp" class="thumbs-up time iconfont" @click="thubmsUp(reply.id, index, replyIndex)"><text v-show="reply.thumbsUpNum > 0" class="thumbs-num">{{reply.thumbsUpNum}}</text></text>
|
<text v-else class="thumbs-up time iconfont" @click="cancelThumbsUp(reply.id, index, replyIndex)"><text v-show="reply.thumbsUpNum > 0" class="thumbs-num">{{reply.thumbsUpNum}}</text></text>
|
</view>
|
</view>
|
</view>
|
</view>
|
<view class="view-more-replies" v-if="comment.replyTotalCount > 0 && !comment.expandReply" @click="loadRepliesPage(comment, index)">
|
<text class="line">——</text>展开{{comment.replyTotalCount}}条回复 ↓
|
</view>
|
<view class="reply-op" v-if="comment.replyTotalCount > replyCommentQuery.pageNumber * replyCommentQuery.pageSize && comment.expandReply">
|
<view @click="loadNextPageReply(index)" class="reply-op-item"><text class="line">——</text>展开更多<text class="iconfont textSideIcon"></text></view>
|
<view @click="retractReplyComment(index)" class="reply-op-item" style="margin-left: 50rpx;">收起<text class="iconfont textSideIcon"></text></view>
|
</view>
|
<view class="reply-op" v-else-if="comment.replyTotalCount <= replyCommentQuery.pageNumber * replyCommentQuery.pageSize && comment.expandReply">
|
<view @click="retractReplyComment(index)" class="reply-op-item"><text class="line">——</text>收起<text class="iconfont textSideIcon"></text></view>
|
</view>
|
</view>
|
</scroll-view>
|
<view class="comment-input-area">
|
<input
|
ref="commentInput"
|
class="comment-input"
|
v-model="commentForm.commentContent"
|
:placeholder="commentForm.replyId ? `回复 @${commentForm.replyUserNickname}` : '写下你的评论...'"
|
placeholder-class="placeholder"
|
/>
|
<button class="submit-btn" @click="submitComment" :disabled="!commentForm.commentContent.trim()">发送</button>
|
</view>
|
</view>
|
</uni-popup>
|
|
|
<custom-tabbar bgColor="#333333" selected="index" selectedTextColor="#ffffff"></custom-tabbar>
|
</view>
|
</template>
|
|
<script>
|
import { getRecommendVideos, savePlayRecord, subscribe, getVideoComments, addVideoComment, thubmsUpComment, cancelThubmsUpComment } from "@/api/video.js";
|
import { changeCollect } from "@/api/collect.js";
|
export default {
|
computed: {
|
hasPlayTime() {
|
return this.sliderFormatTime(this.progress > 0 ? this.duration * this.progress / 100 : 0);
|
}
|
},
|
data() {
|
return {
|
currentImgIndex: 0, // 播放到第几张图--索引
|
currentGoodsIndex: 0, // 播放到第几个商品--索引
|
currentTime: 0,
|
formartDuration: '',
|
duration: 0,
|
startX: 0,
|
progress: 0, // 视频进度
|
startProgress : 0, // 开始滑动时的进度
|
barLeft: 0, // 进度条左边界位置
|
barWidth: 0, // 进度条宽度
|
isDragging: false, // 是否正在拖动
|
processHidenTimer: null, // 进度条隐藏定时器
|
showProcess: false, // 是否显示进度条
|
videoNoMore: false, // 是否还有更多视频
|
commentNoMore: false, // 是否还有更多评论
|
commentQuery: {
|
pageNumber: 1,
|
pageSize: 5,
|
videoId: '',
|
masterCommentId: ''
|
},
|
replyCommentQuery: {
|
pageNumber: 1,
|
pageSize: 5,
|
videoId: '',
|
masterCommentId: ''
|
},
|
commentForm: { // 评论表单数据
|
id: '',
|
videoId: '',
|
commentContent: '',
|
replyId: '',
|
replyUserId: '',
|
replyUserNickname: '',
|
replyUserAvatar: '',
|
masterCommentId: null
|
},
|
comments: [], // 评论列表
|
commentsTotal: 0, // 评论总条数
|
commentLoading: false, // 评论加载状态
|
startHidenTime: 0, // 记录切换至其它页面的时间,用于计算视频观看时间减去的部分
|
totalHidenTime: 0, // 总共隐藏页面的时间
|
startPauseTime: 0, // 开始暂停的时间
|
totalPauseTime: 0, // 总共暂停的时间
|
playRecord: {
|
videoId: null,
|
viewDuration: 0, // 这个视频总共观看了多久
|
playAt: 0 ,// 这个视频播放到哪了
|
startPlayTime: 0 // 这个视频从什么时候开始播放的
|
},
|
currentVideoIsPlaying: true, // 当前视频是否正在播放
|
isFullScreen: false,
|
windowHeight: 0,
|
currentIndex: 0, // 当前播放的视频索引
|
videoList: [
|
|
], // 视频列表数据
|
videoContexts: [], // 视频上下文对象集合
|
loading: false, // 是否正在加载
|
videoQuery: {
|
pageNumber: 1,
|
pageSize: 6,
|
videoFrom: 'recommend'
|
}
|
}
|
},
|
onShow() {
|
this.loadVideos()
|
// 如果视频按下暂停后切换页面再回到页面时,只算暂停时间(因为暂停时间和离开页面时间是重复的,只算一个)
|
if(this.startHidenTime !== 0 && this.currentVideoIsPlaying) {
|
const duration = Date.now() - this.startHidenTime
|
this.totalHidenTime += duration
|
}
|
},
|
onHide() {
|
this.startHidenTime = Date.now()
|
},
|
onLoad() {
|
this.loadVideos();
|
},
|
onReady() {
|
// 初始化视频上下文
|
this.initVideoContexts();
|
},
|
methods: {
|
// 点击商品跳转
|
jumpToPay(videoId) {
|
uni.navigateTo({
|
url: '/pages/video/video-goods-detail?videoId=' + videoId
|
});
|
},
|
// 轮播图变化
|
imgChange(e) {
|
this.currentImgIndex = e.detail.current;
|
},
|
// 商品轮播图变化
|
goodsChange(e) {
|
this.currentGoodsIndex = e.detail.current;
|
},
|
// 获取进度条的位置和尺寸
|
getBarRect() {
|
const query = uni.createSelectorQuery().in(this);
|
query.select('#progressBar').boundingClientRect(rect => {
|
if (rect) {
|
this.barLeft = rect.left;
|
this.barWidth = rect.width;
|
}
|
}).exec();
|
},
|
// 跳转个人主页
|
jumpToHomePage(authorId) {
|
uni.navigateTo({
|
url: "/pages/video/home-page?authorId=" + authorId
|
})
|
},
|
// 取消点赞
|
async cancelThumbsUp(id, commentIndex, replyIndex) {
|
const data = {
|
refId: id,
|
thumbsUpType: 'video_comment'
|
}
|
cancelThubmsUpComment(data).then(res => {
|
if(replyIndex != null) {
|
this.comments[commentIndex].replies[replyIndex].hasThumbsUp = false;
|
this.comments[commentIndex].replies[replyIndex].thumbsUpNum -= 1;
|
} else {
|
this.comments[commentIndex].hasThumbsUp = false;
|
this.comments[commentIndex].thumbsUpNum -= 1;
|
}
|
})
|
},
|
// 评论点赞
|
async thubmsUp(id, commentIndex, replyIndex) {
|
const data = {
|
refId: id,
|
thumbsUpType: 'video_comment'
|
}
|
thubmsUpComment(data).then(res => {
|
if(replyIndex != null) {
|
this.comments[commentIndex].replies[replyIndex].hasThumbsUp = true;
|
this.comments[commentIndex].replies[replyIndex].thumbsUpNum += 1;
|
} else {
|
this.comments[commentIndex].hasThumbsUp = true;
|
this.comments[commentIndex].thumbsUpNum += 1;
|
}
|
})
|
},
|
// 加载下一页回复
|
loadNextPageReply(index) {
|
this.replyCommentQuery.pageNumber++;
|
getVideoComments(this.replyCommentQuery).then(res => {
|
this.comments[index].replies = [
|
...this.comments[index].replies,
|
...res.data.data.filter(
|
(newItem) => !this.comments[index].replies.some((oldItem) => oldItem.id === newItem.id)
|
),
|
];
|
})
|
},
|
// 收起回复
|
retractReplyComment(index) {
|
this.comments[index].expandReply = false;
|
this.comments[index].replies = [];
|
},
|
// 加载回复
|
loadRepliesPage(comment, index) {
|
this.replyCommentQuery.pageNumber = 1;
|
this.replyCommentQuery.masterCommentId = comment.id
|
getVideoComments(this.replyCommentQuery).then(res => {
|
this.comments[index].replies = res.data.data;
|
this.comments[index].expandReply = true;
|
})
|
},
|
resetCommentForm() {
|
const videoId = this.commentForm.videoId;
|
this.commentForm = { // 评论表单数据
|
id: '',
|
videoId: videoId,
|
commentContent: '',
|
replyId: '',
|
replyUserId: '',
|
replyUserNickname: '',
|
replyUserAvatar: '',
|
masterCommentId: null
|
}
|
},
|
// 取消回复
|
cancelReply() {
|
this.resetCommentForm()
|
},
|
// 打开回复框
|
openReply(comment, reply = null) {
|
if(reply) {
|
comment = reply
|
}
|
this.commentForm.masterCommentId = comment.masterCommentId ? comment.masterCommentId : comment.id;
|
this.commentForm.replyId = comment.id;
|
this.commentForm.replyUserId = comment.userId;
|
this.commentForm.replyUserNickname = comment.userNickname;
|
this.commentForm.replyUserAvatar = comment.userAvatar;
|
// 自动聚焦输入框
|
this.$nextTick(() => {
|
const input = this.$refs.commentInput;
|
if (input) input.focus();
|
});
|
},
|
// 进度条时间格式化 (00:00)
|
sliderFormatTime(seconds) {
|
const mins = Math.floor(seconds / 60);
|
const secs = Math.floor(seconds % 60);
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
},
|
// 格式化时间
|
formatTime(time) {
|
const date = new Date(time);
|
const now = new Date();
|
const diff = Math.floor((now - date) / 1000); // 秒
|
|
if (diff < 60) return '刚刚';
|
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
|
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
|
|
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
},
|
// 提交评论
|
async submitComment() {
|
if (!this.commentForm.commentContent.trim()) {
|
uni.showToast({
|
title: '评论内容不能为空',
|
icon: 'none'
|
});
|
return;
|
}
|
// 发表评论
|
addVideoComment(this.commentForm).then(res => {
|
if(res.data.code === 200) {
|
this.resetCommentForm()
|
|
// 如果是评论别人的回复,那么就将这个发布到replies里面
|
if(res.data.data.replyId) {
|
for (const [index, item] of this.comments.entries()) {
|
if (item.id === res.data.data.replyId) {
|
item.replies.unshift(res.data.data);
|
// this.loadRepliesPage(item, index)
|
break; // 跳出循环
|
}
|
}
|
} else {
|
this.comments.unshift(res.data.data);
|
}
|
console.log("新增后",this.comments);
|
uni.showToast({
|
title: '评论成功'
|
});
|
// 当前视频评论数加一
|
this.commentsTotal += 1;
|
this.videoList[this.currentIndex].commentNum += 1;
|
} else {
|
uni.showToast({
|
title: res.data.msg,
|
icon: 'none'
|
});
|
}
|
}).catch(() => {
|
uni.showToast({
|
title: '评论失败',
|
icon: 'none'
|
});
|
})
|
},
|
// 关闭评论弹窗
|
closeCommentPopup() {
|
this.$refs.commentPopup.close()
|
this.showCommentPopup = false;
|
this.comments = [];
|
this.resetCommentForm()
|
this.commentQuery.pageNumber = 1;
|
this.commentNoMore = false;
|
},
|
// 下滑评论区加载评论
|
async getCommentPage() {
|
if(this.commentNoMore) {
|
return;
|
}
|
getVideoComments(this.commentQuery).then(res => {
|
if(this.commentQuery.pageNumber === 1) {
|
this.comments = res.data.data
|
} else {
|
this.comments = [
|
...this.comments,
|
...res.data.data.filter(
|
(newItem) => !this.comments.some((oldItem) => oldItem.id === newItem.id)
|
),
|
];
|
}
|
if (res.data.data.length < this.commentQuery.pageSize) {
|
this.commentNoMore = true;
|
return;
|
}
|
this.commentQuery.pageNumber++;
|
})
|
},
|
// 显示评论弹窗
|
async showComments(item) {
|
this.commentForm.videoId = item.id;
|
this.$refs.commentPopup.open();
|
this.commentLoading = true;
|
this.commentQuery.videoId = item.id
|
this.replyCommentQuery.videoId = item.id
|
// 首次加载评论分页大小增加一倍,以产生滚动条,后续可触发
|
this.commentQuery.pageSize *= 2;
|
getVideoComments(this.commentQuery).then(res => {
|
this.commentsTotal = res.data.total;
|
this.comments = res.data.data;
|
this.commentQuery.pageNumber += 2;
|
this.commentQuery.pageSize /= 2;
|
}).catch(() => {
|
uni.showToast({
|
title: '获取评论失败',
|
icon: 'none'
|
});
|
}).finally(() => {
|
this.commentLoading = false;
|
})
|
},
|
// 关注作者
|
subscribeAuth(index, authorId) {
|
this.videoList.forEach(video => {
|
if(video.authorId === authorId) {
|
video.subscribeThisAuthor = true
|
}
|
})
|
subscribe(authorId).then(res => {
|
if(res.data.code === 200) {
|
uni.showToast({
|
title: '关注成功~',
|
icon: 'none'
|
});
|
} else {
|
this.videoList.forEach(video => {
|
if(video.authorId === authorId) {
|
video.subscribeThisAuthor = false
|
}
|
})
|
}
|
})
|
},
|
// 初始化视频上下文
|
initVideoContexts() {
|
this.videoContexts = this.videoList.map((_, index) => {
|
let videoContent = uni.createVideoContext(`video${index}`, this);
|
return videoContent;
|
});
|
},
|
|
// 加载视频数据
|
async loadVideos() {
|
if (this.loading || this.videoNoMore) return;
|
this.loading = true;
|
|
getRecommendVideos(this.videoQuery).then(res => {
|
console.log(res, "视频数据");
|
if (this.videoQuery.pageNumber === 1) {
|
this.videoList = res.data.data;
|
} else {
|
this.videoList = [
|
...this.videoList,
|
...res.data.data.filter(
|
(newItem) => !this.videoList.some((oldItem) => oldItem.id === newItem.id)
|
),
|
];
|
}
|
this.$nextTick(() => {
|
this.initVideoContexts();
|
});
|
this.loading = false;
|
if(res.data.data.length < this.videoQuery.pageSize) {
|
this.videoNoMore = true;
|
return;
|
}
|
this.videoQuery.pageNumber++;
|
|
})
|
},
|
|
// 滑动切换视频
|
onSwiperChange(e) {
|
// 如果视频处于暂停状态往下刷视频,那么需要再计算一次暂停时间
|
if(!this.currentVideoIsPlaying) {
|
if(this.startPauseTime !== 0) {
|
const duration = Date.now() - this.startPauseTime
|
this.totalPauseTime += duration
|
}
|
}
|
// 保存上一个视频的播放记录
|
this.savePlayRecord()
|
const oldIndex = this.currentIndex;
|
this.currentIndex = e.detail.current;
|
|
// 暂停上一个视频
|
if (this.videoContexts[oldIndex]) {
|
this.videoContexts[oldIndex].pause();
|
}
|
|
this.startPauseTime = 0;
|
// 播放当前视频
|
if (this.videoContexts[this.currentIndex]) {
|
this.videoContexts[this.currentIndex].play();
|
}
|
},
|
|
// 收藏/取消收藏
|
toggleCollect(item, index) {
|
let data = {
|
refId: item.id,
|
collectType: 'video'
|
}
|
const beforeCollected = item.collected
|
const beforeCollectNum = item.collectNum
|
if(item.collected) {
|
this.videoList[index].collected = false
|
this.videoList[index].collectNum -= 1
|
} else {
|
this.videoList[index].collected = true
|
this.videoList[index].collectNum += 1
|
}
|
changeCollect(data).then(res => {
|
if(res.data.code !== 200) {
|
this.videoList[index].collected = beforeCollected
|
this.videoList[index].collectNum = beforeCollectNum
|
}
|
})
|
},
|
// 单击屏幕:暂停或继续播放
|
togglePlay(index) {
|
console.log("单击视频", index, this.videoContexts);
|
if(this.currentVideoIsPlaying) {
|
this.videoContexts[index].pause();
|
} else {
|
this.videoContexts[index].play();
|
}
|
},
|
// 视频播放事件
|
onPlay(id, index) {
|
this.getBarRect()
|
this.progress = 0
|
console.log(id, index, "触发播放");
|
if(index === this.currentIndex) {
|
this.currentVideoIsPlaying = true;
|
} else {
|
this.currentVideoIsPlaying = false;
|
return
|
}
|
this.playRecord.videoId = id;
|
// 没初始化才赋值,因为一个视频重复播放onPlay会重复触发
|
if(this.playRecord.startPlayTime === 0) {
|
this.playRecord.startPlayTime = Date.now();
|
}
|
if(this.startPauseTime !== 0) {
|
const duration = Date.now() - this.startPauseTime
|
this.totalPauseTime += duration
|
}
|
},
|
|
// 视频暂停事件
|
onPause(index) {
|
console.log(index, "触发暂停");
|
if(index === this.currentIndex) {
|
this.currentVideoIsPlaying = false;
|
} else {
|
this.currentVideoIsPlaying = true;
|
return
|
}
|
this.startPauseTime = Date.now()
|
},
|
// 视频结束事件
|
onEnded(index) {
|
// this.currentVideoIsPlaying = false;
|
},
|
|
// 记录播放时长
|
onTimeUpdate(e) {
|
this.playRecord.playAt = e.detail.currentTime;
|
|
this.currentTime = e.detail.currentTime;
|
this.progress = (e.detail.currentTime / this.duration) * 100
|
},
|
// 触摸开始
|
handleTouchStart(e) {
|
this.isDragging = true;
|
this.showProcess = true;
|
this.startProgress = this.progress; // 记录开始时的进度
|
this.startX = e.touches[0].pageX;
|
console.log("记录开始时的进度", this.startProgress);
|
this.videoContexts[this.currentIndex].pause()
|
// this.updateProgress(e);
|
},
|
|
// 触摸移动
|
handleTouchMove(e) {
|
if (!this.isDragging || !this.barWidth) return;
|
clearTimeout(this.processHidenTimer)
|
this.videoContexts[this.currentIndex].pause()
|
this.updateProgress(e);
|
},
|
|
// 触摸结束
|
handleTouchEnd() {
|
this.isDragging = false;
|
console.log("滑动结束", this.duration * this.progress);
|
this.videoContexts[this.currentIndex].seek(this.duration * this.progress / 100)
|
this.videoContexts[this.currentIndex].play()
|
this.processHidenTimer = setTimeout(() => {
|
this.showProcess = false;
|
}, 1000);
|
},
|
|
// 更新进度
|
updateProgress(e) {
|
// 获取当前触摸点X坐标
|
const currentX = e.touches[0].pageX;
|
|
// 计算滑动距离(像素)
|
const deltaX = currentX - this.startX;
|
|
// 将像素距离转换为进度增量
|
const deltaProgress = (deltaX / this.barWidth) * 100;
|
console.log("进度增量", deltaProgress);
|
// 计算新进度 = 开始时的进度 + 滑动增量
|
let newProgress = this.startProgress + deltaProgress;
|
|
// 限制范围在0-100之间
|
newProgress = Math.max(0, Math.min(100, newProgress));
|
|
this.progress = newProgress;
|
},
|
// 获取视频总时长
|
onLoadedMetadata(e) {
|
this.duration = e.detail.duration;
|
this.formartDuration = this.sliderFormatTime(this.duration);
|
console.log("视频总时长", this.duration);
|
},
|
// 保存播放记录
|
async savePlayRecord() {
|
console.log(Date.now(), this.playRecord.startPlayTime, this.totalHidenTime);
|
|
const data = {
|
videoId: this.playRecord.videoId,
|
viewDuration: Date.now() - this.playRecord.startPlayTime - this.totalHidenTime - this.totalPauseTime,
|
playAt: this.playRecord.playAt
|
}
|
this.playRecord = {
|
videoId: null,
|
viewDuration: 0, // 这个视频总共观看了多久
|
playAt: 0 ,// 这个视频播放到哪了
|
startPlayTime: 0 // 这个视频从什么时候开始播放的
|
}
|
this.totalHidenTime = 0
|
this.totalPauseTime = 0
|
savePlayRecord(data)
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
::v-deep .custom-tabbar {
|
border-top: none !important;
|
}
|
.video-container {
|
width: 100%;
|
height: 100vh;
|
background-color: #000;
|
}
|
|
.video-swiper {
|
width: 100%;
|
height: calc(100% - 50px);
|
}
|
|
.video-item {
|
width: 100%;
|
height: 100%;
|
object-fit: cover;
|
}
|
.play-icon {
|
position: absolute;
|
top: 50%;
|
left: 50%;
|
transform: translate(-50%, -50%);
|
width: 45px;
|
height: 45px;
|
z-index: 10;
|
opacity: 0.6;
|
}
|
|
.video-info {
|
width: 70%;
|
position: absolute;
|
bottom: 70px;
|
left: 20px;
|
color: #f8f8f8;
|
z-index: 10;
|
letter-spacing: 1px;
|
}
|
|
.action-buttons {
|
position: absolute;
|
right: 20px;
|
bottom: 150px;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
z-index: 10;
|
}
|
|
.action-item {
|
margin-bottom: 18px;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
color: #fff;
|
}
|
.avatar-container {
|
margin-bottom: 27px;
|
position: relative; /* 为绝对定位的子元素提供定位上下文 */
|
width: 40px;
|
height: 40px;
|
display: inline-block; /* 使容器根据内容调整大小 */
|
}
|
.avatar {
|
border: 2px solid #FFFFFF;
|
box-sizing: border-box;
|
width: 100%;
|
height: 100%;
|
border-radius: 50%; /* 关键属性,设置为50%即可实现圆形 */
|
overflow: hidden; /* 确保图片不会超出圆形边界 */
|
display: block;
|
}
|
.follow-icon {
|
position: absolute;
|
bottom: 0; /* 定位到底部 */
|
left: 50%; /* 水平居中开始位置 */
|
transform: translate(-50%, 50%); /* 水平居中并向下移动50% */
|
|
width: 18px; /* 图标大小 */
|
height: 18px;
|
background-color: #FF5A5F; /* 图标背景色 */
|
border-radius: 50%;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
|
}
|
.video-tag {
|
margin-left: 5px;
|
font-weight: bold;
|
color: #eeeeee;
|
}
|
.video-author {
|
font-size: 1.2em;
|
}
|
/* 商品链接悬挂层样式 */
|
.goods-link-warp {
|
position: absolute;
|
bottom: 160px;
|
left: 20px;
|
color: #f8f8f8;
|
z-index: 10;
|
}
|
.goods-link {
|
position: relative;
|
width: 450rpx;
|
margin: 20rpx 0;
|
padding: 12rpx;
|
background-color: rgba(255, 255, 255, 0.9);
|
border-radius: 12rpx;
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
}
|
|
.goods-container {
|
width: 100%;
|
display: flex;
|
align-items: center;
|
}
|
|
.goods-image {
|
width: 120rpx;
|
height: 120rpx;
|
border-radius: 8rpx;
|
margin-right: 20rpx;
|
}
|
|
.goods-info {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
}
|
|
.goods-name {
|
font-size: 28rpx;
|
color: #333;
|
font-weight: bold;
|
-webkit-line-clamp: 2;
|
-webkit-box-orient: vertical;
|
margin-bottom: 8rpx;
|
width: 280rpx; /* 需要指定宽度 */
|
overflow: hidden;
|
white-space: nowrap;
|
text-overflow: ellipsis;
|
}
|
|
.price-section {
|
display: flex;
|
align-items: center;
|
margin-bottom: 6rpx;
|
}
|
|
.current-price {
|
font-size: 32rpx;
|
color: #ff2e4d;
|
font-weight: bold;
|
margin-right: 12rpx;
|
}
|
|
.original-price {
|
font-size: 28rpx;
|
color: #999;
|
text-decoration: line-through;
|
}
|
|
.sales-count {
|
font-size: 22rpx;
|
color: #999;
|
}
|
|
.buy-button {
|
background: linear-gradient(to right, #ff5a5f, #ff2e4d);
|
color: white;
|
padding: 10rpx 28rpx;
|
border-radius: 20rpx;
|
font-size: 26rpx;
|
font-weight: bold;
|
}
|
/* 评论弹窗样式 */
|
.comment-popup {
|
background-color: #fff;
|
border-radius: 20rpx 20rpx 0 0;
|
padding-bottom: env(safe-area-inset-bottom);
|
height: 60vh;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.popup-header {
|
padding: 30rpx;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
border-bottom: 1rpx solid #f5f5f5;
|
}
|
|
.popup-title {
|
font-size: 32rpx;
|
font-weight: bold;
|
}
|
|
.close-icon {
|
/* font-size: 36rpx; */
|
color: #999;
|
}
|
|
.comment-list {
|
flex: 1;
|
padding: 0rpx 20rpx 20rpx 20rpx;
|
box-sizing: border-box;
|
height: calc(60vh - 260rpx);
|
}
|
|
.comment-item {
|
display: flex;
|
flex-direction: column;
|
padding: 10rpx 0 20rpx 0;
|
}
|
|
.comment-avatar {
|
width: 70rpx;
|
height: 70rpx;
|
border-radius: 50%;
|
margin-right: 10rpx;
|
}
|
.comment-reply-avatar {
|
width: 40rpx;
|
height: 40rpx;
|
border-radius: 50%;
|
margin-right: 10rpx;
|
}
|
|
.comment-content {
|
flex: 1;
|
}
|
|
.nickname {
|
font-size: 28rpx;
|
color: #666;
|
display: block;
|
margin-bottom: 10rpx;
|
}
|
|
.content {
|
font-size: 28rpx;
|
color: #333;
|
display: block;
|
margin-bottom: 10rpx;
|
}
|
|
.time {
|
font-size: 28rpx;
|
color: #999;
|
}
|
|
.comment-input-area {
|
display: flex;
|
padding: 20rpx 30rpx;
|
align-items: center;
|
}
|
|
.comment-input {
|
flex: 1;
|
background-color: #fff;
|
height: 80rpx;
|
border: 1px solid #dcdcdc;
|
border-radius: 40rpx;
|
padding: 0 30rpx;
|
font-size: 28rpx;
|
}
|
|
.placeholder {
|
color: #ccc;
|
}
|
|
.submit-btn {
|
margin-left: 20rpx;
|
background-color: #07c160;
|
color: #fff;
|
border-radius: 40rpx;
|
padding: 0 30rpx;
|
height: 80rpx;
|
line-height: 80rpx;
|
font-size: 28rpx;
|
}
|
|
.loading, .empty {
|
padding: 40rpx 0;
|
text-align: center;
|
color: #999;
|
}
|
.reply-list {
|
margin-top: 20rpx;
|
padding-left: 80rpx;
|
}
|
.reply-op {
|
margin-top: 10rpx;
|
padding-left: 80rpx;
|
display: flex;
|
font-size: 28rpx;
|
color: #333;
|
}
|
.reply-op-item {
|
display: flex;
|
align-items: center;
|
height: 40rpx;
|
}
|
|
.reply-item {
|
display: flex;
|
margin-bottom: 20rpx;
|
}
|
|
.reply-content {
|
flex: 1;
|
}
|
|
.reply-to {
|
color: #576b95;
|
margin: 0 10rpx;
|
font-size: 28rpx;
|
}
|
.reply-title {
|
display: flex;
|
align-items: center;
|
font-size: 28rpx;
|
color: #333;
|
}
|
|
.cancel-reply {
|
margin-left: 20rpx;
|
color: #576b95;
|
font-size: 28rpx;
|
padding: 6rpx 12rpx;
|
background: #f5f5f5;
|
border-radius: 20rpx;
|
}
|
.view-more-replies {
|
color: #576b95;
|
font-size: 28rpx;
|
padding: 10rpx 0;
|
padding-left: 80rpx;
|
}
|
.comment-footer, .reply-footer {
|
display: flex;
|
align-items: center;
|
font-size: 28rpx;
|
color: #999;
|
}
|
.reply-btu {
|
margin-left: 30rpx;
|
}
|
.thumbs-up {
|
position: absolute;
|
right: 20rpx;
|
font-size: 32rpx;
|
width: 120rpx;
|
}
|
.textSideIcon {
|
font-size: 36rpx;
|
margin-left: 5rpx;
|
}
|
.line {
|
margin-right: 10rpx;
|
color: #cccccc;
|
}
|
.thumbs-num {
|
margin-left: 4rpx;
|
}
|
.container {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
position: absolute;
|
bottom: 0;
|
width: 100%;
|
}
|
|
.progress-bar {
|
position: relative;
|
width: 100%;
|
height: 16px;
|
background-color: #eee;
|
overflow: hidden;
|
}
|
|
.progress-fill {
|
position: absolute;
|
left: 0;
|
top: 0;
|
height: 100%;
|
background-color: lightgray;
|
transition: width 0.1s;
|
}
|
.process-warp {
|
width: 100%;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
.progress-text {
|
margin-top: 10px;
|
font-size: 14px;
|
color: #666;
|
}
|
.swiper-box {
|
width: 100%;
|
height: 1400rpx;
|
}
|
.swiper-item {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
width: 100%;
|
height: 100%;
|
}
|
</style>
|