mdm-client 1.0.2 → 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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/App.vue +72 -61
  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/platform_app_icon.png +0 -0
  13. package/src/assets/image/common/platform_mini_icon.png +0 -0
  14. package/src/assets/image/common/platform_pc_icon.png +0 -0
  15. package/src/assets/image/common/platform_volte_icon.png +0 -0
  16. package/src/assets/image/common/rotate_icon1.png +0 -0
  17. package/src/assets/image/common/rotate_icon2.png +0 -0
  18. package/src/assets/image/common/rotate_icon3.png +0 -0
  19. package/src/assets/image/common/rotate_icon4.png +0 -0
  20. package/src/assets/style/base.scss +5 -0
  21. package/src/components/LiveMulti/LiveMulti.vue +26 -15
  22. package/src/components/LiveMultipleMeeting/LiveMultipleMeeting.vue +1443 -228
  23. package/src/components/LiveMultipleMeeting/style/index.scss +145 -14
  24. package/src/components/LivePoint/LivePoint.vue +49 -211
  25. package/src/components/LivePointMeeting/LivePointMeeting.vue +159 -10
  26. package/src/components/LivePointMeeting/style/index.scss +35 -0
  27. package/src/components/MeetingReadyDialog/MeetingReadyDialog.vue +96 -14
  28. package/src/components/other/addressBook.vue +274 -37
  29. package/src/components/other/appointDialog.vue +1 -1
  30. package/src/components/other/customGroupDialog.vue +2 -1
  31. package/src/components/other/customLayout.vue +368 -202
  32. package/src/components/other/editGroupDialog.vue +2 -0
  33. package/src/components/other/layoutSwitch.vue +253 -37
  34. package/src/components/other/leadershipFocus.vue +422 -0
  35. package/src/components/other/leaveOptionDialog.vue +1 -1
  36. package/src/components/other/memberManage.vue +61 -4
  37. package/src/components/other/moreOptionDialog.vue +17 -1
  38. package/src/components/other/selectDialog.vue +1 -1
  39. package/src/components/other/selectSpecialDialog.vue +1 -1
  40. package/src/main.js +4 -4
  41. package/src/utils/api.js +28 -0
  42. package/src/utils/livekit/live-client-esm.js +1 -1
  43. package/src/utils/livekit/live-client-esm-old.js +0 -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: () => ({}),
@@ -441,18 +460,10 @@ export default {
441
460
  type: String,
442
461
  default: "launch",
443
462
  },
444
- isCustomizeMiniInvitations: {
445
- type: Boolean,
446
- default: false,
447
- },
448
463
  miniPagePath: {
449
464
  type: String,
450
465
  default: "",
451
466
  },
452
- defaultInviteWay: {
453
- type: String,
454
- default: "identity",
455
- },
456
467
  isInMeeting: {
457
468
  type: Boolean,
458
469
  default: false,
@@ -461,6 +472,21 @@ export default {
461
472
  type: String,
462
473
  default: "",
463
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
+ },
464
490
  },
