mdm-client 1.0.3 → 1.0.5

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 (36) 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 +1163 -99
  19. package/src/components/LiveMultipleMeeting/style/index.scss +145 -14
  20. package/src/components/LivePoint/LivePoint.vue +71 -208
  21. package/src/components/LivePointMeeting/LivePointMeeting.vue +223 -13
  22. package/src/components/LivePointMeeting/style/index.scss +35 -0
  23. package/src/components/MeetingReadyDialog/MeetingReadyDialog.vue +96 -14
  24. package/src/components/MiniumVideoDialog/MiniumVideoDialog.vue +185 -50
  25. package/src/components/other/addressBook.vue +137 -20
  26. package/src/components/other/appointDialog.vue +1 -1
  27. package/src/components/other/customLayout.vue +368 -202
  28. package/src/components/other/layoutSwitch.vue +253 -37
  29. package/src/components/other/leadershipFocus.vue +422 -0
  30. package/src/components/other/leaveOptionDialog.vue +1 -1
  31. package/src/components/other/moreOptionDialog.vue +17 -1
  32. package/src/components/other/screenShareBoard.vue +2 -2
  33. package/src/components/other/selectDialog.vue +1 -1
  34. package/src/components/other/selectSpecialDialog.vue +1 -1
  35. package/src/utils/api.js +19 -0
  36. 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;
@@ -695,6 +792,33 @@ export default {
695
792
  });
696
793
  },
697
794
  },
795
+ meetingNum: {
796
+ handler(newVal) {
797
+ if (!newVal) return
798
+ try {
799
+ const key = `lk:ss_prefs:${newVal}`
800
+ const raw = sessionStorage.getItem(key)
801
+ if (!raw) return
802
+ const saved = JSON.parse(raw)
803
+ if (saved && typeof saved === 'object') {
804
+ if (saved.systemAudio === 'include' || saved.systemAudio === 'exclude') {
805
+ this.screenShareCaptureOption.systemAudio = saved.systemAudio
806
+ }
807
+ if (saved.resolution && typeof saved.resolution === 'object') {
808
+ const { width, height, frameRate } = saved.resolution
809
+ this.screenShareCaptureOption.resolution = {
810
+ width: width ?? 1920,
811
+ height: height ?? 1080,
812
+ frameRate: typeof frameRate === 'number' ? frameRate : 60,
813
+ }
814
+ }
815
+ }
816
+ } catch (err) {
817
+ // 忽略解析异常
818
+ }
819
+ },
820
+ immediate: true,
821
+ }
698
822
  },
699
823
  created() {
700
824
  this.showMessage = new ShowMessage();
@@ -714,14 +838,148 @@ export default {
714
838
  }
715
839
  this.setPageFooterVisible(5);
716
840
  this.initGlobleEvent();
841
+ // 观察#room尺寸变化,动态同步环形布局顶部高度
842
+ this.observeRoomResizeForRing();
843
+ // 首次挂载后尝试同步一次高度
844
+ requestAnimationFrame(() => this.updateRingTopHeight());
717
845
  },
718
846
  beforeDestroy() {
719
847
  this.stopUnjoinParticipantPolling();
720
848
  this.dispatchLiveClientEvent();
721
849
  this.removeGlobleEvent();
850
+ // 清理observer或事件监听
851
+ if (ringResizeObserver) {
852
+ try { ringResizeObserver.disconnect(); } catch (e) {}
853
+ ringResizeObserver = null;
854
+ } else {
855
+ window.removeEventListener('resize', this.updateRingTopHeight);
856
+ }
722
857
  this.liveClient = null;
723
858
  },
724
859
  methods: {
860
+ handleInviteType(integrationType = 0) {
861
+ // 根据integrationType返回对应的makeCall的type参数
862
+ switch (Number(integrationType)) {
863
+ case 0:
864
+ return 1
865
+ case 1:
866
+ return 2
867
+ case 2:
868
+ return 3
869
+ default:
870
+ return 1
871
+ }
872
+ },
873
+ leaderShipMoreClick(e) {
874
+ // 动态计算选项数量以获得精确的弹窗高度
875
+ const optionCount = this.getOptionCount(e.identity)
876
+
877
+ // 使用工具函数计算弹窗位置(使用真实的MoreOptionDialog尺寸)
878
+ this.optionDialogOffset = this.calculateDialogPosition(e.event, {
879
+ dialogWidth: 130,
880
+ dialogHeight: 350, // 备用高度
881
+ optionCount: optionCount, // 动态计算精确高度
882
+ preferredPosition: 'bottom-left',
883
+ })
884
+
885
+ if (this.optionIdentity === e.identity && this.moreDialogShow) {
886
+ this.moreDialogShow = false
887
+ } else {
888
+ this.moreDialogShow = false
889
+ this.optionIdentity = e.identity
890
+ this.moreDialogShow = true
891
+ }
892
+ },
893
+ async leaderShipMicroClick() {
894
+ await this.liveClient.changeParticipantMicrophoneStatus(e.identity, e.isMuted)
895
+ },
896
+ // 聚焦画面
897
+ async openLeadershipFocus(identity) {
898
+ await this.liveClient.openLeadershipFocus(identity)
899
+ },
900
+ // 取消聚焦画面
901
+ async closeLeadershipFocus(identity) {
902
+ await this.liveClient.closeLeadershipFocus(identity)
903
+ },
904
+ updateRingTopHeight() {
905
+ try {
906
+ const room = document.getElementById('room');
907
+ if (!room) return;
908
+ const ringTop = room.querySelector('.layout-ring-top');
909
+ if (!ringTop) return;
910
+ // 使用clientHeight以获得包含内边距的可见高度
911
+ const roomHeight = room.clientHeight;
912
+ // 设置明确像素高度,覆盖CSS百分比或flex计算值
913
+ ringTop.style.height = roomHeight + 'px';
914
+ } catch (e) {
915
+ console.warn('updateRingTopHeight error:', e);
916
+ }
917
+ },
918
+ observeRoomResizeForRing() {
919
+ const room = document.getElementById('room');
920
+ if (!room) return;
921
+ // 优先使用ResizeObserver,实时响应容器尺寸变化
922
+ if ('ResizeObserver' in window) {
923
+ ringResizeObserver = new ResizeObserver(() => {
924
+ this.updateRingTopHeight();
925
+ });
926
+ ringResizeObserver.observe(room);
927
+ } else {
928
+ // 回退到window resize
929
+ window.addEventListener('resize', this.updateRingTopHeight);
930
+ }
931
+ },
932
+ setGridSize(size) {
933
+ this.currentGridSize = size;
934
+ },
935
+ // 根据identity和度数应用到video元素和旋转按钮的样式/属性
936
+ applyRotation(identity, degree) {
937
+ try {
938
+ const norm = ((degree % 360) + 360) % 360; // 归一化到0-359
939
+ // 更新video元素的transform和objectFit
940
+ const videoElm = document.getElementById(`video-${identity}`);
941
+ if (videoElm) {
942
+ videoElm.style.transformOrigin = 'center center';
943
+ // 针对90/270度,居中并以高度为基准适配,避免布局“怪异”
944
+ if (norm === 90 || norm === 270) {
945
+ const parent = videoElm.parentElement;
946
+ if (parent) {
947
+ const rect = parent.getBoundingClientRect();
948
+ // 旋转后使可视区域与父容器宽高贴合:宽高对调为父容器的高/宽
949
+ videoElm.style.top = '50%';
950
+ videoElm.style.left = '50%';
951
+ videoElm.style.width = `${rect.height}px`;
952
+ videoElm.style.height = `${rect.width}px`;
953
+ videoElm.style.transform = `translate(-50%, -50%) rotate(${norm}deg)`;
954
+ // 使用cover确保充满父容器(必要时会稍作裁剪)
955
+ videoElm.style.objectFit = 'cover';
956
+ }
957
+ } else {
958
+ // 0/180度直接占满容器
959
+ videoElm.style.top = '0';
960
+ videoElm.style.left = '0';
961
+ videoElm.style.width = '100%';
962
+ videoElm.style.height = '100%';
963
+ videoElm.style.transform = `rotate(${norm}deg)`;
964
+ videoElm.style.objectFit = 'contain';
965
+ }
966
+ }
967
+
968
+ // 更新旋转按钮的class以反映当前角度(1..4)
969
+ const rotateElm = document.getElementById(`rotate-${identity}`);
970
+ if (rotateElm) {
971
+ let clsIndex = 1;
972
+ if (norm === 0) clsIndex = 1;
973
+ else if (norm === 90) clsIndex = 2;
974
+ else if (norm === 180) clsIndex = 3;
975
+ else if (norm === 270) clsIndex = 4;
976
+ rotateElm.className = `rotate-icon rotate-icon${clsIndex}`;
977
+ }
978
+ } catch (err) {
979
+ // 忽略DOM异常
980
+ console.error('applyRotation error', err);
981
+ }
982
+ },
725
983
  // DOM操作管理函数
726
984
  async batchUpdateStates(updateFn) {
727
985
  this.isBatchUpdating = true;
@@ -868,12 +1126,12 @@ export default {
868
1126
  // 计算MoreOptionDialog选项数量的辅助函数
869
1127
  getOptionCount(identity) {
870
1128
  const participantItem = this.getUserItemByIdentity(identity);
871
- if (!participantItem || !participantItem.metadata) return 8; // 默认最大选项数
1129
+ if (!participantItem || !participantItem.metadata) return 9; // 默认最大选项数
872
1130
 
873
1131
  const metadata = participantItem.metadata;
874
1132
  const isLocalParticipant = identity === this.localIdentity;
875
1133
 
876
- let optionCount = 3; // 基础选项:设为主屏、麦克风、摄像头
1134
+ let optionCount = 4; // 基础选项:设为主屏、麦克风、摄像头、聚焦画面
877
1135
 
878
1136
  if (this.judgeParticipantIsHost(this.localIdentity)) {
879
1137
  // 本地为主持人
@@ -898,7 +1156,7 @@ export default {
898
1156
  }
899
1157
  }
900
1158
 
901
- return Math.min(optionCount, 8); // 限制最大选项数
1159
+ return Math.min(optionCount, 9); // 限制最大选项数
902
1160
  },
903
1161
  // 弹窗定位工具函数
904
1162
  calculateDialogPosition(event, options = {}) {
@@ -1366,12 +1624,12 @@ export default {
1366
1624
  const deviceCalls = this.deviceList
1367
1625
  .map(item => {
1368
1626
  const id = item?.source === '监控' ? item?.monitorID : item?.equipmentID;
1369
- return id ? { dnis: id, name: item.label } : null;
1627
+ return id ? { dnis: id, name: item.label, type: this.handleInviteType(item?.integrationType) } : null;
1370
1628
  })
1371
1629
  .filter(Boolean);
1372
1630
 
1373
1631
  if (deviceCalls.length > 0) {
1374
- this.liveClient.makeBatchCall(deviceCalls, 1)
1632
+ this.liveClient.makeBatchCall(deviceCalls)
1375
1633
  .then(() => {
1376
1634
  this.showMessage.message('success', '监控设备批量外呼已发起');
1377
1635
  })
@@ -1380,6 +1638,24 @@ export default {
1380
1638
  });
1381
1639
  }
1382
1640
  }
1641
+ // 拉取会议终端
1642
+ if (this.terminalNum > 0 && this.meetingTerminals) {
1643
+ const terminalCalls = this.meetingTerminals
1644
+ .map(item => {
1645
+ return item?.id ? { dnis: item.id, name: item.label, type: 4 } : null
1646
+ })
1647
+ .filter(Boolean)
1648
+ if (terminalCalls.length > 0) {
1649
+ this.liveClient
1650
+ .makeBatchCall(terminalCalls)
1651
+ .then(() => {
1652
+ this.showMessage.message('success', '会议终端批量外呼已发起')
1653
+ })
1654
+ .catch(err => {
1655
+ this.showMessage.message('error', `批量拉取会议终端失败: ${err?.message || err}`)
1656
+ })
1657
+ }
1658
+ }
1383
1659
  }
1384
1660
  // 启动获取未入会和邀请人员轮询
1385
1661
  this.startUnjoinParticipantPolling();
@@ -1475,18 +1751,44 @@ export default {
1475
1751
  }
1476
1752
  });
