Selaa lähdekoodia

添加webrtc日志系统

t_finder 2 viikkoa sitten
vanhempi
commit
f8c07fea60

+ 2 - 1
pages/rtcEngine/components/CloudPhoneClipboard.vue

@@ -36,8 +36,9 @@ import Qs from 'qs';
 
 export default {
   props: {
+    // 指令ws通道
     doConnectDirectivesWs: {
-      type: Function,
+      type: WebSocket,
       default: () => {}
     }
   },

+ 3 - 1
pages/rtcEngine/components/RightPopup.vue

@@ -37,6 +37,8 @@
 </template>
 
 <script>
+import * as uni from '../../../static/static/js/uni.webview.1.5.2.js';
+
 export default {
   name: 'RightPopup',
   props: {
@@ -83,7 +85,7 @@ export default {
           '自动': { width: 720, height: 1280, fps: 30 },
           '高清': { width: 720, height: 1280, fps: 30 },
           '标清': { width: 540, height: 960, fps: 25 },
-          '流畅': { width: 450, height: 800, fps: 20 },
+          '流畅': { width: 360, height: 640, fps: 20 },
       })
     },
     // url中获取的参数, 父组件传递

+ 306 - 0
pages/rtcEngine/components/TimeBalance.vue

@@ -0,0 +1,306 @@
+<template>
+  <div>
+    <!-- 计时卡计时 -->
+    <div class="pfi flex timing" v-if="timingVisible">
+      <div>{{countdownTime}}</div>
+      <img class="wh22" :src="closePath" alt="" @click="handlecountdownTimeClose" />
+    </div>
+
+    <!-- 计费规则 -->
+    <van-dialog v-model="billingRulesVisible" :show-confirm-button="false" class="billing-rules-modal">
+      <div class="tc">
+        <img class="billing-rules-img" :src="countdownPath" />
+      </div>
+      <div class="tc billing-rules-title">
+        计费规则
+      </div>
+      <div class="tc">进入云机开始计时,点击退出并下机停止计时。点击退出云机仍处于计时状态。</div>
+      <div class="tc billing-rules-tips">云机时长剩余:<span class="billing-rules-time">{{countdownTime}}</span>
+      </div>
+      <div class="billing-rules-btn" @click="getRecommend">我知道了</div>
+    </van-dialog>
+
+    <!-- 应用推荐 -->
+    <van-dialog v-model="applyRecommendVisible" :show-confirm-button="false" class="apply-recommend-modal">
+        <div>
+            <div class="tc apply-recommend-title">应用推荐</div>
+            <div class="apply-recommend-list">
+              <div v-for="(item, index) in recommendList" :key="item.id" :class="['flex w100', {'mt-4': index !== 0}]">
+                <div class="left flex-align-c">
+                  <img :src="item.imageUrl" alt="">
+                  <div>
+                    <div class="title ellipsis">{{item.filename}}</div>
+                    <div class="download-num">有{{item.installNum}}个人下载</div>
+                  </div>
+                </div>
+                <div class="right flex-align-c" @click="downAndInstallApk(item)">
+                  <div>下载</div>
+                </div>
+              </div>
+            </div>
+            <div class="apply-recommend-btn tc" @click="getRecommend">换一批</div>
+        </div>
+        <img class="apply-recommend-close pab" :src="applyClosePath" alt="" @click="applyRecommendVisible = false" />
+    </van-dialog>
+  </div>
+</template>
+
+<script>
+/**
+ * 计时卡计时
+ */
+export default {
+  name: 'TimeBalance',
+  props: {
+    // url中获取的参数, 父组件传递
+    parametersData: {
+      type: Object,
+      default: () => ({})
+    },
+  },
+  data() {
+    return {
+      // 计时卡是否显示
+      timingVisible: false,
+      // 倒计时时间
+      countdownTime: 0,
+      // 倒计时定时器
+      countdownTimeInterval: null,
+      closePath: '../static/img/close.png',
+
+      // 计费规则是否显示
+      billingRulesVisible: false,
+      countdownPath: '../rtcEngine/img/countdown.png',
+
+      // 应用推荐列表
+      applyRecommendVisible: false,
+      // 应用推荐列表数据
+      recommendList: [],
+      applyClosePath: '../rtcEngine/img/close.png',
+    }
+  },
+  methods: {
+    // 获取云机剩余时长
+    async getResidueTime() {
+        clearInterval(this.countdownTimeInterval)
+        const { userCardType, isShowCountdown, isShowRule, userCardId } = this.parametersData;
+        if (![1, 2, 3].includes(+userCardType)) return;
+        const res = await this.$axios.get(`/resources/yearMember/getResidueTime?userCardId=${userCardId}`);
+        let time = res.data;
+        if (!res.status) {
+          this.countdownTime = this.residueTimeStamp(time);
+          await this.$axios.get(`/resources/yearMember/startTime?userCardId=${userCardId}`);
+          // 计时卡显示
+          if (+isShowCountdown) this.timingVisible = true;
+          // 计费规则显示
+          if (+isShowRule) this.billingRulesVisible = true;
+          // 倒计时退出云机定时器
+          this.countdownTimeInterval = setInterval(() => {
+              if (time <= 0) {
+                  clearInterval(this.countdownTimeInterval)
+                  // 退出云机
+                  this.$emit('downline');
+                  return
+              }
+              time--
+              this.countdownTime = residueTimeStamp(time)
+          }, 1000)
+        }
+    },
+    // 关闭倒计时弹窗
+    handlecountdownTimeClose() {
+      const { userCardId } = this.parametersData;
+      this.$axios.get(`/resources/yearMember/closeRemind?userCardId=${userCardId}`).then(res => {
+        if (!res.status) {
+          clearInterval(this.countdownTimeInterval);
+          this.timingVisible = false;
+          return
+        }
+        this.$toast(res.msg);
+      })
+    },
+    // 获取推荐列表
+    getRecommend() {
+      const { userCardId } = this.parametersData;
+      this.$axios.get(`/public/v1/market/get/recommend?userCardId=${userCardId}`).then(res => {
+        if (!res.status) {
+          this.billingRulesVisible = false;
+          this.recommendList = res.data;
+          this.recommendList.length && (this.applyRecommendVisible = true)
+        }
+      })
+    },
+    // 倒计时处理的时间
+    residueTimeStamp(value) {
+      let theTime = value;//秒
+      let middle = 0;//分
+      let hour = 0;//小时
+      if (theTime > 59) {
+          middle = parseInt(theTime / 60);
+          theTime = parseInt(theTime % 60);
+      }
+      if (middle > 59) {
+          hour = parseInt(middle / 60);
+          middle = parseInt(middle % 60);
+      }
+      theTime < 10 ? theTime = '0' + theTime : theTime = theTime
+      middle < 10 ? middle = '0' + middle : middle = middle
+      hour < 10 ? hour = '0' + hour : hour = hour
+      return hour + ':' + middle + ':' + theTime
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.pfi{position: fixed;}
+
+.tc {text-align: center;}
+
+.timing {
+  top: 2%;
+  right: 5%;
+  padding: 6px 5px 6px 9px;
+  background: rgba(0, 0, 0, 0.6);
+  border-radius: 30px;
+  font-size: 14px;
+  z-index: 1;
+  color: #FFF;
+
+  & > div {
+    vertical-align: middle;
+    height: 20px;
+    line-height: 20px;
+
+    &::after {
+      border-right: 1px solid #FFFFFF;
+      content: "";
+      width: 2px;
+      height: 45%;
+      display: inline-block;
+      vertical-align: middle;
+      margin-bottom: 3px;
+      padding: 0 3px;
+      opacity: 0.3;
+    }
+  }
+
+  & > img {
+    width: 20px;
+    height: 20px;
+  }
+}
+
+// 计费规则
+.billing-rules-modal {
+  text-align: center;
+  font-size: 14px;
+  color: #757580;
+  text-align: center;
+}
+::v-deep .billing-rules-modal .van-dialog__content {
+  padding: 0 28px 28px;
+}
+.billing-rules-modal .billing-rules-img {
+  width: 104px;
+  height: 140px;
+}
+.billing-rules-modal .billing-rules-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #363636;
+  margin-bottom: 5px;
+}
+.billing-rules-modal .billing-rules-tips {
+  margin-top: 5px;
+}
+.billing-rules-modal .billing-rules-time {
+  color: #00DB88;
+  font-size: 15px;
+}
+.billing-rules-modal .billing-rules-btn {
+  height: 50px;
+  line-height: 50px;
+  text-align: center;
+  margin-top: 24px;
+  background: linear-gradient(90deg, #00A3FF 0%, #04F79A 100%);
+  border-radius: 8px;
+  font-size: 12px;
+  color: #FFF;
+}
+
+// 应用推荐
+.apply-recommend-modal {
+  overflow: visible;
+}
+::v-deep .apply-recommend-modal .van-dialog__content {
+  padding: 10px 28px 28px;
+}
+.apply-recommend-modal .van-dialog__content > div {
+  height: 350px;
+  display: flex;
+  flex-direction: column;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #363636;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list {
+  flex: 1;
+  overflow-y: auto;
+  margin-top: 16px;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list::-webkit-scrollbar {
+  display: none;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list .left {
+  width: calc(100% - 68px);
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list .left > img {
+  width: 36px;
+  height: 36px;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list .left > div {
+  width: calc(100% - 36px);
+  padding-left: 10px;
+  box-sizing: border-box;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list .left .title {
+  width: 100%;
+  font-size: 16px;
+  color: #363636;
+  max-width: 100%;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list .left .download-num {
+  font-size: 12px;
+  color: #757580;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-list .right > div {
+  width: 68px;
+  height: 30px;
+  line-height: 30px;
+  text-align: center;
+  background: linear-gradient(90deg, #00A3FF 0%, #04F79A 100%);
+  border-radius: 8px;
+  font-size: 12px;
+  color: #FFF;
+}
+.apply-recommend-modal .van-dialog__content > div .apply-recommend-btn {
+  height: 50px;
+  line-height: 50px;
+  margin-top: 14px;
+  background: linear-gradient(225deg, #FF62F8 0%, #FF9D5C 100%);
+  border-radius: 8px;
+  font-size: 12px;
+  color: #FFF;
+}
+.apply-recommend-modal .van-dialog__content .apply-recommend-close {
+  bottom: -12%;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 38px;
+  height: 38px;
+  position: absolute;
+}
+</style>

+ 88 - 0
pages/rtcEngine/components/TimeoutNoOps.vue

@@ -0,0 +1,88 @@
+<template>
+  <van-dialog v-model="noOperationSetTimeoutTimeVisible" title="提示" show-cancel-button
+      :message="levitatedSphereContent" :confirm-button-text="confirmButtonText"
+      confirm-button-color="#3cc51f" cancel-button-text="继续操作" @cancel="noOperationSetTimeout('cancel')"
+      @confirm="noOperationSetTimeout('cancel'), exit(), noOperationSetTimeoutTimeVisible = false">
+  </van-dialog>
+</template>
+
+<script>
+/**
+ * 超时无操作
+ * 弹窗提示
+ * 弹窗倒计时无操作则自动退出
+ */
+export default {
+  name: 'TimeoutNoOps',
+  data() {
+    return {
+      // 后台查询,是否是启动不操作自动退出云机功能
+      isFiringNoOperationSetTimeout: false,
+      // 超过指定触碰时间的弹窗是否显示
+      noOperationSetTimeoutTimeVisible: false,
+      // 弹窗提示内容
+      levitatedSphereContent: '由于您长时间未操作,将自动断开视频链接(不影响云手机内应用运行)',
+      // 间隔无操作时间
+      noOperationSetTimeoutTime: 300000,
+      // 超过指定触碰时间的弹窗文案
+      confirmButtonText: '',
+      // 超过指定触碰时间的弹窗定时器
+      noOperationSetTimeoutTimeInterval: null,
+      // 弹窗后的倒计时定时器
+      noOperationSetIntervalTimeInterval: null,
+    }
+  },
+  methods: {
+    // 查询超过指定触碰时间是否提示关闭弹窗
+    pushflowPopup(){
+      this.$axios.get('/public/v5/pushflow/popup').then(res => {
+        if (res.success) {
+          this.isFiringNoOperationSetTimeout = res.data;
+          this.noOperationSetTimeout();
+        }
+      })
+    },
+    // 不触碰屏幕显示退出链接弹窗
+    noOperationSetTimeout(key) {
+      if (!this.isFiringNoOperationSetTimeout) return
+
+      clearTimeout(this.noOperationSetTimeoutTimeInterval);
+
+      if (key === 'cancel') {
+        clearInterval(this.noOperationSetIntervalTimeInterval);
+        // 关闭弹窗
+        this.noOperationSetTimeoutTimeVisible = false;
+        this.noOperationSetTimeout();
+        return;
+      }
+
+      // 超过指定触碰时间的弹窗定时器
+      this.noOperationSetTimeoutTimeInterval = setTimeout(() => {
+        let index = 9;
+        this.confirmButtonText = '退出(10秒)';
+        // 超过指定触碰时间的弹窗定时器
+        this.noOperationSetTimeoutTimeVisible = true;
+        // 弹窗后的倒计时定时器
+        this.noOperationSetIntervalTimeInterval = setInterval(() => {
+          this.confirmButtonText = `退出${index ? `(${index}秒)` : ''}`
+          index--;
+          if (index < 0) {
+            // 关闭弹窗, 并清空倒计时定时器
+            this.noOperationSetTimeout('cancel')
+            // 退出云手机
+            this.exit();
+          }
+        }, 1000);
+      }, this.noOperationSetTimeoutTime)
+    },
+    // 退出
+    exit() {
+      this.$emit('exit');
+    },
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 370 - 0
pages/rtcEngine/config/logReport.js

@@ -0,0 +1,370 @@
+/**
+ * @description 日志上报
+ */
+
+export default class LogReport {
+  URL_API = {
+    'http:': 'http://www.androidscloud.com:8002',
+    'https:': 'https://www.androidscloud.com:8003',
+  }
+  URL_SWITCH = '/api/public/v5/log/card/getLinkLogReportSwitch'; // 日志开关查询接口地址
+  URL_ADDRESS = '/api/public/v5/log/card/reportCardLinkLog';  // 日志上报地址
+  
+  $Request = null; // 用于发送 HTTP 请求的对象
+  version = ''; // 版本号 可通过$Request版本获取
+
+  // 上报参数
+  paramsJson = {
+    'timeConsuming': '', // 进入云机耗时字段 毫秒单位
+    'clientVersion': '', // 客户端版本号 // 通过$Request版本获取
+    'clientType': '', // 客户端类型 // 目前判断出wx或h5
+    'phoneModel': '', // 手机型号 // 无法获取
+    'phoneSystemVersion': '', // 手机系统版本号 // 无法获取
+    'phoneNetwork': '', // 手机网络类型
+    'videoType': '', // 视频类型
+    'imageQuality': '', // 推流质量 [高清 | 流畅] 
+    'userCardId': '', 
+    'cardInfoId': '', // 无法获取
+    'resourceId': '', // 资源ID
+    'transferServerIp': '', // 中转服务器IP
+    'linkStartTime': '', // 链接开始时间 格式 yyyy-MM-dd HH:mm:ss
+    'linkEndTime': '', // 链接结束时间 格式 yyyy-MM-dd HH:mm:ss
+    // 'linkTime': '', // 链接时间 格式 yyyy-MM-dd HH:mm:ss
+    'linkScene': 1, // 链接场景
+    'linkWay': 0, // 链接方式(0:其它原因 1:中转链接、2:打洞链接、3:安卓卡网络状态差、4:接口返回链接信息缺失)
+    'plugFowStatus': '', // 推流状态 int: 1 成功 2:失败
+    'logContent': '' // 日志内容
+  };
+  
+  reportSwitchStatus = false; // 上报日志开关状态
+  logs = []; // 用于存储日志的数组
+
+  // 客户端类型 枚举值
+  CLIENT_TYPE = Object.freeze({
+    'android': 1,
+    'ios': 2,
+    'pc': 3,
+    'miniprogram': 5, // wx小程序 在 uniapp获取类型为miniprogram
+    'h5': 7,
+  });
+
+  // 码率 枚举值
+  BITRATE = Object.freeze({
+    1800: '标清',
+    2200: '标清',
+    2800: '标清',
+    6000: '高清',
+    1243000: '超清',
+  });
+
+  // 链接场景 枚举值
+  LINK_SCENE = Object.freeze({
+    '直连': 1,
+    '重连': 2,
+    '通道断开重连': 3,
+    '信令连接失败重连': 4,
+    '鉴权失败重连': 5
+  });
+
+  // 视频类型 枚举值
+  VIDEO_TYPE = Object.freeze({
+    'h265': 1,
+    'h264': 2,
+  });
+
+  // 接口响应码 枚举值 对应 linkWay字段状态
+  RESPONSE_CODE = Object.freeze({
+    5200: 3, // RBD资源挂载中
+    5220: 3, // 云手机正在一键修复中
+    5203: 3, // 入使用排队9.9,年卡
+    5204: 3, // 9.9年卡连接异常,重新进入排队
+    5228: 3, // '卡的网络状态为差'
+    5229: 4, // '接口返回链接信息缺失'
+  });
+  
+  maxLogs = 1; // 存储最大日志数量
+  timer = null; // 定时器
+  timerTime = 6000; // // 日志上报间隔时间
+
+  timeStartTime = 0; // 链接开始时间
+
+	/**
+	 * 构造函数,初始化 LogReport 类的实例
+	 * @param {Object} $Request - 用于发送 HTTP 请求的对象
+	 * @param {Object} opt - 可选的配置对象
+	 */
+	constructor(opt) {
+
+    /**
+      // 扩展API是否准备好,如果没有则监听“plusready"事件
+      // if (window.plus) {
+      //   this.plusReady()
+      // } else {
+      //   document.addEventListener('plusready', this.plusReady, false) 
+      // }
+
+      document.addEventListener('plusready', ()=> {
+        console.log('plusReady')
+        console.log(plus)
+        console.log(plus.device.model)
+        console.log(plus.networkinfo.getCurrentType())
+        // plus.device.model
+        // plus.networkinfo.getCurrentType()
+        // plus.device.networkinfo.getCurrentType()
+      }, false);
+    */
+  
+		// 初始化 $Request 属性
+		this.$Request = opt.request;
+    this.version = this.$Request.defaults.headers.versionname;
+
+		this.init();
+	}
+
+  // 初始化
+  async init() {
+    const now = new Date();
+    // 开始连接的时间戳
+    this.timeStartTime = now.getTime();
+    // 设置本次日志上报时间
+    this.paramsJson.linkStartTime = this.formatDate(now);
+
+    // 调用 checkSwitch 方法,检查后端上报日志开关是否打开
+		await this.checkSwitch();
+
+    this.netWorkChange();
+
+    // 创建定时器
+    // await this.createTimer();
+  }
+
+  // 获取浏览器网络类型 isDestroy 是否移除监听
+  netWorkChange(isDestroy = false) {
+    if(!isDestroy && 'connection' in navigator) {
+      const connection = navigator.connection;
+      // 初始化时获取一次
+      this.paramsJson.phoneNetwork = connection.effectiveType;
+
+      // 监听网络类型变化
+      const updateResourceLoad = () => {
+        this.paramsJson.phoneNetwork = connection.effectiveType;
+      }
+    
+      // 监听网络类型变化
+      connection.addEventListener('change', updateResourceLoad);
+
+      if(isDestroy) {
+        // 移除事件监听
+        connection.removeEventListener('change', updateResourceLoad);
+      }
+    }
+  }
+
+	/**
+	 * 检查后端上报日志开关是否打开
+	*/
+	async checkSwitch() {
+		try{
+			const res = await this.getLinkLogReportSwitch();
+      if(res.status === 0 && res.success){
+        let { cardLinkRodeSwitch } = res.data;
+        this.reportSwitchStatus = cardLinkRodeSwitch;
+      }else{
+        console.error('检查日志上报开关失败')
+      }
+		}catch(e){
+			console.error(e)
+		}
+	}
+
+  /**
+   * 查询webRTC日志上报开关状态
+   * 无参
+   * @return {Request} 
+   * {
+   *  "status": 0,
+   *  "msg": "",
+   *  "data": {
+   *    "cardLinkRodeSwitch":true, // 是否上报记录
+   *    "cardLinkLogFileSwitch":true // 是否收集错误日志
+   *	}
+  * }
+  */ 
+  getLinkLogReportSwitch() {
+    return this.$Request.get(this.URL_API[location.protocol] + this.URL_SWITCH);
+  }
+
+  // 采集日志, 等待日志收集到一定数量或一定时间后再上报
+  collectLog(log) {
+    try {
+      // 日志开关关闭,直接返回
+      if(!this.reportSwitchStatus) {return;}
+  
+      // 组装本次日志上报参数
+      let logData = this.combinativeParam();
+
+      logData.logContent = log;
+  
+      this.logs.push(logData);
+  
+      // 超过最大日志数量,上报日志
+      if(this.logs.length >= this.maxLogs && this.reportSwitchStatus){
+        this.report();
+  
+        // 重置定时器
+        // this.createTimer();
+      }
+    } catch (error) {
+      console.error('collectLog内部错误');
+      console.log(error);
+      console.dir(error);
+      console.log('log', log);
+    }
+  }
+
+  // 设置日志上报参数
+  setParams(obj) {
+    try {
+      // 合并参数
+      this.paramsJson = {
+        ...this.paramsJson,
+        ...obj
+      }
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+  // 组装日志上报固定参数
+  combinativeParam() {
+    try {
+      let params = {
+        ...this.paramsJson,
+      };
+  
+      params.clientVersion = this.version;
+  
+      // 客户端类型 枚举值赋值
+      params.clientType = this.enumAssignment(this.CLIENT_TYPE, this.paramsJson.clientType);
+  
+      params.imageQuality = this.enumAssignment(this.BITRATE, this.paramsJson.imageQuality);
+  
+      params.linkScene = this.enumAssignment(this.LINK_SCENE, this.paramsJson.linkScene);
+
+      params.videoType = this.enumAssignment(this.VIDEO_TYPE, this.paramsJson.videoType);
+  
+      return params;
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+  // 日志记录上报 字符串日志上报
+  report() {
+    this.logs.forEach(() => {
+      this.$Request.post(this.URL_API[location.protocol] + this.URL_ADDRESS, { ...this.logs.shift() })
+      .then(res => {
+        console.log('日志上报成功', res);
+      });
+    })
+  }
+
+  // 生成or重置定时器
+  async createTimer() {  	
+    await this.clearTimer();
+
+    if(this.reportSwitchStatus){
+      this.timer = setInterval(() => {
+        if(this.logs.length > 0){
+          this.report();
+        }
+      }, this.timerTime);
+    }
+  }
+
+  // 清空定时器
+  async clearTimer() {
+    this.timer && clearInterval(this.timer);
+    return true;
+  } 
+
+  // 检查是否为枚举值
+  isCheckEnum(enumObj, velue) {
+    return Object.values(enumObj).includes(velue);
+  }
+
+  // 使用枚举值赋值
+  enumAssignment(enumObj, velue) {
+    try {
+      let str = '';
+      if(velue.toString() !== '') {
+        // 判断是否已为枚举值
+        str = this.isCheckEnum(enumObj, velue) ? velue : enumObj?.[velue];
+      }
+  
+      return str;
+    } catch (error) {
+      console.error('enumAssignment内部错误');
+      console.log(error);
+      console.dir(error);
+      console.log('enumObj', enumObj);
+      console.log('velue', velue);
+    }
+  }
+
+  // 关闭销毁
+  async destroy() {
+    // 清空日志
+    this.logs = [];
+    // 关闭日志上报开关
+    this.reportSwitchStatus = false;
+    // 清空定时器
+    await this.clearTimer();
+
+    // 移除事件监听
+    this.netWorkChange(true);
+  }
+
+  /**
+   * 格式化日期
+   * @param {Date} date - 要格式化的日期对象
+   * @param {string} format - 格式化字符串,使用以下占位符:
+   *   - 'Y' 表示年份(4 位数)
+   *   - 'm' 表示月份(2 位数)
+   *   - 'd' 表示日期(2 位数)
+   *   - 'H' 表示小时(24 小时制)
+   *   - 'h' 表示小时(12 小时制)
+   *   - 'i' 表示分钟
+   *   - 's' 表示秒数
+   *   - 'u' 表示毫秒
+   *   - 'a' 表示上午/下午标识(小写)
+   *   - 'A' 表示上午/下午标识(大写)
+   * @returns {string} 格式化后的日期字符串 
+  */
+  formatDate(date, format='Y-m-d H:i:s.u') {
+    // 判断输入是否为 Date 对象
+    if (!(date instanceof Date)) {
+        throw new TypeError('The first parameter must be a Date object.');
+    }
+
+    // 定义时间单位的获取方法
+    const formatObj = {
+        'Y': date.getFullYear(), // 年份
+        'm': date.getMonth() + 1, // 月份(+1 是因为 getMonth() 返回的月份是从 0 开始的)
+        'd': date.getDate(), // 日期
+        'H': date.getHours(), // 小时(24 小时制)
+        'h': date.getHours() % 12 || 12, // 小时(12 小时制)
+        'i': date.getMinutes(), // 分钟
+        's': date.getSeconds(), // 秒数
+        'u': date.getMilliseconds(), // 毫秒
+        'a': date.getHours() >= 12 ? 'pm' : 'am', // 上午/下午标识
+        'A': date.getHours() >= 12 ? 'PM' : 'AM' // 上午/下午标识(大写)
+    };
+
+    // 替换格式字符串中的占位符
+    return format.replace(/Y|m|d|H|h|i|s|u|a|A/g, (match) => {
+        return formatObj[match];
+    });
+}
+
+}

+ 150 - 46
pages/rtcEngine/rtc.vue

@@ -5,32 +5,43 @@
     
     <!-- 三menu键 -->
     <div id="foot-menu-wrap" :style="`height: ${footMenuWrapHeight}px`">
-      <div @click.stop="()=>{sendKey(187); noOperationSetTimeout}"><van-icon name="wap-nav" size="24px"/></div>
-      <div @click.stop="()=>{sendKey(3); noOperationSetTimeout}"><van-icon name="wap-home-o" size="24px"/></div>
-      <div @click.stop="()=>{sendKey(4); noOperationSetTimeout}"><van-icon name="arrow-left" size="24px"/></div>
+      <div @click.stop="sendKey(187)"><van-icon name="wap-nav" size="24px"/></div>
+      <div @click.stop="sendKey(3)"><van-icon name="wap-home-o" size="24px"/></div>
+      <div @click.stop="sendKey(4)"><van-icon name="arrow-left" size="24px"/></div>
     </div>
 
     <!-- 悬浮按钮 -->
     <FloatBtn :width="pageData.width" :height="pageData.height" @onClick="levitatedSphereVisible = true"/>
 
     <!-- 右侧popup -->
-    <RightPopup :engine="engine" :userCardId="this.parametersData.userCardId" :levitatedSphereVisible.sync="levitatedSphereVisible" @shearplate="shearplate" @exit="exit"/>
+    <RightPopup ref="rightPopupRef" :engine="engine" :userCardId="this.parametersData.userCardId" :levitatedSphereVisible.sync="levitatedSphereVisible" @shearplate="shearplate" @exit="exit"/>
 
     <!-- 输入并复制到粘贴板 -->
     <InputCopy ref="inputCopyRef" @openPasteboard="openPasteboard"/>
 
     <!-- 云机内部的粘贴板内容 -->
     <CloudPhoneClipboard ref="cloudPhoneClipboardRef" :doConnectDirectivesWs="doConnectDirectivesWs"/>
+
+    <!-- 超时无操作 -->
+    <TimeoutNoOps ref="timeoutNoOpsRef" />
+
+    <!-- 计时卡计时 | 计费规则 | 应用推荐 -->
+    <TimeBalance ref="timeBalanceRef" :parametersData="parametersData" @downline="$refs.rightPopupRef.downline()"/>
   </div>
 </template>
 
 <script>
 import meta from './config/meta.js';
 import request from './config/request.js';
+import logReport from './config/logReport.js';
+import * as uni from '../../static/static/js/uni.webview.1.5.2.js';
+
 import FloatBtn from './components/FloatBtn.vue';
 import RightPopup from './components/RightPopup.vue';
 import InputCopy from './components/InputCopy.vue';
 import CloudPhoneClipboard from './components/CloudPhoneClipboard.vue';
+import TimeoutNoOps from './components/TimeoutNoOps.vue';
+import TimeBalance from './components/TimeBalance.vue';
 
 /**
  * @description: 判断当前页面运行环境
@@ -70,6 +81,8 @@ export default {
     RightPopup,
     InputCopy,
     CloudPhoneClipboard,
+    TimeoutNoOps,
+    TimeBalance,
   },
   head() {
     return {
@@ -85,18 +98,21 @@ export default {
           onload: '$_script_loadHandler()', // 加载成功回调created生命周期中定义的方法
           onerror: '$_script_errHandler()', // 加载失败回调created生命周期中定义的方法
         },
-        {
-          // ./ 路径指向nuxt.config.js同级目录的static文件夹
-          src: './static/js/uni.webview.1.5.2.js', // uniapp webview 1.5.2文件
-          type: 'text/javascript',
-          async: true,
-          defer: false,
-        }
+        // {
+        //   // ./ 路径指向nuxt.config.js同级目录的static文件夹
+        //   src: './static/js/uni.webview.1.5.2.js', // uniapp webview 1.5.2文件
+        //   type: 'text/javascript',
+        //   async: true,
+        //   defer: false,
+        //   onload: '$_script_uni_loadHandler()', // 加载成功回调created生命周期中定义的方法
+        // }
       ]
     }
   },
   data() {
     return {
+      // 日志上报实例
+      logReportObj: null,
       // SDK加载状态
       sdkLoadStatus: 'loading', // sdk 加载状态 [loading|success|error]
       // 页面数据
@@ -162,6 +178,9 @@ export default {
       console.log('$_script_loadHandler: SDK加载成功');
       this.sdkLoadStatus = 'success';
 
+      // 初始化日志上报 TODO 添加日志系统
+      this.initLogReport();
+
       // 调用接口获取卡连接数据
       const cardData = await this.getConnectData(this.parametersData);
       
@@ -194,9 +213,6 @@ export default {
     this.destroyListener();
   },
   methods: {
-    inii(){
-      console.log('initi')
-    },
     // 初始化页面监听事件
     initListener() {
       // 禁止双击缩放
@@ -274,14 +290,37 @@ export default {
     },
     // 获取卡的信息
     async getConnectData(params) {
+      let userCardId = params.userCardId;
       try {
-        const res = await this.$axios.$post('/resources/user/cloud/connect', { userCardId: params.userCardId}, {
+        // 设置上报参数
+        this.logReportObj.setParams({userCardId});
+
+        let isWx = this.$userAgent.isWx;
+        let { isWeixin } = params;
+        let clientType = (+isWeixin || isWx) ? 'wx' : undefined;
+
+        // 设置上报参数
+        this.logReportObj.setParams({userCardId});
+        clientType && this.logReportObj.setParams({clientType});
+
+        const res = await this.$axios.$post('/resources/user/cloud/connect', { userCardId }, {
           headers: {
             merchantSign: params.merchantSign,
           },
         });
         if (!res.success) {
-          // TODO 提示错误信息 和 日志参数及上报
+          // 设置日志 推流状态为失败,和链接状态
+          this.logReportObj.setParams({plugFowStatus: 2, linkWay: this.logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: this.logReportObj.formatDate(new Date())});
+
+          // 日志上报
+          this.logReportObj.collectLog(
+            `接口获取数据失败:
+            url: /api/resources/user/cloud/connect
+            method: post
+            参数: ${JSON.stringify({ userCardId })}
+            响应: ${JSON.stringify(res)}`
+          );
+
           // res.status状态码枚举值 0: 正常
           const statusEnum = {
             // 5200:RBD资源挂载中
@@ -293,13 +332,25 @@ export default {
             5204: '云机异常,正在为你重新分配云机',
             5228: '卡的网络状态为差',
             5229: '接口返回链接信息缺失',
-          }
+          };
+          // NOTE 这里可设置重连机制, 重连次数上限6次, 每次重连间隔3秒, 暂不做重连
+
           // 提示错误信息
-          this.$toast(statusEnum[res.status] || '网络异常,请稍后重试')
+          this.$toast(statusEnum[res.status] || '网络异常,请稍后重试');
           return Promise.reject(new Error(statusEnum[res.status] || '网络异常,请稍后重试'));
         }
         return res.data;
       }catch (error) {
+        // 设置上报参数
+        this.logReportObj.setParams({linkWay: 4, plugFowStatus: 2, linkEndTime: this.logReportObj.formatDate(new Date())});
+        // 日志上报
+        this.logReportObj.collectLog(
+          `接口获取数据失败:
+          url: /api/resources/user/cloud/connect
+          method: post
+          参数: ${JSON.stringify({ userCardId })}
+          响应: ${JSON.stringify(error)}`
+        );
         console.log('error connectAxios:', error);
         return Promise.reject(error);
       }
@@ -307,15 +358,26 @@ export default {
     // 判断卡的连接方式
     async judgeConnectType(cardData) {
       try {
+        // 设置上报参数
+        this.logReportObj.setParams({videoType: data.videoCode.toLowerCase(), resourceId: data.resourceId});
+
         // 不支持webRTC跳转到指定的页面进行拉流
         if (!cardData.isWebrtc) {
-            // 跳转指定页面
-            location.replace(`${location.origin}/h5/webRtcYJ/WXtrialInterface.html${location.search}`)
-            return Promise.reject();
+          // 关闭日志上报 TODO 可能还需要关闭其他定时器等
+          this.logReportObj.destroy();
+
+          // 跳转指定页面
+          location.replace(`${location.origin}/h5/webRtcYJ/WXtrialInterface.html${location.search}`)
+          return Promise.reject();
         }
 
         // 是否支持webRTC
         if (!this.isSupportRtc) {
+          // 设置日志 推流状态为失败
+          this.logReportObj.setParams({plugFowStatus: 2, linkEndTime: this.logReportObj.formatDate(new Date())});
+          // 日志上报
+          this.logReportObj.collectLog(`${+this.parametersData.isWeixin ? '微信小程序' : ''}当前版本暂不支持使用`);
+
           // TODO 提示错误信息 和 日志参数及上报
           this.$dialog.alert({
             title: '提示',
@@ -338,13 +400,46 @@ export default {
           if (webrtcNetworkAnalysisReq !== null && webrtcNetworkAnalysisReq.success && webrtcNetworkAnalysisReq.data) {
               // 保存获取的连接地址到connect的请求的响应中, 方便后面使用
               cardData.webrtcNetwork = webrtcNetworkAnalysisReq.data;
+              // 设置上报参数
+              this.logReportObj.setParams({transferServerIp: webrtcNetworkAnalysisReq.data});
               return cardData;
           }else{
-            // TODO 提示错误信息 和 日志参数及上报
+            // 设置上报参数
+            this.logReportObj.setParams({linkWay: 4, plugFowStatus: 2, linkEndTime: this.logReportObj.formatDate(new Date())});
+            // 日志上报
+            this.logReportObj.collectLog(
+              `webRtc连接,获取中转地址失败:
+              url: ${cardData.webrtcNetworkAnalysis}
+              method: get
+              参数: 无
+              响应: ${JSON.stringify(webrtcNetworkAnalysisReq)}`
+            );
+
+            // 弹窗并退出
+            this.$dialog.alert({
+              title: '提示',
+              message: '访问失败,请稍后重试',
+              confirmButtonText: '确定',
+              confirmButtonColor: '#3cc51f',
+              beforeClose: (action, done) => {
+                done()
+                this.exit();
+              }
+            })
+            
             return Promise.reject(new Error('网络分析请求失败'));
           }
         }else{
-          // TODO 提示错误信息 和 日志参数及上报
+          // 设置上报参数
+          this.logReportObj.setParams({linkWay: 4, plugFowStatus: 2});
+          // 日志上报
+          this.logReportObj.collectLog(
+            `webRtc连接,获取请求中转地址为空:
+            url: /api/resources/user/cloud/connect
+            method: post
+            参数: ${JSON.stringify({ userCardId: this.parametersData.userCardId })}
+            响应: ${JSON.stringify(res)}`
+          );
           return Promise.reject(new Error('网络分析请求地址不存在'));
         }
       } catch (error) {
@@ -402,6 +497,10 @@ export default {
           callback: (event)=> {}
         };
 
+        // 设置日志参数 推流质量 
+        // TODO 明天继续,需要对现在的高清,标清,流畅重新做一个映射,日志枚举值也需要修改
+        this.logReportObj.setParams({imageQuality: 6000});
+
         // 获取SDK类
         const MediaSdk = window.rtc_sdk.MediaSdk;
         // 初始化 SDK
@@ -425,10 +524,13 @@ export default {
         console.log("webrtc连接成功====★★★★★", r);
         if (r.code === 1005) { // 1005: 拉流鉴权成功
           this.$toast.clear();
-          // 播放视频
-          setTimeout(() => {
-            // engine.mediaElement.node.play();
-          }, 50)
+          // 初始化业务指令通道
+          this.initControlChannel();
+          
+          // 查询超过指定触碰时间是否提示关闭弹窗
+          this.$refs.timeoutNoOpsRef.pushflowPopup();
+          // 获取云机剩余时长
+          this.$refs.timeBalanceRef.getResidueTime();
         }
       });
 
@@ -441,6 +543,12 @@ export default {
       engine.on('CONNECT_ERROR', (r) => {
         console.log("webrtc异常状态====★★★★★", r);
       });
+
+      // 显示区域大小发生改变 响应
+      engine.on('RECEIVE_RESOLUTION', (r) => {
+        // 分辨率大小发生改变,响应
+        console.log("分辨率大小发生改变,响应 => RECEIVE_RESOLUTION", r);
+      });
     },
     // 业务指令通道初始化
     initControlChannel() {
@@ -458,6 +566,7 @@ export default {
 
         // 链接成功
         this.doConnectDirectivesWs.onopen = (e) => {
+          // 设置定时器 每3秒发送一次心跳
           this.doConnectDirectivesIntervalerPing = setInterval(() => {
             if (this.doConnectDirectivesWs.readyState === 1) {
               this.doConnectDirectivesWs.send(JSON.stringify({ type: 'ping' }));
@@ -465,13 +574,9 @@ export default {
               clearInterval(this.doConnectDirectivesIntervalerPing);
             }
           }, 3000);
-          this.doConnectDirectivesWs.send(JSON.stringify({ type: 'getVsStatus' }))
-          this.doConnectDirectivesWs.send(JSON.stringify({ type: 'bitRate', data: { bitRate: 1243000 } }))
-          // // 设置日志参数 推流质量
-          // logReportObj.setParams({ imageQuality: 1243000 });
 
-          this.doConnectDirectivesWs.send(JSON.stringify({ type: 'InputMethod', data: { type: 2 } }))
-          this.doConnectDirectivesWs.send(JSON.stringify({ type: 'getPhoneSize' }))
+          // 输入法: 本地输入法 1 云机输入法 2 
+          this.doConnectDirectivesWs.send(JSON.stringify({ type: 'InputMethod', data: { type: 2 } }));
         }
 
         // 接受到的消息
@@ -509,8 +614,9 @@ export default {
     // 三键
     sendKey (keyCode) {
       try {
-        console.log('sendKey', keyCode);
         this.engine?.sendKey(keyCode);
+        // 重置超时无操作定时器
+        this.$refs.timeoutNoOpsRef?.noOperationSetTimeout();
       } catch (error) {
         console.log('sendKey error', error);
       }
@@ -524,18 +630,6 @@ export default {
     openPasteboard(text){
       this.$refs.cloudPhoneClipboardRef.init(text);
     },
-    // 超过指定触碰时间是否提示关闭链接
-    async pushflowPopup() {
-      try {
-        const res = await this.$axios.get('/public/v5/pushflow/popup');
-        if (res.success) {
-          this.isFiringNoOperationSetTimeout = res.data;
-          this.noOperationSetTimeout();
-        }
-      } catch (error) {
-        console.log(error);
-      }
-    },
     // 退出功能
     exit() {
       // 关闭日志上报
@@ -557,6 +651,16 @@ export default {
       }
       uni && uni.reLaunch({ url: '/pages/index/index' });
     },
+    // 初始化日志上报实例
+    initLogReport() {
+      // 初始化日志上报实例
+      this.logReportObj = new logReport({ request: this.$axios });
+
+      uni.getEnv((res) => {
+        // 设置上报参数
+        this.logReportObj.setParams({ clientType: Object.keys(res)[0] });
+      })
+    },
   }
 }
 </script>