mdm-client 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/App.vue +72 -67
  3. package/src/assets/image/common/layout-active16.png +0 -0
  4. package/src/assets/image/common/layout-active25.png +0 -0
  5. package/src/assets/image/common/layout-active3.png +0 -0
  6. package/src/assets/image/common/layout-active9.png +0 -0
  7. package/src/assets/image/common/layout16.png +0 -0
  8. package/src/assets/image/common/layout25.png +0 -0
  9. package/src/assets/image/common/layout3.png +0 -0
  10. package/src/assets/image/common/layout9.png +0 -0
  11. package/src/assets/image/common/mirror.png +0 -0
  12. package/src/assets/image/common/rotate_icon1.png +0 -0
  13. package/src/assets/image/common/rotate_icon2.png +0 -0
  14. package/src/assets/image/common/rotate_icon3.png +0 -0
  15. package/src/assets/image/common/rotate_icon4.png +0 -0
  16. package/src/assets/style/base.scss +5 -0
  17. package/src/components/LiveMulti/LiveMulti.vue +27 -6
  18. package/src/components/LiveMultipleMeeting/LiveMultipleMeeting.vue +1063 -98
  19. package/src/components/LiveMultipleMeeting/style/index.scss +145 -14
  20. package/src/components/LivePoint/LivePoint.vue +49 -211
  21. package/src/components/LivePointMeeting/LivePointMeeting.vue +159 -10
  22. package/src/components/LivePointMeeting/style/index.scss +35 -0
  23. package/src/components/MeetingReadyDialog/MeetingReadyDialog.vue +96 -14
  24. package/src/components/other/addressBook.vue +137 -20
  25. package/src/components/other/appointDialog.vue +1 -1
  26. package/src/components/other/customLayout.vue +368 -202
  27. package/src/components/other/layoutSwitch.vue +253 -37
  28. package/src/components/other/leadershipFocus.vue +422 -0
  29. package/src/components/other/leaveOptionDialog.vue +1 -1
  30. package/src/components/other/moreOptionDialog.vue +17 -1
  31. package/src/components/other/selectDialog.vue +1 -1
  32. package/src/components/other/selectSpecialDialog.vue +1 -1
  33. package/src/utils/api.js +19 -0
  34. package/src/utils/livekit/live-client-esm.js +1 -1
@@ -43,10 +43,12 @@
43
43
  <LayoutSwitch
44
44
  v-if="layoutSwitchShow"
45
45
  :currentLayout="currentLayout"
46
+ :current-grid-size="currentGridSize"
46
47
  :layoutList="layoutList"
47
48
  :layoutLabels="layoutLabels"
48
49
  :currentRoomMode="currentRoomMode"
49
50
  @setLayout="manualSetLayout"
51
+ @setGridSize="setGridSize"
50
52
  @mouseenter.native="layoutSwitchShow = true"
51
53
  @mouseleave.native="layoutSwitchShow = false"
52
54
  ></LayoutSwitch>
@@ -86,6 +88,12 @@
86
88
  @screenShareOptionChange="screenShareOptionChange"
87
89
  @stopScreenShare="stopScreenShare"
88
90
  ></ScreenShareBoard>
91
+ <LeadershipFocus
92
+ v-if="isLeaderShipFocusShow"
93
+ :participant="curLeadershipFocus"
94
+ :local-metadata="localMetadata"
95
+ @moreClick="leaderShipMoreClick"
96
+ @microphoneClick="leaderShipMicroClick"></LeadershipFocus>
89
97
  </div>
90
98
 
91
99
  <!-- 会议控制栏 -->
@@ -321,6 +329,7 @@
321
329
  :participants="participants"
322
330
  :meetingHost="meetingHost"
323
331
  :meetingCoHost="meetingCoHost"
332
+ :leaderShipFocus="leaderShipFocus"
324
333
  @close="moreDialogShow = false"
325
334
  @setToBlur="setMemberBlur"
326
335
  @removeFromBlur="removeCurBlur"
@@ -335,14 +344,17 @@
335
344
  @remove="removeMember"
336
345
  @setToCoHost="setToCoHost"
337
346
  @removeFromCoHost="removeFromCoHost"
347
+ @openLeadershipFocus="openLeadershipFocus"
348
+ @closeLeadershipFocus="closeLeadershipFocus"
338
349
  >
339
350
  </MoreOptionDialog>
340
351
  <AddressBook
341
- :inviteList="inviteList"
342
352
  :baseUrl="baseUrl"
343
353
  :token="token"
354
+ :tab-list="addressBookTabList"
344
355
  @appendInvitePeople="appendInvitePeople"
345
356
  @appendInviteDevice="appendInviteDevice"
357
+ @appendInviteTerminal="appendInviteTerminal"
346
358
  ></AddressBook>
347
359
  <UpdateNameDialog @updateNameConfirm="updateNameConfirm"></UpdateNameDialog>
348
360
  <SettingDialog
@@ -375,7 +387,9 @@ import SelectSpecialDialog from "../other/selectSpecialDialog.vue";
375
387
  import ScreenShareBoard from "../other/screenShareBoard.vue";
376
388
  import SettingDialog from "../other/settingDialog.vue";
377
389
  import CustomLayout from "../other/customLayout.vue";
390
+ import LeadershipFocus from "../other/leadershipFocus.vue";
378
391
 
