Explorar o código

基于对接sdk2.0,完成线网功能移植的基础上,开发v5.9.4.1功能需求

t_finder hai 1 semana
pai
achega
13e56aaf69

BIN=BIN
assets/image/rtc/icon-blow.png


BIN=BIN
assets/image/rtc/icon-clipboard.png


BIN=BIN
assets/image/rtc/icon-exit.png


BIN=BIN
assets/image/rtc/icon-hide.png


BIN=BIN
assets/image/rtc/icon-restart.png


BIN=BIN
assets/image/rtc/icon-screenshot.png


BIN=BIN
assets/image/rtc/icon-shake.png


BIN=BIN
assets/image/rtc/icon-show.png


BIN=BIN
assets/image/rtc/userMealUpgradeVO_icon.png


+ 155 - 0
components/Dropdown/Dropdown.vue

@@ -0,0 +1,155 @@
+<template>
+	<div class="dropdown pre">
+		<!-- 触发区 -->
+		<div @click.stop="visible = true" class="dropdown-trigger">
+			<slot></slot>
+		</div>
+		<!-- 遮罩 -->
+		<div class="pfi" @click="visible = false" v-if="visible" :style="[maskStyle]"></div>
+		<!-- 下拉菜单 -->
+		<template v-if="visible">
+			<div :class="['dropdown-menu pab', visible ? 'animation-fade' : '']" :style="[dropdownMenuStyle]">
+				<slot name="dropdown-menu">
+					<template v-if="list.length">
+						<div :class="['dropdown-menu-item tc', {line: index !== 0, active: isActvie && item[defaultprop.value] === value}]" :style="isActvie && item[defaultprop.value] === value ? activeStyle : {}" v-for="(item, index) in list" :key="item[defaultprop.value]" @click="change(item)">
+							<span class="menu-name">{{ item[defaultprop.label] }}</span>
+						</div>
+					</template>
+				</slot>
+			</div>
+		</template>
+		
+	</div>
+</template>
+
+<script>
+	export default {
+		name: 'dropdown',
+		props: {
+			// 下拉框数据
+			list: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+			// 替换回显以及key字段
+			prop: {
+				type: Object,
+				default: () => {
+					return {}
+				}
+			},
+			dropdownMenuStyle: {
+				type: Object,
+				default: () => {
+					return {}
+				}
+			},
+			value: {
+				type: [String, Number],
+				default: ''
+			},
+			// 下拉菜单是否激活选中项
+			isActvie: {
+				type: Boolean,
+				default: false
+			},
+			// 下拉菜单选中时的样式
+			activeStyle: {
+				type: [Object, Array],
+				default: () => ({})
+			}
+		},
+		data() {
+			return {
+				// 下拉框及遮罩是否显示
+				visible: false,
+				// 默认的prop
+				defaultprop: {
+					label: 'label',
+					icon: 'icon',
+					value: 'value'
+				},
+        // 屏幕高度
+			  windowHeight: 0,
+			}
+		},
+		mounted() {
+      // 获取屏幕高度
+      this.windowHeight = window.innerHeight;
+
+			// 如果有自定义的prop,那么就直接替换
+			if (Object.keys(this.prop).length) Object.assign(this.defaultprop, this.prop)
+		},
+		watch: {
+			visible(val) {
+        // 监听visible的变化
+				this.$emit('visible', val)
+			}
+		},
+		computed: {
+			maskStyle() {
+				return {
+					height: this.windowHeight + 'px',
+					width: '100vw'
+				}
+			}
+		},
+		methods: {
+			change(data) {
+				this.visible = false
+				this.$emit('input', data[this.defaultprop.value])
+				this.$emit('change', data[this.defaultprop.value])
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.pre {
+		position: relative;
+	}
+	.pab {
+    position: absolute;
+	}
+
+	.dropdown-menu {
+		min-width: 120px;
+		background-color: #2C2C2D;
+		box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
+		animation-duration: .2s !important;
+		z-index: 2501;
+		padding: 0 12px;
+		font-family: PingFangSC, PingFang SC;
+		font-weight: 400;
+		font-size: 14px;
+		color: #FFF;
+		line-height: 18px;
+		text-align: left;
+		font-style: normal;
+		border-radius: 8px;
+
+		.dropdown-menu-item {
+			display: flex;
+			align-items: center;
+			padding: 12px 0;
+
+			.menu-name {
+				flex: 1;
+			}
+		}
+	}
+
+	.pfi {
+		left: 0px;
+		top: 0px;
+		z-index: 2500;
+		background-color: rgba(0, 0, 0, .5);
+		position: fixed;
+	}
+
+	.active {
+		color: #3666F2;
+	}
+</style>

+ 14 - 0
pages/rtcEngine/components/CapsuleSelect.vue

@@ -0,0 +1,14 @@
+<template>
+  
+</template>
+
+<script>
+// 胶囊选择器
+export default {
+  name: 'CapsuleSelect',
+}
+</script>
+
+<style>
+
+</style>

+ 202 - 0
pages/rtcEngine/components/CloudGroupDropdown.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="group-dropdown pt-2 pb-2">
+    <!-- 设备分组下拉 -->
+    <Dropdown key="deviceList" v-model="activeDevice" :isActvie="true" @change="deviceChange" @visible="(visible)=>($emit('dropdownVisibleChange', visible))" :list="deviceGroupList" :dropdownMenuStyle="{ left: '0', top: '30px' }" :activeStyle="activeStyle">
+      <!-- 触发下拉 -->
+      <div class="menu-trigger"><div class="device-trigger-ex">全部设备</div><van-icon class="icon-arrow rotate-down" name="play" color="#fff" size="10" /></div>
+    </Dropdown>
+
+    <!-- 清晰度下拉 -->
+    <Dropdown key="definition" ref="definitionDropdown" @visible="(visible)=>($emit('dropdownVisibleChange', visible))" :dropdownMenuStyle="{ left: '0', top: '30px' }">
+      <!-- 触发下拉 -->
+      <div class="menu-trigger">
+        <div class="definition-trigger-ex">
+          <span>{{ activeDefinition }}</span>
+          <!-- 0-50ms绿色;51-200ms黄色;201-999ms红色;最多显示999ms;变色部分为延迟变色 -->
+          <span class="definition-val" :class="{success: latency < 51, warning: latency > 50 && latency < 201, danger: latency > 200}">{{ latency }}ms</span>
+        </div>
+        <van-icon class="icon-arrow rotate-down" name="play" color="#fff" size="10" />
+      </div>
+
+      <!-- 清晰度下拉 -->
+      <div slot="dropdown-menu">
+        <DropdownMenu :activeValue="activeDefinition" :list="definitionList" :isActvie="true"  :activeStyle="activeStyle" @change="selectFeatures" />
+      </div>
+    </Dropdown>
+  </div>
+</template>
+
+<script>
+/**
+ * 云机分组下拉框
+*/
+// 引入自定义下拉项目组件
+import DropdownMenu from './DropdownMenu.vue'
+
+export default {
+  name: 'CloudGroupDropdown',
+  components: {
+    DropdownMenu,
+  },
+  props: {
+    // 拉流渲染引擎实例
+    engine: {
+      type: Object,
+      default: () => ({})
+    },
+    // 网络延迟
+    latency: {
+      type: Number,
+      default: 0,
+    },
+    // 设备分组下拉菜单数据
+    deviceGroupList: {
+      type: Array,
+      default: () => ([{
+        label: "批量操作批量操作批量操作",
+        icon: 'batchOperation_icon',
+        value: 'batchOperation'
+      }, {
+        label: "显示模式",
+        icon: 'showMode_icon',
+        value: 'showMode'
+      }])
+    },
+    // 清晰度默认值
+    definitionValue: {
+      type: String,
+      default: ()=> localStorage.getItem('definitionValue') ?? '自动',
+    },
+    // 清晰度选项列表
+    definitionList: {
+      type: Array,
+      default: () => [{
+        label: '自动',
+        value: '720P',
+        bitrate: 2800, // 码率
+        fps: 30, // 帧率
+        width: 720, // 宽度
+        height: 1280, // 高度
+      }, {
+        label: '高清',
+        value: '720P',
+        bitrate: 2800, // 码率
+        fps: 30, // 帧率
+        width: 720, // 宽度
+        height: 1280, // 高度
+      }, {
+        label: '标清',
+        value: '540P',
+        bitrate: 1500, // 码率
+        fps: 25, // 帧率
+        width: 540, // 宽度
+        height: 960, // 高度
+      }, {
+        label: '流畅',
+        value: '360P',
+        bitrate: 1000, // 码率
+        fps: 20, // 帧率
+        width: 360, // 宽度
+        height: 640, // 高度
+      }]
+    },
+  },
+  data() {
+    return {
+      // 高亮项样式
+      activeStyle: {
+        color: '#FEAE4D',
+      },
+      activeDevice: 'batchOperation', // 当前选中的设备分组
+      activeDefinition: this.definitionValue, // 当前选中的清晰度
+    }
+  },
+  methods: {
+    // 设备分组下拉框变化回调
+    deviceChange(val) {
+      console.log('设备分组变化:', val);
+      this.activeDevice = val;
+    },
+    // 选择清晰度
+    selectFeatures(item) {
+      // 更新清晰度值
+      this.activeDefinition = item.label;
+      localStorage.setItem('definitionValue', item.label);
+      // 设置码率
+      this.setBitrate(item);
+      // 关闭清晰度下拉框
+      this.$refs.definitionDropdown.visible = false;
+    },
+    // 设置码率
+    setBitrate({width, height, fps, bitrate, label}) {
+      // 设置码率和是否允许自动
+      this.engine?.setCustomBitrate(bitrate, label === '自动');
+  
+      // 设置编码器分辨率和帧率
+      this.engine?.setEncoderSize({
+        width,
+        height,
+        fps,
+      });
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.group-dropdown{
+  display: flex;
+  justify-content: space-around;
+}
+
+.menu-trigger{
+  background-color: #5C5C5C;
+  box-shadow: 0px 0px 30px 0px #1E2022;
+  border-radius: 14px;
+  padding: 6px 15px;
+  display:  inline-block;
+  font-weight: 400;
+  font-size: 12px;
+  color: #F9F9F9;
+
+  // 设备分组触发下拉
+  .device-trigger-ex{
+    display: inline-block;
+    text-align: left;
+    width: 88px;
+  }
+
+  // 清晰度触发下拉
+  .definition-trigger-ex{
+    display: inline-flex;
+    justify-content: space-between;
+    width: 94px;
+
+    // 0-50ms绿色;51-200ms黄色;201-999ms红色;最多显示999ms;变色部分为延迟变色
+    .definition-val{
+      &.danger{
+        color: #FF2627;
+      }
+      &.warning{
+        color: #FF9800;
+      }
+      &.success{
+        color: #44E2B1;
+      }
+    }
+  }
+
+
+  .icon-arrow{
+    transition: transform 0.2s ease;
+
+    &.rotate-down{
+      transform: rotate(90deg);
+    }
+
+    &.rotate-up{
+      transform: rotate(0270deg);
+    }
+  }
+}
+</style>

+ 79 - 0
pages/rtcEngine/components/CloudList.vue

@@ -0,0 +1,79 @@
+<template>
+  <!-- 云机列表 -->
+  <div class="cloud-list-wrap" >
+    <van-list :style="listStyle" :finished="false">
+      <div v-for="item in list" :key="item" class="cloud-list-item-wrap flex" :class="{'cloud-list-item-active': activeId === item}" @click="activeId = item">
+        <!-- 云机套餐头像 -->
+        <div class="cloud-id-avatar-wrap items-center pr-2">
+          <van-image
+            width="24"
+            height="24"
+            src="https://img01.yzcdn.cn/vant/cat.jpeg"
+          />
+        </div>
+        <!-- 云机名称和ID -->
+        <div class="cloud-id-desc-wrap">
+          <div class="cloud-id-name mb-1">云手机名称最多显示12位</div>
+          <div class="cloud-id-detail-wrap">ID 333434343434</div>
+        </div>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CloudList',
+  props: {
+    // 云机列表高度
+    height: {
+      type: String,
+      default: '50px'
+    }
+  },
+  data() {
+    return {
+      activeId: '云机1',
+      list: [
+        '云机1',
+        '云机2',
+        '云机3',
+        '云机4',
+        '云机5',
+        '云机6',
+        '云机7',
+        '云机8',
+        '云机9',
+        '云机10'
+      ]
+    }
+  },
+  computed: {
+    // 云机列表高度
+    listStyle() {
+      return {height: this.height, overflowY: 'auto'}
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.cloud-list-item-wrap{
+  padding: 8px 16px;
+  border: 1px solid transparent;
+
+  &.cloud-list-item-active{
+    background-color: rgba(254,174,77,0.1);
+    border: 1px solid #FEAE4D;
+  }
+}
+.cloud-id-avatar-wrap{
+  display: flex;
+}
+.cloud-id-desc-wrap{
+  text-align: left;
+  flex: 1;
+  color: #F9F9F9;
+  font-size: 10px;
+}
+</style>

+ 64 - 0
pages/rtcEngine/components/CloudMainPanel.vue

@@ -0,0 +1,64 @@
+<template>
+  <!-- 头部云机id等信息 -->
+  <div class="cloud-id-info-wrap">
+    <!-- 云机套餐头像 -->
+    <div class="cloud-id-avatar-wrap">
+      <van-image
+        width="24"
+        height="24"
+        src="https://img01.yzcdn.cn/vant/cat.jpeg"
+      />
+    </div>
+    <!-- 云机名称和ID -->
+    <div class="cloud-id-desc-wrap">
+      <div class="cloud-id-name mb-1">云手机名称最多显示12位</div>
+      <div class="cloud-id-detail-wrap">
+        <div class="cloud-id">ID 333434343434</div>
+        <div class="cloud-id-active-group">当前分组:最多显示6个字符</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+/**
+ * 头部云机id等信息
+*/
+export default {
+  name: 'CloudMainPanel',
+}
+</script>
+
+<style lang="scss" scoped>
+// 文字高亮颜色
+$active-color: #FEAE4D;
+
+.cloud-id-info-wrap{
+  display: flex;
+  color: #f9f9f9;
+  font-size: 10px;
+  line-height: 10px;
+  background-color: #565656;
+  padding: 12px 16px;
+
+  .cloud-id-avatar-wrap{
+    padding-right: 8px;
+  }
+  .cloud-id-desc-wrap{
+    flex: 1;
+
+    .cloud-id-name{
+      text-align: left;
+    }
+    .cloud-id-detail-wrap{
+      display: flex;
+      flex-wrap: nowrap;
+      justify-content: space-between;
+
+      .cloud-id{
+        color: $active-color;
+      }
+    }
+  }
+}
+</style>

+ 72 - 0
pages/rtcEngine/components/DropdownMenu.vue

@@ -0,0 +1,72 @@
+vue
+<template>
+  <div class="dropdown-list-wrap">
+    <div class="list-ite-wrap" :class="[{active: isActvie && item[defaultprop.label] === activeValue}]" :style="isActvie && item[defaultprop.label] === activeValue ? activeStyle : {}" v-for="(item, index) in list" :key="index" @click="selectHandle(item)">
+      <span class="list-ite-label">{{ item.label }}</span>
+      <span class="list-ite-value">{{ item.value }}</span>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DropdownMenu',
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    },
+    // 替换回显以及key字段
+    prop: {
+      type: Object,
+      default: () => ({})
+    },
+    // 选中的值
+    activeValue: '',
+    // 下拉菜单是否激活选中项
+    isActvie: {
+      type: Boolean,
+      default: false
+    },
+    // 下拉菜单选中时的样式
+    activeStyle: {
+      type: [Object, Array],
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      // 默认的prop
+      defaultprop: {
+        label: 'label',
+        value: 'value'
+      },
+    };
+  },
+  mounted() {
+    // 如果有自定义的prop,那么就直接替换
+    if (Object.keys(this.prop).length) Object.assign(this.defaultprop, this.prop)
+  },
+  methods: {
+    selectHandle(item) {
+      // 触发选择事件
+      this.$emit('change', item);
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dropdown-list-wrap{
+  .list-ite-wrap{
+    display: flex;
+    padding: 12px 0;
+    width: 110px;
+    justify-content: space-between;
+  }
+}
+
+.active {
+  color: #3666F2;
+}
+</style>

+ 68 - 10
pages/rtcEngine/components/FloatBtn.vue

@@ -12,7 +12,18 @@
     >
     <div class="round-outside">
       <div class="round-small">
-        <div class="status"></div>
+        <div class="status"
+          :class="{
+            success: latency < 51,
+            warning: latency > 50 && latency < 201,
+            danger: latency > 200
+          }"
+        >
+          <!-- 信号 0-50ms绿色;51-200ms黄色;201-999ms红色;最多显示999ms;变色部分为延迟变色 -->
+          <span class="short" />
+          <span class="middle" />
+          <span class="high" />
+        </div>
       </div>
     </div>
   </div>
@@ -31,7 +42,12 @@ export default {
     height: {
       type: Number,
       default: 0
-    }
+    },
+    // 网络延迟
+    latency: {
+      type: Number,
+      default: 0,
+    },
   },
   data() {
     return {
@@ -110,25 +126,67 @@ export default {
   z-index: 1;
 
   .round-outside,
-  .round-small,
-  .status{
+  .round-small{
     border-radius: 50%;
   }
 
   .round-outside{
     padding: 4px;
-    background-color: #4D4D4D;
+    background-color: #A5A5A5;
     // 设置透明度
     opacity: 0.8;
     .round-small{
       padding: 4px;
-      background-color: #A5A5A5;
+      width: 28px;
+      height: 28px;
+      background-color: #EDEDED;
       opacity: 0.8;
+      
       .status{
-        width: 20px;
-        height: 20px;
-        background-color: #EDEDED;
-        opacity: 0.8;
+        display: flex;
+        justify-content: space-around;
+        align-items: flex-end;
+        width: 100%;
+        height: 100%;
+        padding-bottom: 4px;
+        .short,
+        .middle,
+        .high{
+          width: 4px;
+          background-color: #A5A5A5;
+          margin: 0 1px;
+        }
+
+      &.success{
+        .short,
+        .middle,
+        .high{
+            background-color: #44E2B1;
+          }
+      }
+      &.warning{
+        .short,
+        .middle,
+        .high{
+          background-color: #FF9800;
+        }
+      }
+      &.danger{
+        .short,
+        .middle,
+        .high{
+          background-color: #FF2627;
+        }
+      }
+      .short{
+         height: 40%;
+       }
+      .middle{
+         height: 60%;
+       }
+      .high{
+         height: 80%;
+       }
       }
     }
   }

+ 137 - 0
pages/rtcEngine/components/FunctionMenu.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="menu-btn-wrap">
+    <van-row>
+      <!-- 菜单常驻按钮 最多只显示4个 -->
+      <div class="menu-btn-fixed">
+        <van-col span="6" v-for="(item, index) in funcList.filter((_, i)=>(i < 4))" :key="index">
+          <!-- 状态切换按钮 -->
+          <template v-if="item.switch">
+            <div class="menu-item" @click="funcHandle(item)">
+              <!-- 按钮块 -->
+              <div class="icon-bg">
+                <!-- 图标 -->
+                <img width="36" height="36" :src="require(`~/assets/image/rtc/${item.switch[+item.status].icon}.png`)" />
+              </div>
+              <div class="menu-name">{{ item.switch[+item.status].label }}</div>
+            </div>
+          </template>
+
+          <!-- 单一状态按钮 -->
+          <template v-else>
+            <div class="menu-item" @click="funcHandle(item)">
+              <!-- 按钮块 -->
+              <div class="icon-bg">
+                <!-- 图标 -->
+                <img width="36" height="36" :src="require(`~/assets/image/rtc/${item.icon}.png`)" />
+              </div>
+              <div class="menu-name">{{ item.label }}</div>
+            </div>
+          </template>
+        </van-col>
+      </div>
+
+      <!-- 菜单抽屉内按钮 -->
+      <div class="menu-drawer-warp" v-show="functionMenuVisible">
+        <!-- 项 -->
+        <van-col span="6" v-for="(item, index) in funcList.filter((_, i)=>(i > 3))" :key="index">
+          <div class="menu-item" @click="funcHandle(item)">
+            <div class="icon-bg">
+              <img width="36" height="36" :src="require(`~/assets/image/rtc/${item.icon}.png`)" />
+            </div>
+            <div class="menu-name">{{ item.label }}</div>
+          </div>
+        </van-col>
+      </div>
+    </van-row>
+
+    <div class="triger-menu-wrap">
+      <!-- 展开/收起按钮 -->
+      <div class="triger-btn" @click="hideOrShow">
+        {{ functionMenuVisible ? '收起' : '展开' }}
+        <!-- 图标 -->
+        <van-icon :name="functionMenuVisible ? 'arrow-up' : 'arrow-down'" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+/**
+ * 菜单按钮
+*/
+export default {
+  name: 'FunctionMenu',
+  data() {
+    return {
+      // 菜单是否展开
+      functionMenuVisible: false,
+      // 功能列表
+      funcList: [
+        {
+          switch: [{label: '显示虚拟键', code: 'showKey', icon: 'icon-show'}, {label: '隐藏虚拟键', code: 'hideKey', icon: 'icon-hide'}],
+          status: true,
+        },
+        {label: '剪切板', code: 'shearplate', icon: 'icon-clipboard'},
+        // {label: '音量 +', code: 'volumeUp', icon: 'icon-clipboard'},
+        // {label: '音量 -', code: 'volumeDown', icon: 'icon-clipboard'},
+        {label: '摇一摇', code: 'shake', icon: 'icon-shake'},
+        {label: '吹一吹', code: 'blow', icon: 'icon-blow'},
+        {label: '截图', code: 'screenshot', icon: 'icon-screenshot'},
+        // {label: '重启', code: 'estart', icon: 'icon-restart'},
+      ]
+    }
+  },
+  methods: {
+    hideOrShow(){
+      this.functionMenuVisible = !this.functionMenuVisible;
+      // 延迟触发避免父组件获取不到最新的值
+      this.$nextTick(() => {
+        // 触发父组件事件
+        this.$emit('functionMenuVisible', this.functionMenuVisible);
+      })
+    },
+    funcHandle(item){
+      if(item.switch){
+        // 触发父组件事件
+        this.$emit('funcHandle', item.switch[+item.status]);
+
+        this.$nextTick(() => {
+          // 切换状态
+          item.status = !item.status;
+        });
+        return;
+      }
+      
+      // 触发父组件事件
+      this.$emit('funcHandle', item);
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.menu-btn-wrap{
+  color: #fff;
+  font-size: 12px;
+  padding: 6px 0;
+  .menu-item{
+    padding: 6px 0;
+    .icon-bg{
+      width: 36px;
+      height: 36px;
+      border-radius: 50%;
+      overflow: hidden;
+    }
+  }
+
+  .triger-menu-wrap{
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .triger-btn{
+      color: #979797;
+      font-size: 10px;
+    }
+  }
+}
+</style>

+ 212 - 0
pages/rtcEngine/components/LeftMenuPopup.vue

@@ -0,0 +1,212 @@
+<template>
+  <!-- 左侧弹窗 -->
+  <div class="left-menu-popup">
+    <van-popup :value="levitatedSphereVisible" class="levitated-sphere-drawer" :class="{'overflow-y-initial': popupMaskOverflowY}" @input="showChange" position="left"
+     overlay-class="levitated-sphere-overlay" :overlay-style="{'background-color': 'transparent !important'}" :style="{ height: '100%', 'background-color': '#2C2C2D' }">
+      <div class="menu-wrap">
+        <!-- 头部云机id等信息 -->
+        <CloudMainPanel id="pupop-header" />
+
+        <!-- 功能区域 -->
+        <FunctionMenu id="function-menu" @functionMenuVisible="scrollHeight" @funcHandle="funcHandle"/>
+
+        <!-- 包一层是为了获取区域的height计算云机列表滚动高度 -->
+        <div id="select-wrap">
+          <!-- 下拉选项区 -->
+          <CloudGroupDropdown v-bind="$attrs" v-on="$listeners" :engine="engine" @dropdownVisibleChange="(val)=>(popupMaskOverflowY = val)"/>
+        </div>
+        
+        <!-- 云机列表 -->
+        <CloudList id="cloud-list" :height="`${cloudListScrollHeight}px`"/>
+
+        <!-- 退出 -->
+        <div id="exit-wrap">
+          <van-button class="exit-btn" type="primary" size="small" color="#3370FF">退出并下机</van-button>
+        </div>
+      </div>
+    </van-popup>
+  </div>
+</template>
+
+<script>
+import * as uni from '../../../static/static/js/uni.webview.1.5.2.js';
+
+import CloudMainPanel from './CloudMainPanel.vue';
+import FunctionMenu from './FunctionMenu.vue';
+import CloudList from './CloudList.vue';
+import CapsuleSelect from './CapsuleSelect.vue';
+import CloudGroupDropdown from './CloudGroupDropdown.vue';
+
+export default {
+  name: 'LeftMenuPopup',
+  props: {
+    // 拉流渲染引擎实例
+    engine: {
+      type: Object,
+      default: () => ({})
+    },
+    userCardId: {
+      type: String,
+      default: ''
+    },
+    // popup是否显示
+    levitatedSphereVisible: {
+      type: Boolean,
+      default: false,
+    },
+    // 清晰度默认值
+    definitionValue: {
+      type: String,
+      default: ()=> localStorage.getItem('definitionValue') ?? '自动',
+    },
+    // 清晰度列表
+    definitionList: {
+      type: Array,
+      default: () => [{
+            name: '自动',
+            value: 2800
+        }, {
+            name: '高清',
+            value: 2800
+        }, {
+            name: '标清',
+            value: 1500,
+        }, {
+            name: '流畅',
+            value: 1000,
+        }]
+    },
+    // 清晰度对应分辨率和帧率列表
+    resolutionRatioList: {
+      type: Object,
+      default: () => ({
+          '自动': { width: 720, height: 1280, fps: 30 },
+          '高清': { width: 720, height: 1280, fps: 30 },
+          '标清': { width: 540, height: 960, fps: 25 },
+          '流畅': { width: 360, height: 640, fps: 20 },
+      })
+    },
+    // url中获取的参数, 父组件传递
+    parametersData: {
+      type: Object,
+      default: () => ({})
+    },
+  },
+  components: {
+    CloudMainPanel,
+    FunctionMenu,
+    CloudList,
+    CapsuleSelect,
+    CloudGroupDropdown,
+  },
+  data() {
+    return {
+      // 当前清晰度
+      actDefinition: this.definitionValue,
+      // 云机列表高度
+      cloudListScrollHeight: 0,
+
+      /**
+       * popup遮罩的overflow-y属性是否启用
+       * @type {Boolean}
+       * @description 解决下拉框的遮罩会裁剪的问题, 当下拉框弹出时,需设置此属性,否则下拉框的遮罩会裁剪
+       * 下拉框弹出时,点击任意地方可以关闭下拉框, 但是遮罩的overflow-y属性会被设置为hidden, 导致下拉框的遮罩会裁剪
+       * 解决方法: 在下拉框弹出时,设置popupMaskOverflowY为true, 关闭下拉框时,设置popupMaskOverflowY为false
+       * */
+      popupMaskOverflowY: true,
+    }
+  },
+  computed: {
+    
+  },
+  watch: {
+    levitatedSphereVisible(val) {
+      // 弹窗显示时,获取云机列表的高度
+      if (val) {
+        this.$nextTick(() => {
+          this.scrollHeight();
+        });
+      }
+    },
+  },
+  methods: {
+    // 获取并设置云机列表的滚动高度
+    // 1. 显示弹窗时,获取云机列表的高度
+    // 2. 功能展开及收起时,获取云机列表的高度
+    scrollHeight() {
+      try {
+        // 获取popup总高度
+        let popupHheight = document.getElementsByClassName('levitated-sphere-drawer')[0]?.offsetHeight ?? 0;
+        // 获取顶部云机id等信息高度
+        let headerHeight = document.getElementById('pupop-header')?.offsetHeight ?? 0;
+        // 获取功能区域高度
+        let functionMenuHeight = document.getElementById('function-menu').offsetHeight ?? 0;
+        // 获取下拉选项区高度
+        let selectWrapHeight = document.getElementById('select-wrap').offsetHeight ?? 0;
+        // 获取退出按钮高度
+        let exitWrapHeight = document.getElementById('exit-wrap').offsetHeight ?? 0;
+
+        // 计算出云机列表的高度
+        let scrollHeight = popupHheight - headerHeight - functionMenuHeight - selectWrapHeight - exitWrapHeight;
+
+        // 如果计算出的云机列表的高度小于100,那么就设置为100, 防止云机列表高度为0
+        scrollHeight < 100 ? this.cloudListScrollHeight = 100 : this.cloudListScrollHeight = scrollHeight;
+      } catch (error) {
+        console.error('Error calculating scroll height:', error);
+        // 如果计算失败,则设置为0
+        this.cloudListScrollHeight = 100;
+      }
+    },
+    // 处理功能菜单的事件
+    funcHandle(val){
+      // 关闭popup
+      this.$emit('update:levitatedSphereVisible', false);
+      // 触发父组件事件
+      this.$emit('funcHandle', val);
+    },
+    /**
+		 * 根据卡套餐获取对应图标
+		 * @method imgFun 
+		 * @param {String} type--1 套餐类型 VIP|SVIP|STARRYSKY
+		 * @param {String} androidVersion--2 安卓系统版本
+		 * @param {String} key='previewUrl'--3 ['previewUrl' | 'phonePreviewUrl'] 判断是预览图还是套餐图标用的
+		 * @returns {String} 图片路径
+		 * 
+		*/
+		imgFun(type, androidVersion = '', key = 'previewUrl') {
+			let obj = this.mealTypeObj[type + androidVersion]
+			// obj[key]的值是default或defaultPhonePreviewUrl时候,就是后端没有返回图标还有预览图过来,显示默认的
+			return obj[key] === 'default' ? '/static/img/userMealUpgradeVO_icon.png' : (obj[key] === 'defaultPhonePreviewUrl' ?
+				this.remoteImgUrl + 'defalut-preview.png' : obj[key])
+		},
+    showChange(val){
+      this.$emit('update:levitatedSphereVisible', val);
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+// 文字高亮颜色
+$active-color: #FEAE4D;
+.left-menu-popup {
+  // 当胶囊下拉框(下拉选项区)弹出时,需设置此属性,否则下拉框的遮罩会裁剪
+  .levitated-sphere-drawer.overflow-y-initial{
+    overflow-y: initial !important;
+  }
+
+  .menu-wrap{
+    width: 300px;
+    display: flex;
+    flex-direction: column;
+
+    #exit-wrap{
+      padding: 9px 0;
+      background-color: #1F1F1F;
+      .exit-btn{
+        width: 200px;
+      }
+    }
+  }
+}
+</style>

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

@@ -112,6 +112,7 @@ export default {
           key: 'signout',
           img: '../static/img/wx/tuichu_icon.png'
       }]
+      // 1、2、3:年卡、普通计时、自动续费普通计时卡
       if ([1, 2, 3].includes(+this.parametersData.userCardType)) {
           arr.push({
               name: '退出并下机',

BIN=BIN
pages/rtcEngine/components/icon-restart.png


BIN=BIN
pages/rtcEngine/components/icon-screenshot.png


+ 102 - 0
pages/rtcEngine/mixins/public.js

@@ -0,0 +1,102 @@
+export default {
+data() {
+		return {
+      imgUrl: `${this.$config.FILE_URL}/document/newFile/download/0/${this.$config.FILE_UPLOAD_KEY}?fileKey=`,
+			// 远程图片
+			remoteImgUrl: `${this.$config.STATIC_IMG_URL}/`,
+      // 云机列表
+      cloudList: [],
+      // 套餐类型
+      mealTypeList: [],
+      mealTypeObj: {},
+    }
+  },
+  methods: {
+  	/**
+		 * 根据卡套餐获取对应图标
+		 * @method imgFun 
+		 * @param {String} type--1 套餐类型 VIP|SVIP|STARRYSKY
+		 * @param {String} androidVersion--2 安卓系统版本
+		 * @param {String} key='previewUrl'--3 ['previewUrl' | 'phonePreviewUrl'] 判断是预览图还是套餐图标用的
+		 * @returns {String} 图片路径
+		 */
+		imgFun(type, androidVersion = '', key = 'previewUrl') {
+			let obj = this.mealTypeObj[type + androidVersion]
+			// obj[key]的值是default或defaultPhonePreviewUrl时候,就是后端没有返回图标还有预览图过来,显示默认的
+			return obj[key] === 'default' ? '/static/img/userMealUpgradeVO_icon.png' : (obj[key] === 'defaultPhonePreviewUrl' ?
+				this.remoteImgUrl + 'defalut-preview.png' : obj[key])
+		},
+    // 获取用户云手机列表
+    async getCloudList() {
+      try {
+        const result = await this.$axios.get('/resources/v6/client/device/info/getDeviceList');
+        console.log('获取云手机列表', result)
+        console.log('获取云手机列表', result.status === 200, result.data.status === 0, result.data.success)
+        if(result.status === 200 && result.data.status === 0 && result.data.success) {
+          console.log('获取云手机列表1')
+          this.cloudList = result.data.data.diskInfo ?? [];
+        }
+        console.log('获取云手机列表2')
+      } catch (error) {
+        console.log('获取云手机列表3')
+        console.error('获取云手机列表失败', error)
+      }
+    },
+    // 获取云手机套餐,显示套餐名称和图标
+    async getMealIconInfo() {
+      try {
+        const result = await this.$axios.get('/pay/v2/meal/info/getMealIconInfo');
+        console.log('获取套餐图标数据', result)
+        if(result.status === 200 && result.data.status === 0 && result.data.success) {
+          console.log('获取套餐图标数据1')
+          
+          const res = result.data;
+          let obj = {} // eg: {VIP7: xxx, VIP10: xxx, SVIP7: xxx,...}
+            let casualObj = {} // eg: {VIP: xxx, SVIP: xxx}
+            let mealTypeList = [] // eg: [{label:xxx, value: xxx, previewUrl: xxx, androidVersionList: [7,10]}, ...]
+            let mealTypeObj = {} // 同obj对象
+            let index = 0
+            for (let i of res.data) {
+              if (!casualObj[i.phoneType]) {
+                casualObj[i.phoneType] = {
+                  label: i.phoneTypeName,
+                  value: i.phoneType,
+                  previewUrl: 'default',
+                  phonePreviewUrl: 'defaultPhonePreviewUrl',
+                  androidVersionList: [],
+                  index
+                }
+                index++
+              }
+  
+              let androidVersionObj = {
+                label: `安卓${i.androidVersion}`,
+                value: i.androidVersion,
+                previewUrl: i.previewUrl
+              }
+  
+              casualObj[i.phoneType].androidVersionList.push(androidVersionObj)
+  
+              // 排序
+              casualObj[i.phoneType].androidVersionList.sort((a, b) => a.value - b.value)
+  
+              if (obj[i.phoneType + i.androidVersion]) continue
+              obj[i.phoneType + i.androidVersion] = i
+            }
+            mealTypeList = Object.values(casualObj);
+            mealTypeObj = obj;
+            Object.assign(mealTypeObj, casualObj);
+            this.mealTypeList = mealTypeList;
+            this.mealTypeObj = mealTypeObj;
+  
+            console.log('套餐图标数据', mealTypeList, mealTypeObj)
+        }
+        console.log('获取套餐图标数据2')
+      } catch (error) {
+        console.log('获取套餐图标数据3')
+        console.error('获取套餐图标数据失败', error)
+      }
+
+    },
+  }
+}

+ 67 - 18
pages/rtcEngine/rtc.vue

@@ -4,20 +4,29 @@
     <div class="video-wrapper" id="videoRef" :style="{width: pageData.videoWidth + 'px', height: pageData.videoHeight + 'px'}"></div>
     
     <!-- 三menu键 -->
-    <div id="foot-menu-wrap" :style="`height: ${footMenuWrapHeight}px`">
+    <div id="foot-menu-wrap" :style="`height: ${pageData.footMenuHeight}px`">
       <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"/>
+    <FloatBtn :width="pageData.width" :height="pageData.height" :latency="rtcNetwork.currentRoundTripTime" @onClick="levitatedSphereVisible = true"/>
 
     <!-- 左侧popup -->
-    <LeftMenuPopup ref="leftMenuPopupRef" :engine="engine" :userCardId="this.parametersData.userCardId" :levitatedSphereVisible.sync="levitatedSphereVisible" @shearplate="shearplate" @exit="exit"/>
+    <LeftMenuPopup
+      ref="leftMenuPopupRef"
+      :engine="engine"
+      :userCardId="this.parametersData.userCardId"
+      :levitatedSphereVisible.sync="levitatedSphereVisible"
+      :latency="rtcNetwork.currentRoundTripTime"
+      @shearplate="shearplate"
+      @funcHandle="funcHandle"
+      @exit="exit"
+    />
     
     <!-- 右侧popup -->
-    <RightPopup ref="rightPopupRef" :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"/>
@@ -37,6 +46,7 @@
 import meta from './config/meta.js';
 import request from './config/request.js';
 import logReport from './config/logReport.js';
+import publicMixin from './mixins/public.js';
 import * as uni from '../../static/static/js/uni.webview.1.5.2.js';
 
 import FloatBtn from './components/FloatBtn.vue';
@@ -114,6 +124,7 @@ export default {
       ]
     }
   },
+  mixins: [publicMixin],
   data() {
     return {
       // 日志上报实例
@@ -160,7 +171,10 @@ export default {
       // 卡的连接信息
       connectData: {},
       // 云手机引擎 播放器实例
-      engine: null,
+      engine: {},
+      rtcNetwork: {
+        currentRoundTripTime: 0, // 当前往返时间(网络延迟)
+      }, // webRtc网络分析数据
       doConnectDirectivesWs: null, // 云手机指令通道
       doConnectDirectivesIntervalerPing: null, // 业务通道定时标识 云手机指令通道心跳
       doConnectDirectivesRequestNum: 1, // 业务通道重连次数
@@ -172,24 +186,17 @@ export default {
   },
   // 页面初始化后触发
   async fetch() {
-    // 预生产环境
-    // http://192.168.211.37:3000/h5/rtcEngine/rtc/?record=2990744&userCardId=2990744&mealType=VIP&sourceType=0&userCardType=0&validTime=43200&rm=preZJHZ&authPhone=none&username=CGJkh1646034892SZX&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyYW5kb20iOiI5MTY4OCIsImNsaWVudCI6IjciLCJ1c2VyVHlwZSI6IjIiLCJtZXJjaGFudFNpZ24iOiJTWlgiLCJleHAiOjE3NDc1MjExMTIsInVzZXJuYW1lIjoiQ0dKa2gxNjQ2MDM0ODkyU1pYIn0.zqRNm0uW79VX2KV0sv3r9OsXHFOzkIpZP_tem4-lL4M&isTips=1&isWeixin=0&merchantSign=SZX
-    // 测试环境
-    // http://192.168.211.37:3000/h5/rtcEngine/rtc/?record=902481&userCardId=902481&mealType=sq&sourceType=0&userCardType=0&validTime=10334&rm=tencent-ap-tianjin-1&authPhone=none&username=0EsH01666175530SZX&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyYW5kb20iOiI0MTg5MyIsImNsaWVudCI6IjciLCJ1c2VyVHlwZSI6IjIiLCJtZXJjaGFudFNpZ24iOiJTWlgiLCJleHAiOjE3NDczODQ2MTgsInVzZXJuYW1lIjoiMEVzSDAxNjY2MTc1NTMwU1pYIn0.fTXawDu4SyEj4mIGr61ZXt0RSXzt3JztPq8-rFe-Zes&isTips=1&isWeixin=0&merchantSign=SZX
     // 获取页面传递参数
     this.parametersData = this.$route.query;
+
+    this.getCloudList();
+    this.getMealIconInfo();
   },
   computed: {
     // 是否为微信浏览器环境
     isWeChatBrowser() {
       return this.$userAgent.isWx;
     },
-    // 底部菜单高度
-    footMenuWrapHeight() {
-      let num = 40;
-      this.pageData.footMenuHeight = num;
-      return num;
-    }
   },
   created() {
     this.$toast.loading({
@@ -309,8 +316,8 @@ export default {
       } else {
         // 当前视口的宽高比小于目标比例,说明高度“过高”,需要以宽度为基准
         console.log("当前视口高度过高,应以宽度为基准调整高度");
-        this.pageData.videoWidth = videoWidth / targetRatio;
-        this.pageData.videoHeight = videoWidth;
+        this.pageData.videoWidth = videoWidth;
+        this.pageData.videoHeight = videoWidth / targetRatio;
         console.log(`2目标: 宽${this.pageData.videoWidth},高${this.pageData.videoHeight}`);
       }
     },
@@ -567,7 +574,12 @@ export default {
 
       // 连接关闭
       engine.on('CONNECT_CLOSE', (r) => {
-          console.log("webrtc关闭====★★★★★", r);
+        console.log("webrtc关闭====★★★★★", r);
+      });
+      
+      // 网络连接统计信息监听
+      engine.on('NETWORK_STATS', (r) => {
+        this.rtcNetwork = r;
       });
 
       // 连接异常
@@ -687,6 +699,43 @@ export default {
         console.log('initControlChannel error', error);
       }
     },
+    /**
+     * popup 功能按钮点击事件
+     * @param {Object} val 传递的参数
+     * @description 传递的参数格式: { code: 'hideKey', label: '隐藏虚拟键', icon: 'icon-hide' } 
+     */
+    funcHandle({code}) {
+      console.log('funcHandle code:', code);
+      switch (code) {
+        case 'hideKey':
+          // 隐藏虚拟按键
+          document.getElementById('foot-menu-wrap').style.display = 'none';
+          this.pageData.footMenuHeight = 0; // 设置底部菜单高度为0
+          this.getInitSize(); // 重新计算视频尺寸
+          this.changeVideoStyle(); // 重新设置视频尺寸
+          break;
+        case 'showKey':
+          // 显示虚拟按键
+          document.getElementById('foot-menu-wrap').style.display = 'flex';
+          this.pageData.footMenuHeight = 40; // 设置底部菜单高度为40px
+          this.getInitSize(); // 重新计算视频尺寸
+          this.changeVideoStyle(); // 重新设置视频尺寸
+          break
+        case 'shearplate':
+          // 粘贴板按钮
+          this.shearplate();
+          break;
+      }
+    },
+    // 重新设置视频尺寸
+    changeVideoStyle() {
+      this.$nextTick(() => {
+        // 获取video元素
+        const video = document.getElementById("videoRef").getElementsByTagName('video')[0];
+        video.style.width = this.pageData.videoWidth + 'px';
+        video.style.height = this.pageData.videoHeight + 'px';
+      });
+    },
     // 三键
     sendKey (keyCode) {
       try {