|
|
@@ -685,6 +685,24 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <!-- 完课积分弹窗 -->
|
|
|
+ <u-popup :show="showCompletionPoints" round="20rpx" mode="center" bgColor="#ffffff" zIndex="10076">
|
|
|
+ <view class="completion-points-popup">
|
|
|
+ <view class="popup-icon">🎉</view>
|
|
|
+ <view class="popup-title">恭喜完成观看任务!</view>
|
|
|
+ <view class="popup-content">
|
|
|
+ <view class="content-item">观看时长达标</view>
|
|
|
+ <view class="content-item highlight">奖励积分:{{ completionPointsData?.pointsAwarded || 0 }} 分</view>
|
|
|
+ <view class="content-item">连续完课:第 {{ completionPointsData?.continuousDays || 1 }} 天</view>
|
|
|
+ </view>
|
|
|
+ <view class="popup-buttons">
|
|
|
+ <view class="popup-button secondary" @click="closeCompletionPoints">稍后再说</view>
|
|
|
+ <view class="popup-button primary" @click="receiveCompletionPoints">立即领取</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </u-popup>
|
|
|
+
|
|
|
<view v-if="liveItem.status == 1" class="ash_bg"></view>
|
|
|
</view>
|
|
|
</view>
|
|
|
@@ -785,6 +803,8 @@
|
|
|
totalTraffic: 0, // 总流量(字节)
|
|
|
bitrate: 800, // 录播默认码率 0.16Mbps
|
|
|
bitrateLive: 1600, // 直播默认码率 0.16Mbps
|
|
|
+ videoLoaded: false, // 视频是否加载成功
|
|
|
+ trafficStartTime: 0, // 流量计算开始时间
|
|
|
|
|
|
//定时器
|
|
|
trafficTimer: null,
|
|
|
@@ -815,6 +835,10 @@
|
|
|
|
|
|
stayTime: 0,
|
|
|
startTime: 0,
|
|
|
+ watchStartTime: 0, // 观看开始时间(毫秒)
|
|
|
+ totalWatchTime: 0, // 总观看时长(秒)
|
|
|
+ showCompletionPoints: false, // 是否显示完课积分弹窗
|
|
|
+ completionPointsData: null, // 完课积分数据
|
|
|
|
|
|
scrollTop: 0,
|
|
|
currentScrollTop: 0, // 当前实际滚动位置
|
|
|
@@ -1174,6 +1198,9 @@
|
|
|
this.startTime = 0;
|
|
|
this.totalTraffic = 0;
|
|
|
}
|
|
|
+ // 重置视频加载状态和流量计算开始时间
|
|
|
+ this.videoLoaded = false;
|
|
|
+ this.trafficStartTime = 0;
|
|
|
this.startTimer();
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
@@ -1333,8 +1360,14 @@
|
|
|
// 清除所有定时器(使用增强清理)
|
|
|
// this.clearAllTimersEnhanced();
|
|
|
// this.stopHeartBeat();
|
|
|
+ // 页面隐藏时,提交当前流量数据
|
|
|
+ if (this.videoLoaded && this.trafficStartTime && this.trafficTimer) {
|
|
|
+ const watchDuration = Math.floor((Date.now() - this.trafficStartTime) / 1000);
|
|
|
+ this.submitTraffic(watchDuration);
|
|
|
+ }
|
|
|
if (this.trafficTimer) {
|
|
|
clearInterval(this.trafficTimer);
|
|
|
+ this.trafficTimer = null;
|
|
|
}
|
|
|
|
|
|
// 页面隐藏时清理部分数据,减少内存占用
|
|
|
@@ -1351,6 +1384,14 @@
|
|
|
// 保存视频进度
|
|
|
this.saveVideoProgress();
|
|
|
|
|
|
+ // 用户退出时,再次请求一次10s流量接口
|
|
|
+ if (this.videoLoaded && this.trafficStartTime) {
|
|
|
+ const watchDuration = Math.floor((Date.now() - this.trafficStartTime) / 1000);
|
|
|
+ // 提交最后一次流量数据(至少10秒)
|
|
|
+ const finalDuration = Math.max(watchDuration, 10);
|
|
|
+ this.submitTraffic(finalDuration);
|
|
|
+ }
|
|
|
+
|
|
|
// 清理直播相关定时器
|
|
|
if (this.liveItem) {
|
|
|
this.pauseVideo();
|
|
|
@@ -2585,62 +2626,110 @@
|
|
|
// that.calculateTraffic(bitrate);
|
|
|
// }, 10000); // 每10秒计算一次
|
|
|
// },
|
|
|
- //直播计算流量
|
|
|
+ //直播计算流量 - 统一流量计算方法
|
|
|
startTrafficCalculation() {
|
|
|
+ // 检查必要数据
|
|
|
+ if (!this.liveItem || !this.liveItem.videoFileSize) {
|
|
|
+ console.warn('视频文件大小数据不完整,无法启动流量计算');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查视频总时长
|
|
|
+ const totalDuration = this.liveItem.videoDuration || this.liveItem.duration;
|
|
|
+ if (!totalDuration) {
|
|
|
+ console.warn('视频总时长数据不完整,无法启动流量计算');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除已有定时器
|
|
|
if (this.trafficTimer) {
|
|
|
clearInterval(this.trafficTimer);
|
|
|
this.trafficTimer = null;
|
|
|
}
|
|
|
|
|
|
- this.startTime = Date.now();
|
|
|
+ // 记录流量计算开始时间
|
|
|
+ this.trafficStartTime = Date.now();
|
|
|
var that = this;
|
|
|
|
|
|
- // 计算码率
|
|
|
- let bitrate = this.calculateBitrate();
|
|
|
+ // 立即提交一次10s流量(模拟10秒观看)
|
|
|
+ setTimeout(() => {
|
|
|
+ that.submitTraffic(10);
|
|
|
+ }, 100); // 延迟100ms确保数据已准备好
|
|
|
|
|
|
+ // 启动定时器,每10秒计算并提交一次流量
|
|
|
this.trafficTimer = setInterval(() => {
|
|
|
- that.calculateTraffic(bitrate);
|
|
|
+ that.calculateAndSubmitTraffic();
|
|
|
}, 10000); // 每10秒计算一次
|
|
|
},
|
|
|
- // 计算流量
|
|
|
- // calculateTraffic(bitrate) {
|
|
|
- // const currentTime = Date.now();
|
|
|
- // const duration = (currentTime - this.startTime) / 1000; // 持续时间(秒)
|
|
|
- // // 流量 = 码率 × 时间
|
|
|
- // // 码率单位: bps, 时间单位: 秒, 流量单位: 比特
|
|
|
- // const trafficBits = bitrate * duration;
|
|
|
- // // 转换为字节
|
|
|
- // this.totalTraffic = trafficBits / 8;
|
|
|
- // this.getLiveInternetTraffic();
|
|
|
- // },
|
|
|
- calculateBitrate() {
|
|
|
- // 如果接口返回了视频文件大小和时长,使用这些数据计算码率
|
|
|
- if (this.liveItem.videoFileSize && this.liveItem.videoDuration) {
|
|
|
- // 码率 = 文件大小(字节) / 时长(秒) × 8 (转换为bps) × 5
|
|
|
- const calculatedBitrate = (this.liveItem.videoFileSize / this.liveItem.videoDuration) * 8 * 5;
|
|
|
- console.log(
|
|
|
- `使用接口数据计算码率: ${calculatedBitrate} bps (文件大小: ${this.liveItem.videoFileSize} 字节, 时长: ${this.liveItem.videoDuration} 秒)`
|
|
|
- );
|
|
|
- return calculatedBitrate;
|
|
|
- } else {
|
|
|
- // 如果任一字段为空,使用默认码率 1500 bps
|
|
|
- console.log('接口数据不完整,使用默认码率: 1500 bps');
|
|
|
- return 1500;
|
|
|
+ // 计算并提交流量
|
|
|
+ calculateAndSubmitTraffic() {
|
|
|
+ if (!this.liveItem || !this.liveItem.videoFileSize) {
|
|
|
+ const totalDuration = this.liveItem.videoDuration || this.liveItem.duration;
|
|
|
+ if (!totalDuration) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ // 计算用户观看视频时长(秒)
|
|
|
+ const watchDuration = Math.floor((Date.now() - this.trafficStartTime) / 1000);
|
|
|
+
|
|
|
+ // 获取视频总时长(秒)
|
|
|
+ const totalDuration = this.liveItem.videoDuration || this.liveItem.duration || 0;
|
|
|
+
|
|
|
+ // 获取视频文件大小(字节)
|
|
|
+ const videoFileSize = this.liveItem.videoFileSize || 0;
|
|
|
+
|
|
|
+ if (totalDuration <= 0 || videoFileSize <= 0) {
|
|
|
+ console.warn('视频总时长或文件大小无效,无法计算流量');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 流量计算方法:用户观看视频时长/视频总时长*视频文件大小
|
|
|
+ const calculatedTraffic = (watchDuration / totalDuration) * videoFileSize;
|
|
|
+
|
|
|
+ // 更新总流量
|
|
|
+ this.totalTraffic = calculatedTraffic;
|
|
|
+
|
|
|
+ // 提交流量数据
|
|
|
+ this.submitTraffic(watchDuration);
|
|
|
},
|
|
|
- calculateTraffic(bitrate) {
|
|
|
- const currentTime = Date.now();
|
|
|
- const duration = (currentTime - this.startTime) / 1000; // 持续时间(秒)
|
|
|
+ // 提交流量数据到后端
|
|
|
+ submitTraffic(watchDuration) {
|
|
|
+ if (!this.liveId || !this.userInfo || !this.userInfo.userId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // 流量 = 码率 × 时间
|
|
|
- // 码率单位: bps, 时间单位: 秒, 流量单位: 比特
|
|
|
- const trafficBits = bitrate * duration;
|
|
|
+ // 计算当前观看时长对应的流量
|
|
|
+ const totalDuration = this.liveItem.videoDuration || this.liveItem.duration || 0;
|
|
|
+ const videoFileSize = this.liveItem.videoFileSize || 0;
|
|
|
+
|
|
|
+ if (totalDuration <= 0 || videoFileSize <= 0) {
|
|
|
+ console.warn('视频总时长或文件大小无效,无法提交流量', {
|
|
|
+ totalDuration,
|
|
|
+ videoFileSize,
|
|
|
+ videoDuration: this.liveItem.videoDuration,
|
|
|
+ duration: this.liveItem.duration
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // 转换为字节
|
|
|
- this.totalTraffic = trafficBits / 8;
|
|
|
+ // 流量 = 用户观看视频时长/视频总时长*视频文件大小
|
|
|
+ const traffic = (watchDuration / totalDuration) * videoFileSize;
|
|
|
+
|
|
|
+ const param = {
|
|
|
+ userId: this.userInfo.userId || '',
|
|
|
+ liveId: this.liveId || '',
|
|
|
+ uuId: dayjs().format('YYYYMMDD') + this.uuId,
|
|
|
+ internetTraffic: Math.round(traffic), // 四舍五入到整数
|
|
|
+ watchDuration: watchDuration, // 观看时长(秒)
|
|
|
+ totalDuration: totalDuration, // 视频总时长(秒)
|
|
|
+ videoFileSize: videoFileSize // 视频文件大小(字节)
|
|
|
+ };
|
|
|
|
|
|
- // 调用流量上报接口
|
|
|
- this.getLiveInternetTraffic();
|
|
|
+ console.log('提交流量数据:', param);
|
|
|
+ liveInternetTraffic(param).catch(err => {
|
|
|
+ console.error('流量数据提交失败:', err);
|
|
|
+ });
|
|
|
},
|
|
|
startTimer() {
|
|
|
this.startTime = Date.now();
|
|
|
@@ -2917,14 +3006,18 @@
|
|
|
}
|
|
|
this.lastHeartBeatTime = Date.now();
|
|
|
|
|
|
+ // 计算观看时长(秒)
|
|
|
+ const watchDuration = this.watchStartTime > 0 ? Math.floor((this.lastHeartBeatTime - this.watchStartTime) / 1000) : 0;
|
|
|
+
|
|
|
try {
|
|
|
const heartBeatMsg = JSON.stringify({
|
|
|
cmd: 'heartbeat',
|
|
|
msg: 'ping',
|
|
|
- userId: this.userInfo.userId || '',
|
|
|
- liveId: this.liveId,
|
|
|
timestamp: this.lastHeartBeatTime,
|
|
|
- networkType: this.networkType
|
|
|
+ networkType: this.networkType,
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: this.userInfo.userId || '',
|
|
|
+ data: String(watchDuration) // 观看时长(秒),字符串格式
|
|
|
});
|
|
|
|
|
|
this.socket.send({
|
|
|
@@ -3246,6 +3339,15 @@
|
|
|
// }
|
|
|
this.videoProgressKey = `videoProgress_${this.liveId}`;
|
|
|
this.setVideoProgress();
|
|
|
+
|
|
|
+ // 视频加载成功后,启动流量计算
|
|
|
+ if (!this.videoLoaded) {
|
|
|
+ this.videoLoaded = true;
|
|
|
+ // 延迟一下确保数据已加载
|
|
|
+ setTimeout(() => {
|
|
|
+ this.startTrafficCalculation();
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
},
|
|
|
setVideoProgress() {
|
|
|
// 只有录播和回放需要设置进度
|
|
|
@@ -3500,6 +3602,55 @@
|
|
|
this.havePrize = false;
|
|
|
uni.setStorageSync('havePrize', this.havePrize);
|
|
|
},
|
|
|
+ // 领取完课积分
|
|
|
+ async receiveCompletionPoints() {
|
|
|
+ if (!this.completionPointsData || !this.completionPointsData.id) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '数据异常,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uni.showLoading({
|
|
|
+ title: '领取中...'
|
|
|
+ });
|
|
|
+
|
|
|
+ const res = await this.$u.post('/api/live/completion/receive', {
|
|
|
+ recordId: this.completionPointsData.id,
|
|
|
+ userId: this.userInfo.userId
|
|
|
+ });
|
|
|
+
|
|
|
+ uni.hideLoading();
|
|
|
+
|
|
|
+ if (res.code === 0 || res.code === 200) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '积分领取成功!',
|
|
|
+ icon: 'success'
|
|
|
+ });
|
|
|
+ // 关闭弹窗
|
|
|
+ this.showCompletionPoints = false;
|
|
|
+ this.completionPointsData = null;
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: res.msg || '领取失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading();
|
|
|
+ console.error('领取积分失败:', error);
|
|
|
+ uni.showToast({
|
|
|
+ title: '网络异常,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 稍后领取
|
|
|
+ closeCompletionPoints() {
|
|
|
+ this.showCompletionPoints = false;
|
|
|
+ },
|
|
|
// 商品收藏
|
|
|
onGoodsCollect(item) {
|
|
|
if (!item || item.length === 0 || !item.goodsId) {
|
|
|
@@ -4237,10 +4388,10 @@
|
|
|
},
|
|
|
startHeartBeat() {
|
|
|
this.stopHeartBeat(); // 先停止现有心跳,防止重复
|
|
|
- // 使用自适应间隔循环发送心跳
|
|
|
+ // 使用自适应间隔循环发送心跳(改为30秒)
|
|
|
this.heartBeatTimer = setInterval(() => {
|
|
|
this.sendHeartBeat();
|
|
|
- }, this.adaptiveHeartBeatInterval);
|
|
|
+ }, 30000); // 30秒间隔
|
|
|
},
|
|
|
initSocket() {
|
|
|
// 检查是否正在连接中
|
|
|
@@ -4349,6 +4500,9 @@
|
|
|
this.isConnecting = false;
|
|
|
this.isSocketOpen = true;
|
|
|
|
|
|
+ // 记录观看开始时间
|
|
|
+ this.watchStartTime = Date.now();
|
|
|
+
|
|
|
// 计算连接延迟(性能监控)
|
|
|
if (this.connectionStartTime > 0) {
|
|
|
this.connectionLatency = Date.now() - this.connectionStartTime;
|
|
|
@@ -4747,6 +4901,16 @@
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
+ } else if (socketMessage.cmd == 'completionPoints') {
|
|
|
+ // 完课积分弹窗
|
|
|
+ try {
|
|
|
+ const pointsData = socketMessage.data ? JSON.parse(socketMessage.data) : {};
|
|
|
+ this.completionPointsData = pointsData;
|
|
|
+ this.showCompletionPoints = true;
|
|
|
+ console.log('收到完课积分弹窗:', pointsData);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('解析完课积分数据失败:', err);
|
|
|
+ }
|
|
|
}
|
|
|
} else {
|
|
|
uni.showToast({
|
|
|
@@ -6967,6 +7131,73 @@
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 完课积分弹窗
|
|
|
+ .completion-points-popup {
|
|
|
+ width: 600rpx;
|
|
|
+ padding: 60rpx 40rpx 40rpx;
|
|
|
+ text-align: center;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+
|
|
|
+ .popup-icon {
|
|
|
+ font-size: 80rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .popup-title {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333333;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .popup-content {
|
|
|
+ padding: 30rpx 20rpx;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+
|
|
|
+ .content-item {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666666;
|
|
|
+ margin: 8rpx 0;
|
|
|
+
|
|
|
+ &.highlight {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #ff5c03;
|
|
|
+ margin: 16rpx 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .popup-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 20rpx;
|
|
|
+ justify-content: space-between;
|
|
|
+
|
|
|
+ .popup-button {
|
|
|
+ flex: 1;
|
|
|
+ height: 80rpx;
|
|
|
+ line-height: 80rpx;
|
|
|
+ border-radius: 40rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ &.secondary {
|
|
|
+ background: #f5f7fa;
|
|
|
+ color: #666666;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.primary {
|
|
|
+ background: linear-gradient(270deg, #ff5c03 0%, #ffac64 100%);
|
|
|
+ color: #ffffff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
</style>
|