392
+ let ringResizeObserver = null;
379
393
  export default {
380
394
  name: "LiveMultipleMeeting",
381
395
  components: {
@@ -394,6 +408,7 @@ export default {
394
408
  ScreenShareBoard,
395
409
  SettingDialog,
396
410
  CustomLayout,
411
+ LeadershipFocus
397
412
  },
398
413
  props: {
399
414
  tempMeetingName: {
@@ -425,6 +440,10 @@ export default {
425
440
  type: Array,
426
441
  default: () => [],
427
442
  },
443
+ meetingTerminals: {
444
+ type: Array,
445
+ default: () => [],
446
+ },
428
447
  userData: {
429
448
  type: Object,
430
449
  default: () => ({}),
@@ -453,6 +472,21 @@ export default {
453
472
  type: String,
454
473
  default: "",
455
474
  },
475
+ // 通讯录可见的标签页配置
476
+ addressBookTabList: {
477
+ type: Array,
478
+ default: () => ["组织架构", "人员", "会议终端", "volte", "设备", "监控", "常用分组"],
479
+ },
480
+ // 语音激励开关
481
+ isVoiceMotivationOpen: {
482
+ type: Boolean,
483
+ default: false,
484
+ },
485
+ // 镜像开关
486
+ isMirror: {
487
+ type: Boolean,
488
+ default: false,
489
+ },
456
490
  },
457
491
  data() {
458
492
  return {
@@ -496,9 +530,9 @@ export default {
496
530
  // 布局数据
497
531
  currentLayout: "grid",
498
532
  currentRoomMode: "normal",
499
- layoutList: ["grid", "rightSide"],
500
- layoutLabels: ["宫格", "右侧边栏"],
501
-
533
+ layoutList: ["grid", "rightSide", "ring"],
534
+ layoutLabels: ["宫格", "右侧边栏", "环形"],
535
+ currentGridSize: 9,
502
536
  // 状态数据
503
537
  duration: 0,
504
538
  memberManageShow: false,
@@ -582,6 +616,15 @@ export default {
582
616
  durationInterval: null,
583
617
  footerInterval: null,
584
618
  unjoinParticipantInterval: null,
619
+ // 保存与会者画面旋转状态
620
+ rotateDegreeMap: new Map(),
621
+ // 当前聚焦画面
622
+ leaderShipFocus: ""
623
+ };
624
+ },
625
+ provide() {
626
+ return {
627
+ rotateDegreeMap: this.rotateDegreeMap,
585
628
  };
586
629
  },
587
630
  computed: {
@@ -594,12 +637,33 @@ export default {
594
637
  participantIdentities() {
595
638
  return this.participantNum > 0 ? this.participants.map((item) => item.identity) : [];
596
639
  },
640
+ isLeaderShipFocusShow() {
641
+ if (!this.leaderShipFocus) {
642
+ return false
643
+ }
644
+ if (this.participantNum <= 0) {
645
+ return false
646
+ }
647
+ if (this.participantIdentities.includes(this.leaderShipFocus)) {
648
+ return true
649
+ }
650
+ },
651
+ curLeadershipFocus() {
652
+ if (this.participantNum <= 0 || !this.leaderShipFocus) {
653
+ return {}
654
+ } else {
655
+ return this.participants.find(item => item.identity === this.leaderShipFocus)
656
+ }
657
+ },
597
658
  inviteNum() {
598
659
  return this.inviteList.length;
599
660
  },
600
661
  deviceNum() {
601
662
  return this.deviceList.length;
602
663
  },
664
+ terminalNum() {
665
+ return this.meetingTerminals.length;
666
+ },
603
667
  invitedNum() {
604
668
  return this.tempInvitedList?.length || 0;
605
669
  },
@@ -621,22 +685,55 @@ export default {
621
685
  return "point-turn";
622
686
  } else {
623
687
  if (this.currentLayout === "grid") {
624
- if (this.participantNum <= 1) {
625
- return "grid1";
626
- } else if (this.participantNum <= 2) {
627
- return "grid2";
628
- } else if (this.participantNum <= 4) {
629
- return "grid3";
630
- } else if (this.participantNum <= 6) {
631
- return "grid4";
632
- } else if (this.participantNum <= 9) {
633
- return "grid5";
634
- } else if (this.participantNum <= 12) {
635
- return "grid6";
636
- } else if (this.participantNum <= 16) {
637
- return "grid7";
638
- } else {
639
- return "grid8";
688
+ // 当前为宫格布局时, 不同与会者数量对应不同样式
689
+ if (this.currentGridSize === 9) {
690
+ if (this.participantNum <= 1) {
691
+ return 'grid1'
692
+ } else if (this.participantNum <= 2) {
693
+ return 'grid2'
694
+ } else if (this.participantNum <= 4) {
695
+ return 'grid3'
696
+ } else if (this.participantNum <= 6) {
697
+ return 'grid4'
698
+ } else if (this.participantNum <= 9) {
699
+ return 'grid5'
700
+ } else {
701
+ return 'grid5'
702
+ }
703
+ } else if (this.currentGridSize === 16) {
704
+ if (this.participantNum <= 1) {
705
+ return 'grid1'
706
+ } else if (this.participantNum <= 2) {
707
+ return 'grid2'
708
+ } else if (this.participantNum <= 4) {
709
+ return 'grid3'
710
+ } else if (this.participantNum <= 6) {
711
+ return 'grid4'
712
+ } else if (this.participantNum <= 9) {
713
+ return 'grid5'
714
+ } else if (this.participantNum <= 16) {
715
+ return 'grid6'
716
+ } else {
717
+ return 'grid6'
718
+ }
719
+ } else if (this.currentGridSize === 25) {
720
+ if (this.participantNum <= 1) {
721
+ return 'grid1'
722
+ } else if (this.participantNum <= 2) {
723
+ return 'grid2'
724
+ } else if (this.participantNum <= 4) {
725
+ return 'grid3'
726
+ } else if (this.participantNum <= 6) {
727
+ return 'grid4'
728
+ } else if (this.participantNum <= 9) {
729
+ return 'grid5'
730
+ } else if (this.participantNum <= 16) {
731
+ return 'grid6'
732
+ } else if (this.participantNum <= 25) {
733
+ return 'grid7'
734
+ } else {
735
+ return 'grid7'
736
+ }
640
737
  }
641
738
  } else {
642
739
  return this.currentLayout;
@@ -714,14 +811,148 @@ export default {
714
811
  }
715
812
  this.setPageFooterVisible(5);
716
813
  this.initGlobleEvent();
814
+ // 观察#room尺寸变化,动态同步环形布局顶部高度
815
+ this.observeRoomResizeForRing();
816
+ // 首次挂载后尝试同步一次高度
817
+ requestAnimationFrame(() => this.updateRingTopHeight());
717
818
  },
718
819
  beforeDestroy() {
719
820
  this.stopUnjoinParticipantPolling();
720
821
  this.dispatchLiveClientEvent();
721
822
  this.removeGlobleEvent();
823
+ // 清理observer或事件监听
824
+ if (ringResizeObserver) {
825
+ try { ringResizeObserver.disconnect(); } catch (e) {}
826
+ ringResizeObserver = null;
827
+ } else {
828
+ window.removeEventListener('resize', this.updateRingTopHeight);
829
+ }
722
830
  this.liveClient = null;
723
831
  },
724
832
  methods: {
833
+ handleInviteType(integrationType = 0) {
834
+ // 根据integrationType返回对应的makeCall的type参数
835
+ switch (Number(integrationType)) {
836
+ case 0:
837
+ return 1
838
+ case 1:
839
+ return 2
840
+ case 2:
841
+ return 3
842
+ default:
843
+ return 1
844
+ }
845
+ },
846
+ leaderShipMoreClick(e) {
847
+ // 动态计算选项数量以获得精确的弹窗高度
848
+ const optionCount = this.getOptionCount(e.identity)
849
+
850
+ // 使用工具函数计算弹窗位置(使用真实的MoreOptionDialog尺寸)
851
+ this.optionDialogOffset = this.calculateDialogPosition(e.event, {
852
+ dialogWidth: 130,
853
+ dialogHeight: 350, // 备用高度
854
+ optionCount: optionCount, // 动态计算精确高度
855
+ preferredPosition: 'bottom-left',
856
+ })
857
+
858
+ if (this.optionIdentity === e.identity && this.moreDialogShow) {
859
+ this.moreDialogShow = false
860
+ } else {
861
+ this.moreDialogShow = false
862
+ this.optionIdentity = e.identity
863
+ this.moreDialogShow = true
864
+ }
865
+ },
866
+ async leaderShipMicroClick() {
867
+ await this.liveClient.changeParticipantMicrophoneStatus(e.identity, e.isMuted)
868
+ },
869
+ // 聚焦画面
870
+ async openLeadershipFocus(identity) {
871
+ await this.liveClient.openLeadershipFocus(identity)
872
+ },
873
+ // 取消聚焦画面
874
+ async closeLeadershipFocus(identity) {
875
+ await this.liveClient.closeLeadershipFocus(identity)
876
+ },
877
+ updateRingTopHeight() {
878
+ try {
879
+ const room = document.getElementById('room');
880
+ if (!room) return;
881
+ const ringTop = room.querySelector('.layout-ring-top');
882
+ if (!ringTop) return;
883
+ // 使用clientHeight以获得包含内边距的可见高度
884
+ const roomHeight = room.clientHeight;
885
+ // 设置明确像素高度,覆盖CSS百分比或flex计算值
886
+ ringTop.style.height = roomHeight + 'px';
887
+ } catch (e) {
888
+ console.warn('updateRingTopHeight error:', e);
889
+ }
890
+ },
891
+ observeRoomResizeForRing() {
892
+ const room = document.getElementById('room');
893
+ if (!room) return;
894
+ // 优先使用ResizeObserver,实时响应容器尺寸变化
895
+ if ('ResizeObserver' in window) {
896
+ ringResizeObserver = new ResizeObserver(() => {
897
+ this.updateRingTopHeight();
898
+ });
899
+ ringResizeObserver.observe(room);
900
+ } else {
901
+ // 回退到window resize
902
+ window.addEventListener('resize', this.updateRingTopHeight);
903
+ }
904
+ },
905
+ setGridSize(size) {
906
+ this.currentGridSize = size;
907
+ },
908
+ // 根据identity和度数应用到video元素和旋转按钮的样式/属性
909
+ applyRotation(identity, degree) {
910
+ try {
911
+ const norm = ((degree % 360) + 360) % 360; // 归一化到0-359
912
+ // 更新video元素的transform和objectFit
913
+ const videoElm = document.getElementById(`video-${identity}`);
914
+ if (videoElm) {
915
+ videoElm.style.transformOrigin = 'center center';
916
+ // 针对90/270度,居中并以高度为基准适配,避免布局“怪异”
917
+ if (norm === 90 || norm === 270) {
918
+ const parent = videoElm.parentElement;
919
+ if (parent) {
920
+ const rect = parent.getBoundingClientRect();
921
+ // 旋转后使可视区域与父容器宽高贴合:宽高对调为父容器的高/宽
922
+ videoElm.style.top = '50%';
923
+ videoElm.style.left = '50%';
924
+ videoElm.style.width = `${rect.height}px`;
925
+ videoElm.style.height = `${rect.width}px`;
926
+ videoElm.style.transform = `translate(-50%, -50%) rotate(${norm}deg)`;
927
+ // 使用cover确保充满父容器(必要时会稍作裁剪)
928
+ videoElm.style.objectFit = 'cover';
929
+ }
930
+ } else {
931
+ // 0/180度直接占满容器
932
+ videoElm.style.top = '0';
933
+ videoElm.style.left = '0';
934
+ videoElm.style.width = '100%';
935
+ videoElm.style.height = '100%';
936
+ videoElm.style.transform = `rotate(${norm}deg)`;
937
+ videoElm.style.objectFit = 'contain';
938
+ }
939
+ }
940
+
941
+ // 更新旋转按钮的class以反映当前角度(1..4)
942
+ const rotateElm = document.getElementById(`rotate-${identity}`);
943
+ if (rotateElm) {
944
+ let clsIndex = 1;
945
+ if (norm === 0) clsIndex = 1;
946
+ else if (norm === 90) clsIndex = 2;
947
+ else if (norm === 180) clsIndex = 3;
948
+ else if (norm === 270) clsIndex = 4;
949
+ rotateElm.className = `rotate-icon rotate-icon${clsIndex}`;
950
+ }
951
+ } catch (err) {
952
+ // 忽略DOM异常
953
+ console.error('applyRotation error', err);
954
+ }
955
+ },
725
956
  // DOM操作管理函数
726
957
  async batchUpdateStates(updateFn) {
727
958
  this.isBatchUpdating = true;
@@ -868,12 +1099,12 @@ export default {
868
1099
  // 计算MoreOptionDialog选项数量的辅助函数
869
1100
  getOptionCount(identity) {
870
1101
  const participantItem = this.getUserItemByIdentity(identity);
871
- if (!participantItem || !participantItem.metadata) return 8; // 默认最大选项数
1102
+ if (!participantItem || !participantItem.metadata) return 9; // 默认最大选项数
872
1103
 
873
1104
  const metadata = participantItem.metadata;
874
1105
  const isLocalParticipant = identity === this.localIdentity;
875
1106
 
876
- let optionCount = 3; // 基础选项:设为主屏、麦克风、摄像头
1107
+ let optionCount = 4; // 基础选项:设为主屏、麦克风、摄像头、聚焦画面
877
1108
 
878
1109
  if (this.judgeParticipantIsHost(this.localIdentity)) {
879
1110
  // 本地为主持人
@@ -898,7 +1129,7 @@ export default {
898
1129
  }
899
1130
  }
900
1131
 
901
- return Math.min(optionCount, 8); // 限制最大选项数
1132
+ return Math.min(optionCount, 9); // 限制最大选项数
902
1133
  },
903
1134
  // 弹窗定位工具函数
904
1135
  calculateDialogPosition(event, options = {}) {
@@ -1366,12 +1597,12 @@ export default {
1366
1597
  const deviceCalls = this.deviceList
1367
1598
  .map(item => {
1368
1599
  const id = item?.source === '监控' ? item?.monitorID : item?.equipmentID;
1369
- return id ? { dnis: id, name: item.label } : null;
1600
+ return id ? { dnis: id, name: item.label, type: this.handleInviteType(item?.integrationType) } : null;
1370
1601
  })
1371
1602
  .filter(Boolean);
1372
1603
 
1373
1604
  if (deviceCalls.length > 0) {
1374
- this.liveClient.makeBatchCall(deviceCalls, 1)
1605
+ this.liveClient.makeBatchCall(deviceCalls)
1375
1606
  .then(() => {
1376
1607
  this.showMessage.message('success', '监控设备批量外呼已发起');
1377
1608
  })
@@ -1380,6 +1611,24 @@ export default {
1380
1611
  });
1381
1612
  }
1382
1613
  }
1614
+ // 拉取会议终端
1615
+ if (this.terminalNum > 0 && this.meetingTerminals) {
1616
+ const terminalCalls = this.meetingTerminals
1617
+ .map(item => {
1618
+ return item?.id ? { dnis: item.id, name: item.label, type: 4 } : null
1619
+ })
1620
+ .filter(Boolean)
1621
+ if (terminalCalls.length > 0) {
1622
+ this.liveClient
1623
+ .makeBatchCall(terminalCalls)
1624
+ .then(() => {
1625
+ this.showMessage.message('success', '会议终端批量外呼已发起')
1626
+ })
1627
+ .catch(err => {
1628
+ this.showMessage.message('error', `批量拉取会议终端失败: ${err?.message || err}`)
1629
+ })
1630
+ }
1631
+ }
1383
1632
  }
1384
1633
  // 启动获取未入会和邀请人员轮询
1385
1634
  this.startUnjoinParticipantPolling();
@@ -1475,18 +1724,44 @@ export default {
1475
1724
  }
1476
1725
  });
1477
1726
  if (maxAudioLevelIndex >= 0) {
1478
- this.switchActiveSpeakerToFirst(e[maxAudioLevelIndex]);
1727
+ this.highlightCurrentSpeaker(e[maxAudioLevelIndex])
1728
+ if (this.isVoiceMotivationOpen) {
1729
+ this.switchActiveSpeakerToFirst(e[maxAudioLevelIndex])
1730
+ }
1731
+ } else {
1732
+ this.highlightCurrentSpeaker(-1)
1479
1733
  }
1734
+ } else {
1735
+ this.highlightCurrentSpeaker(-1)
1480
1736
  }
1481
1737
  });
1482
1738
  this.liveClient.on("resMeetingRefresh", this.handleResMeetingRefresh);
1483
1739
  },
1484
- handleResMeetingRefresh: (e) => {
1740
+ // 高亮当前与会者
1741
+ highlightCurrentSpeaker(speaker) {
1742
+ const participantElms = document.querySelectorAll('#room .participant')
1743
+ if (speaker === -1) {
1744
+ if (participantElms.length > 0) {
1745
+ participantElms.forEach(elm => {
1746
+ elm.style.border = 'none'
1747
+ })
1748
+ }
1749
+ } else if (speaker && speaker.identity) {
1750
+ const speakerElm = document.getElementById(`participant-${speaker.identity}`)
1751
+ if (participantElms.length > 0) {
1752
+ participantElms.forEach(elm => {
1753
+ elm.style.border = 'none'
1754
+ })
1755
+ }
1756
+ speakerElm && (speakerElm.style.border = '2px solid #95ec69')
1757
+ }
1758
+ },
1759
+ handleResMeetingRefresh(e) {
1485
1760
  console.log('resMeetingRefresh事件触发', e);
1486
-
1487
- if(e.includes("queryAllInvite") || e.includes("queryUnjoined")) {
1761
+
1762
+ if (e.includes("queryAllInvite") || e.includes("queryUnjoined")) {
1488
1763
  console.log('this', this, this.getUnjoinParticipant);
1489
-
1764
+
1490
1765
  this.getUnjoinParticipant();
1491
1766
  this.queryAllInviteParticipant();
1492
1767
  this.startUnjoinParticipantPolling();
@@ -1632,7 +1907,7 @@ export default {
1632
1907
  } else {
1633
1908
  this.meetingCoHost = [];
1634
1909
  }
1635
-
1910
+ this.leaderShipFocus = metadata.leaderShipFocus;
1636
1911
  this.roomMetadata = metadata;
1637
1912
 
1638
1913
  // 批量设置关键状态
@@ -1708,6 +1983,7 @@ export default {
1708
1983
  this.meetingCoHost = [];
1709
1984
  }
1710
1985
 
1986
+ this.leaderShipFocus = metadata.leaderShipFocus;
1711
1987
  this.roomMetadata = metadata;
1712
1988
 
1713
1989
  // 批量设置关键状态
@@ -1955,7 +2231,8 @@ export default {
1955
2231
  // 构建批量外呼数据
1956
2232
  const volteCalls = volteList.map(item => ({
1957
2233
  dnis: item.phone,
1958
- name: item.label
2234
+ name: item.label,
2235
+ type: 0
1959
2236
  }));
1960
2237
 
1961
2238
  promises.push(
@@ -1997,9 +2274,9 @@ export default {
1997
2274
  roomNum: this.meetingNum,
1998
2275
  cameraStatus: this.isCameraEnabled,
1999
2276
  microphoneStatus: this.isMicrophoneEnabled,
2000
- audioDeviceId: this.activeDevice.audioInputDevice,
2001
- videoDeviceId: this.activeDevice.videoDevice,
2002
- outputDeviceId: this.activeDevice.audioOutputDevice,
2277
+ audioDeviceId: this.tempActiveDevice.audioInputDevice,
2278
+ videoDeviceId: this.tempActiveDevice.videoDevice,
2279
+ outputDeviceId: this.tempActiveDevice.audioOutputDevice,
2003
2280
  };
2004
2281
  let tempActiveDevice = null;
2005
2282
  try {
@@ -2133,36 +2410,83 @@ export default {
2133
2410
  </div>
2134
2411
  `;
2135
2412
  } else if (videoItem.isCameraEnabled) {
2136
- videoDom.innerHTML = `
2137
- <video id="video-${
2138
- videoItem.identity
2139
- }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2140
- <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2141
- <div id="more-${videoItem.identity}" class="more-icon"></div>
2142
- <div class="describe">
2143
- <div id="microphone-${videoItem.identity}" class="microphone"></div>
2144
- <div id="${videoItem.identity}" class="identity">${
2145
- videoItem.isLocal ? videoItem.name + "(我)" : videoItem.name
2146
- }</div>
2147
- </div>
2148
- `;
2413
+ if (videoItem.metadata?.platformID == 5) {
2414
+ videoDom.innerHTML = `
2415
+ <video id="video-${
2416
+ videoItem.identity
2417
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2418
+ <div id="loadingIndicator-${videoItem.identity}" class="loadingIndicator">
2419
+ <div class="loading-icon"></div>
2420
+ <div class="loading-text">视频流加载中...</div>
2421
+ </div>
2422
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2423
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2424
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2425
+ <div class="describe">
2426
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2427
+ <div id="${videoItem.identity}" class="identity">${
2428
+ videoItem.isLocal ? videoItem.name + "(我)" : videoItem.name
2429
+ }</div>
2430
+ </div>
2431
+ `;
2432
+ } else {
2433
+ videoDom.innerHTML = `
2434
+ <video id="video-${
2435
+ videoItem.identity
2436
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2437
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2438
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2439
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2440
+ <div class="describe">
2441
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2442
+ <div id="${videoItem.identity}" class="identity">${
2443
+ videoItem.isLocal ? videoItem.name + '(我)' : videoItem.name
2444
+ }</div>
2445
+ </div>
2446
+ `;
2447
+ }
2149
2448
  } else {
2150
- videoDom.innerHTML = `
2151
- <video id="video-${
2152
- videoItem.identity
2153
- }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2154
- <div id="board-${videoItem.identity}" class="board">
2155
- <div class="board-icon"></div>
2156
- </div>
2157
- <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2158
- <div id="more-${videoItem.identity}" class="more-icon"></div>
2159
- <div class="describe">
2160
- <div id="microphone-${videoItem.identity}" class="microphone"></div>
2161
- <div id="${videoItem.identity}" class="identity">${
2162
- videoItem.isLocal ? videoItem.name + "(我)" : videoItem.name
2163
- }</div>
2164
- </div>
2165
- `;
2449
+ if (videoItem.metadata?.platformID == 5) {
2450
+ videoDom.innerHTML = `
2451
+ <video id="video-${
2452
+ videoItem.identity
2453
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2454
+ <div id="loadingIndicator-${videoItem.identity}" class="loadingIndicator">
2455
+ <div class="loading-icon"></div>
2456
+ <div class="loading-text">视频流加载中...</div>
2457
+ </div>
2458
+ <div id="board-${videoItem.identity}" class="board">
2459
+ <div class="board-icon"></div>
2460
+ </div>
2461
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2462
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2463
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2464
+ <div class="describe">
2465
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2466
+ <div id="${videoItem.identity}" class="identity">${
2467
+ videoItem.isLocal ? videoItem.name + '(我)' : videoItem.name
2468
+ }</div>
2469
+ </div>
2470
+ `
2471
+ } else {
2472
+ videoDom.innerHTML = `
2473
+ <video id="video-${
2474
+ videoItem.identity
2475
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2476
+ <div id="board-${videoItem.identity}" class="board">
2477
+ <div class="board-icon"></div>
2478
+ </div>
2479
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2480
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2481
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2482
+ <div class="describe">
2483
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2484
+ <div id="${videoItem.identity}" class="identity">${
2485
+ videoItem.isLocal ? videoItem.name + '(我)' : videoItem.name
2486
+ }</div>
2487
+ </div>
2488
+ `
2489
+ }
2166
2490
  }
2167
2491
  return videoDom;
2168
2492
  },
@@ -2311,6 +2635,57 @@ export default {
2311
2635
  layoutRightSideEle.appendChild(videoDiv);
2312
2636
  }
2313
2637
  }
2638
+ } else if (this.currentLayout === 'ring') {
2639
+ // 环形布局:确保容器存在并将与会者放入合适位置
2640
+ let layoutRing = document.querySelector('#room .layout-ring');
2641
+ let layoutRingTop = layoutRing?.querySelector('.layout-ring-top');
2642
+ let layoutRingBottom = layoutRing?.querySelector('.layout-ring-bottom');
2643
+ let ringCenter = layoutRing?.querySelector('.ring-center');
2644
+
2645
+ // 如果容器不存在(可能由于时序问题),创建它
2646
+ if (!layoutRing || !layoutRingTop || !layoutRingBottom || !ringCenter) {
2647
+ layoutRing = document.createElement('div');
2648
+ layoutRing.className = 'layout-ring';
2649
+ layoutRingTop = document.createElement('div');
2650
+ layoutRingTop.className = 'layout-ring-top';
2651
+ layoutRingBottom = document.createElement('div');
2652
+ layoutRingBottom.className = 'layout-ring-bottom';
2653
+ // 创建12个环槽位
2654
+ for (let i = 0; i < 12; i++) {
2655
+ const slot = document.createElement('div');
2656
+ slot.className = 'ring-slot';
2657
+ layoutRingTop.appendChild(slot);
2658
+ }
2659
+ ringCenter = document.createElement('div');
2660
+ ringCenter.className = 'ring-center';
2661
+ layoutRingTop.appendChild(ringCenter);
2662
+ layoutRing.appendChild(layoutRingTop);
2663
+ layoutRing.appendChild(layoutRingBottom);
2664
+ container.insertBefore(layoutRing, container.firstChild ?? null);
2665
+ // 确保环形顶部高度与#room一致
2666
+ requestAnimationFrame(() => this.updateRingTopHeight());
2667
+ }
2668
+
2669
+ const appendToFirstEmptySlot = (el) => {
2670
+ const slots = layoutRingTop.querySelectorAll('.ring-slot');
2671
+ for (let i = 0; i < slots.length; i++) {
2672
+ if (!slots[i].firstChild) {
2673
+ slots[i].appendChild(el);
2674
+ return true;
2675
+ }
2676
+ }
2677
+ return false;
2678
+ };
2679
+
2680
+ // 主持人优先放置在中心;否则进入第一个空槽;都满了则放到底部
2681
+ if (this.curHostIdentity && videoItem.identity === this.curHostIdentity) {
2682
+ ringCenter.appendChild(videoDiv);
2683
+ } else {
2684
+ const placed = appendToFirstEmptySlot(videoDiv);
2685
+ if (!placed) {
2686
+ layoutRingBottom.appendChild(videoDiv);
2687
+ }
2688
+ }
2314
2689
  }
2315
2690
  }
2316
2691
  return container;
@@ -2338,6 +2713,25 @@ export default {
2338
2713
  videoDiv = this.constructParticipantDom(videoItem);
2339
2714
  // 构建会议室布局
2340
2715
  container = this.constructRoomLayout(container, videoDiv, videoItem);
2716
+
2717
+ // 显示与会者画面加载中状态
2718
+ let videoElm = document.getElementById(`video-${videoItem.identity}`)
2719
+ let loadingIndicatorElm = document.getElementById(`loadingIndicator-${videoItem.identity}`)
2720
+ if (videoItem.metadata?.platformID == 5 && videoElm && loadingIndicatorElm) {
2721
+ setTimeout(() => {
2722
+ loadingIndicatorElm.style.display = 'none'
2723
+ }, 2000)
2724
+ }
2725
+ if (videoItem.metadata?.platformID == 4 && videoElm && loadingIndicatorElm) {
2726
+ setTimeout(() => {
2727
+ loadingIndicatorElm.style.display = 'none'
2728
+ }, 4000)
2729
+ }
2730
+
2731
+ // 本地与会者镜像处理
2732
+ if (videoItem.isLocal) {
2733
+ videoElm && this.isMirror && (videoElm.style.transform = 'rotateY(180deg)')
2734
+ }
2341
2735
  // 添加到participant数组
2342
2736
  this.addToParticipantList(videoItem);
2343
2737
  }
@@ -2352,6 +2746,8 @@ export default {
2352
2746
  let signalElm = document.getElementById(`signal-${videoItem.identity}`);
2353
2747
  // 与会者更多操作按钮元素
2354
2748
  let moreElm = document.getElementById(`more-${videoItem.identity}`);
2749
+ // 与会者画面旋转按钮元素
2750
+ let rotateElm = document.getElementById(`rotate-${videoItem.identity}`)
2355
2751
  // 声明麦克风按钮点击事件回调
2356
2752
  const unableMicrophone = () => {
2357
2753
  this.liveClient.changeParticipantMicrophoneStatus(videoItem.identity, true);
@@ -2366,6 +2762,8 @@ export default {
2366
2762
  const currentMoreElm = document.getElementById(`more-${videoItem.identity}`);
2367
2763
  currentSignalElm && (currentSignalElm.style.visibility = "visible");
2368
2764
  currentMoreElm && (currentMoreElm.style.visibility = "visible");
2765
+ const currentRotateElm = document.getElementById(`rotate-${videoItem.identity}`);
2766
+ currentRotateElm && (currentRotateElm.style.visibility = "visible");
2369
2767
  };
2370
2768
  const signalAndMoreHide = () => {
2371
2769
  // 每次执行时重新获取最新的元素
@@ -2373,6 +2771,8 @@ export default {
2373
2771
  const currentMoreElm = document.getElementById(`more-${videoItem.identity}`);
2374
2772
  currentSignalElm && (currentSignalElm.style.visibility = "hidden");
2375
2773
  currentMoreElm && (currentMoreElm.style.visibility = "hidden");
2774
+ const currentRotateElm = document.getElementById(`rotate-${videoItem.identity}`);
2775
+ currentRotateElm && (currentRotateElm.style.visibility = "hidden");
2376
2776
  };
2377
2777
  // 声明操作按钮元素点击事件回调
2378
2778
  const moreElmClick = (event) => {
@@ -2395,6 +2795,13 @@ export default {
2395
2795
  this.moreDialogShow = true;
2396
2796
  }
2397
2797
  };
2798
+ // 旋转按钮点击事件:顺时针旋转90度(并居中适配)
2799
+ const rotateElmClick = () => {
2800
+ const current = this.rotateDegreeMap.get(videoItem.identity) || 0;
2801
+ const next = (current + 90) % 360;
2802
+ this.rotateDegreeMap.set(videoItem.identity, next);
2803
+ this.applyRotation(videoItem.identity, next);
2804
+ };
2398
2805
  // 为麦克风元素绑定事件
2399
2806
  if (microElm && videoItem.isMicrophoneEnabled) {
2400
2807
  microElm.className = "microphone microphone-active";
@@ -2422,12 +2829,47 @@ export default {
2422
2829
  if (moreElm) {
2423
2830
  moreElm.onclick = moreElmClick;
2424
2831
  }
2832
+ if (rotateElm) {
2833
+ rotateElm.onclick = rotateElmClick;
2834
+ }
2425
2835
  if (signalElm) {
2426
2836
  if (videoItem.connectionQuality === "excellent" || videoItem.connectionQuality === "good") {
2427
2837
  signalElm.className = "signal-icon signal-icon-good";
2428
2838
  } else {
2429
2839
  signalElm.className = "signal-icon signal-icon-poor";
2430
2840
  }
2841
+ // 为信号图标绑定鼠标悬停事件,显示码率信息
2842
+ signalElm.onmouseenter = () => {
2843
+ const bitrateElm = document.getElementById(`bitrate-${videoItem.identity}`)
2844
+ if (bitrateElm) {
2845
+ const bitrate = this.liveClient.getParticipantBitrate(videoItem.identity)
2846
+ console.log('bitrate', bitrate)
2847
+ if (bitrate !== null) {
2848
+ bitrateElm.innerText = `${bitrate} kbps`
2849
+ bitrateElm.style.display = 'block'
2850
+ }
2851
+ }
2852
+ }
2853
+ signalElm.onmouseleave = () => {
2854
+ const bitrateElm = document.getElementById(`bitrate-${videoItem.identity}`)
2855
+ if (bitrateElm) {
2856
+ bitrateElm.style.display = 'none'
2857
+ }
2858
+ }
2859
+ }
2860
+ // 根据platformID设定元素样式
2861
+ if (
2862
+ videoItem.metadata?.platformID == 1 ||
2863
+ videoItem.metadata?.platformID == 4 ||
2864
+ videoItem.metadata?.platformID == 7
2865
+ ) {
2866
+ videoElm && (videoElm.style.objectFit = 'contain')
2867
+ } else {
2868
+ if (videoItem?.source == 'camera') {
2869
+ videoElm && (videoElm.style.objectFit = 'cover')
2870
+ } else {
2871
+ videoElm && (videoElm.style.objectFit = 'contain')
2872
+ }
2431
2873
  }
2432
2874
  let screenShareElm = document.getElementById(`screenshare-${videoItem.identity}`);
2433
2875
  // 与会者dom元素内部样式重新渲染(摄像头或屏幕共享切换后)
@@ -2477,6 +2919,15 @@ export default {
2477
2919
  signalElm.className = "signal-icon signal-icon-poor";
2478
2920
  }
2479
2921
  videoDiv.appendChild(signalElm);
2922
+
2923
+ // 构建码率显示元素
2924
+ let bitrateElm = document.getElementById(`bitrate-${identity}`)
2925
+ if (!bitrateElm) {
2926
+ bitrateElm = document.createElement('div')
2927
+ bitrateElm.id = `bitrate-${videoItem.identity}`
2928
+ bitrateElm.className = 'bitrate-indicator'
2929
+ videoDiv.appendChild(bitrateElm)
2930
+ }
2480
2931
  }
2481
2932
  // if (!moreElm) {
2482
2933
  // // 构建更多操作按钮元素
@@ -2499,6 +2950,28 @@ export default {
2499
2950
  moreElm = null;
2500
2951
  }
2501
2952
  }
2953
+ // 仅在platformID为4展示旋转按钮(对所有与会者可见)
2954
+ if (videoItem.metadata?.platformID === 4) {
2955
+ if (!rotateElm) {
2956
+ rotateElm = document.createElement('div');
2957
+ rotateElm.id = `rotate-${videoItem.identity}`;
2958
+ rotateElm.className = 'rotate-icon rotate-icon1';
2959
+ videoDiv && videoDiv.appendChild(rotateElm);
2960
+ rotateElm.onclick = rotateElmClick;
2961
+ }
2962
+ // 初始化map中度数(如果不存在)并应用到元素
2963
+ if (!this.rotateDegreeMap.has(videoItem.identity)) {
2964
+ this.rotateDegreeMap.set(videoItem.identity, 0);
2965
+ }
2966
+ this.applyRotation(videoItem.identity, this.rotateDegreeMap.get(videoItem.identity));
2967
+ } else {
2968
+ // 如果不是platformID 4,移除旋转按钮及map记录
2969
+ if (rotateElm) {
2970
+ try { videoDiv.removeChild(rotateElm); } catch(e) {}
2971
+ rotateElm = null;
2972
+ }
2973
+ this.rotateDegreeMap.delete(videoItem.identity);
2974
+ }
2502
2975
  } else {
2503
2976
  if (!boardElm) {
2504
2977
  // 之前为屏幕共享状态或摄像头开启状态
@@ -2532,6 +3005,11 @@ export default {
2532
3005
  this.removeFromParticipantList(videoItem.identity);
2533
3006
  this.filterParticipantList();
2534
3007
 
3008
+ // 清理可能存在的旋转状态
3009
+ if (this.rotateDegreeMap.has(videoItem.identity)) {
3010
+ this.rotateDegreeMap.delete(videoItem.identity);
3011
+ }
3012
+
2535
3013
  // 处理焦点用户离开的情况
2536
3014
  if (this.curBlurIdentity === videoItem.identity) {
2537
3015
  console.log("焦点用户离开会议:", videoItem.identity);
@@ -2559,18 +3037,6 @@ export default {
2559
3037
  this.curHostIdentity = null;
2560
3038
  // 主持人离开后具体的逻辑处理放到watch中处理
2561
3039
  }
2562
- // pointTurn模式下,主持人离开处理
2563
- // if (currentRoomMode.value === "pointTurn" && videoItem.identity === curHostIdentity.value) {
2564
- // 当前离开与会者为会议主持人
2565
- // const pointTurnCenter = document.querySelector("#room .point-turn-center");
2566
- // if (pointTurnCenter) {
2567
- // // 会议中没有其他主持人,显示默认占位元素
2568
- // const placeHolderElm = document.createElement("div");
2569
- // placeHolderElm.className = "participant";
2570
- // placeHolderElm.innerHTML = placeholderTemplate;
2571
- // pointTurnCenter.insertBefore(placeHolderElm, pointTurnCenter.firstChild ?? null);
2572
- // }
2573
- // }
2574
3040
  // pointTurn模式下的其他用户离开处理
2575
3041
  if (
2576
3042
  this.currentRoomMode === "pointTurn" &&
@@ -2679,6 +3145,10 @@ export default {
2679
3145
  // 更新与会者数组
2680
3146
  this.addToParticipantList(videoItem);
2681
3147
  this.filterParticipantList();
3148
+ // 每次render时,如果该用户在rotateDegreeMap中,则应用其旋转到video元素(确保DOM更新后生效)
3149
+ if (this.rotateDegreeMap.has(videoItem.identity)) {
3150
+ this.applyRotation(videoItem.identity, this.rotateDegreeMap.get(videoItem.identity));
3151
+ }
2682
3152
  // 与会者结束共享,切换回到之前布局(仅主持人执行)
2683
3153
  if (this.currentRoomMode === "normal" && this.judgeParticipantIsHost(this.localIdentity)) {
2684
3154
  console.log(
@@ -2891,7 +3361,73 @@ export default {
2891
3361
 
2892
3362
  if (oldLayout === "grid" && newLayout === "ring") {
2893
3363
  // 从宫格布局切换到环状布局
2894
- // TODO: 实现环状布局逻辑
3364
+ // 实现环形布局:上半部分为4x4网格(中心2x2为主持人独占),其余12个位置按从上到下、从左到右顺序填充;
3365
+ // 超过13人的用户放到下半部分按4x4顺序排列
3366
+
3367
+ // 清理可能残留的环形容器
3368
+ const existingRing = document.querySelector('#room .layout-ring');
3369
+ if (existingRing) {
3370
+ console.warn('发现残留的环形布局容器,正在清理');
3371
+ existingRing.remove();
3372
+ }
3373
+
3374
+ // 创建环形布局容器
3375
+ const layoutRing = document.createElement('div');
3376
+ layoutRing.className = 'layout-ring';
3377
+
3378
+ const layoutRingTop = document.createElement('div');
3379
+ layoutRingTop.className = 'layout-ring-top';
3380
+
3381
+ const layoutRingBottom = document.createElement('div');
3382
+ layoutRingBottom.className = 'layout-ring-bottom';
3383
+
3384
+ // 按照4x4网格的顺序,但排除中心2x2的位置(索引 5,6,9,10),插入12个slot
3385
+ const slotOrder = [0,1,2,3,4,7,8,11,12,13,14,15];
3386
+ slotOrder.forEach((pos, idx) => {
3387
+ const slot = document.createElement('div');
3388
+ slot.className = 'ring-slot';
3389
+ slot.dataset.pos = String(pos);
3390
+ layoutRingTop.appendChild(slot);
3391
+ });
3392
+
3393
+ // 中央占位,用于主持人占据2x2
3394
+ const ringCenter = document.createElement('div');
3395
+ ringCenter.className = 'ring-center';
3396
+ layoutRingTop.appendChild(ringCenter);
3397
+
3398
+ // 将元素从主容器中收集并按规则分配到环形布局
3399
+ const participantElements = Array.from(container.children).filter(child => child.classList && child.classList.contains('participant') && child.id && child.id.startsWith('participant-'));
3400
+
3401
+ // 将主持人放到center,其余依序填充top slots,溢出进入bottom
3402
+ let filledCount = 0;
3403
+ participantElements.forEach((child) => {
3404
+ try {
3405
+ const id = child.id.replace('participant-', '');
3406
+ if (this.curHostIdentity && id === this.curHostIdentity) {
3407
+ ringCenter.appendChild(child);
3408
+ } else {
3409
+ if (filledCount < 12) {
3410
+ const slot = layoutRingTop.querySelectorAll('.ring-slot')[filledCount];
3411
+ slot && slot.appendChild(child);
3412
+ filledCount++;
3413
+ } else {
3414
+ layoutRingBottom.appendChild(child);
3415
+ }
3416
+ }
3417
+ } catch (err) {
3418
+ console.error('分配参与者到环形布局时出错', err);
3419
+ }
3420
+ });
3421
+
3422
+ layoutRing.appendChild(layoutRingTop);
3423
+ layoutRing.appendChild(layoutRingBottom);
3424
+
3425
+ // 清理主容器中可能存在的非participant占位元素(避免重复)
3426
+ // 移除原有非布局容器元素
3427
+ // 插入新的环形布局
3428
+ container.insertBefore(layoutRing, container.firstChild ?? null);
3429
+ // 切换到环形后,同步一次顶部高度
3430
+ requestAnimationFrame(() => this.updateRingTopHeight());
2895
3431
  }
2896
3432
 
2897
3433
  if (oldLayout === "grid" && newLayout === "downLSide") {
@@ -2939,6 +3475,178 @@ export default {
2939
3475
  // 添加其他元素
2940
3476
  container.appendChild(fragment);
2941
3477
  }
3478
+ // 从焦点布局切换到环形布局
3479
+ if (oldLayout === 'rightSide' && newLayout === 'ring') {
3480
+ // 将右侧/左侧容器拆解回主容器,然后重用 grid->ring 的逻辑
3481
+ let layoutRightSideEle = document.querySelector('#room .layout-rightside');
3482
+ let layoutLeftSideEle = document.querySelector('#room .layout-leftside');
3483
+
3484
+ // 把左右容器内的元素移动回主容器顺序(左侧优先)
3485
+ const moveChildrenToContainer = (ele) => {
3486
+ if (!ele) return;
3487
+ let child = null;
3488
+ while ((child = ele.firstChild)) {
3489
+ container.appendChild(child);
3490
+ }
3491
+ if (container.contains(ele)) container.removeChild(ele);
3492
+ };
3493
+ moveChildrenToContainer(layoutLeftSideEle);
3494
+ moveChildrenToContainer(layoutRightSideEle);
3495
+
3496
+ // 触发自身来走 grid->ring 分支,通过重新调用 executeLayoutChange 从 container 状态转换
3497
+ // 为避免递归/竞态,我们直接调用 the grid->ring block by setting variables here.
3498
+ // 简化做法:复制 grid->ring behavior here
3499
+ const existingRing = document.querySelector('#room .layout-ring');
3500
+ if (existingRing) existingRing.remove();
3501
+ const layoutRing = document.createElement('div');
3502
+ layoutRing.className = 'layout-ring';
3503
+ const layoutRingTop = document.createElement('div');
3504
+ layoutRingTop.className = 'layout-ring-top';
3505
+ const layoutRingBottom = document.createElement('div');
3506
+ layoutRingBottom.className = 'layout-ring-bottom';
3507
+ const slotOrder = [0,1,2,3,4,7,8,11,12,13,14,15];
3508
+ slotOrder.forEach((pos) => {
3509
+ const slot = document.createElement('div');
3510
+ slot.className = 'ring-slot';
3511
+ slot.dataset.pos = String(pos);
3512
+ layoutRingTop.appendChild(slot);
3513
+ });
3514
+ const ringCenter = document.createElement('div');
3515
+ ringCenter.className = 'ring-center';
3516
+ layoutRingTop.appendChild(ringCenter);
3517
+ const participantElements = Array.from(container.children).filter(child => child.classList && child.classList.contains('participant') && child.id && child.id.startsWith('participant-'));
3518
+ let filledCount = 0;
3519
+ participantElements.forEach((child) => {
3520
+ const id = child.id.replace('participant-', '');
3521
+ if (this.curHostIdentity && id === this.curHostIdentity) {
3522
+ ringCenter.appendChild(child);
3523
+ } else {
3524
+ if (filledCount < 12) {
3525
+ const slot = layoutRingTop.querySelectorAll('.ring-slot')[filledCount];
3526
+ slot && slot.appendChild(child);
3527
+ filledCount++;
3528
+ } else {
3529
+ layoutRingBottom.appendChild(child);
3530
+ }
3531
+ }
3532
+ });
3533
+ layoutRing.appendChild(layoutRingTop);
3534
+ layoutRing.appendChild(layoutRingBottom);
3535
+ container.insertBefore(layoutRing, container.firstChild ?? null);
3536
+ // 右侧边栏 -> 环形:创建后同步高度
3537
+ requestAnimationFrame(() => this.updateRingTopHeight());
3538
+ }
3539
+
3540
+ // 从环形布局切换到宫格布局
3541
+ if (oldLayout === 'ring' && newLayout === 'grid') {
3542
+ const layoutRing = document.querySelector('#room .layout-ring');
3543
+ if (layoutRing) {
3544
+ const top = layoutRing.querySelector('.layout-ring-top');
3545
+ const bottom = layoutRing.querySelector('.layout-ring-bottom');
3546
+ // 将top中的slot里的参与者按slot顺序移动回主容器
3547
+ if (top) {
3548
+ const slots = top.querySelectorAll('.ring-slot');
3549
+ slots.forEach(slot => {
3550
+ while (slot.firstChild) {
3551
+ container.appendChild(slot.firstChild);
3552
+ }
3553
+ slot.remove();
3554
+ });
3555
+ const center = top.querySelector('.ring-center');
3556
+ if (center) {
3557
+ while (center.firstChild) {
3558
+ container.appendChild(center.firstChild);
3559
+ }
3560
+ center.remove();
3561
+ }
3562
+ }
3563
+ if (bottom) {
3564
+ while (bottom.firstChild) {
3565
+ container.appendChild(bottom.firstChild);
3566
+ }
3567
+ }
3568
+ layoutRing.remove();
3569
+ }
3570
+ }
3571
+
3572
+ // 从环形布局切换到焦点布局
3573
+ if (oldLayout === 'ring' && newLayout === 'rightSide') {
3574
+ // 先把ring拆平回主容器,再复用 rightSide creation logic
3575
+ const layoutRing = document.querySelector('#room .layout-ring');
3576
+ if (layoutRing) {
3577
+ const top = layoutRing.querySelector('.layout-ring-top');
3578
+ const bottom = layoutRing.querySelector('.layout-ring-bottom');
3579
+ if (top) {
3580
+ const slots = top.querySelectorAll('.ring-slot');
3581
+ slots.forEach(slot => {
3582
+ while (slot.firstChild) {
3583
+ container.appendChild(slot.firstChild);
3584
+ }
3585
+ });
3586
+ const center = top.querySelector('.ring-center');
3587
+ if (center) {
3588
+ while (center.firstChild) {
3589
+ container.appendChild(center.firstChild);
3590
+ }
3591
+ }
3592
+ }
3593
+ if (bottom) {
3594
+ while (bottom.firstChild) {
3595
+ container.appendChild(bottom.firstChild);
3596
+ }
3597
+ }
3598
+ layoutRing.remove();
3599
+ }
3600
+
3601
+ // 现在容器已被平铺,复用已有 grid->rightSide 代码 path by creating left/right containers
3602
+ let layoutRightSideEle = document.createElement('div');
3603
+ layoutRightSideEle.className = 'layout-rightside';
3604
+ let layoutLeftSideEle = document.createElement('div');
3605
+ layoutLeftSideEle.className = 'layout-leftside';
3606
+
3607
+ // 确定焦点用户 - 优先级:当前焦点用户 > 主持人 > 本地用户
3608
+ let focusVideoItem = null;
3609
+ let blurDom = null;
3610
+ if (this.curBlurIdentity) {
3611
+ focusVideoItem = getUserItemByIdentity(this.curBlurIdentity);
3612
+ if (focusVideoItem) blurDom = document.getElementById(`participant-${focusVideoItem.identity}`);
3613
+ }
3614
+ if (!blurDom && this.curHostIdentity) {
3615
+ focusVideoItem = getUserItemByIdentity(this.curHostIdentity);
3616
+ if (focusVideoItem) blurDom = document.getElementById(`participant-${focusVideoItem.identity}`);
3617
+ }
3618
+ if (!blurDom) {
3619
+ focusVideoItem = getLocalParticipant();
3620
+ if (focusVideoItem) blurDom = document.getElementById(`participant-${focusVideoItem.identity}`);
3621
+ }
3622
+
3623
+ if (blurDom) {
3624
+ if (layoutLeftSideEle.hasChildNodes()) {
3625
+ let child;
3626
+ while ((child = layoutLeftSideEle.firstChild)) {
3627
+ layoutRightSideEle.appendChild(child);
3628
+ }
3629
+ }
3630
+ layoutLeftSideEle.appendChild(blurDom);
3631
+
3632
+ // 将剩余元素移动到右侧
3633
+ if (container.hasChildNodes()) {
3634
+ let child;
3635
+ while ((child = container.firstElementChild)) {
3636
+ if (child && child !== layoutLeftSideEle && child !== layoutRightSideEle) {
3637
+ layoutRightSideEle.appendChild(child);
3638
+ } else {
3639
+ if (child) container.removeChild(child);
3640
+ }
3641
+ }
3642
+ }
3643
+
3644
+ container.insertBefore(layoutLeftSideEle, container.firstChild ?? null);
3645
+ container.appendChild(layoutRightSideEle);
3646
+ } else {
3647
+ this.showMessage.warning('无法找到有效的焦点用户');
3648
+ }
3649
+ }
2942
3650
  }
2943
3651
  });
2944
3652
  },
@@ -3038,7 +3746,6 @@ export default {
3038
3746
  this.liveClient.deleteUnjoinParticipant(this.meetingNum, item.identity).then((res) => {
3039
3747
  if (res.code == 200) {
3040
3748
  this.showMessage.message("success", "成功删除未入会人员");
3041
- // this.getUnjoinParticipant();
3042
3749
  this.removeFromInviteList(item.identity);
3043
3750
  } else {
3044
3751
  this.showMessage.message("error", res?.msg);
@@ -3278,12 +3985,12 @@ export default {
3278
3985
  },
3279
3986
  async openCamera(e) {
3280
3987
  if (this.liveClient) {
3281
- await this.liveClient.changeParticipantCameraStatus(e, true);
3988
+ await this.liveClient.changeParticipantCameraStatus(e, false);
3282
3989
  }
3283
3990
  },
3284
3991
  async closeCamera(e) {
3285
3992
  if (this.liveClient) {
3286
- await this.liveClient.changeParticipantCameraStatus(e, false);
3993
+ await this.liveClient.changeParticipantCameraStatus(e, true);
3287
3994
  }
3288
3995
  },
3289
3996
  async setToHost(e) {
@@ -3335,9 +4042,43 @@ export default {
3335
4042
  async appendInviteDevice(e) {
3336
4043
  console.log("通讯录邀请设备", e);
3337
4044
  if (e && e.length > 0) {
3338
- e.forEach((item) => {
3339
- this.pullMonitorDevice(item?.equipmentID || item?.monitorID, item.label);
3340
- });
4045
+ // 构建批量外呼参数并一次性发起
4046
+ const deviceCalls = e
4047
+ .map(item => {
4048
+ const id = item?.equipmentID || item?.monitorID
4049
+ return id ? { dnis: id, name: item.label, type: this.handleInviteType(item?.integrationType) } : null
4050
+ })
4051
+ .filter(Boolean)
4052
+
4053
+ if (deviceCalls.length > 0) {
4054
+ try {
4055
+ await this.liveClient.makeBatchCall(deviceCalls)
4056
+ this.showMessage.message('success', '已发起通讯录设备批量外呼')
4057
+ } catch (err) {
4058
+ this.showMessage.message('error', `通讯录设备批量外呼失败: ${err?.message || err}`)
4059
+ }
4060
+ }
4061
+ }
4062
+ },
4063
+ async appendInviteTerminal(e) {
4064
+ console.log('通讯录邀请会议终端', e)
4065
+ if (e && e.length > 0) {
4066
+ // 构建批量外呼参数并一次性发起
4067
+ const terminalCalls = e
4068
+ .map(item => {
4069
+ const id = item?.id
4070
+ return id ? { dnis: id, name: item.label, type: 4 } : null
4071
+ })
4072
+ .filter(Boolean)
4073
+
4074
+ if (terminalCalls.length > 0) {
4075
+ try {
4076
+ await this.liveClient.makeBatchCall(terminalCalls)
4077
+ this.showMessage.message('success', '已发起会议终端批量外呼')
4078
+ } catch (err) {
4079
+ this.showMessage.message('error', `会议终端批量外呼失败: ${err?.message || err}`)
4080
+ }
4081
+ }
3341
4082
  }
3342
4083
  },
3343
4084
  async updateNameConfirm(e) {
@@ -3369,7 +4110,30 @@ export default {
3369
4110
  type,
3370
4111
  });
3371
4112
  },
4113
+ dispatchLocalTrack(kind = '') {
4114
+ if (this.participantNum > 0) {
4115
+ let index = this.participants.findIndex(item => item.isLocal)
4116
+ if (index !== -1) {
4117
+ let localVideoTrack = this.participants[index]?.videoTrack
4118
+ let localAudioTrack = this.participants[index]?.audioTrack
4119
+ if (!kind) {
4120
+ localVideoTrack?.detach()
4121
+ localVideoTrack?.stop()
4122
+ localAudioTrack?.detach()
4123
+ localAudioTrack?.stop()
4124
+ } else if (kind === 'video') {
4125
+ localVideoTrack?.detach()
4126
+ localVideoTrack?.stop()
4127
+ } else if (kind === 'audio') {
4128
+ localAudioTrack?.detach()
4129
+ localAudioTrack?.stop()
4130
+ }
4131
+ }
4132
+ }
4133
+ },
3372
4134
  async chooseVideoDevice(e) {
4135
+ this.dispatchLocalTrack('video')
4136
+ await this.sleep(100)
3373
4137
  await this.changeActiveDevice("videoinput", e);
3374
4138
  this.videoSelectShow = false;
3375
4139
  },
@@ -3431,6 +4195,7 @@ export default {
3431
4195
  appendInvite(e, inviteWay) {
3432
4196
  let tempList = [];
3433
4197
  let tempDeviceList = []
4198
+ let tempTerminalList = []
3434
4199
  let index = -1;
3435
4200
  if (e && e.length > 0) {
3436
4201
  e.forEach((item) => {
@@ -3451,12 +4216,15 @@ export default {
3451
4216
  tempList.push(item);
3452
4217
  }
3453
4218
  this.addToInviteList(item);
3454
- } else {
4219
+ } else if (item.source == '设备' || item.source == '监控') {
3455
4220
  tempDeviceList.push(item)
4221
+ } else if (item.source == '会议终端') {
4222
+ tempTerminalList.push(item)
3456
4223
  }
3457
4224
  });
3458
4225
  console.log("本次追加邀请人员", tempList);
3459
4226
  console.log("本次追加邀请设备", tempDeviceList);
4227
+ console.log("本次追加邀请会议终端", tempTerminalList);
3460
4228
  // 追加邀请人员
3461
4229
  if(tempList.length > 0) {
3462
4230
  const promises = [];
@@ -3485,7 +4253,8 @@ export default {
3485
4253
  const volteCalls = tempList.map(item => {
3486
4254
  return {
3487
4255
  dnis: item.phone,
3488
- name: item?.label || item?.userName || '未知用户'
4256
+ name: item?.label || item?.userName || '未知用户',
4257
+ type: 0,
3489
4258
  }
3490
4259
  })
3491
4260
  promises.push(
@@ -3524,25 +4293,52 @@ export default {
3524
4293
  });
3525
4294
  }
3526
4295
  // 追加邀请设备
3527
- if(tempDeviceList.length > 0) {
3528
- tempDeviceList.forEach(item => {
3529
- if(item.source == "设备") {
3530
- pullMonitorDevice(item.equipmentID, item.label)
3531
- } else if(item.source == "监控") {
3532
- pullMonitorDevice(item.monitorID, item.label)
3533
- }
3534
- })
4296
+ if (tempDeviceList && tempDeviceList.length > 0) {
4297
+ const deviceCalls = tempDeviceList
4298
+ .map(item => {
4299
+ const id = item?.source === '监控' ? item?.monitorID : item?.equipmentID;
4300
+ return id ? { dnis: id, name: item.label, type: this.handleInviteType(item?.integrationType) } : null;
4301
+ })
4302
+ .filter(Boolean);
4303
+
4304
+ if (deviceCalls.length > 0) {
4305
+ this.liveClient.makeBatchCall(deviceCalls)
4306
+ .then(() => {
4307
+ this.showMessage.message('success', '监控设备批量外呼已发起');
4308
+ })
4309
+ .catch(err => {
4310
+ this.showMessage.message('error', `批量拉取监控设备失败: ${err?.message || err}`);
4311
+ });
4312
+ }
4313
+ }
4314
+ // 追加邀请终端
4315
+ if (tempTerminalList.length > 0) {
4316
+ const terminalCalls = tempTerminalList
4317
+ .map(item => {
4318
+ return item?.id ? { dnis: item.id, name: item.label, type: 4 } : null
4319
+ })
4320
+ .filter(Boolean)
4321
+ if (terminalCalls.length > 0) {
4322
+ this.liveClient
4323
+ .makeBatchCall(terminalCalls)
4324
+ .then(() => {
4325
+ this.showMessage.message('success', '会议终端批量外呼已发起')
4326
+ })
4327
+ .catch(err => {
4328
+ this.showMessage.message('error', `批量拉取会议终端失败: ${err?.message || err}`)
4329
+ })
4330
+ }
3535
4331
  }
3536
4332
  }
3537
4333
  },
3538
- pullMonitorDevice(monitorID, monitorName) {
4334
+ pullMonitorDevice(monitorID, monitorName, integrationType = 0) {
3539
4335
  this.liveClient.judgeUserInMeeting(this.meetingNum, monitorID).then((res) => {
3540
4336
  if (res && res?.code == 200) {
3541
4337
  if (res.data == 1) {
3542
4338
  this.showMessage.message("error", "该监控设备已进入会议");
3543
4339
  return;
3544
4340
  } else {
3545
- this.liveClient.makeCall(monitorName, monitorID, 1);
4341
+ this.liveClient.makeCall(monitorName, monitorID, this.handleInviteType(integrationType));
3546
4342
  }
3547
4343
  } else {
3548
4344
  this.showMessage.message("error", "获取监控设备进会状态失败");
@@ -3557,7 +4353,8 @@ export default {
3557
4353
  const platformGroup = {
3558
4354
  '1_2': [], // platformID 为 1 或 2
3559
4355
  '7': [], // platformID 为 7
3560
- '4': [] // platformID 为 4
4356
+ '4': [], // platformID 为 4
4357
+ '8': [] // platformID 为 8
3561
4358
  };
3562
4359
 
3563
4360
  uninviteList.forEach(item => {
@@ -3569,6 +4366,8 @@ export default {
3569
4366
  platformGroup['7'].push({ userName, identity, phone });
3570
4367
  } else if (platformID === 4) {
3571
4368
  platformGroup['4'].push({ userName, identity, phone });
4369
+ } else if (platformID === 8) {
4370
+ platformGroup['8'].push({ userName, identity, phone });
3572
4371
  }
3573
4372
  });
3574
4373
 
@@ -3619,7 +4418,8 @@ export default {
3619
4418
  // 构建批量外呼数据
3620
4419
  const volteCalls = platformGroup['4'].map(item => ({
3621
4420
  dnis: item.phone,
3622
- name: item.userName
4421
+ name: item.userName,
4422
+ type: 0
3623
4423
  }));
3624
4424
 
3625
4425
  promises.push(
@@ -3636,6 +4436,27 @@ export default {
3636
4436
  );
3637
4437
  }
3638
4438
 
4439
+ // 处理 platformID 为 8 的呼叫
4440
+ if (platformGroup['8'].length > 0) {
4441
+ // 构建批量外呼数据
4442
+ const terminalCalls = platformGroup['8'].map(item => ({
4443
+ dnis: item.phone,
4444
+ name: item.userName,
4445
+ type: 4
4446
+ }))
4447
+
4448
+ promises.push(
4449
+ this.liveClient
4450
+ .makeBatchCall(terminalCalls)
4451
+ .then(() => {
4452
+ this.showMessage.message('success', '已发起终端批量外呼')
4453
+ })
4454
+ .catch(err => {
4455
+ this.showMessage.message('error', `批量呼叫失败 (platformID 8): ${err.message}`)
4456
+ })
4457
+ )
4458
+ }
4459
+
3639
4460
  // 等待所有邀请完成后更新列表
3640
4461
  Promise.allSettled(promises).then(() => {
3641
4462
  // 可以在这里添加统一的后续处理,如刷新邀请列表
@@ -4159,6 +4980,64 @@ export default {
4159
4980
  }
4160
4981
  }
4161
4982
 
4983
+ if (this.currentLayout === 'ring') {
4984
+ // 从环形布局到点调模式:先拆解环形容器,将参与者恢复到主容器,然后按照点调规则分配
4985
+ const layoutRing = document.querySelector('#room .layout-ring');
4986
+ if (layoutRing) {
4987
+ const ringTop = layoutRing.querySelector('.layout-ring-top');
4988
+ const ringBottom = layoutRing.querySelector('.layout-ring-bottom');
4989
+ const ringCenter = layoutRing.querySelector('.ring-center');
4990
+ // 先将center、slots、bottom中的参与者移动回主容器
4991
+ if (ringTop) {
4992
+ const slots = ringTop.querySelectorAll('.ring-slot');
4993
+ slots.forEach(slot => {
4994
+ while (slot.firstChild) {
4995
+ container.appendChild(slot.firstChild);
4996
+ }
4997
+ });
4998
+ if (ringCenter) {
4999
+ while (ringCenter.firstChild) {
5000
+ container.appendChild(ringCenter.firstChild);
5001
+ }
5002
+ }
5003
+ }
5004
+ if (ringBottom) {
5005
+ while (ringBottom.firstChild) {
5006
+ container.appendChild(ringBottom.firstChild);
5007
+ }
5008
+ }
5009
+ layoutRing.remove();
5010
+ }
5011
+
5012
+ // 收集参与者元素
5013
+ const participantElements = Array.from(container.children).filter(child =>
5014
+ child.classList && child.classList.contains('participant') && child.id && child.id.startsWith('participant-')
5015
+ );
5016
+
5017
+ // 分配到点调容器:主持人优先到center[0],焦点到center[1](如果存在且不同),其余进入top/bottom/other
5018
+ // 注意:pointTurnCenter已有两个占位符 placeHolderElm1, placeHolderElm2
5019
+ participantElements.forEach(child => {
5020
+ if (this.curHostIdentity && child.id === `participant-${this.curHostIdentity}`) {
5021
+ // 主持人放在中心第一个位置
5022
+ pointTurnCenter.removeChild(placeHolderElm1);
5023
+ pointTurnCenter.insertBefore(child, pointTurnCenter.firstChild ?? null);
5024
+ } else if (this.curBlurIdentity && child.id === `participant-${this.curBlurIdentity}` && this.curBlurIdentity !== this.curHostIdentity) {
5025
+ // 焦点用户放在中心第二个位置(若不同于主持人)
5026
+ pointTurnCenter.removeChild(placeHolderElm2);
5027
+ pointTurnCenter.appendChild(child);
5028
+ } else {
5029
+ if (pointTurnTop.children.length >= 5) {
5030
+ if (pointTurnBottom.children.length >= 5) {
5031
+ pointTurnOther.appendChild(child);
5032
+ } else {
5033
+ pointTurnBottom.appendChild(child);
5034
+ }
5035
+ } else {
5036
+ pointTurnTop.appendChild(child);
5037
+ }
5038
+ }
5039
+ });
5040
+ }
4162
5041
  // 添加点调容器之前的最终检查
4163
5042
  const remainingLayoutElements = container.querySelectorAll(
4164
5043
  ".layout-leftside, .layout-rightside, .point-turn-top, .point-turn-center, .point-turn-bottom, .point-turn-other"
@@ -4327,6 +5206,92 @@ export default {
4327
5206
  container.insertBefore(layoutLeftSideEle, container.firstChild ?? null);
4328
5207
  container.appendChild(layoutRightSideEle);
4329
5208
  }
5209
+
5210
+ if (this.currentLayout === 'ring') {
5211
+ // 从点调模式到环形布局:创建环形容器,将与会者分配到center/slots/bottom
5212
+ const existingRing = document.querySelector('#room .layout-ring');
5213
+ if (existingRing) {
5214
+ existingRing.remove();
5215
+ }
5216
+ const layoutRing = document.createElement('div');
5217
+ layoutRing.className = 'layout-ring';
5218
+ const layoutRingTop = document.createElement('div');
5219
+ layoutRingTop.className = 'layout-ring-top';
5220
+ const layoutRingBottom = document.createElement('div');
5221
+ layoutRingBottom.className = 'layout-ring-bottom';
5222
+ // 创建12个槽位
5223
+ for (let i = 0; i < 12; i++) {
5224
+ const slot = document.createElement('div');
5225
+ slot.className = 'ring-slot';
5226
+ layoutRingTop.appendChild(slot);
5227
+ }
5228
+ const ringCenter = document.createElement('div');
5229
+ ringCenter.className = 'ring-center';
5230
+ layoutRingTop.appendChild(ringCenter);
5231
+
5232
+ // 将点调容器中的参与者按顺序分配:center(主持人 -> 焦点)、再从top、bottom、other依序填充slots;超出放到bottom
5233
+ const collect = (ele) => (ele ? Array.from(ele.children).filter(c => c.id && c.classList.contains('participant')) : []);
5234
+ const topList = collect(pointTurnTop);
5235
+ const centerList = collect(pointTurnCenter);
5236
+ const bottomList = collect(pointTurnBottom);
5237
+ const otherList = collect(pointTurnOther);
5238
+
5239
+ // 先处理中心:主持人在center首位,若有焦点且不同于主持人,作为第二位
5240
+ let hostEl = centerList.find(c => this.curHostIdentity && c.id === `participant-${this.curHostIdentity}`);
5241
+ if (!hostEl) {
5242
+ // 尝试从其它列表找到主持人
5243
+ hostEl = [...topList, ...bottomList, ...otherList].find(c => this.curHostIdentity && c.id === `participant-${this.curHostIdentity}`);
5244
+ if (hostEl && hostEl.parentElement) hostEl.parentElement.removeChild(hostEl);
5245
+ }
5246
+ if (hostEl) {
5247
+ ringCenter.appendChild(hostEl);
5248
+ }
5249
+ let blurEl = null;
5250
+ if (this.curBlurIdentity && this.curBlurIdentity !== this.curHostIdentity) {
5251
+ blurEl = centerList.find(c => c.id === `participant-${this.curBlurIdentity}`)
5252
+ || topList.find(c => c.id === `participant-${this.curBlurIdentity}`)
5253
+ || bottomList.find(c => c.id === `participant-${this.curBlurIdentity}`)
5254
+ || otherList.find(c => c.id === `participant-${this.curBlurIdentity}`);
5255
+ if (blurEl && blurEl.parentElement) {
5256
+ blurEl.parentElement.removeChild(blurEl);
5257
+ // 注意:ring 只在中心留主持人,焦点不会占用中心,因此将其回到 slots 序列,不放中心
5258
+ }
5259
+ }
5260
+
5261
+ // 构建顺序序列:top -> bottom -> other(去除已被拿走的host/blur)
5262
+ const rest = [...topList, ...bottomList, ...otherList].filter(el => el !== hostEl && el !== blurEl);
5263
+ const slots = layoutRingTop.querySelectorAll('.ring-slot');
5264
+ let filled = 0;
5265
+ // 先将可能存在的焦点用户放在第一个可用槽(如果存在)
5266
+ if (blurEl) {
5267
+ if (filled < slots.length) {
5268
+ slots[filled++].appendChild(blurEl);
5269
+ } else {
5270
+ layoutRingBottom.appendChild(blurEl);
5271
+ }
5272
+ }
5273
+ // 填充剩余slots
5274
+ for (const el of rest) {
5275
+ if (filled < slots.length) {
5276
+ slots[filled++].appendChild(el);
5277
+ } else {
5278
+ layoutRingBottom.appendChild(el);
5279
+ }
5280
+ }
5281
+
5282
+ // 清理点调容器并挂载环容器
5283
+ const removeIfChild = (ele) => { if (ele && container.contains(ele)) container.removeChild(ele); };
5284
+ removeIfChild(pointTurnTop);
5285
+ removeIfChild(pointTurnCenter);
5286
+ removeIfChild(pointTurnBottom);
5287
+ removeIfChild(pointTurnOther);
5288
+
5289
+ layoutRing.appendChild(layoutRingTop);
5290
+ layoutRing.appendChild(layoutRingBottom);
5291
+ container.insertBefore(layoutRing, container.firstChild ?? null);
5292
+ // 点调 -> 环形:创建后同步高度
5293
+ requestAnimationFrame(() => this.updateRingTopHeight());
5294
+ }
4330
5295
  }
4331
5296
  });
4332
5297
  },
@@ -4550,7 +5515,7 @@ export default {
4550
5515
  align-items: center;
4551
5516
  justify-content: space-between;
4552
5517
  position: absolute;
4553
- z-index: 100;
5518
+ z-index: 2100;
4554
5519
  bottom: -66px;
4555
5520
  left: 0;
4556
5521
  transition: all 0.2s ease;