465
491
  data() {
466
492
  return {
@@ -504,9 +530,9 @@ export default {
504
530
  // 布局数据
505
531
  currentLayout: "grid",
506
532
  currentRoomMode: "normal",
507
- layoutList: ["grid", "rightSide"],
508
- layoutLabels: ["宫格", "右侧边栏"],
509
-
533
+ layoutList: ["grid", "rightSide", "ring"],
534
+ layoutLabels: ["宫格", "右侧边栏", "环形"],
535
+ currentGridSize: 9,
510
536
  // 状态数据
511
537
  duration: 0,
512
538
  memberManageShow: false,
@@ -590,6 +616,15 @@ export default {
590
616
  durationInterval: null,
591
617
  footerInterval: null,
592
618
  unjoinParticipantInterval: null,
619
+ // 保存与会者画面旋转状态
620
+ rotateDegreeMap: new Map(),
621
+ // 当前聚焦画面
622
+ leaderShipFocus: ""
623
+ };
624
+ },
625
+ provide() {
626
+ return {
627
+ rotateDegreeMap: this.rotateDegreeMap,
593
628
  };
594
629
  },
595
630
  computed: {
@@ -602,12 +637,33 @@ export default {
602
637
  participantIdentities() {
603
638
  return this.participantNum > 0 ? this.participants.map((item) => item.identity) : [];
604
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
+ },
605
658
  inviteNum() {
606
659
  return this.inviteList.length;
607
660
  },
608
661
  deviceNum() {
609
662
  return this.deviceList.length;
610
663
  },
664
+ terminalNum() {
665
+ return this.meetingTerminals.length;
666
+ },
611
667
  invitedNum() {
612
668
  return this.tempInvitedList?.length || 0;
613
669
  },
@@ -629,22 +685,55 @@ export default {
629
685
  return "point-turn";
630
686
  } else {
631
687
  if (this.currentLayout === "grid") {
632
- if (this.participantNum <= 1) {
633
- return "grid1";
634
- } else if (this.participantNum <= 2) {
635
- return "grid2";
636
- } else if (this.participantNum <= 4) {
637
- return "grid3";
638
- } else if (this.participantNum <= 6) {
639
- return "grid4";
640
- } else if (this.participantNum <= 9) {
641
- return "grid5";
642
- } else if (this.participantNum <= 12) {
643
- return "grid6";
644
- } else if (this.participantNum <= 16) {
645
- return "grid7";
646
- } else {
647
- 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
+ }
648
737
  }
649
738
  } else {
650
739
  return this.currentLayout;
@@ -722,14 +811,148 @@ export default {
722
811
  }
723
812
  this.setPageFooterVisible(5);
724
813
  this.initGlobleEvent();
814
+ // 观察#room尺寸变化,动态同步环形布局顶部高度
815
+ this.observeRoomResizeForRing();
816
+ // 首次挂载后尝试同步一次高度
817
+ requestAnimationFrame(() => this.updateRingTopHeight());
725
818
  },
726
819
  beforeDestroy() {
727
820
  this.stopUnjoinParticipantPolling();
728
821
  this.dispatchLiveClientEvent();
729
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
+ }
730
830
  this.liveClient = null;
731
831
  },
732
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
+ },
733
956
  // DOM操作管理函数
734
957
  async batchUpdateStates(updateFn) {
735
958
  this.isBatchUpdating = true;
@@ -876,12 +1099,12 @@ export default {
876
1099
  // 计算MoreOptionDialog选项数量的辅助函数
877
1100
  getOptionCount(identity) {
878
1101
  const participantItem = this.getUserItemByIdentity(identity);
879
- if (!participantItem || !participantItem.metadata) return 8; // 默认最大选项数
1102
+ if (!participantItem || !participantItem.metadata) return 9; // 默认最大选项数
880
1103
 
881
1104
  const metadata = participantItem.metadata;
882
1105
  const isLocalParticipant = identity === this.localIdentity;
883
1106
 
884
- let optionCount = 3; // 基础选项:设为主屏、麦克风、摄像头
1107
+ let optionCount = 4; // 基础选项:设为主屏、麦克风、摄像头、聚焦画面
885
1108
 
886
1109
  if (this.judgeParticipantIsHost(this.localIdentity)) {
887
1110
  // 本地为主持人
@@ -906,7 +1129,7 @@ export default {
906
1129
  }
907
1130
  }
908
1131
 
909
- return Math.min(optionCount, 8); // 限制最大选项数
1132
+ return Math.min(optionCount, 9); // 限制最大选项数
910
1133
  },
911
1134
  // 弹窗定位工具函数
912
1135
  calculateDialogPosition(event, options = {}) {
@@ -1120,16 +1343,16 @@ export default {
1120
1343
  if (this.inviteNum <= 0) {
1121
1344
  this.inviteList.push(userItem);
1122
1345
  } else {
1123
- if (this.judgePersonIsInvited(userItem.id) < 0) {
1346
+ if (this.judgePersonIsInvited(userItem.phone) < 0) {
1124
1347
  this.inviteList.push(userItem);
1125
1348
  }
1126
1349
  }
1127
1350
  },
1128
1351
  // 判断是否已经在邀请列表中
1129
- judgePersonIsInvited(id) {
1352
+ judgePersonIsInvited(phone) {
1130
1353
  if (this.inviteNum > 0) {
1131
1354
  let index = this.inviteList.findIndex((item) => {
1132
- return item.id === id;
1355
+ return item.phone === phone;
1133
1356
  });
1134
1357
  return index;
1135
1358
  } else {
@@ -1367,16 +1590,45 @@ export default {
1367
1590
  this.$emit("meetingStart");
1368
1591
  if (this.joinType == "launch") {
1369
1592
  // 发送邀请信息
1370
- this.sendInviteMessage([], this.defaultInviteWay == "mini" ? 3 : 0);
1593
+ this.sendInviteMessage([]);
1371
1594
  // 拉取监控设备
1372
- this.deviceNum > 0 &&
1373
- this.deviceList.forEach((item) => {
1374
- if (item.source == "监控") {
1375
- this.pullMonitorDevice(item.monitorID, item.label);
1376
- } else if (item.source == "设备") {
1377
- this.pullMonitorDevice(item.equipmentID, item.label);
1378
- }
1379
- });
1595
+ // 使用批量外呼替换逐条外呼
1596
+ if (this.deviceNum > 0 && this.deviceList) {
1597
+ const deviceCalls = this.deviceList
1598
+ .map(item => {
1599
+ const id = item?.source === '监控' ? item?.monitorID : item?.equipmentID;
1600
+ return id ? { dnis: id, name: item.label, type: this.handleInviteType(item?.integrationType) } : null;
1601
+ })
1602
+ .filter(Boolean);
1603
+
1604
+ if (deviceCalls.length > 0) {
1605
+ this.liveClient.makeBatchCall(deviceCalls)
1606
+ .then(() => {
1607
+ this.showMessage.message('success', '监控设备批量外呼已发起');
1608
+ })
1609
+ .catch(err => {
1610
+ this.showMessage.message('error', `批量拉取监控设备失败: ${err?.message || err}`);
1611
+ });
1612
+ }
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
+ }
1380
1632
  }
1381
1633
  // 启动获取未入会和邀请人员轮询
1382
1634
  this.startUnjoinParticipantPolling();
@@ -1472,10 +1724,48 @@ export default {
1472
1724
  }
1473
1725
  });
1474
1726
  if (maxAudioLevelIndex >= 0) {
1475
- 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)
1476
1733
  }
1734
+ } else {
1735
+ this.highlightCurrentSpeaker(-1)
1477
1736
  }
1478
1737
  });
1738
+ this.liveClient.on("resMeetingRefresh", this.handleResMeetingRefresh);
1739
+ },
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) {
1760
+ console.log('resMeetingRefresh事件触发', e);
1761
+
1762
+ if (e.includes("queryAllInvite") || e.includes("queryUnjoined")) {
1763
+ console.log('this', this, this.getUnjoinParticipant);
1764
+
1765
+ this.getUnjoinParticipant();
1766
+ this.queryAllInviteParticipant();
1767
+ this.startUnjoinParticipantPolling();
1768
+ }
1479
1769
  },
1480
1770
  handleRoomDataReceived(e) {
1481
1771
  switch (e.topic) {
@@ -1617,7 +1907,7 @@ export default {
1617
1907
  } else {
1618
1908
  this.meetingCoHost = [];
1619
1909
  }
1620
-
1910
+ this.leaderShipFocus = metadata.leaderShipFocus;
1621
1911
  this.roomMetadata = metadata;
1622
1912
 
1623
1913
  // 批量设置关键状态
@@ -1693,6 +1983,7 @@ export default {
1693
1983
  this.meetingCoHost = [];
1694
1984
  }
1695
1985
 
1986
+ this.leaderShipFocus = metadata.leaderShipFocus;
1696
1987
  this.roomMetadata = metadata;
1697
1988
 
1698
1989
  // 批量设置关键状态
@@ -1765,73 +2056,200 @@ export default {
1765
2056
  clearInterval(this.unjoinParticipantInterval);
1766
2057
  this.unjoinParticipantInterval = null;
1767
2058
  }
1768
-
1769
- // 立即执行一次
1770
- this.getUnjoinParticipant();
1771
- this.queryAllInviteParticipant();
1772
2059
  // 设置5秒间隔的轮询
1773
2060
  this.unjoinParticipantInterval = setInterval(() => {
1774
2061
  this.getUnjoinParticipant();
1775
2062
  this.queryAllInviteParticipant();
1776
- }, 6000);
2063
+ }, 30000);
1777
2064
  },
1778
2065
  initLiveClient() {
1779
2066
  this.liveClient = window["liveClient"];
1780
2067
  this.initLiveClientEvent();
1781
2068
  },
1782
- sendInviteMessage(tempList = [], inviteWay = 0) {
1783
- if (tempList.length <= 0) {
1784
- if (this.tempInviteList.length > 0) {
1785
- this.liveClient
1786
- .inviteParticipant(
1787
- this.meetingNum,
1788
- this.tempInviteList.map((item) => {
1789
- return {
1790
- userName: item.label,
1791
- identity: item?.loginCode ? item.loginCode : item.phone,
1792
- phone: item.phone,
1793
- };
1794
- }),
1795
- false,
1796
- inviteWay
1797
- )
1798
- .then((res) => {
1799
- if (res.code == 200) {
1800
- this.showMessage.message("success", "成功发送邀请");
1801
- this.tempInviteList.forEach((item) => {
1802
- this.addToInviteList(item);
1803
- });
1804
- this.getUnjoinParticipant();
1805
- this.queryAllInviteParticipant();
2069
+ sendInviteMessage(tempList = []) {
2070
+ // 确定使用的邀请列表
2071
+ const inviteList = tempList.length <= 0 ? this.tempInviteList : tempList;
2072
+
2073
+ if (!inviteList || inviteList.length <= 0) {
2074
+ console.log('没有邀请人员');
2075
+ return;
2076
+ }
2077
+
2078
+ // 去重处理:按 phone 字段去重,按优先级保留
2079
+ const deduplicatedList = [];
2080
+ const phoneMap = new Map();
2081
+
2082
+ // 定义优先级:人员 > 组织架构/外部 > volte > 常用分组 > 未知
2083
+ const getPriority = (source) => {
2084
+ if (source === '人员') return 1;
2085
+ if (source === '组织架构' || source === '外部') return 2;
2086
+ if (source === 'volte') return 3;
2087
+ if (source === '常用分组') return 4;
2088
+ return 5; // 未知类型优先级最低
2089
+ };
2090
+
2091
+ // 第一轮:处理非常用分组的项目,建立 phoneMap
2092
+ inviteList.forEach((item, index) => {
2093
+ const phone = item.phone;
2094
+ if (!phone) {
2095
+ // 没有 phone 字段的直接加入
2096
+ deduplicatedList.push(item);
2097
+ return;
2098
+ }
2099
+
2100
+ // 如果是常用分组,先跳过,后续单独处理
2101
+ if (item.source === '常用分组') {
2102
+ return;
2103
+ }
2104
+
2105
+ const existing = phoneMap.get(phone);
2106
+ if (!existing) {
2107
+ // 第一次遇到这个 phone
2108
+ phoneMap.set(phone, { item, index });
2109
+ } else {
2110
+ // 已存在相同 phone,比较优先级
2111
+ const existingPriority = getPriority(existing.item.source);
2112
+ const currentPriority = getPriority(item.source);
2113
+
2114
+ if (currentPriority < existingPriority) {
2115
+ // 当前项优先级更高,替换
2116
+ phoneMap.set(phone, { item, index });
2117
+ } else if (currentPriority === existingPriority && index < existing.index) {
2118
+ // 优先级相同,保留顺序靠前的
2119
+ phoneMap.set(phone, { item, index });
2120
+ }
2121
+ // 否则保持原有的
2122
+ }
2123
+ });
2124
+
2125
+ // 第二轮:处理常用分组项目,根据 original 字段分类
2126
+ const commonGroupItems = inviteList.filter(item => item.source === '常用分组');
2127
+ commonGroupItems.forEach((item, index) => {
2128
+ const phone = item.phone;
2129
+ if (!phone) {
2130
+ // 没有 phone 字段的直接加入
2131
+ deduplicatedList.push(item);
2132
+ return;
2133
+ }
2134
+
2135
+ // 如果 phone 已经在 phoneMap 中,跳过(已有更高优先级的项)
2136
+ if (phoneMap.has(phone)) {
2137
+ return;
2138
+ }
2139
+
2140
+ // 根据 original 字段确定实际的 source
2141
+ let actualSource = item.original;
2142
+
2143
+ // 如果 original 字段无效,默认为组织架构
2144
+ if (!actualSource || !['人员', '组织架构', 'volte', '外部'].includes(actualSource)) {
2145
+ actualSource = '组织架构';
2146
+ }
2147
+
2148
+ // 创建新的项目,使用 original 作为实际 source
2149
+ const processedItem = {
2150
+ ...item,
2151
+ source: actualSource
2152
+ };
2153
+
2154
+ // 添加到 phoneMap
2155
+ phoneMap.set(phone, { item: processedItem, index: index + inviteList.length });
2156
+ });
2157
+
2158
+ // 将去重后的项添加到结果列表
2159
+ phoneMap.forEach(({ item }) => {
2160
+ if (!deduplicatedList.find(existing => existing.phone === item.phone)) {
2161
+ deduplicatedList.push(item);
2162
+ }
2163
+ });
2164
+
2165
+ // 按 source 分组处理
2166
+ const personList = [];
2167
+ const orgAndExternalList = [];
2168
+ const volteList = [];
2169
+
2170
+ deduplicatedList.forEach(item => {
2171
+ if (item.source === '人员') {
2172
+ personList.push(item);
2173
+ } else if (item.source === '组织架构' || item.source === '外部') {
2174
+ orgAndExternalList.push(item);
2175
+ } else if (item.source === 'volte') {
2176
+ volteList.push(item);
2177
+ }
2178
+ });
2179
+
2180
+ // 发送邀请
2181
+ const promises = [];
2182
+
2183
+ // 处理人员邀请(invitationMethod = 0)
2184
+ if (personList.length > 0) {
2185
+ const personInviteData = personList.map(item => ({
2186
+ userName: item.label,
2187
+ identity: item?.loginCode ? item.loginCode : item.phone,
2188
+ phone: item.phone
2189
+ }));
2190
+
2191
+ promises.push(
2192
+ this.liveClient.inviteParticipant(this.meetingNum, personInviteData, false, 0)
2193
+ .then(res => {
2194
+ if (res.code === 200) {
2195
+ personList.forEach(item => this.addToInviteList(item));
1806
2196
  } else {
1807
- this.showMessage.message("error", res?.msg);
2197
+ this.showMessage.message("error", `邀请人员失败: ${res?.msg}`);
1808
2198
  }
1809
- });
1810
- }
1811
- } else {
1812
- this.liveClient
1813
- .inviteParticipant(
1814
- this.meetingNum,
1815
- tempList.map((item) => {
1816
- return {
1817
- userName: item.label,
1818
- identity: item?.loginCode ? item.loginCode : item.phone,
1819
- phone: item.phone,
1820
- };
1821
- }),
1822
- false,
1823
- inviteWay
1824
- )
1825
- .then((res) => {
1826
- if (res.code == 200) {
1827
- this.showMessage.message("success", "成功发送邀请");
1828
- this.getUnjoinParticipant();
1829
- this.queryAllInviteParticipant();
1830
- } else {
1831
- this.showMessage.message("error", res?.msg);
1832
- }
1833
- });
2199
+ })
2200
+ .catch(err => {
2201
+ this.showMessage.message("error", `邀请人员失败: ${err.message}`);
2202
+ })
2203
+ );
2204
+ }
2205
+
2206
+ // 处理组织架构和外部邀请(invitationMethod = 3)
2207
+ if (orgAndExternalList.length > 0) {
2208
+ const orgInviteData = orgAndExternalList.map(item => ({
2209
+ userName: item.label,
2210
+ identity: item?.loginCode ? item.loginCode : item.phone,
2211
+ phone: item.phone
2212
+ }));
2213
+
2214
+ promises.push(
2215
+ this.liveClient.inviteParticipant(this.meetingNum, orgInviteData, false, 3)
2216
+ .then(res => {
2217
+ if (res.code === 200) {
2218
+ orgAndExternalList.forEach(item => this.addToInviteList(item));
2219
+ } else {
2220
+ this.showMessage.message("error", `邀请组织/外部人员失败: ${res?.msg}`);
2221
+ }
2222
+ })
2223
+ .catch(err => {
2224
+ this.showMessage.message("error", `邀请组织/外部人员失败: ${err.message}`);
2225
+ })
2226
+ );
1834
2227
  }
2228
+
2229
+ // 处理 volte 呼叫
2230
+ if (volteList.length > 0) {
2231
+ // 构建批量外呼数据
2232
+ const volteCalls = volteList.map(item => ({
2233
+ dnis: item.phone,
2234
+ name: item.label,
2235
+ type: 0
2236
+ }));
2237
+
2238
+ promises.push(
2239
+ this.liveClient.makeBatchCall(volteCalls)
2240
+ .then(() => {
2241
+ volteList.forEach(item => this.addToInviteList(item));
2242
+ })
2243
+ .catch(err => {
2244
+ this.showMessage.message("error", `批量呼叫失败: ${err.message}`);
2245
+ })
2246
+ );
2247
+ }
2248
+
2249
+ // 等待所有邀请完成后更新列表
2250
+ Promise.allSettled(promises).then(() => {
2251
+ // 可以在这里添加统一的后续处理,如刷新邀请列表
2252
+ });
1835
2253
  },
1836
2254
  queryAllInviteParticipant() {
1837
2255
  this.liveClient.getAllInviteParticipant(this.meetingNum).then((res) => {
@@ -1856,9 +2274,9 @@ export default {
1856
2274
  roomNum: this.meetingNum,
1857
2275
  cameraStatus: this.isCameraEnabled,
1858
2276
  microphoneStatus: this.isMicrophoneEnabled,
1859
- audioDeviceId: this.activeDevice.audioInputDevice,
1860
- videoDeviceId: this.activeDevice.videoDevice,
1861
- outputDeviceId: this.activeDevice.audioOutputDevice,
2277
+ audioDeviceId: this.tempActiveDevice.audioInputDevice,
2278
+ videoDeviceId: this.tempActiveDevice.videoDevice,
2279
+ outputDeviceId: this.tempActiveDevice.audioOutputDevice,
1862
2280
  };
1863
2281
  let tempActiveDevice = null;
1864
2282
  try {
@@ -1992,36 +2410,83 @@ export default {
1992
2410
  </div>
1993
2411
  `;
1994
2412
  } else if (videoItem.isCameraEnabled) {
1995
- videoDom.innerHTML = `
1996
- <video id="video-${
1997
- videoItem.identity
1998
- }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
1999
- <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2000
- <div id="more-${videoItem.identity}" class="more-icon"></div>
2001
- <div class="describe">
2002
- <div id="microphone-${videoItem.identity}" class="microphone"></div>
2003
- <div id="${videoItem.identity}" class="identity">${
2004
- videoItem.isLocal ? videoItem.name + "(我)" : videoItem.name
2005
- }</div>
2006
- </div>
2007
- `;
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
+ }
2008
2448
  } else {
2009
- videoDom.innerHTML = `
2010
- <video id="video-${
2011
- videoItem.identity
2012
- }" class="p-video" autoplay webkit-playsinline playsinline x5-video-player-type="h5"></video>
2013
- <div id="board-${videoItem.identity}" class="board">
2014
- <div class="board-icon"></div>
2015
- </div>
2016
- <div id="signal-${videoItem.identity}" class="signal-icon signal-icon-good"></div>
2017
- <div id="more-${videoItem.identity}" class="more-icon"></div>
2018
- <div class="describe">
2019
- <div id="microphone-${videoItem.identity}" class="microphone"></div>
2020
- <div id="${videoItem.identity}" class="identity">${
2021
- videoItem.isLocal ? videoItem.name + "(我)" : videoItem.name
2022
- }</div>
2023
- </div>
2024
- `;
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
+ }
2025
2490
  }
2026
2491
  return videoDom;
2027
2492
  },
@@ -2170,6 +2635,57 @@ export default {
2170
2635
  layoutRightSideEle.appendChild(videoDiv);
2171
2636
  }
2172
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
+ }
2173
2689
  }
2174
2690
  }
2175
2691
  return container;
@@ -2197,6 +2713,25 @@ export default {
2197
2713
  videoDiv = this.constructParticipantDom(videoItem);
2198
2714
  // 构建会议室布局
2199
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
+ }
2200
2735
  // 添加到participant数组
2201
2736
  this.addToParticipantList(videoItem);
2202
2737
  }
@@ -2211,6 +2746,8 @@ export default {
2211
2746
  let signalElm = document.getElementById(`signal-${videoItem.identity}`);
2212
2747
  // 与会者更多操作按钮元素
2213
2748
  let moreElm = document.getElementById(`more-${videoItem.identity}`);
2749
+ // 与会者画面旋转按钮元素
2750
+ let rotateElm = document.getElementById(`rotate-${videoItem.identity}`)
2214
2751
  // 声明麦克风按钮点击事件回调
2215
2752
  const unableMicrophone = () => {
2216
2753
  this.liveClient.changeParticipantMicrophoneStatus(videoItem.identity, true);
@@ -2225,6 +2762,8 @@ export default {
2225
2762
  const currentMoreElm = document.getElementById(`more-${videoItem.identity}`);
2226
2763
  currentSignalElm && (currentSignalElm.style.visibility = "visible");
2227
2764
  currentMoreElm && (currentMoreElm.style.visibility = "visible");
2765
+ const currentRotateElm = document.getElementById(`rotate-${videoItem.identity}`);
2766
+ currentRotateElm && (currentRotateElm.style.visibility = "visible");
2228
2767
  };
2229
2768
  const signalAndMoreHide = () => {
2230
2769
  // 每次执行时重新获取最新的元素
@@ -2232,6 +2771,8 @@ export default {
2232
2771
  const currentMoreElm = document.getElementById(`more-${videoItem.identity}`);
2233
2772
  currentSignalElm && (currentSignalElm.style.visibility = "hidden");
2234
2773
  currentMoreElm && (currentMoreElm.style.visibility = "hidden");
2774
+ const currentRotateElm = document.getElementById(`rotate-${videoItem.identity}`);
2775
+ currentRotateElm && (currentRotateElm.style.visibility = "hidden");
2235
2776
  };
2236
2777
  // 声明操作按钮元素点击事件回调
2237
2778
  const moreElmClick = (event) => {
@@ -2254,6 +2795,13 @@ export default {
2254
2795
  this.moreDialogShow = true;
2255
2796
  }
2256
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
+ };
2257
2805
  // 为麦克风元素绑定事件
2258
2806
  if (microElm && videoItem.isMicrophoneEnabled) {
2259
2807
  microElm.className = "microphone microphone-active";
@@ -2281,12 +2829,47 @@ export default {
2281
2829
  if (moreElm) {
2282
2830
  moreElm.onclick = moreElmClick;
2283
2831
  }
2832
+ if (rotateElm) {
2833
+ rotateElm.onclick = rotateElmClick;
2834
+ }
2284
2835
  if (signalElm) {
2285
2836
  if (videoItem.connectionQuality === "excellent" || videoItem.connectionQuality === "good") {
2286
2837
  signalElm.className = "signal-icon signal-icon-good";
2287
2838
  } else {
2288
2839
  signalElm.className = "signal-icon signal-icon-poor";
2289
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
+ }
2290
2873
  }
2291
2874
  let screenShareElm = document.getElementById(`screenshare-${videoItem.identity}`);
2292
2875
  // 与会者dom元素内部样式重新渲染(摄像头或屏幕共享切换后)
@@ -2336,6 +2919,15 @@ export default {
2336
2919
  signalElm.className = "signal-icon signal-icon-poor";
2337
2920
  }
2338
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
+ }
2339
2931
  }
2340
2932
  // if (!moreElm) {
2341
2933
  // // 构建更多操作按钮元素
@@ -2358,6 +2950,28 @@ export default {
2358
2950
  moreElm = null;
2359
2951
  }
2360
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
+ }
2361
2975
  } else {
2362
2976
  if (!boardElm) {
2363
2977
  // 之前为屏幕共享状态或摄像头开启状态
@@ -2391,6 +3005,11 @@ export default {
2391
3005
  this.removeFromParticipantList(videoItem.identity);
2392
3006
  this.filterParticipantList();
2393
3007
 
3008
+ // 清理可能存在的旋转状态
3009
+ if (this.rotateDegreeMap.has(videoItem.identity)) {
3010
+ this.rotateDegreeMap.delete(videoItem.identity);
3011
+ }
3012
+
2394
3013
  // 处理焦点用户离开的情况
2395
3014
  if (this.curBlurIdentity === videoItem.identity) {
2396
3015
  console.log("焦点用户离开会议:", videoItem.identity);
@@ -2418,18 +3037,6 @@ export default {
2418
3037
  this.curHostIdentity = null;
2419
3038
  // 主持人离开后具体的逻辑处理放到watch中处理
2420
3039
  }
2421
- // pointTurn模式下,主持人离开处理
2422
- // if (currentRoomMode.value === "pointTurn" && videoItem.identity === curHostIdentity.value) {
2423
- // 当前离开与会者为会议主持人
2424
- // const pointTurnCenter = document.querySelector("#room .point-turn-center");
2425
- // if (pointTurnCenter) {
2426
- // // 会议中没有其他主持人,显示默认占位元素
2427
- // const placeHolderElm = document.createElement("div");
2428
- // placeHolderElm.className = "participant";
2429
- // placeHolderElm.innerHTML = placeholderTemplate;
2430
- // pointTurnCenter.insertBefore(placeHolderElm, pointTurnCenter.firstChild ?? null);
2431
- // }
2432
- // }
2433
3040
  // pointTurn模式下的其他用户离开处理
2434
3041
  if (
2435
3042
  this.currentRoomMode === "pointTurn" &&
@@ -2538,6 +3145,10 @@ export default {
2538
3145
  // 更新与会者数组
2539
3146
  this.addToParticipantList(videoItem);
2540
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
+ }
2541
3152
  // 与会者结束共享,切换回到之前布局(仅主持人执行)
2542
3153
  if (this.currentRoomMode === "normal" && this.judgeParticipantIsHost(this.localIdentity)) {
2543
3154
  console.log(
@@ -2750,7 +3361,73 @@ export default {
2750
3361
 
2751
3362
  if (oldLayout === "grid" && newLayout === "ring") {
2752
3363
  // 从宫格布局切换到环状布局
2753
- // 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());
2754
3431
  }
2755
3432
 
2756
3433
  if (oldLayout === "grid" && newLayout === "downLSide") {
@@ -2798,6 +3475,178 @@ export default {
2798
3475
  // 添加其他元素
2799
3476
  container.appendChild(fragment);
2800
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
+ }
2801
3650
  }
2802
3651
  });
2803
3652
  },
@@ -2897,7 +3746,6 @@ export default {
2897
3746
  this.liveClient.deleteUnjoinParticipant(this.meetingNum, item.identity).then((res) => {
2898
3747
  if (res.code == 200) {
2899
3748
  this.showMessage.message("success", "成功删除未入会人员");
2900
- this.getUnjoinParticipant();
2901
3749
  this.removeFromInviteList(item.identity);
2902
3750
  } else {
2903
3751
  this.showMessage.message("error", res?.msg);
@@ -3137,12 +3985,12 @@ export default {
3137
3985
  },
3138
3986
  async openCamera(e) {
3139
3987
  if (this.liveClient) {
3140
- await this.liveClient.changeParticipantCameraStatus(e, true);
3988
+ await this.liveClient.changeParticipantCameraStatus(e, false);
3141
3989
  }
3142
3990
  },
3143
3991
  async closeCamera(e) {
3144
3992
  if (this.liveClient) {
3145
- await this.liveClient.changeParticipantCameraStatus(e, false);
3993
+ await this.liveClient.changeParticipantCameraStatus(e, true);
3146
3994
  }
3147
3995
  },
3148
3996
  async setToHost(e) {
@@ -3188,35 +4036,49 @@ export default {
3188
4036
  e.forEach((item) => {
3189
4037
  this.addToInviteList(item);
3190
4038
  });
4039
+ this.sendInviteMessage(e)
3191
4040
  }
3192
- let tempList = [];
3193
- let index = -1;
3194
- this.inviteList.forEach((item) => {
3195
- if (this.invitedNum > 0) {
3196
- index = this.tempInvitedList.findIndex((ele) => {
3197
- if (item?.loginCode) {
3198
- return ele.identity === item.loginCode;
3199
- } else {
3200
- return ele.identity === item.phone;
3201
- }
3202
- });
3203
- if (index < 0) {
3204
- // 当前人员尚未被邀请
3205
- tempList.push(item);
3206
- }
3207
- } else {
3208
- tempList.push(item);
3209
- }
3210
- });
3211
-
3212
- this.sendInviteMessage(tempList, this.defaultInviteWay == "mini" ? 3 : 0);
3213
4041
  },
3214
4042
  async appendInviteDevice(e) {
3215
4043
  console.log("通讯录邀请设备", e);
3216
4044
  if (e && e.length > 0) {
3217
- e.forEach((item) => {
3218
- this.pullMonitorDevice(item?.equipmentID || item?.monitorID, item.label);
3219
- });
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
+ }
3220
4082
  }
3221
4083
  },
3222
4084
  async updateNameConfirm(e) {
@@ -3248,7 +4110,30 @@ export default {
3248
4110
  type,
3249
4111
  });
3250
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
+ },
3251
4134
  async chooseVideoDevice(e) {
4135
+ this.dispatchLocalTrack('video')
4136
+ await this.sleep(100)
3252
4137
  await this.changeActiveDevice("videoinput", e);
3253
4138
  this.videoSelectShow = false;
3254
4139
  },
@@ -3309,17 +4194,18 @@ export default {
3309
4194
  },
3310
4195
  appendInvite(e, inviteWay) {
3311
4196
  let tempList = [];
3312
- let tempDeviceList = [];
4197
+ let tempDeviceList = []
4198
+ let tempTerminalList = []
3313
4199
  let index = -1;
3314
4200
  if (e && e.length > 0) {
3315
4201
  e.forEach((item) => {
3316
- if (item.source == "人员") {
4202
+ if(item.source == "人员") {
3317
4203
  if (this.invitedNum > 0) {
3318
4204
  index = this.tempInvitedList.findIndex((ele) => {
3319
- if (item?.loginCode) {
3320
- return ele.identity === item.loginCode;
4205
+ if(item?.loginCode) {
4206
+ return ele.identity === item.loginCode
3321
4207
  } else {
3322
- return ele.identity === item.phone;
4208
+ return ele.identity === item.phone
3323
4209
  }
3324
4210
  });
3325
4211
  if (index < 0) {
@@ -3330,32 +4216,129 @@ export default {
3330
4216
  tempList.push(item);
3331
4217
  }
3332
4218
  this.addToInviteList(item);
3333
- } else {
3334
- tempDeviceList.push(item);
4219
+ } else if (item.source == '设备' || item.source == '监控') {
4220
+ tempDeviceList.push(item)
4221
+ } else if (item.source == '会议终端') {
4222
+ tempTerminalList.push(item)
3335
4223
  }
3336
4224
  });
3337
4225
  console.log("本次追加邀请人员", tempList);
3338
4226
  console.log("本次追加邀请设备", tempDeviceList);
3339
- this.sendInviteMessage(tempList, inviteWay == "mini" ? 3 : 0);
3340
- if (tempDeviceList.length > 0) {
3341
- tempDeviceList.forEach((item) => {
3342
- if (item.source == "设备") {
3343
- this.pullMonitorDevice(item.equipmentID, item.label);
3344
- } else if (item.source == "监控") {
3345
- this.pullMonitorDevice(item.monitorID, item.label);
3346
- }
4227
+ console.log("本次追加邀请会议终端", tempTerminalList);
4228
+ // 追加邀请人员
4229
+ if(tempList.length > 0) {
4230
+ const promises = [];
4231
+ if(inviteWay == "identity") {
4232
+ const applicationInviteData = tempList.map(item => {
4233
+ return {
4234
+ userName: item?.label || item?.userName || '未知用户',
4235
+ identity: item?.loginCode || item.phone,
4236
+ phone: item.phone,
4237
+ }
4238
+ })
4239
+ promises.push(
4240
+ this.liveClient.inviteParticipant(this.meetingNum, applicationInviteData, false, 0)
4241
+ .then(res => {
4242
+ if (res.code === 200) {
4243
+ console.log("邀请用户成功", applicationInviteData);
4244
+ } else {
4245
+ this.showMessage.message("error", `邀请用户失败: ${res?.msg}`);
4246
+ }
4247
+ })
4248
+ .catch(err => {
4249
+ this.showMessage.message("error", `邀请用户失败: ${err.message}`);
4250
+ })
4251
+ );
4252
+ } else if(inviteWay == "volte") {
4253
+ const volteCalls = tempList.map(item => {
4254
+ return {
4255
+ dnis: item.phone,
4256
+ name: item?.label || item?.userName || '未知用户',
4257
+ type: 0,
4258
+ }
4259
+ })
4260
+ promises.push(
4261
+ this.liveClient.makeBatchCall(volteCalls)
4262
+ .then(() => {
4263
+ console.log("批量呼叫成功", volteCalls);
4264
+ })
4265
+ .catch(err => {
4266
+ this.showMessage.message("error", `批量呼叫失败: ${err.message}`);
4267
+ })
4268
+ );
4269
+ } else if(inviteWay == "mini") {
4270
+ const miniInviteData = tempList.map(item => {
4271
+ return {
4272
+ userName: item?.label || item?.userName || '未知用户',
4273
+ phone: item.phone,
4274
+ identity: item?.loginCode || item.phone,
4275
+ }
4276
+ })
4277
+ promises.push(
4278
+ this.liveClient.inviteParticipant(this.meetingNum, miniInviteData, false, 3)
4279
+ .then(res => {
4280
+ if (res.code === 200) {
4281
+ console.log("邀请用户成功", miniInviteData);
4282
+ } else {
4283
+ this.showMessage.message("error", `邀请用户失败: ${res?.msg}`);
4284
+ }
4285
+ })
4286
+ .catch(err => {
4287
+ this.showMessage.message("error", `邀请用户失败: ${err.message}`);
4288
+ })
4289
+ )
4290
+ }
4291
+ Promise.allSettled(promises).then(() => {
4292
+ // 添加邀请成功后统一的后续处理
3347
4293
  });
3348
4294
  }
4295
+ // 追加邀请设备
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
+ }
4331
+ }
3349
4332
  }
3350
4333
  },
3351
- pullMonitorDevice(monitorID, monitorName) {
4334
+ pullMonitorDevice(monitorID, monitorName, integrationType = 0) {
3352
4335
  this.liveClient.judgeUserInMeeting(this.meetingNum, monitorID).then((res) => {
3353
4336
  if (res && res?.code == 200) {
3354
4337
  if (res.data == 1) {
3355
4338
  this.showMessage.message("error", "该监控设备已进入会议");
3356
4339
  return;
3357
4340
  } else {
3358
- this.liveClient.makeCall(monitorName, monitorID, 1);
4341
+ this.liveClient.makeCall(monitorName, monitorID, this.handleInviteType(integrationType));
3359
4342
  }
3360
4343
  } else {
3361
4344
  this.showMessage.message("error", "获取监控设备进会状态失败");
@@ -3365,51 +4348,138 @@ export default {
3365
4348
 
3366
4349
  async sendAppendInviteMessage(uninviteList = []) {
3367
4350
  // 政协项目新增
3368
- if (this.isCustomizeMiniInvitations) {
3369
- this.$emit("inviteMessageSend", {
3370
- inviteUserList: uninviteList,
3371
- meetingName: this.meetingName,
3372
- meetingNum: this.meetingNum,
3373
- fromUser: this.userData.username,
4351
+ if (uninviteList.length > 0) {
4352
+ // 按 platformID 分组处理
4353
+ const platformGroup = {
4354
+ '1_2': [], // platformID 为 1 或 2
4355
+ '7': [], // platformID 为 7
4356
+ '4': [], // platformID 为 4
4357
+ '8': [] // platformID 为 8
4358
+ };
4359
+
4360
+ uninviteList.forEach(item => {
4361
+ const { userName, identity, phone, platformID } = item;
4362
+
4363
+ if (platformID === 30) {
4364
+ platformGroup['1_2'].push({ userName, identity, phone });
4365
+ } else if (platformID === 7) {
4366
+ platformGroup['7'].push({ userName, identity, phone });
4367
+ } else if (platformID === 4) {
4368
+ platformGroup['4'].push({ userName, identity, phone });
4369
+ } else if (platformID === 8) {
4370
+ platformGroup['8'].push({ userName, identity, phone });
4371
+ }
3374
4372
  });
3375
- } else {
3376
- if (uninviteList.length > 0) {
3377
- this.liveClient
3378
- .inviteParticipant(
3379
- this.meetingNum,
3380
- uninviteList.map((item) => {
3381
- return {
3382
- userName: item.userName,
3383
- identity: item.identity,
3384
- phone: item.phone,
3385
- };
4373
+
4374
+ const promises = [];
4375
+
4376
+ // 处理 platformID 为 1 或 2 的邀请(invitationMethod = 0)
4377
+ if (platformGroup['1_2'].length > 0) {
4378
+ promises.push(
4379
+ this.liveClient.inviteParticipant(this.meetingNum, platformGroup['1_2'], false, 0)
4380
+ .then(res => {
4381
+ if (res.code === 200) {
4382
+ platformGroup['1_2'].forEach(item => {
4383
+ const inviteItem = uninviteList.find(u => u.identity === item.identity);
4384
+ if (inviteItem) this.addToInviteList(inviteItem);
4385
+ });
4386
+ } else {
4387
+ this.showMessage.message("error", `邀请用户失败 (platformID 1/2): ${res?.msg}`);
4388
+ }
3386
4389
  })
3387
- )
3388
- .then((res) => {
3389
- if (res.code == 200) {
3390
- this.showMessage.message("success", "成功发送邀请");
3391
- this.getUnjoinParticipant();
3392
- } else {
3393
- this.showMessage.message("error", res?.msg);
3394
- }
3395
- });
4390
+ .catch(err => {
4391
+ this.showMessage.message("error", `邀请用户失败 (platformID 1/2): ${err.message}`);
4392
+ })
4393
+ );
4394
+ }
4395
+
4396
+ // 处理 platformID 为 7 的邀请(invitationMethod = 3)
4397
+ if (platformGroup['7'].length > 0) {
4398
+ promises.push(
4399
+ this.liveClient.inviteParticipant(this.meetingNum, platformGroup['7'], false, 3)
4400
+ .then(res => {
4401
+ if (res.code === 200) {
4402
+ platformGroup['7'].forEach(item => {
4403
+ const inviteItem = uninviteList.find(u => u.identity === item.identity);
4404
+ if (inviteItem) this.addToInviteList(inviteItem);
4405
+ });
4406
+ } else {
4407
+ this.showMessage.message("error", `邀请用户失败 (platformID 7): ${res?.msg}`);
4408
+ }
4409
+ })
4410
+ .catch(err => {
4411
+ this.showMessage.message("error", `邀请用户失败 (platformID 7): ${err.message}`);
4412
+ })
4413
+ );
4414
+ }
4415
+
4416
+ // 处理 platformID 为 4 的呼叫
4417
+ if (platformGroup['4'].length > 0) {
4418
+ // 构建批量外呼数据
4419
+ const volteCalls = platformGroup['4'].map(item => ({
4420
+ dnis: item.phone,
4421
+ name: item.userName,
4422
+ type: 0
4423
+ }));
4424
+
4425
+ promises.push(
4426
+ this.liveClient.makeBatchCall(volteCalls)
4427
+ .then(() => {
4428
+ platformGroup['4'].forEach(item => {
4429
+ const inviteItem = uninviteList.find(u => u.identity === item.identity);
4430
+ if (inviteItem) this.addToInviteList(inviteItem);
4431
+ });
4432
+ })
4433
+ .catch(err => {
4434
+ this.showMessage.message("error", `批量呼叫失败 (platformID 4): ${err.message}`);
4435
+ })
4436
+ );
3396
4437
  }
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
+
4460
+ // 等待所有邀请完成后更新列表
4461
+ Promise.allSettled(promises).then(() => {
4462
+ // 可以在这里添加统一的后续处理,如刷新邀请列表
4463
+ });
3397
4464
  }
3398
4465
  },
3399
4466
  phoneCall(num) {
3400
4467
  this.liveClient.makeCall(num, num);
3401
4468
  },
3402
4469
  async miniCall(num) {
3403
- if (this.isCustomizeMiniInvitations) {
3404
- this.$emit("miniInviteSend", {
3405
- inviteMobile: num,
3406
- fromUser: this.userData.username,
3407
- meetingName: this.meetingName,
3408
- meetingNum: this.meetingNum,
3409
- });
3410
- } else {
3411
- this.showMessage.message("info", "当前功能暂未实现");
3412
- }
4470
+ this.liveClient.inviteParticipant(this.meetingNum, [{
4471
+ userName: num,
4472
+ phone: num,
4473
+ identity: num,
4474
+ }], false, 3).then(res => {
4475
+ if (res.code === 200) {
4476
+ this.showMessage.message("success", "成功邀请小程序用户");
4477
+ } else {
4478
+ this.showMessage.message("error", `邀请小程序用户失败: ${res?.msg}`);
4479
+ }
4480
+ }).catch(err => {
4481
+ this.showMessage.message("error", `邀请小程序用户失败: ${err.message}`);
4482
+ });
3413
4483
  },
3414
4484
  sleep(waitTimeInMs) {
3415
4485
  return new Promise((resolve) => setTimeout(resolve, waitTimeInMs));
@@ -3910,6 +4980,64 @@ export default {
3910
4980
  }
3911
4981
  }
3912
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
+ }
3913
5041
  // 添加点调容器之前的最终检查
3914
5042
  const remainingLayoutElements = container.querySelectorAll(
3915
5043
  ".layout-leftside, .layout-rightside, .point-turn-top, .point-turn-center, .point-turn-bottom, .point-turn-other"
@@ -4078,6 +5206,92 @@ export default {
4078
5206
  container.insertBefore(layoutLeftSideEle, container.firstChild ?? null);
4079
5207
  container.appendChild(layoutRightSideEle);
4080
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
+ }
4081
5295
  }
4082
5296
  });
4083
5297
  },
@@ -4113,10 +5327,11 @@ export default {
4113
5327
  this.liveClient.off(eventName);
4114
5328
  });
4115
5329
  }
5330
+ this.liveClient.off("resMeetingRefresh", this.handleResMeetingRefresh);
4116
5331
  }
4117
- },
5332
+ }
4118
5333
  },
4119
- };
5334
+ }
4120
5335
  </script>
4121
5336
 
4122
5337
  <style lang="scss" scoped>
@@ -4300,7 +5515,7 @@ export default {
4300
5515
  align-items: center;
4301
5516
  justify-content: space-between;
4302
5517
  position: absolute;
4303
- z-index: 100;
5518
+ z-index: 2100;
4304
5519
  bottom: -66px;
4305
5520
  left: 0;
4306
5521
  transition: all 0.2s ease;