1477
1753
  if (maxAudioLevelIndex >= 0) {
1478
- this.switchActiveSpeakerToFirst(e[maxAudioLevelIndex]);
1754
+ this.highlightCurrentSpeaker(e[maxAudioLevelIndex])
1755
+ if (this.isVoiceMotivationOpen) {
1756
+ this.switchActiveSpeakerToFirst(e[maxAudioLevelIndex])
1757
+ }
1758
+ } else {
1759
+ this.highlightCurrentSpeaker(-1)
1479
1760
  }
1761
+ } else {
1762
+ this.highlightCurrentSpeaker(-1)
1480
1763
  }
1481
1764
  });
1482
1765
  this.liveClient.on("resMeetingRefresh", this.handleResMeetingRefresh);
1483
1766
  },
1484
- handleResMeetingRefresh: (e) => {
1767
+ // 高亮当前与会者
1768
+ highlightCurrentSpeaker(speaker) {
1769
+ const participantElms = document.querySelectorAll('#room .participant')
1770
+ if (speaker === -1) {
1771
+ if (participantElms.length > 0) {
1772
+ participantElms.forEach(elm => {
1773
+ elm.style.border = 'none'
1774
+ })
1775
+ }
1776
+ } else if (speaker && speaker.identity) {
1777
+ const speakerElm = document.getElementById(`participant-${speaker.identity}`)
1778
+ if (participantElms.length > 0) {
1779
+ participantElms.forEach(elm => {
1780
+ elm.style.border = 'none'
1781
+ })
1782
+ }
1783
+ speakerElm && (speakerElm.style.border = '2px solid #95ec69')
1784
+ }
1785
+ },
1786
+ handleResMeetingRefresh(e) {
1485
1787
  console.log('resMeetingRefresh事件触发', e);
1486
-
1487
- if(e.includes("queryAllInvite") || e.includes("queryUnjoined")) {
1788
+
1789
+ if (e.includes("queryAllInvite") || e.includes("queryUnjoined")) {
1488
1790
  console.log('this', this, this.getUnjoinParticipant);
1489
-
1791
+
1490
1792
  this.getUnjoinParticipant();
1491
1793
  this.queryAllInviteParticipant();
1492
1794
  this.startUnjoinParticipantPolling();
@@ -1632,7 +1934,7 @@ export default {
1632
1934
  } else {
1633
1935
  this.meetingCoHost = [];
1634
1936
  }
1635
-
1937
+ this.leaderShipFocus = metadata.leaderShipFocus;
1636
1938
  this.roomMetadata = metadata;
1637
1939
 
1638
1940
  // 批量设置关键状态
@@ -1708,6 +2010,7 @@ export default {
1708
2010
  this.meetingCoHost = [];
1709
2011
  }
1710
2012
 
2013
+ this.leaderShipFocus = metadata.leaderShipFocus;
1711
2014
  this.roomMetadata = metadata;
1712
2015
 
1713
2016
  // 批量设置关键状态
@@ -1955,7 +2258,8 @@ export default {
1955
2258
  // 构建批量外呼数据
1956
2259
  const volteCalls = volteList.map(item => ({
1957
2260
  dnis: item.phone,
1958
- name: item.label
2261
+ name: item.label,
2262
+ type: 0
1959
2263
  }));
1960
2264
 
1961
2265
  promises.push(
@@ -1997,9 +2301,9 @@ export default {
1997
2301
  roomNum: this.meetingNum,
1998
2302
  cameraStatus: this.isCameraEnabled,
1999
2303
  microphoneStatus: this.isMicrophoneEnabled,
2000
- audioDeviceId: this.activeDevice.audioInputDevice,
2001
- videoDeviceId: this.activeDevice.videoDevice,
2002
- outputDeviceId: this.activeDevice.audioOutputDevice,
2304
+ audioDeviceId: this.tempActiveDevice.audioInputDevice,
2305
+ videoDeviceId: this.tempActiveDevice.videoDevice,
2306
+ outputDeviceId: this.tempActiveDevice.audioOutputDevice,
2003
2307
  };
2004
2308
  let tempActiveDevice = null;
2005
2309
  try {
@@ -2133,36 +2437,83 @@ export default {
2133
2437
  </div>
2134
2438
  `;
2135
2439
  } 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
- `;
2440
+ if (videoItem.metadata?.platformID == 5) {
2441
+ videoDom.innerHTML = `
2442
+ <video id="video-${
2443
+ videoItem.identity
2444
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2445
+ <div id="loadingIndicator-${videoItem.identity}" class="loadingIndicator">
2446
+ <div class="loading-icon"></div>
2447
+ <div class="loading-text">视频流加载中...</div>
2448
+ </div>
2449
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2450
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2451
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2452
+ <div class="describe">
2453
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2454
+ <div id="${videoItem.identity}" class="identity">${
2455
+ videoItem.isLocal ? videoItem.name + "(我)" : videoItem.name
2456
+ }</div>
2457
+ </div>
2458
+ `;
2459
+ } else {
2460
+ videoDom.innerHTML = `
2461
+ <video id="video-${
2462
+ videoItem.identity
2463
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2464
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2465
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2466
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2467
+ <div class="describe">
2468
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2469
+ <div id="${videoItem.identity}" class="identity">${
2470
+ videoItem.isLocal ? videoItem.name + '(我)' : videoItem.name
2471
+ }</div>
2472
+ </div>
2473
+ `;
2474
+ }
2149
2475
  } 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
- `;
2476
+ if (videoItem.metadata?.platformID == 5) {
2477
+ videoDom.innerHTML = `
2478
+ <video id="video-${
2479
+ videoItem.identity
2480
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2481
+ <div id="loadingIndicator-${videoItem.identity}" class="loadingIndicator">
2482
+ <div class="loading-icon"></div>
2483
+ <div class="loading-text">视频流加载中...</div>
2484
+ </div>
2485
+ <div id="board-${videoItem.identity}" class="board">
2486
+ <div class="board-icon"></div>
2487
+ </div>
2488
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2489
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2490
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2491
+ <div class="describe">
2492
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2493
+ <div id="${videoItem.identity}" class="identity">${
2494
+ videoItem.isLocal ? videoItem.name + '(我)' : videoItem.name
2495
+ }</div>
2496
+ </div>
2497
+ `
2498
+ } else {
2499
+ videoDom.innerHTML = `
2500
+ <video id="video-${
2501
+ videoItem.identity
2502
+ }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2503
+ <div id="board-${videoItem.identity}" class="board">
2504
+ <div class="board-icon"></div>
2505
+ </div>
2506
+ <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2507
+ <div id="more-${videoItem.identity}" class="more-icon"></div>
2508
+ <div id="bitrate-${videoItem.identity}" class="bitrate-indicator"></div>
2509
+ <div class="describe">
2510
+ <div id="microphone-${videoItem.identity}" class="microphone"></div>
2511
+ <div id="${videoItem.identity}" class="identity">${
2512
+ videoItem.isLocal ? videoItem.name + '(我)' : videoItem.name
2513
+ }</div>
2514
+ </div>
2515
+ `
2516
+ }
2166
2517
  }
2167
2518
  return videoDom;
2168
2519
  },
@@ -2311,6 +2662,57 @@ export default {
2311
2662
  layoutRightSideEle.appendChild(videoDiv);
2312
2663
  }
2313
2664
  }
2665
+ } else if (this.currentLayout === 'ring') {
2666
+ // 环形布局:确保容器存在并将与会者放入合适位置
2667
+ let layoutRing = document.querySelector('#room .layout-ring');
2668
+ let layoutRingTop = layoutRing?.querySelector('.layout-ring-top');
2669
+ let layoutRingBottom = layoutRing?.querySelector('.layout-ring-bottom');
2670
+ let ringCenter = layoutRing?.querySelector('.ring-center');
2671
+
2672
+ // 如果容器不存在(可能由于时序问题),创建它
2673
+ if (!layoutRing || !layoutRingTop || !layoutRingBottom || !ringCenter) {
2674
+ layoutRing = document.createElement('div');
2675
+ layoutRing.className = 'layout-ring';
2676
+ layoutRingTop = document.createElement('div');
2677
+ layoutRingTop.className = 'layout-ring-top';
2678
+ layoutRingBottom = document.createElement('div');
2679
+ layoutRingBottom.className = 'layout-ring-bottom';
2680
+ // 创建12个环槽位
2681
+ for (let i = 0; i < 12; i++) {
2682
+ const slot = document.createElement('div');
2683
+ slot.className = 'ring-slot';
2684
+ layoutRingTop.appendChild(slot);
2685
+ }
2686
+ ringCenter = document.createElement('div');
2687
+ ringCenter.className = 'ring-center';
2688
+ layoutRingTop.appendChild(ringCenter);
2689
+ layoutRing.appendChild(layoutRingTop);
2690
+ layoutRing.appendChild(layoutRingBottom);
2691
+ container.insertBefore(layoutRing, container.firstChild ?? null);
2692
+ // 确保环形顶部高度与#room一致
2693
+ requestAnimationFrame(() => this.updateRingTopHeight());
2694
+ }
2695
+
2696
+ const appendToFirstEmptySlot = (el) => {
2697
+ const slots = layoutRingTop.querySelectorAll('.ring-slot');
2698
+ for (let i = 0; i < slots.length; i++) {
2699
+ if (!slots[i].firstChild) {
2700
+ slots[i].appendChild(el);
2701
+ return true;
2702
+ }
2703
+ }
2704
+ return false;
2705
+ };
2706
+
2707
+ // 主持人优先放置在中心;否则进入第一个空槽;都满了则放到底部
2708
+ if (this.curHostIdentity && videoItem.identity === this.curHostIdentity) {
2709
+ ringCenter.appendChild(videoDiv);
2710
+ } else {
2711
+ const placed = appendToFirstEmptySlot(videoDiv);
2712
+ if (!placed) {
2713
+ layoutRingBottom.appendChild(videoDiv);
2714
+ }
2715
+ }
2314
2716
  }
2315
2717
  }
2316
2718
  return container;
@@ -2338,6 +2740,25 @@ export default {
2338
2740
  videoDiv = this.constructParticipantDom(videoItem);
2339
2741
  // 构建会议室布局
2340
2742
  container = this.constructRoomLayout(container, videoDiv, videoItem);
2743
+
2744
+ // 显示与会者画面加载中状态
2745
+ let videoElm = document.getElementById(`video-${videoItem.identity}`)
2746
+ let loadingIndicatorElm = document.getElementById(`loadingIndicator-${videoItem.identity}`)
2747
+ if (videoItem.metadata?.platformID == 5 && videoElm && loadingIndicatorElm) {
2748
+ setTimeout(() => {
2749
+ loadingIndicatorElm.style.display = 'none'
2750
+ }, 2000)
2751
+ }
2752
+ if (videoItem.metadata?.platformID == 4 && videoElm && loadingIndicatorElm) {
2753
+ setTimeout(() => {
2754
+ loadingIndicatorElm.style.display = 'none'
2755
+ }, 4000)
2756
+ }
2757
+
2758
+ // 本地与会者镜像处理
2759
+ if (videoItem.isLocal) {
2760
+ videoElm && this.isMirror && (videoElm.style.transform = 'rotateY(180deg)')
2761
+ }
2341
2762
  // 添加到participant数组
2342
2763
  this.addToParticipantList(videoItem);
2343
2764
  }
@@ -2352,6 +2773,8 @@ export default {
2352
2773
  let signalElm = document.getElementById(`signal-${videoItem.identity}`);
2353
2774
  // 与会者更多操作按钮元素
2354
2775
  let moreElm = document.getElementById(`more-${videoItem.identity}`);
2776
+ // 与会者画面旋转按钮元素
2777
+ let rotateElm = document.getElementById(`rotate-${videoItem.identity}`)
2355
2778
  // 声明麦克风按钮点击事件回调
2356
2779
  const unableMicrophone = () => {
2357
2780
  this.liveClient.changeParticipantMicrophoneStatus(videoItem.identity, true);
@@ -2366,6 +2789,8 @@ export default {
2366
2789
  const currentMoreElm = document.getElementById(`more-${videoItem.identity}`);
2367
2790
  currentSignalElm && (currentSignalElm.style.visibility = "visible");
2368
2791
  currentMoreElm && (currentMoreElm.style.visibility = "visible");
2792
+ const currentRotateElm = document.getElementById(`rotate-${videoItem.identity}`);
2793
+ currentRotateElm && (currentRotateElm.style.visibility = "visible");
2369
2794
  };
2370
2795
  const signalAndMoreHide = () => {
2371
2796
  // 每次执行时重新获取最新的元素
@@ -2373,6 +2798,8 @@ export default {
2373
2798
  const currentMoreElm = document.getElementById(`more-${videoItem.identity}`);
2374
2799
  currentSignalElm && (currentSignalElm.style.visibility = "hidden");
2375
2800
  currentMoreElm && (currentMoreElm.style.visibility = "hidden");
2801
+ const currentRotateElm = document.getElementById(`rotate-${videoItem.identity}`);
2802
+ currentRotateElm && (currentRotateElm.style.visibility = "hidden");
2376
2803
  };
2377
2804
  // 声明操作按钮元素点击事件回调
2378
2805
  const moreElmClick = (event) => {
@@ -2395,6 +2822,13 @@ export default {
2395
2822
  this.moreDialogShow = true;
2396
2823
  }
2397
2824
  };
2825
+ // 旋转按钮点击事件:顺时针旋转90度(并居中适配)
2826
+ const rotateElmClick = () => {
2827
+ const current = this.rotateDegreeMap.get(videoItem.identity) || 0;
2828
+ const next = (current + 90) % 360;
2829
+ this.rotateDegreeMap.set(videoItem.identity, next);
2830
+ this.applyRotation(videoItem.identity, next);
2831
+ };
2398
2832
  // 为麦克风元素绑定事件
2399
2833
  if (microElm && videoItem.isMicrophoneEnabled) {
2400
2834
  microElm.className = "microphone microphone-active";
@@ -2422,12 +2856,47 @@ export default {
2422
2856
  if (moreElm) {
2423
2857
  moreElm.onclick = moreElmClick;
2424
2858
  }
2859
+ if (rotateElm) {
2860
+ rotateElm.onclick = rotateElmClick;
2861
+ }
2425
2862
  if (signalElm) {
2426
2863
  if (videoItem.connectionQuality === "excellent" || videoItem.connectionQuality === "good") {
2427
2864
  signalElm.className = "signal-icon signal-icon-good";
2428
2865
  } else {
2429
2866
  signalElm.className = "signal-icon signal-icon-poor";
2430
2867
  }
2868
+ // 为信号图标绑定鼠标悬停事件,显示码率信息
2869
+ signalElm.onmouseenter = () => {
2870
+ const bitrateElm = document.getElementById(`bitrate-${videoItem.identity}`)
2871
+ if (bitrateElm) {
2872
+ const bitrate = this.liveClient.getParticipantBitrate(videoItem.identity)
2873
+ console.log('bitrate', bitrate)
2874
+ if (bitrate !== null) {
2875
+ bitrateElm.innerText = `${bitrate} kbps`
2876
+ bitrateElm.style.display = 'block'
2877
+ }
2878
+ }
2879
+ }
2880
+ signalElm.onmouseleave = () => {
2881
+ const bitrateElm = document.getElementById(`bitrate-${videoItem.identity}`)
2882
+ if (bitrateElm) {
2883
+ bitrateElm.style.display = 'none'
2884
+ }
2885
+ }
2886
+ }
2887
+ // 根据platformID设定元素样式
2888
+ if (
2889
+ videoItem.metadata?.platformID == 1 ||
2890
+ videoItem.metadata?.platformID == 4 ||
2891
+ videoItem.metadata?.platformID == 7
2892
+ ) {
2893
+ videoElm && (videoElm.style.objectFit = 'contain')
2894
+ } else {
2895
+ if (videoItem?.source == 'camera') {
2896
+ videoElm && (videoElm.style.objectFit = 'cover')
2897
+ } else {
2898
+ videoElm && (videoElm.style.objectFit = 'contain')
2899
+ }
2431
2900
  }
2432
2901
  let screenShareElm = document.getElementById(`screenshare-${videoItem.identity}`);
2433
2902
  // 与会者dom元素内部样式重新渲染(摄像头或屏幕共享切换后)
@@ -2477,6 +2946,15 @@ export default {
2477
2946
  signalElm.className = "signal-icon signal-icon-poor";
2478
2947
  }
2479
2948
  videoDiv.appendChild(signalElm);
2949
+
2950
+ // 构建码率显示元素
2951
+ let bitrateElm = document.getElementById(`bitrate-${videoItem.identity}`)
2952
+ if (!bitrateElm) {
2953
+ bitrateElm = document.createElement('div')
2954
+ bitrateElm.id = `bitrate-${videoItem.identity}`
2955
+ bitrateElm.className = 'bitrate-indicator'
2956
+ videoDiv.appendChild(bitrateElm)
2957
+ }
2480
2958
  }
2481
2959
  // if (!moreElm) {
2482
2960
  // // 构建更多操作按钮元素
@@ -2499,6 +2977,28 @@ export default {
2499
2977
  moreElm = null;
2500
2978
  }
2501
2979
  }
2980
+ // 仅在platformID为4展示旋转按钮(对所有与会者可见)
2981
+ if (videoItem.metadata?.platformID === 4) {
2982
+ if (!rotateElm) {
2983
+ rotateElm = document.createElement('div');
2984
+ rotateElm.id = `rotate-${videoItem.identity}`;
2985
+ rotateElm.className = 'rotate-icon rotate-icon1';
2986
+ videoDiv && videoDiv.appendChild(rotateElm);
2987
+ rotateElm.onclick = rotateElmClick;
2988
+ }
2989
+ // 初始化map中度数(如果不存在)并应用到元素
2990
+ if (!this.rotateDegreeMap.has(videoItem.identity)) {
2991
+ this.rotateDegreeMap.set(videoItem.identity, 0);
2992
+ }
2993
+ this.applyRotation(videoItem.identity, this.rotateDegreeMap.get(videoItem.identity));
2994
+ } else {
2995
+ // 如果不是platformID 4,移除旋转按钮及map记录
2996
+ if (rotateElm) {
2997
+ try { videoDiv.removeChild(rotateElm); } catch(e) {}
2998
+ rotateElm = null;
2999
+ }
3000
+ this.rotateDegreeMap.delete(videoItem.identity);
3001
+ }
2502
3002
  } else {
2503
3003
  if (!boardElm) {
2504
3004
  // 之前为屏幕共享状态或摄像头开启状态
@@ -2532,6 +3032,11 @@ export default {
2532
3032
  this.removeFromParticipantList(videoItem.identity);
2533
3033
  this.filterParticipantList();
2534
3034
 
3035
+ // 清理可能存在的旋转状态
3036
+ if (this.rotateDegreeMap.has(videoItem.identity)) {
3037
+ this.rotateDegreeMap.delete(videoItem.identity);
3038
+ }
3039
+
2535
3040
  // 处理焦点用户离开的情况
2536
3041
  if (this.curBlurIdentity === videoItem.identity) {
2537
3042
  console.log("焦点用户离开会议:", videoItem.identity);
@@ -2559,18 +3064,6 @@ export default {
2559
3064
  this.curHostIdentity = null;
2560
3065
  // 主持人离开后具体的逻辑处理放到watch中处理
2561
3066
  }
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
3067
  // pointTurn模式下的其他用户离开处理
2575
3068
  if (
2576
3069
  this.currentRoomMode === "pointTurn" &&
@@ -2608,6 +3101,11 @@ export default {
2608
3101
  if (videoItem?.audioTrack) {
2609
3102
  videoItem.audioTrack.detach();
2610
3103
  }
3104
+ if (videoItem?.screenShareAudioTrack) {
3105
+ try {
3106
+ videoItem.screenShareAudioTrack.detach()
3107
+ } catch (e) {}
3108
+ }
2611
3109
  return;
2612
3110
  }
2613
3111
  // 与会者屏幕共享布局切换
@@ -2670,6 +3168,32 @@ export default {
2670
3168
  if (videoItem?.audioTrack) {
2671
3169
  videoItem.audioTrack.attach(videoElm);
2672
3170
  }
3171
+ // 屏幕共享系统音频:使用独立的隐藏 audio 元素,避免某些浏览器在多音轨时替换 video 的音频源
3172
+ if (videoItem?.isScreenShareEnabled && videoItem?.screenShareAudioTrack) {
3173
+ const audioHolderId = `screenshare-audio-${videoItem.identity}`
3174
+ let audioHolder = document.getElementById(audioHolderId)
3175
+ if (!audioHolder) {
3176
+ audioHolder = document.createElement('audio')
3177
+ audioHolder.id = audioHolderId
3178
+ audioHolder.style.display = 'none'
3179
+ audioHolder.setAttribute('playsinline', '')
3180
+ audioHolder.autoplay = true
3181
+ // 将隐藏audio元素插入到与会者容器,便于随与会者一起清理
3182
+ videoDiv && videoDiv.appendChild(audioHolder)
3183
+ }
3184
+ try {
3185
+ videoItem.screenShareAudioTrack.attach(audioHolder)
3186
+ } catch (e) {
3187
+ console.warn('屏幕共享系统音频附加失败', videoItem.identity, e)
3188
+ }
3189
+ } else {
3190
+ // 如果当前不再共享屏幕,清理残留的隐藏 audio 元素
3191
+ const orphanAudio = document.getElementById(`screenshare-audio-${videoItem.identity}`)
3192
+ if (orphanAudio) {
3193
+ try { videoItem.screenShareAudioTrack?.detach(orphanAudio) } catch (_) {}
3194
+ orphanAudio.remove()
3195
+ }
3196
+ }
2673
3197
  }
2674
3198
  // 更新与会者名称
2675
3199
  let nameDom = document.getElementById(videoItem.identity);
@@ -2679,6 +3203,10 @@ export default {
2679
3203
  // 更新与会者数组
2680
3204
  this.addToParticipantList(videoItem);
2681
3205
  this.filterParticipantList();
3206
+ // 每次render时,如果该用户在rotateDegreeMap中,则应用其旋转到video元素(确保DOM更新后生效)
3207
+ if (this.rotateDegreeMap.has(videoItem.identity)) {
3208
+ this.applyRotation(videoItem.identity, this.rotateDegreeMap.get(videoItem.identity));
3209
+ }
2682
3210
  // 与会者结束共享,切换回到之前布局(仅主持人执行)
2683
3211
  if (this.currentRoomMode === "normal" && this.judgeParticipantIsHost(this.localIdentity)) {
2684
3212
  console.log(
@@ -2891,7 +3419,73 @@ export default {
2891
3419
 
2892
3420
  if (oldLayout === "grid" && newLayout === "ring") {
2893
3421
  // 从宫格布局切换到环状布局
2894
- // TODO: 实现环状布局逻辑
3422
+ // 实现环形布局:上半部分为4x4网格(中心2x2为主持人独占),其余12个位置按从上到下、从左到右顺序填充;
3423
+ // 超过13人的用户放到下半部分按4x4顺序排列
3424
+
3425
+ // 清理可能残留的环形容器
3426
+ const existingRing = document.querySelector('#room .layout-ring');
3427
+ if (existingRing) {
3428
+ console.warn('发现残留的环形布局容器,正在清理');
3429
+ existingRing.remove();
3430
+ }
3431
+
3432
+ // 创建环形布局容器
3433
+ const layoutRing = document.createElement('div');
3434
+ layoutRing.className = 'layout-ring';
3435
+
3436
+ const layoutRingTop = document.createElement('div');
3437
+ layoutRingTop.className = 'layout-ring-top';
3438
+
3439
+ const layoutRingBottom = document.createElement('div');
3440
+ layoutRingBottom.className = 'layout-ring-bottom';
3441
+
3442
+ // 按照4x4网格的顺序,但排除中心2x2的位置(索引 5,6,9,10),插入12个slot
3443
+ const slotOrder = [0,1,2,3,4,7,8,11,12,13,14,15];
3444
+ slotOrder.forEach((pos, idx) => {
3445
+ const slot = document.createElement('div');
3446
+ slot.className = 'ring-slot';
3447
+ slot.dataset.pos = String(pos);
3448
+ layoutRingTop.appendChild(slot);
3449
+ });
3450
+
3451
+ // 中央占位,用于主持人占据2x2
3452
+ const ringCenter = document.createElement('div');
3453
+ ringCenter.className = 'ring-center';
3454
+ layoutRingTop.appendChild(ringCenter);
3455
+
3456
+ // 将元素从主容器中收集并按规则分配到环形布局
3457
+ const participantElements = Array.from(container.children).filter(child => child.classList && child.classList.contains('participant') && child.id && child.id.startsWith('participant-'));
3458
+
3459
+ // 将主持人放到center,其余依序填充top slots,溢出进入bottom
3460
+ let filledCount = 0;
3461
+ participantElements.forEach((child) => {
3462
+ try {
3463
+ const id = child.id.replace('participant-', '');
3464
+ if (this.curHostIdentity && id === this.curHostIdentity) {
3465
+ ringCenter.appendChild(child);
3466
+ } else {
3467
+ if (filledCount < 12) {
3468
+ const slot = layoutRingTop.querySelectorAll('.ring-slot')[filledCount];
3469
+ slot && slot.appendChild(child);
3470
+ filledCount++;
3471
+ } else {
3472
+ layoutRingBottom.appendChild(child);
3473
+ }
3474
+ }
3475
+ } catch (err) {
3476
+ console.error('分配参与者到环形布局时出错', err);
3477
+ }
3478
+ });
3479
+
3480
+ layoutRing.appendChild(layoutRingTop);
3481
+ layoutRing.appendChild(layoutRingBottom);
3482
+
3483
+ // 清理主容器中可能存在的非participant占位元素(避免重复)
3484
+ // 移除原有非布局容器元素
3485
+ // 插入新的环形布局
3486
+ container.insertBefore(layoutRing, container.firstChild ?? null);
3487
+ // 切换到环形后,同步一次顶部高度
3488
+ requestAnimationFrame(() => this.updateRingTopHeight());
2895
3489
  }
2896
3490
 
2897
3491
  if (oldLayout === "grid" && newLayout === "downLSide") {
@@ -2939,6 +3533,178 @@ export default {
2939
3533
  // 添加其他元素
2940
3534
  container.appendChild(fragment);
2941
3535
  }
3536
+ // 从焦点布局切换到环形布局
3537
+ if (oldLayout === 'rightSide' && newLayout === 'ring') {
3538
+ // 将右侧/左侧容器拆解回主容器,然后重用 grid->ring 的逻辑
3539
+ let layoutRightSideEle = document.querySelector('#room .layout-rightside');
3540
+ let layoutLeftSideEle = document.querySelector('#room .layout-leftside');
3541
+
3542
+ // 把左右容器内的元素移动回主容器顺序(左侧优先)
3543
+ const moveChildrenToContainer = (ele) => {
3544
+ if (!ele) return;
3545
+ let child = null;
3546
+ while ((child = ele.firstChild)) {
3547
+ container.appendChild(child);
3548
+ }
3549
+ if (container.contains(ele)) container.removeChild(ele);
3550
+ };
3551
+ moveChildrenToContainer(layoutLeftSideEle);
3552
+ moveChildrenToContainer(layoutRightSideEle);
3553
+
3554
+ // 触发自身来走 grid->ring 分支,通过重新调用 executeLayoutChange 从 container 状态转换
3555
+ // 为避免递归/竞态,我们直接调用 the grid->ring block by setting variables here.
3556
+ // 简化做法:复制 grid->ring behavior here
3557
+ const existingRing = document.querySelector('#room .layout-ring');
3558
+ if (existingRing) existingRing.remove();
3559
+ const layoutRing = document.createElement('div');
3560
+ layoutRing.className = 'layout-ring';
3561
+ const layoutRingTop = document.createElement('div');
3562
+ layoutRingTop.className = 'layout-ring-top';
3563
+ const layoutRingBottom = document.createElement('div');
3564
+ layoutRingBottom.className = 'layout-ring-bottom';
3565
+ const slotOrder = [0,1,2,3,4,7,8,11,12,13,14,15];
3566
+ slotOrder.forEach((pos) => {
3567
+ const slot = document.createElement('div');
3568
+ slot.className = 'ring-slot';
3569
+ slot.dataset.pos = String(pos);
3570
+ layoutRingTop.appendChild(slot);
3571
+ });
3572
+ const ringCenter = document.createElement('div');
3573
+ ringCenter.className = 'ring-center';
3574
+ layoutRingTop.appendChild(ringCenter);
3575
+ const participantElements = Array.from(container.children).filter(child => child.classList && child.classList.contains('participant') && child.id && child.id.startsWith('participant-'));
3576
+ let filledCount = 0;
3577
+ participantElements.forEach((child) => {
3578
+ const id = child.id.replace('participant-', '');
3579
+ if (this.curHostIdentity && id === this.curHostIdentity) {
3580
+ ringCenter.appendChild(child);
3581
+ } else {
3582
+ if (filledCount < 12) {
3583
+ const slot = layoutRingTop.querySelectorAll('.ring-slot')[filledCount];
3584
+ slot && slot.appendChild(child);
3585
+ filledCount++;
3586
+ } else {
3587
+ layoutRingBottom.appendChild(child);
3588
+ }
3589
+ }
3590
+ });
3591
+ layoutRing.appendChild(layoutRingTop);
3592
+ layoutRing.appendChild(layoutRingBottom);
3593
+ container.insertBefore(layoutRing, container.firstChild ?? null);
3594
+ // 右侧边栏 -> 环形:创建后同步高度
3595
+ requestAnimationFrame(() => this.updateRingTopHeight());
3596
+ }
3597
+
3598
+ // 从环形布局切换到宫格布局
3599
+ if (oldLayout === 'ring' && newLayout === 'grid') {
3600
+ const layoutRing = document.querySelector('#room .layout-ring');
3601
+ if (layoutRing) {
3602
+ const top = layoutRing.querySelector('.layout-ring-top');
3603
+ const bottom = layoutRing.querySelector('.layout-ring-bottom');
3604
+ // 将top中的slot里的参与者按slot顺序移动回主容器
3605
+ if (top) {
3606
+ const slots = top.querySelectorAll('.ring-slot');
3607
+ slots.forEach(slot => {
3608
+ while (slot.firstChild) {
3609
+ container.appendChild(slot.firstChild);
3610
+ }
3611
+ slot.remove();
3612
+ });
3613
+ const center = top.querySelector('.ring-center');
3614
+ if (center) {
3615
+ while (center.firstChild) {
3616
+ container.appendChild(center.firstChild);
3617
+ }
3618
+ center.remove();
3619
+ }
3620
+ }
3621
+ if (bottom) {
3622
+ while (bottom.firstChild) {
3623
+ container.appendChild(bottom.firstChild);
3624
+ }
3625
+ }
3626
+ layoutRing.remove();
3627
+ }
3628
+ }
3629
+
3630
+ // 从环形布局切换到焦点布局
3631
+ if (oldLayout === 'ring' && newLayout === 'rightSide') {
3632
+ // 先把ring拆平回主容器,再复用 rightSide creation logic
3633
+ const layoutRing = document.querySelector('#room .layout-ring');
3634
+ if (layoutRing) {
3635
+ const top = layoutRing.querySelector('.layout-ring-top');
3636
+ const bottom = layoutRing.querySelector('.layout-ring-bottom');
3637
+ if (top) {
3638
+ const slots = top.querySelectorAll('.ring-slot');
3639
+ slots.forEach(slot => {
3640
+ while (slot.firstChild) {
3641
+ container.appendChild(slot.firstChild);
3642
+ }
3643
+ });
3644
+ const center = top.querySelector('.ring-center');
3645
+ if (center) {
3646
+ while (center.firstChild) {
3647
+ container.appendChild(center.firstChild);
3648
+ }
3649
+ }
3650
+ }
3651
+ if (bottom) {
3652
+ while (bottom.firstChild) {
3653
+ container.appendChild(bottom.firstChild);
3654
+ }
3655
+ }
3656
+ layoutRing.remove();
3657
+ }
3658
+
3659
+ // 现在容器已被平铺,复用已有 grid->rightSide 代码 path by creating left/right containers
3660
+ let layoutRightSideEle = document.createElement('div');
3661
+ layoutRightSideEle.className = 'layout-rightside';
3662
+ let layoutLeftSideEle = document.createElement('div');
3663
+ layoutLeftSideEle.className = 'layout-leftside';
3664
+
3665
+ // 确定焦点用户 - 优先级:当前焦点用户 > 主持人 > 本地用户
3666
+ let focusVideoItem = null;
3667
+ let blurDom = null;
3668
+ if (this.curBlurIdentity) {
3669
+ focusVideoItem = getUserItemByIdentity(this.curBlurIdentity);
3670
+ if (focusVideoItem) blurDom = document.getElementById(`participant-${focusVideoItem.identity}`);
3671
+ }
3672
+ if (!blurDom && this.curHostIdentity) {
3673
+ focusVideoItem = getUserItemByIdentity(this.curHostIdentity);
3674
+ if (focusVideoItem) blurDom = document.getElementById(`participant-${focusVideoItem.identity}`);
3675
+ }
3676
+ if (!blurDom) {
3677
+ focusVideoItem = getLocalParticipant();
3678
+ if (focusVideoItem) blurDom = document.getElementById(`participant-${focusVideoItem.identity}`);
3679
+ }
3680
+
3681
+ if (blurDom) {
3682
+ if (layoutLeftSideEle.hasChildNodes()) {
3683
+ let child;
3684
+ while ((child = layoutLeftSideEle.firstChild)) {
3685
+ layoutRightSideEle.appendChild(child);
3686
+ }
3687
+ }
3688
+ layoutLeftSideEle.appendChild(blurDom);
3689
+
3690
+ // 将剩余元素移动到右侧
3691
+ if (container.hasChildNodes()) {
3692
+ let child;
3693
+ while ((child = container.firstElementChild)) {
3694
+ if (child && child !== layoutLeftSideEle && child !== layoutRightSideEle) {
3695
+ layoutRightSideEle.appendChild(child);
3696
+ } else {
3697
+ if (child) container.removeChild(child);
3698
+ }
3699
+ }
3700
+ }
3701
+
3702
+ container.insertBefore(layoutLeftSideEle, container.firstChild ?? null);
3703
+ container.appendChild(layoutRightSideEle);
3704
+ } else {
3705
+ this.showMessage.warning('无法找到有效的焦点用户');
3706
+ }
3707
+ }
2942
3708
  }
2943
3709
  });
2944
3710
  },
@@ -3038,7 +3804,6 @@ export default {
3038
3804
  this.liveClient.deleteUnjoinParticipant(this.meetingNum, item.identity).then((res) => {
3039
3805
  if (res.code == 200) {
3040
3806
  this.showMessage.message("success", "成功删除未入会人员");
3041
- // this.getUnjoinParticipant();
3042
3807
  this.removeFromInviteList(item.identity);
3043
3808
  } else {
3044
3809
  this.showMessage.message("error", res?.msg);
@@ -3278,12 +4043,12 @@ export default {
3278
4043
  },
3279
4044
  async openCamera(e) {
3280
4045
  if (this.liveClient) {
3281
- await this.liveClient.changeParticipantCameraStatus(e, true);
4046
+ await this.liveClient.changeParticipantCameraStatus(e, false);
3282
4047
  }
3283
4048
  },
3284
4049
  async closeCamera(e) {
3285
4050
  if (this.liveClient) {
3286
- await this.liveClient.changeParticipantCameraStatus(e, false);
4051
+ await this.liveClient.changeParticipantCameraStatus(e, true);
3287
4052
  }
3288
4053
  },
3289
4054
  async setToHost(e) {
@@ -3335,9 +4100,43 @@ export default {
3335
4100
  async appendInviteDevice(e) {
3336
4101
  console.log("通讯录邀请设备", e);
3337
4102
  if (e && e.length > 0) {
3338
- e.forEach((item) => {
3339
- this.pullMonitorDevice(item?.equipmentID || item?.monitorID, item.label);
3340
- });
4103
+ // 构建批量外呼参数并一次性发起
4104
+ const deviceCalls = e
4105
+ .map(item => {
4106
+ const id = item?.equipmentID || item?.monitorID
4107
+ return id ? { dnis: id, name: item.label, type: this.handleInviteType(item?.integrationType) } : null
4108
+ })
4109
+ .filter(Boolean)
4110
+
4111
+ if (deviceCalls.length > 0) {
4112
+ try {
4113
+ await this.liveClient.makeBatchCall(deviceCalls)
4114
+ this.showMessage.message('success', '已发起通讯录设备批量外呼')
4115
+ } catch (err) {
4116
+ this.showMessage.message('error', `通讯录设备批量外呼失败: ${err?.message || err}`)
4117
+ }
4118
+ }
4119
+ }
4120
+ },
4121
+ async appendInviteTerminal(e) {
4122
+ console.log('通讯录邀请会议终端', e)
4123
+ if (e && e.length > 0) {
4124
+ // 构建批量外呼参数并一次性发起
4125
+ const terminalCalls = e
4126
+ .map(item => {
4127
+ const id = item?.id
4128
+ return id ? { dnis: id, name: item.label, type: 4 } : null
4129
+ })
4130
+ .filter(Boolean)
4131
+
4132
+ if (terminalCalls.length > 0) {
4133
+ try {
4134
+ await this.liveClient.makeBatchCall(terminalCalls)
4135
+ this.showMessage.message('success', '已发起会议终端批量外呼')
4136
+ } catch (err) {
4137
+ this.showMessage.message('error', `会议终端批量外呼失败: ${err?.message || err}`)
4138
+ }
4139
+ }
3341
4140
  }
3342
4141
  },
3343
4142
  async updateNameConfirm(e) {
@@ -3369,6 +4168,27 @@ export default {
3369
4168
  type,
3370
4169
  });
3371
4170
  },
4171
+ dispatchLocalTrack(kind = '') {
4172
+ if (this.participantNum > 0) {
4173
+ let index = this.participants.findIndex(item => item.isLocal)
4174
+ if (index !== -1) {
4175
+ let localVideoTrack = this.participants[index]?.videoTrack
4176
+ let localAudioTrack = this.participants[index]?.audioTrack
4177
+ if (!kind) {
4178
+ localVideoTrack?.detach()
4179
+ localVideoTrack?.stop()
4180
+ localAudioTrack?.detach()
4181
+ localAudioTrack?.stop()
4182
+ } else if (kind === 'video') {
4183
+ localVideoTrack?.detach()
4184
+ localVideoTrack?.stop()
4185
+ } else if (kind === 'audio') {
4186
+ localAudioTrack?.detach()
4187
+ localAudioTrack?.stop()
4188
+ }
4189
+ }
4190
+ }
4191
+ },
3372
4192
  async chooseVideoDevice(e) {
3373
4193
  await this.changeActiveDevice("videoinput", e);
3374
4194
  this.videoSelectShow = false;
@@ -3393,7 +4213,50 @@ export default {
3393
4213
  await this.liveClient.changeScreenShareStatus(this.screenShareCaptureOption);
3394
4214
  },
3395
4215
  screenShareOptionChange(e) {
3396
- console.log("屏幕共享设置变更", e);
4216
+ if (!e) return
4217
+ const { isFluencyPriority, systemAudioInclude } = e
4218
+
4219
+ // 1) 生效到当前响应式配置(将于下一次开始共享时使用;不对正在共享做自动重启)
4220
+ this.screenShareCaptureOption.systemAudio = systemAudioInclude ? 'include' : 'exclude'
4221
+ const targetFrameRate = isFluencyPriority ? 60 : 30
4222
+ if (
4223
+ !this.screenShareCaptureOption.resolution ||
4224
+ typeof this.screenShareCaptureOption.resolution !== 'object'
4225
+ ) {
4226
+ this.screenShareCaptureOption.resolution = { width: 1920, height: 1080, frameRate: targetFrameRate }
4227
+ } else {
4228
+ this.screenShareCaptureOption.resolution.frameRate = targetFrameRate
4229
+ }
4230
+
4231
+ // 2) 正在共享时尽量实时生效:
4232
+ // - 帧率:对现有轨道调用 applyConstraints(frameRate)
4233
+ // - 系统音频:单独发布/取消发布 ScreenShareAudio 轨道,避免影响视频轨道
4234
+ // if (isScreenShareEnabled?.value) {
4235
+ // try {
4236
+ // const fr = screenShareCaptureOption?.resolution?.frameRate
4237
+ // if (typeof fr === 'number') {
4238
+ // await liveClient.updateScreenShareFrameRate(fr)
4239
+ // }
4240
+ // } catch (_) {}
4241
+ // try {
4242
+ // await liveClient.setScreenShareSystemAudio(systemAudioInclude)
4243
+ // } catch (_) {}
4244
+ // }
4245
+
4246
+ // 3) 按会议号持久化(当前会话内保留,退出会议后不跨会保留)
4247
+ const roomId = this.meetingNum
4248
+ if (roomId) {
4249
+ try {
4250
+ const key = `lk:ss_prefs:${roomId}`
4251
+ const toSave = {
4252
+ systemAudio: this.screenShareCaptureOption.systemAudio,
4253
+ resolution: { ...this.screenShareCaptureOption.resolution },
4254
+ }
4255
+ sessionStorage.setItem(key, JSON.stringify(toSave))
4256
+ } catch (err) {
4257
+ // 忽略持久化异常
4258
+ }
4259
+ }
3397
4260
  },
3398
4261
  stopScreenShare() {
3399
4262
  this.changeScreenShareStatus(this.screenShareCaptureOption);
@@ -3431,6 +4294,7 @@ export default {
3431
4294
  appendInvite(e, inviteWay) {
3432
4295
  let tempList = [];
3433
4296
  let tempDeviceList = []
4297
+ let tempTerminalList = []
3434
4298
  let index = -1;
3435
4299
  if (e && e.length > 0) {
3436
4300
  e.forEach((item) => {
@@ -3451,12 +4315,15 @@ export default {
3451
4315
  tempList.push(item);
3452
4316
  }
3453
4317
  this.addToInviteList(item);
3454
- } else {
4318
+ } else if (item.source == '设备' || item.source == '监控') {
3455
4319
  tempDeviceList.push(item)
4320
+ } else if (item.source == '会议终端') {
4321
+ tempTerminalList.push(item)
3456
4322
  }
3457
4323
  });
3458
4324
  console.log("本次追加邀请人员", tempList);
3459
4325
  console.log("本次追加邀请设备", tempDeviceList);
4326
+ console.log("本次追加邀请会议终端", tempTerminalList);
3460
4327
  // 追加邀请人员
3461
4328
  if(tempList.length > 0) {
3462
4329
  const promises = [];
@@ -3485,7 +4352,8 @@ export default {
3485
4352
  const volteCalls = tempList.map(item => {
3486
4353
  return {
3487
4354
  dnis: item.phone,
3488
- name: item?.label || item?.userName || '未知用户'
4355
+ name: item?.label || item?.userName || '未知用户',
4356
+ type: 0,
3489
4357
  }
3490
4358
  })
3491
4359
  promises.push(
@@ -3524,25 +4392,52 @@ export default {
3524
4392
  });
3525
4393
  }
3526
4394
  // 追加邀请设备
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
- })
4395
+ if (tempDeviceList && tempDeviceList.length > 0) {
4396
+ const deviceCalls = tempDeviceList
4397
+ .map(item => {
4398
+ const id = item?.source === '监控' ? item?.monitorID : item?.equipmentID;
4399
+ return id ? { dnis: id, name: item.label, type: this.handleInviteType(item?.integrationType) } : null;
4400
+ })
4401
+ .filter(Boolean);
4402
+
4403
+ if (deviceCalls.length > 0) {
4404
+ this.liveClient.makeBatchCall(deviceCalls)
4405
+ .then(() => {
4406
+ this.showMessage.message('success', '监控设备批量外呼已发起');
4407
+ })
4408
+ .catch(err => {
4409
+ this.showMessage.message('error', `批量拉取监控设备失败: ${err?.message || err}`);
4410
+ });
4411
+ }
4412
+ }
4413
+ // 追加邀请终端
4414
+ if (tempTerminalList.length > 0) {
4415
+ const terminalCalls = tempTerminalList
4416
+ .map(item => {
4417
+ return item?.id ? { dnis: item.id, name: item.label, type: 4 } : null
4418
+ })
4419
+ .filter(Boolean)
4420
+ if (terminalCalls.length > 0) {
4421
+ this.liveClient
4422
+ .makeBatchCall(terminalCalls)
4423
+ .then(() => {
4424
+ this.showMessage.message('success', '会议终端批量外呼已发起')
4425
+ })
4426
+ .catch(err => {
4427
+ this.showMessage.message('error', `批量拉取会议终端失败: ${err?.message || err}`)
4428
+ })
4429
+ }
3535
4430
  }
3536
4431
  }
3537
4432
  },
3538
- pullMonitorDevice(monitorID, monitorName) {
4433
+ pullMonitorDevice(monitorID, monitorName, integrationType = 0) {
3539
4434
  this.liveClient.judgeUserInMeeting(this.meetingNum, monitorID).then((res) => {
3540
4435
  if (res && res?.code == 200) {
3541
4436
  if (res.data == 1) {
3542
4437
  this.showMessage.message("error", "该监控设备已进入会议");
3543
4438
  return;
3544
4439
  } else {
3545
- this.liveClient.makeCall(monitorName, monitorID, 1);
4440
+ this.liveClient.makeCall(monitorName, monitorID, this.handleInviteType(integrationType));
3546
4441
  }
3547
4442
  } else {
3548
4443
  this.showMessage.message("error", "获取监控设备进会状态失败");
@@ -3557,7 +4452,8 @@ export default {
3557
4452
  const platformGroup = {
3558
4453
  '1_2': [], // platformID 为 1 或 2
3559
4454
  '7': [], // platformID 为 7
3560
- '4': [] // platformID 为 4
4455
+ '4': [], // platformID 为 4
4456
+ '8': [] // platformID 为 8
3561
4457
  };
3562
4458
 
3563
4459
  uninviteList.forEach(item => {
@@ -3569,6 +4465,8 @@ export default {
3569
4465
  platformGroup['7'].push({ userName, identity, phone });
3570
4466
  } else if (platformID === 4) {
3571
4467
  platformGroup['4'].push({ userName, identity, phone });
4468
+ } else if (platformID === 8) {
4469
+ platformGroup['8'].push({ userName, identity, phone });
3572
4470
  }
3573
4471
  });
3574
4472
 
@@ -3619,7 +4517,8 @@ export default {
3619
4517
  // 构建批量外呼数据
3620
4518
  const volteCalls = platformGroup['4'].map(item => ({
3621
4519
  dnis: item.phone,
3622
- name: item.userName
4520
+ name: item.userName,
4521
+ type: 0
3623
4522
  }));
3624
4523
 
3625
4524
  promises.push(
@@ -3636,6 +4535,27 @@ export default {
3636
4535
  );
3637
4536
  }
3638
4537
 
4538
+ // 处理 platformID 为 8 的呼叫
4539
+ if (platformGroup['8'].length > 0) {
4540
+ // 构建批量外呼数据
4541
+ const terminalCalls = platformGroup['8'].map(item => ({
4542
+ dnis: item.phone,
4543
+ name: item.userName,
4544
+ type: 4
4545
+ }))
4546
+
4547
+ promises.push(
4548
+ this.liveClient
4549
+ .makeBatchCall(terminalCalls)
4550
+ .then(() => {
4551
+ this.showMessage.message('success', '已发起终端批量外呼')
4552
+ })
4553
+ .catch(err => {
4554
+ this.showMessage.message('error', `批量呼叫失败 (platformID 8): ${err.message}`)
4555
+ })
4556
+ )
4557
+ }
4558
+
3639
4559
  // 等待所有邀请完成后更新列表
3640
4560
  Promise.allSettled(promises).then(() => {
3641
4561
  // 可以在这里添加统一的后续处理,如刷新邀请列表
@@ -4159,6 +5079,64 @@ export default {
4159
5079
  }
4160
5080
  }
4161
5081
 
5082
+ if (this.currentLayout === 'ring') {
5083
+ // 从环形布局到点调模式:先拆解环形容器,将参与者恢复到主容器,然后按照点调规则分配
5084
+ const layoutRing = document.querySelector('#room .layout-ring');
5085
+ if (layoutRing) {
5086
+ const ringTop = layoutRing.querySelector('.layout-ring-top');
5087
+ const ringBottom = layoutRing.querySelector('.layout-ring-bottom');
5088
+ const ringCenter = layoutRing.querySelector('.ring-center');
5089
+ // 先将center、slots、bottom中的参与者移动回主容器
5090
+ if (ringTop) {
5091
+ const slots = ringTop.querySelectorAll('.ring-slot');
5092
+ slots.forEach(slot => {
5093
+ while (slot.firstChild) {
5094
+ container.appendChild(slot.firstChild);
5095
+ }
5096
+ });
5097
+ if (ringCenter) {
5098
+ while (ringCenter.firstChild) {
5099
+ container.appendChild(ringCenter.firstChild);
5100
+ }
5101
+ }
5102
+ }
5103
+ if (ringBottom) {
5104
+ while (ringBottom.firstChild) {
5105
+ container.appendChild(ringBottom.firstChild);
5106
+ }
5107
+ }
5108
+ layoutRing.remove();
5109
+ }
5110
+
5111
+ // 收集参与者元素
5112
+ const participantElements = Array.from(container.children).filter(child =>
5113
+ child.classList && child.classList.contains('participant') && child.id && child.id.startsWith('participant-')
5114
+ );
5115
+
5116
+ // 分配到点调容器:主持人优先到center[0],焦点到center[1](如果存在且不同),其余进入top/bottom/other
5117
+ // 注意:pointTurnCenter已有两个占位符 placeHolderElm1, placeHolderElm2
5118
+ participantElements.forEach(child => {
5119
+ if (this.curHostIdentity && child.id === `participant-${this.curHostIdentity}`) {
5120
+ // 主持人放在中心第一个位置
5121
+ pointTurnCenter.removeChild(placeHolderElm1);
5122
+ pointTurnCenter.insertBefore(child, pointTurnCenter.firstChild ?? null);
5123
+ } else if (this.curBlurIdentity && child.id === `participant-${this.curBlurIdentity}` && this.curBlurIdentity !== this.curHostIdentity) {
5124
+ // 焦点用户放在中心第二个位置(若不同于主持人)
5125
+ pointTurnCenter.removeChild(placeHolderElm2);
5126
+ pointTurnCenter.appendChild(child);
5127
+ } else {
5128
+ if (pointTurnTop.children.length >= 5) {
5129
+ if (pointTurnBottom.children.length >= 5) {
5130
+ pointTurnOther.appendChild(child);
5131
+ } else {
5132
+ pointTurnBottom.appendChild(child);
5133
+ }
5134
+ } else {
5135
+ pointTurnTop.appendChild(child);
5136
+ }
5137
+ }
5138
+ });
5139
+ }
4162
5140
  // 添加点调容器之前的最终检查
4163
5141
  const remainingLayoutElements = container.querySelectorAll(
4164
5142
  ".layout-leftside, .layout-rightside, .point-turn-top, .point-turn-center, .point-turn-bottom, .point-turn-other"
@@ -4327,6 +5305,92 @@ export default {
4327
5305
  container.insertBefore(layoutLeftSideEle, container.firstChild ?? null);
4328
5306
  container.appendChild(layoutRightSideEle);
4329
5307
  }
5308
+
5309
+ if (this.currentLayout === 'ring') {
5310
+ // 从点调模式到环形布局:创建环形容器,将与会者分配到center/slots/bottom
5311
+ const existingRing = document.querySelector('#room .layout-ring');
5312
+ if (existingRing) {
5313
+ existingRing.remove();
5314
+ }
5315
+ const layoutRing = document.createElement('div');
5316
+ layoutRing.className = 'layout-ring';
5317
+ const layoutRingTop = document.createElement('div');
5318
+ layoutRingTop.className = 'layout-ring-top';
5319
+ const layoutRingBottom = document.createElement('div');
5320
+ layoutRingBottom.className = 'layout-ring-bottom';
5321
+ // 创建12个槽位
5322
+ for (let i = 0; i < 12; i++) {
5323
+ const slot = document.createElement('div');
5324
+ slot.className = 'ring-slot';
5325
+ layoutRingTop.appendChild(slot);
5326
+ }
5327
+ const ringCenter = document.createElement('div');
5328
+ ringCenter.className = 'ring-center';
5329
+ layoutRingTop.appendChild(ringCenter);
5330
+
5331
+ // 将点调容器中的参与者按顺序分配:center(主持人 -> 焦点)、再从top、bottom、other依序填充slots;超出放到bottom
5332
+ const collect = (ele) => (ele ? Array.from(ele.children).filter(c => c.id && c.classList.contains('participant')) : []);
5333
+ const topList = collect(pointTurnTop);
5334
+ const centerList = collect(pointTurnCenter);
5335
+ const bottomList = collect(pointTurnBottom);
5336
+ const otherList = collect(pointTurnOther);
5337
+
5338
+ // 先处理中心:主持人在center首位,若有焦点且不同于主持人,作为第二位
5339
+ let hostEl = centerList.find(c => this.curHostIdentity && c.id === `participant-${this.curHostIdentity}`);
5340
+ if (!hostEl) {
5341
+ // 尝试从其它列表找到主持人
5342
+ hostEl = [...topList, ...bottomList, ...otherList].find(c => this.curHostIdentity && c.id === `participant-${this.curHostIdentity}`);
5343
+ if (hostEl && hostEl.parentElement) hostEl.parentElement.removeChild(hostEl);
5344
+ }
5345
+ if (hostEl) {
5346
+ ringCenter.appendChild(hostEl);
5347
+ }
5348
+ let blurEl = null;
5349
+ if (this.curBlurIdentity && this.curBlurIdentity !== this.curHostIdentity) {
5350
+ blurEl = centerList.find(c => c.id === `participant-${this.curBlurIdentity}`)
5351
+ || topList.find(c => c.id === `participant-${this.curBlurIdentity}`)
5352
+ || bottomList.find(c => c.id === `participant-${this.curBlurIdentity}`)
5353
+ || otherList.find(c => c.id === `participant-${this.curBlurIdentity}`);
5354
+ if (blurEl && blurEl.parentElement) {
5355
+ blurEl.parentElement.removeChild(blurEl);
5356
+ // 注意:ring 只在中心留主持人,焦点不会占用中心,因此将其回到 slots 序列,不放中心
5357
+ }
5358
+ }
5359
+
5360
+ // 构建顺序序列:top -> bottom -> other(去除已被拿走的host/blur)
5361
+ const rest = [...topList, ...bottomList, ...otherList].filter(el => el !== hostEl && el !== blurEl);
5362
+ const slots = layoutRingTop.querySelectorAll('.ring-slot');
5363
+ let filled = 0;
5364
+ // 先将可能存在的焦点用户放在第一个可用槽(如果存在)
5365
+ if (blurEl) {
5366
+ if (filled < slots.length) {
5367
+ slots[filled++].appendChild(blurEl);
5368
+ } else {
5369
+ layoutRingBottom.appendChild(blurEl);
5370
+ }
5371
+ }
5372
+ // 填充剩余slots
5373
+ for (const el of rest) {
5374
+ if (filled < slots.length) {
5375
+ slots[filled++].appendChild(el);
5376
+ } else {
5377
+ layoutRingBottom.appendChild(el);
5378
+ }
5379
+ }
5380
+
5381
+ // 清理点调容器并挂载环容器
5382
+ const removeIfChild = (ele) => { if (ele && container.contains(ele)) container.removeChild(ele); };
5383
+ removeIfChild(pointTurnTop);
5384
+ removeIfChild(pointTurnCenter);
5385
+ removeIfChild(pointTurnBottom);
5386
+ removeIfChild(pointTurnOther);
5387
+
5388
+ layoutRing.appendChild(layoutRingTop);
5389
+ layoutRing.appendChild(layoutRingBottom);
5390
+ container.insertBefore(layoutRing, container.firstChild ?? null);
5391
+ // 点调 -> 环形:创建后同步高度
5392
+ requestAnimationFrame(() => this.updateRingTopHeight());
5393
+ }
4330
5394
  }
4331
5395
  });
4332
5396
  },
@@ -4550,7 +5614,7 @@ export default {
4550
5614
  align-items: center;
4551
5615
  justify-content: space-between;
4552
5616
  position: absolute;
4553
- z-index: 100;
5617
+ z-index: 4100;
4554
5618
  bottom: -66px;
4555
5619
  left: 0;
4556
5620
  transition: all 0.2s ease;