| | |
| | | @change="onSwiperChange" |
| | | > |
| | | <swiper-item v-for="(item, index) in videoList" :key="item.id"> |
| | | <!-- 播放按钮(仅当视频暂停时显示) --> |
| | | <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 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> |
| | | <video |
| | | :id="'video'+index" |
| | | :ref="'video'+index" |
| | | :src="item.videoUrl" |
| | | :autoplay="currentIndex === index" |
| | | :controls="false" |
| | | :loop="true" |
| | | :object-fit="item.objectFit" |
| | | class="video-item" |
| | | @play="onPlay(item.id, index)" |
| | | @pause="onPause(index)" |
| | | @ended="onEnded(index)" |
| | | @click="togglePlay(index)" |
| | | @timeupdate="onTimeUpdate($event)" |
| | | ></video> |
| | | <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"> |
| | | <view class="goods-link-warp" v-if="item.goodsList.length > 0"> |
| | | <view class="goods-link"> |
| | | <view class="goods-container"> |
| | | <!-- 商品图片 --> |
| | | <image class="goods-image" :src="item.goods.imageUrl" mode="aspectFill"></image> |
| | | |
| | | <!-- 商品信息 --> |
| | | <view class="goods-info"> |
| | | <text class="goods-name">{{item.goods.name}}</text> |
| | | <view class="price-section"> |
| | | <text class="current-price">¥{{item.goods.price}}</text> |
| | | <text class="original-price" v-if="item.goods.originalPrice">¥{{item.goods.originalPrice}}</text> |
| | | </view> |
| | | <text class="sales-count">{{item.goods.saleNum}}人已购</text> |
| | | </view> |
| | | |
| | | <!-- 购买按钮 --> |
| | | <view class="buy-button"> |
| | | <text>购买</text> |
| | | </view> |
| | | </view> |
| | | <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> |
| | | <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">#{{tag.tagName}}</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" :src="item.authorAvatar" mode="aspectFill"></image> |
| | | <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> |
| | |
| | | <text class="iconfont"></text> |
| | | <text style="font-size: 10px;font-weight: lighter;">{{item.commentNum}}</text> |
| | | </view> |
| | | <view class="action-item"> |
| | | <button open-type="share" class="custom-share-btn" :data-obj="item"> |
| | | <text class="iconfont"></text> |
| | | </button> |
| | | |
| | | </view> |
| | | </view> |
| | | |
| | | </swiper-item> |
| | |
| | | <uni-popup ref="commentPopup" type="bottom" :is-mask-click="true" @maskClick="closeCommentPopup"> |
| | | <view class="comment-popup"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">评论({{commentsTotal}})</text> |
| | | <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> |
| | | |
| | |
| | | 暂无评论,快来发表第一条评论吧~ |
| | | </view> |
| | | |
| | | <view v-else class="comment-item" v-for="comment in comments" :key="comment.id"> |
| | | <image class="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> |
| | | <text class="time">{{formatTime(comment.createTime)}}</text> |
| | | </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 |
| | | class="comment-input" |
| | | v-model="commentForm.commentContent" |
| | | placeholder="写下你的评论..." |
| | | placeholder-class="placeholder" |
| | | /> |
| | | <button class="submit-btn" @click="submitComment">发送</button> |
| | | </view> |
| | | <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> |
| | | |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { getRecommendVideos, savePlayRecord, subscribe, getVideoComments, addVideoComment } from "@/api/video.js"; |
| | | import { getRecommendVideos, savePlayRecord, subscribe, getVideoComments, addVideoComment, thubmsUpComment, cancelThubmsUpComment } from "@/api/video.js"; |
| | | import { changeCollect } from "@/api/collect.js"; |
| | | import { saveShare } from "@/api/share.js"; |
| | | import { silentLogin } from "@/api/connect.js"; |
| | | import { getUserInfo } from "@/api/members"; |
| | | import storage from "@/utils/storage.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, |
| | |
| | | videoId: '', |
| | | masterCommentId: '' |
| | | }, |
| | | replyCommentQuery: { |
| | | pageNumber: 1, |
| | | pageSize: 5, |
| | | videoId: '', |
| | | masterCommentId: '' |
| | | }, |
| | | commentForm: { // 评论表单数据 |
| | | id: null, |
| | | videoId: null, |
| | | id: '', |
| | | videoId: '', |
| | | commentContent: '', |
| | | replyId: null |
| | | replyId: '', |
| | | replyUserId: '', |
| | | replyUserNickname: '', |
| | | replyUserAvatar: '', |
| | | masterCommentId: null |
| | | }, |
| | | comments: [], // 评论列表 |
| | | commentsTotal: 0, // 评论总条数 |
| | |
| | | ], // 视频列表数据 |
| | | videoContexts: [], // 视频上下文对象集合 |
| | | loading: false, // 是否正在加载 |
| | | page: 1, // 当前页码 |
| | | pageSize: 10 // 每页数量 |
| | | videoQuery: { |
| | | pageNumber: 1, |
| | | pageSize: 6, |
| | | videoFrom: 'recommend' |
| | | } |
| | | } |
| | | }, |
| | | onShow() { |
| | | this.loadVideos() |
| | | // const token = storage.getAccessToken(); |
| | | // if (! token) { |
| | | // this.wxSilentLogin(() => { |
| | | // this.loadVideos(); |
| | | // }) |
| | | // } else { |
| | | // this.loadVideos(); |
| | | // } |
| | | // 如果视频按下暂停后切换页面再回到页面时,只算暂停时间(因为暂停时间和离开页面时间是重复的,只算一个) |
| | | if(this.startHidenTime !== 0 && this.currentVideoIsPlaying) { |
| | | const duration = Date.now() - this.startHidenTime |
| | |
| | | this.startHidenTime = Date.now() |
| | | }, |
| | | onLoad() { |
| | | this.loadVideos(); |
| | | const token = storage.getAccessToken(); |
| | | if (! token) { |
| | | this.wxSilentLogin(() => { |
| | | this.loadVideos(); |
| | | }) |
| | | } else { |
| | | this.loadVideos(); |
| | | } |
| | | }, |
| | | onReady() { |
| | | // 初始化视频上下文 |
| | | this.initVideoContexts(); |
| | | }, |
| | | onShareAppMessage(e) { |
| | | const userInfo = storage.getUserInfo(); |
| | | if(!userInfo) { |
| | | console.log("未登录不能分享"); |
| | | return |
| | | } |
| | | const videoInfo = e.target.dataset.obj; |
| | | // 保存分享记录 |
| | | const data = { |
| | | shareType: 'video', |
| | | refId: videoInfo.id, |
| | | shareUser: userInfo.id |
| | | } |
| | | saveShare(data) |
| | | return { |
| | | title: videoInfo.title, |
| | | path: `/pages/tabbar/index/home?videoId=${videoInfo.id}&userId=${userInfo.id}`, |
| | | imageUrl: videoInfo.coverUrl |
| | | } |
| | | }, |
| | | methods: { |
| | | // 静默登录 |
| | | wxSilentLogin(callback) { |
| | | //获取code |
| | | uni.login({ |
| | | success: (codeRes) => { |
| | | if(codeRes.errMsg === "login:ok") { |
| | | // 静默登录 |
| | | silentLogin({code: codeRes.code}).then(res => { |
| | | storage.setAccessToken(res.data.data.accessToken); |
| | | storage.setRefreshToken(res.data.data.refreshToken); |
| | | //获取用户信息 |
| | | getUserInfo().then((user) => { |
| | | storage.setUserInfo(user.data.result); |
| | | storage.setHasLogin(true); |
| | | callback() |
| | | }); |
| | | }) |
| | | } else { |
| | | uni.showToast({ |
| | | title: "系统异常,请联系管理员!" |
| | | }) |
| | | } |
| | | }, |
| | | }); |
| | | }, |
| | | // 点击商品跳转 |
| | | 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); |
| | |
| | | // 发表评论 |
| | | addVideoComment(this.commentForm).then(res => { |
| | | if(res.data.code === 200) { |
| | | this.commentForm = { |
| | | id: null, |
| | | videoId: null, |
| | | commentContent: '', |
| | | replyId: null |
| | | 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); |
| | | } |
| | | this.comments.unshift(res.data.data); |
| | | console.log("新增后",this.comments); |
| | | uni.showToast({ |
| | | title: '评论成功' |
| | |
| | | this.$refs.commentPopup.close() |
| | | this.showCommentPopup = false; |
| | | this.comments = []; |
| | | this.commentForm = { |
| | | id: null, |
| | | videoId: null, |
| | | commentContent: '', |
| | | replyId: null |
| | | } |
| | | this.resetCommentForm() |
| | | this.commentQuery.pageNumber = 1; |
| | | this.commentNoMore = false; |
| | | }, |
| | |
| | | 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 => { |
| | |
| | | |
| | | // 加载视频数据 |
| | | async loadVideos() { |
| | | if (this.loading) return; |
| | | if (this.loading || this.videoNoMore) return; |
| | | this.loading = true; |
| | | |
| | | getRecommendVideos({pageNumber: this.page, pageSize: this.pageSize}).then(res => { |
| | | getRecommendVideos(this.videoQuery).then(res => { |
| | | console.log(res, "视频数据"); |
| | | if (this.page === 1) { |
| | | if (this.videoQuery.pageNumber === 1) { |
| | | this.videoList = res.data.data; |
| | | } else { |
| | | this.videoList = [...this.videoList, ...res.data.data]; |
| | | this.videoList = [ |
| | | ...this.videoList, |
| | | ...res.data.data.filter( |
| | | (newItem) => !this.videoList.some((oldItem) => oldItem.id === newItem.id) |
| | | ), |
| | | ]; |
| | | } |
| | | |
| | | this.page++; |
| | | this.$nextTick(() => { |
| | | this.initVideoContexts(); |
| | | }); |
| | | this.loading = false; |
| | | if(res.data.data.length < this.videoQuery.pageSize) { |
| | | this.videoNoMore = true; |
| | | return; |
| | | } |
| | | this.videoQuery.pageNumber++; |
| | | |
| | | }) |
| | | }, |
| | | |
| | |
| | | }, |
| | | // 单击屏幕:暂停或继续播放 |
| | | togglePlay(index) { |
| | | console.log("单击视频", index, this.videoContexts); |
| | | if(this.currentVideoIsPlaying) { |
| | | this.videoContexts[index].pause(); |
| | | } else { |
| | |
| | | }, |
| | | // 视频播放事件 |
| | | onPlay(id, index) { |
| | | this.getBarRect() |
| | | this.progress = 0 |
| | | console.log(id, index, "触发播放"); |
| | | if(index === this.currentIndex) { |
| | | this.currentVideoIsPlaying = true; |
| | |
| | | } |
| | | this.startPauseTime = Date.now() |
| | | }, |
| | | |
| | | // 视频结束事件 |
| | | onEnded(index) { |
| | | // this.currentVideoIsPlaying = false; |
| | |
| | | |
| | | // 记录播放时长 |
| | | onTimeUpdate(e) { |
| | | this.playRecord.playAt = e.detail.currentTime |
| | | 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); |
| | |
| | | |
| | | .video-swiper { |
| | | width: 100%; |
| | | height: 100%; |
| | | height: calc(100% - 50px); |
| | | } |
| | | |
| | | .video-item { |
| | |
| | | } |
| | | .goods-link { |
| | | position: relative; |
| | | width: 450rpx; |
| | | margin: 20rpx 0; |
| | | padding: 12rpx; |
| | | background-color: rgba(255, 255, 255, 0.9); |
| | |
| | | } |
| | | |
| | | .goods-container { |
| | | width: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | |
| | | font-size: 28rpx; |
| | | color: #333; |
| | | font-weight: bold; |
| | | display: -webkit-box; |
| | | -webkit-line-clamp: 2; |
| | | -webkit-box-orient: vertical; |
| | | overflow: hidden; |
| | | margin-bottom: 8rpx; |
| | | width: 280rpx; /* 需要指定宽度 */ |
| | | overflow: hidden; |
| | | white-space: nowrap; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .price-section { |
| | |
| | | } |
| | | |
| | | .original-price { |
| | | font-size: 24rpx; |
| | | font-size: 28rpx; |
| | | color: #999; |
| | | text-decoration: line-through; |
| | | } |
| | |
| | | .buy-button { |
| | | background: linear-gradient(to right, #ff5a5f, #ff2e4d); |
| | | color: white; |
| | | padding: 10rpx 24rpx; |
| | | padding: 10rpx 28rpx; |
| | | border-radius: 20rpx; |
| | | font-size: 26rpx; |
| | | font-weight: bold; |
| | |
| | | |
| | | .comment-item { |
| | | display: flex; |
| | | padding: 10rpx 0; |
| | | flex-direction: column; |
| | | padding: 10rpx 0 20rpx 0; |
| | | } |
| | | |
| | | .avatar { |
| | | width: 80rpx; |
| | | height: 80rpx; |
| | | .comment-avatar { |
| | | width: 70rpx; |
| | | height: 70rpx; |
| | | border-radius: 50%; |
| | | margin-right: 20rpx; |
| | | margin-right: 10rpx; |
| | | } |
| | | .comment-reply-avatar { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | border-radius: 50%; |
| | | margin-right: 10rpx; |
| | | } |
| | | |
| | | .comment-content { |
| | |
| | | } |
| | | |
| | | .nickname { |
| | | font-size: 24rpx; |
| | | font-size: 28rpx; |
| | | color: #666; |
| | | display: block; |
| | | margin-bottom: 10rpx; |
| | | } |
| | | |
| | | .content { |
| | | font-size: 24rpx; |
| | | font-size: 28rpx; |
| | | color: #333; |
| | | display: block; |
| | | margin-bottom: 10rpx; |
| | | } |
| | | |
| | | .time { |
| | | font-size: 24rpx; |
| | | font-size: 28rpx; |
| | | color: #999; |
| | | } |
| | | |
| | |
| | | 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%; |
| | | } |
| | | .custom-share-btn { |
| | | font-size: unset; |
| | | background: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | line-height: normal; |
| | | border: none; |
| | | } |
| | | .custom-share-btn::after { |
| | | border: none; |
| | | } |
| | | </style> |