| | |
| | | <template> |
| | | <view class="video-container"> |
| | | <!-- 视频列表 --> |
| | | <swiper |
| | | class="video-swiper" |
| | | vertical |
| | | circular |
| | | <swiper |
| | | class="video-swiper" |
| | | vertical |
| | | :current="currentIndex" |
| | | @change="onSwiperChange" |
| | | easing-function="linear" |
| | | > |
| | | <swiper-item v-for="(item, index) in videoList" :key="item.id"> |
| | | <swiper-item |
| | | v-for="(item, index) in videoList" |
| | | :key="item.id" |
| | | @touchstart="handleSwiperStart" |
| | | @touchmove="handleSwiperMove" |
| | | @touchend="handleSwiperEnd(item)" |
| | | > |
| | | <view style="width: 100%;height: 100%;" v-if="item.videoContentType === 'video'"> |
| | | <!-- 播放按钮(仅当视频暂停时显示) --> |
| | | <view |
| | | class="play-icon" |
| | | <view |
| | | class="play-icon" |
| | | @click="togglePlay(index)" |
| | | v-if="!currentVideoIsPlaying" |
| | | v-show="!currentVideoIsPlaying" |
| | | > |
| | | <image src="/static/video/play.png" style="width: 45px;height: 45px" mode="aspectFit"></image> |
| | | </view> |
| | |
| | | :id="'video'+index" |
| | | :ref="'video'+index" |
| | | :src="item.videoUrl" |
| | | :autoplay="currentIndex === index" |
| | | :autoplay="false" |
| | | :controls="false" |
| | | :loop="true" |
| | | :object-fit="item.objectFit" |
| | |
| | | @click="togglePlay(index)" |
| | | @timeupdate="onTimeUpdate($event)" |
| | | @loadedmetadata="onLoadedMetadata($event)" |
| | | |
| | | ></video> |
| | | <!-- 自定义控制条 --> |
| | | <view |
| | | <view |
| | | @touchstart="handleTouchStart" |
| | | @touchmove="handleTouchMove" |
| | | @touchend="handleTouchEnd" |
| | |
| | | <view class="process-warp" :style="{ opacity: showProcess ? 1 : 0 }"> |
| | | <!-- 显示当前进度 --> |
| | | <view class="progress-text">{{ hasPlayTime }}/{{formartDuration}}</view> |
| | | <view |
| | | class="progress-bar" |
| | | <view |
| | | class="progress-bar" |
| | | id="progressBar" |
| | | > |
| | | |
| | | |
| | | <!-- 已填充部分 --> |
| | | <view class="progress-fill" :style="{ width: progress + '%' }"></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" |
| | | <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-item v-for="img in item.imgs" :key="img"> |
| | | <view class="swiper-item"> |
| | | <!-- 调整 image 样式,使其居中且按比例缩放 --> |
| | | <image |
| | | :src="img" |
| | | mode="aspectFit" |
| | | <image |
| | | :src="img" |
| | | mode="aspectFit" |
| | | style="width: 100%; height: 100%; display: block; margin: 0 auto;" |
| | | ></image> |
| | | </view> |
| | |
| | | </swiper> |
| | | </uni-swiper-dot> |
| | | </view> |
| | | |
| | | |
| | | |
| | | |
| | | <!-- 悬挂商品链接层 --> |
| | | <view class="goods-link-warp" v-if="item.goodsList.length > 0"> |
| | | <view class="goods-link"> |
| | |
| | | <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> |
| | |
| | | </swiper> |
| | | </view> |
| | | </view> |
| | | |
| | | |
| | | |
| | | |
| | | <!-- 视频信息层 --> |
| | | <view class="video-info"> |
| | | <view> |
| | |
| | | <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"> |
| | |
| | | <button open-type="share" class="custom-share-btn" :data-obj="item"> |
| | | <text class="iconfont"></text> |
| | | </button> |
| | | |
| | | |
| | | </view> |
| | | </view> |
| | | |
| | | |
| | | </swiper-item> |
| | | </swiper> |
| | | |
| | | |
| | | <!-- 评论弹窗 --> |
| | | <uni-popup ref="commentPopup" type="bottom" :is-mask-click="true" @maskClick="closeCommentPopup"> |
| | | <view class="comment-popup"> |
| | |
| | | </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;"> |
| | | <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> |
| | |
| | | </view> |
| | | </scroll-view> |
| | | <view class="comment-input-area"> |
| | | <input |
| | | <input |
| | | ref="commentInput" |
| | | class="comment-input" |
| | | v-model="commentForm.commentContent" |
| | | :placeholder="commentForm.replyId ? `回复 @${commentForm.replyUserNickname}` : '写下你的评论...'" |
| | | 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> |
| | |
| | | isFullScreen: false, |
| | | windowHeight: 0, |
| | | currentIndex: 0, // 当前播放的视频索引 |
| | | videoList: [ |
| | | |
| | | ], // 视频列表数据 |
| | | videoList: [], // 视频列表数据 |
| | | videoContexts: [], // 视频上下文对象集合 |
| | | videoBufferOffset: 0.1 ,// 视频预加载参数 |
| | | videoLiveOffset: 5, // 保留当前视频前后各多少个视频上下文 |
| | | touchXY: { // 监听左滑右滑 |
| | | startX: 0, |
| | | endX: 0, |
| | | startY: 0, |
| | | endY: 0 |
| | | }, |
| | | loading: false, // 是否正在加载 |
| | | videoQuery: { |
| | | pageNumber: 1, |
| | | pageSize: 6, |
| | | pageSize: 10, |
| | | videoFrom: 'recommend' |
| | | } |
| | | } |
| | |
| | | // this.wxSilentLogin(() => { |
| | | // this.loadVideos(); |
| | | // }) |
| | | // } else { |
| | | // } else { |
| | | // this.loadVideos(); |
| | | // } |
| | | // 如果视频按下暂停后切换页面再回到页面时,只算暂停时间(因为暂停时间和离开页面时间是重复的,只算一个) |
| | |
| | | saveShareClickRecord({refId: option.videoId, shareUserId: option.userId}) |
| | | } |
| | | }) |
| | | } else { |
| | | } else { |
| | | this.loadVideos(); |
| | | } |
| | | }, |
| | | onReady() { |
| | | // 初始化视频上下文 |
| | | this.initVideoContexts(); |
| | | }, |
| | | onShareAppMessage(e) { |
| | | const userInfo = storage.getUserInfo(); |
| | |
| | | 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()}日`; |
| | | }, |
| | | // 提交评论 |
| | |
| | | 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()) { |
| | |
| | | }, |
| | | // 初始化视频上下文 |
| | | initVideoContexts() { |
| | | this.videoContexts = this.videoList.map((_, index) => { |
| | | let videoContent = uni.createVideoContext(`video${index}`, this); |
| | | return videoContent; |
| | | }); |
| | | const start = Math.max(0, this.currentIndex - this.videoLiveOffset); |
| | | const end = Math.min(this.currentIndex + this.videoLiveOffset, this.videoList.length - 1); |
| | | let contextsLength = this.videoContexts.length; |
| | | if (contextsLength === 0) { |
| | | // 第一次初始化 |
| | | for (let i = 0; i < this.videoList.length; i++) { |
| | | if (i < start || i > end) { |
| | | this.videoContexts.push(null) |
| | | } else { |
| | | let videoContent = uni.createVideoContext(`video${i}`, this); |
| | | videoContent.seek(this.videoBufferOffset); |
| | | videoContent.pause(); |
| | | this.videoContexts.push(videoContent); |
| | | } |
| | | } |
| | | } else { |
| | | for (let i = 0; i < this.videoList.length; i++) { |
| | | contextsLength = this.videoContexts.length |
| | | if (contextsLength - 1 >= i) { |
| | | // 如果已经是null了就不用管,因为视频加载只会在后面push,前面已经设置为null则无需处理 |
| | | if (this.videoContexts[i] == null) { |
| | | continue |
| | | } |
| | | // 超出可视化范围的视频直接释放资源,并置为null |
| | | if (i < start || i > end) { |
| | | if (this.videoContexts[i]) { |
| | | this.videoContexts[i].stop(); |
| | | this.videoContexts[i] = null |
| | | } |
| | | } |
| | | } else { |
| | | if (i < start || i > end) { |
| | | this.videoContexts.push(null); |
| | | } else { |
| | | let videoContent = uni.createVideoContext(`video${i}`, this); |
| | | videoContent.seek(this.videoBufferOffset); |
| | | videoContent.pause(); |
| | | this.videoContexts.push(videoContent); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | // 将当前视频设置为播放 |
| | | if (this.videoContexts[this.currentIndex]) { |
| | | this.videoContexts[this.currentIndex].play() |
| | | } |
| | | |
| | | }, |
| | | |
| | | |
| | | // 加载视频数据 |
| | | async loadVideos() { |
| | | if (this.loading || this.videoNoMore) return; |
| | | this.loading = true; |
| | | |
| | | |
| | | getRecommendVideos(this.videoQuery).then(res => { |
| | | console.log(res, "视频数据"); |
| | | if (this.videoQuery.pageNumber === 1) { |
| | |
| | | return; |
| | | } |
| | | this.videoQuery.pageNumber++; |
| | | |
| | | |
| | | }) |
| | | }, |
| | | |
| | | |
| | | // 滑动切换视频 |
| | | onSwiperChange(e) { |
| | | // 如果视频处于暂停状态往下刷视频,那么需要再计算一次暂停时间 |
| | |
| | | if (this.videoContexts[oldIndex]) { |
| | | this.videoContexts[oldIndex].pause(); |
| | | } |
| | | |
| | | |
| | | this.startPauseTime = 0; |
| | | |
| | | // 设置当前播放视频的总时长 |
| | | this.duration = this.videoList[this.currentIndex].videoDuration; |
| | | this.formartDuration = this.sliderFormatTime(this.duration); |
| | | // 播放当前视频 |
| | | if (this.videoContexts[this.currentIndex]) { |
| | | this.videoContexts[this.currentIndex].play(); |
| | | } |
| | | // 设置当前播放视频的总时长 |
| | | this.duration = this.videoList[this.currentIndex].videoDuration; |
| | | this.formartDuration = this.sliderFormatTime(this.duration); |
| | | this.clearVideoContext() |
| | | }, |
| | | |
| | | |
| | | // 清除超出视频可视化区域的视频上下文 |
| | | async clearVideoContext() { |
| | | // 对超出可视化区域的视频上下文做销毁处理 |
| | | const start = Math.max(0, this.currentIndex - this.videoLiveOffset); |
| | | const end = Math.min(this.currentIndex + this.videoLiveOffset, this.videoList.length - 1); |
| | | for (let i = 0; i < this.videoContexts.length; i++) { |
| | | if (i < start || i > end) { |
| | | if (this.videoContexts[i]) { |
| | | this.videoContexts[i].stop(); |
| | | this.videoContexts[i] = null |
| | | } |
| | | } else { |
| | | if (this.videoContexts[i] == null) { |
| | | let videoContent = uni.createVideoContext(`video${i}`, this); |
| | | videoContent.seek(this.videoBufferOffset); |
| | | videoContent.pause(); |
| | | this.videoContexts[i] = videoContent; |
| | | } |
| | | } |
| | | } |
| | | // 如果剩余视频不足,触发请求获取更多视频 |
| | | if (this.videoList.length - 1 < this.currentIndex + this.videoLiveOffset) { |
| | | this.loadVideos() |
| | | } |
| | | }, |
| | | |
| | | // 开始触摸 |
| | | handleSwiperStart(e) { |
| | | console.log("开始触摸", e); |
| | | this.touchXY.startX = e.touches[0].pageX |
| | | this.touchXY.startY = e.touches[0].pageY |
| | | }, |
| | | // 触摸中 |
| | | handleSwiperMove(e) { |
| | | console.log("触摸中", e); |
| | | this.touchXY.endX = e.touches[0].pageX |
| | | this.touchXY.endY = e.touches[0].pageY |
| | | }, |
| | | // 结束触摸 |
| | | handleSwiperEnd(item) { |
| | | const diffX = this.touchXY.endX - this.touchXY.startX |
| | | const diffY = this.touchXY.endY - this.touchXY.startY |
| | | |
| | | // 判断是否是横向滑动(X轴变化大于Y轴变化) |
| | | if (Math.abs(diffX) > Math.abs(diffY)) { |
| | | if (diffX > 0) { |
| | | console.log('右滑') |
| | | if (item.goodsList && item.goodsList.length > 0) { |
| | | this.jumpToPay(item.id) |
| | | } |
| | | } else { |
| | | console.log('左滑') |
| | | } |
| | | } |
| | | // 重置坐标 |
| | | this.touchXY = { |
| | | startX: 0, |
| | | endX: 0, |
| | | startY: 0, |
| | | endY: 0 |
| | | } |
| | | }, |
| | | |
| | | // 收藏/取消收藏 |
| | | toggleCollect(item, index) { |
| | | let data = { |
| | |
| | | const duration = Date.now() - this.startPauseTime |
| | | this.totalPauseTime += duration |
| | | } |
| | | |
| | | |
| | | }, |
| | | |
| | | |
| | | // 视频暂停事件 |
| | | onPause(index) { |
| | | console.log(index, "触发暂停"); |
| | |
| | | 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 |
| | | }, |
| | |
| | | this.videoContexts[this.currentIndex].pause() |
| | | // this.updateProgress(e); |
| | | }, |
| | | |
| | | |
| | | // 触摸移动 |
| | | handleTouchMove(e) { |
| | | if (!this.isDragging || !this.barWidth) return; |
| | |
| | | this.videoContexts[this.currentIndex].pause() |
| | | this.updateProgress(e); |
| | | }, |
| | | |
| | | |
| | | // 触摸结束 |
| | | handleTouchEnd() { |
| | | this.isDragging = false; |
| | |
| | | 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; |
| | | }, |
| | | // 获取视频总时长 |
| | |
| | | // 保存播放记录 |
| | | 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, |
| | |
| | | height: 100vh; |
| | | background-color: #000; |
| | | } |
| | | |
| | | |
| | | .video-swiper { |
| | | width: 100%; |
| | | height: calc(100% - 50px); |
| | | } |
| | | |
| | | |
| | | .video-item { |
| | | width: 100%; |
| | | height: 100%; |
| | |
| | | z-index: 10; |
| | | opacity: 0.6; |
| | | } |
| | | |
| | | |
| | | .video-info { |
| | | width: 70%; |
| | | position: absolute; |
| | |
| | | z-index: 10; |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | |
| | | .action-buttons { |
| | | position: absolute; |
| | | right: 20px; |
| | |
| | | align-items: center; |
| | | z-index: 10; |
| | | } |
| | | |
| | | |
| | | .action-item { |
| | | margin-bottom: 18px; |
| | | display: flex; |
| | |
| | | bottom: 0; /* 定位到底部 */ |
| | | left: 50%; /* 水平居中开始位置 */ |
| | | transform: translate(-50%, 50%); /* 水平居中并向下移动50% */ |
| | | |
| | | |
| | | width: 18px; /* 图标大小 */ |
| | | height: 18px; |
| | | background-color: #FF5A5F; /* 图标背景色 */ |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | | color: #333; |
| | | } |
| | | |
| | | |
| | | .cancel-reply { |
| | | margin-left: 20rpx; |
| | | color: #576b95; |
| | |
| | | bottom: 0; |
| | | width: 100%; |
| | | } |
| | | |
| | | |
| | | .progress-bar { |
| | | position: relative; |
| | | width: 100%; |
| | |
| | | background-color: #eee; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | |
| | | .progress-fill { |
| | | position: absolute; |
| | | left: 0; |
| | |
| | | .custom-share-btn::after { |
| | | border: none; |
| | | } |
| | | </style> |
| | | </style> |