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
@@ -19,6 +19,10 @@
19
19
  ></div>
20
20
  <div class="identity">{{ curSpeaker.name }}</div>
21
21
  </div>
22
+ <!-- 当当前讲话者未开启摄像头时,显示占位板(与 liveMultipleMeeting 的 board 样式一致) -->
23
+ <div v-if="!curSpeakerCameraStatus" class="board">
24
+ <div class="board-icon"></div>
25
+ </div>
22
26
  </div>
23
27
  <div class="speaking-wrapper" v-show="!isExpand">
24
28
  <div :class="['cur-speaking-mic', curSpeakerMicrophoneStatus ? 'mic-on' : 'mic-off']"></div>
@@ -34,8 +38,10 @@
34
38
  >
35
39
  <div class="minium-video-dialog-footer-btn mic-on" v-if="localMicrophoneStatus" @click="changeLocalMicrophoneStatus"></div>
36
40
  <div class="minium-video-dialog-footer-btn mic-off" v-else @click="changeLocalMicrophoneStatus"></div>
37
- <div class="minium-video-dialog-footer-btn camera-on" v-if="localCameraStatus" @click="changeLocalCameraStatus"></div>
38
- <div class="minium-video-dialog-footer-btn camera-off" v-else @click="changeLocalCameraStatus"></div>
41
+ <template v-if="roomMode === 'auto'">
42
+ <div class="minium-video-dialog-footer-btn camera-on" v-if="localCameraStatus" @click="changeLocalCameraStatus"></div>
43
+ <div class="minium-video-dialog-footer-btn camera-off" v-else @click="changeLocalCameraStatus"></div>
44
+ </template>
39
45
  <div class="minium-video-dialog-footer-btn reset" @click="reset"></div>
40
46
  <div :class="['minium-video-dialog-footer-btn', isExpand ? 'slide' : 'expand']" @click="changeExpand"></div>
41
47
  </div>
@@ -59,6 +65,8 @@ export default {
59
65
  isExpand: true,
60
66
  curSpeakers: [],
61
67
  curSpeaker: null,
68
+ // 统一维护的 liveClient 实例,避免在各处重复通过 window 访问
69
+ liveClient: null,
62
70
  localCameraStatus: false,
63
71
  localMicrophoneStatus: false,
64
72
  curSpeakerMicrophoneStatus: false,
@@ -66,6 +74,8 @@ export default {
66
74
  curSpeakerTrack: null,
67
75
  showFooter: false,
68
76
  footerTimer: null,
77
+ // room模式,auto-音视频,audio-仅音频
78
+ roomMode: 'auto',
69
79
  };
70
80
  },
71
81
  computed: {
@@ -84,7 +94,10 @@ export default {
84
94
  },
85
95
  beforeDestroy() {
86
96
  this.clearFooterTimer();
97
+ this.detachCurrentTrack();
87
98
  this.dispatchLiveClientEvent();
99
+ // 组件销毁时清理引用
100
+ this.liveClient = null;
88
101
  },
89
102
  methods: {
90
103
  changeExpand() {
@@ -131,19 +144,21 @@ export default {
131
144
 
132
145
  // 初始化liveClient
133
146
  initLiveClient() {
134
- const liveClient = window['liveClient'];
135
- if(!liveClient) {
147
+ this.liveClient = window['liveClient'] || null;
148
+ if(!this.liveClient) {
136
149
  return;
137
150
  }
151
+ this.roomMode = this.liveClient?.roomMode || 'auto';
138
152
  this.curSpeaker = {
139
- identity: liveClient.room.localParticipant.identity,
140
- name: liveClient.room.localParticipant.name,
153
+ identity: this.liveClient.room.localParticipant.identity,
154
+ name: this.liveClient.room.localParticipant.name,
141
155
  };
142
- this.localCameraStatus = liveClient.room.localParticipant.isCameraEnabled;
143
- this.localMicrophoneStatus = liveClient.room.localParticipant.isMicrophoneEnabled;
144
- this.curSpeakerMicrophoneStatus = liveClient.room.localParticipant.isMicrophoneEnabled;
145
- this.curSpeakerCameraStatus = liveClient.room.localParticipant.isCameraEnabled;
146
- this.curSpeakerTrack = this.trackData.videoTrack;
156
+ this.localCameraStatus = this.liveClient.room.localParticipant.isCameraEnabled;
157
+ this.localMicrophoneStatus = this.liveClient.room.localParticipant.isMicrophoneEnabled;
158
+ this.curSpeakerMicrophoneStatus = this.liveClient.room.localParticipant.isMicrophoneEnabled;
159
+ this.curSpeakerCameraStatus = this.liveClient.room.localParticipant.isCameraEnabled;
160
+ // 仅在视频会议模式下记录并绑定视频轨道
161
+ this.curSpeakerTrack = this.roomMode === 'auto' ? this.trackData.videoTrack : null;
147
162
  // 初始化视频轨道绑定
148
163
  this.initVideoAttach();
149
164
  // 监听并处理liveclient事件
@@ -194,22 +209,27 @@ export default {
194
209
  },
195
210
 
196
211
  initVideoAttach() {
197
- if(this.curSpeakerTrack) {
198
- const videoElm = document.getElementById('video-contain');
199
- videoElm && this.curSpeakerTrack.attach(videoElm);
212
+ if (this.roomMode !== 'auto') return
213
+ if (this.curSpeakerTrack) {
214
+ const videoElm = document.getElementById('video-contain')
215
+ if (videoElm) {
216
+ // 先解绑,确保不会重复附加
217
+ try { this.curSpeakerTrack.detach(videoElm) } catch (e) {}
218
+ this.curSpeakerTrack.attach(videoElm)
219
+ }
200
220
  }
201
221
  },
202
222
 
203
223
  initLiveClientEvent() {
204
- const liveClient = window['liveClient'];
205
- if(!liveClient) {
224
+ if(!this.liveClient) {
206
225
  return;
207
226
  }
208
- liveClient.on("localCameraChange", this.handleLocalCameraChange);
209
- liveClient.on("localMicrophoneChange", this.handleLocalMicrophoneChange);
210
- liveClient.on("activeSpeakerChange", this.handleActiveSpeakerChange);
211
- liveClient.on("videoCallRender", this.handleVideoCallRender);
212
- liveClient.on("roomDisconnected", this.handleRoomDisconnected);
227
+ this.liveClient.on("localCameraChange", this.handleLocalCameraChange);
228
+ this.liveClient.on("localMicrophoneChange", this.handleLocalMicrophoneChange);
229
+ this.liveClient.on("activeSpeakerChange", this.handleActiveSpeakerChange);
230
+ this.liveClient.on("videoCallRender", this.handleVideoCallRender);
231
+ this.liveClient.on("audioCallRender", this.handleAudioCallRender);
232
+ this.liveClient.on("roomDisconnected", this.handleRoomDisconnected);
213
233
  },
214
234
 
215
235
  handleLocalCameraChange(e) {
@@ -221,19 +241,35 @@ export default {
221
241
  },
222
242
 
223
243
  handleActiveSpeakerChange(e) {
224
- if (e.length > 0) {
225
- let maxAudioLevel = 0;
226
- let maxAudioLevelIndex = -1;
227
- e.forEach((speaker, index) => {
228
- if (speaker.audioLevel > maxAudioLevel) {
229
- maxAudioLevel = speaker.audioLevel;
230
- maxAudioLevelIndex = index;
244
+ this.curSpeakers = Array.isArray(e) ? e : []
245
+
246
+ if(this.curSpeakers.length > 0) {
247
+ // 找到音量最大的讲话者
248
+ let maxAudioLevel = -1
249
+ let winner = null
250
+ this.curSpeakers.forEach(speaker => {
251
+ const level = typeof speaker.audioLevel === 'number' ? speaker.audioLevel : 0
252
+ if (level > maxAudioLevel) {
253
+ maxAudioLevel = level
254
+ winner = speaker
255
+ }
256
+ })
257
+
258
+ if (!winner) return
259
+
260
+ // 仅当 identity 变化时才切换,以减少抖动
261
+ if (winner.identity !== this.curSpeaker?.identity) {
262
+ // 解绑旧轨道,等待 videoCallRender 到来后再绑定新轨道
263
+ // detachCurrentTrack()
264
+ this.curSpeaker = {
265
+ identity: winner.identity,
266
+ name: winner.name || winner.identity,
267
+ isLocal: !!winner.isLocal,
231
268
  }
232
- });
233
- if (maxAudioLevelIndex >= 0) {
234
- this.curSpeaker = e[maxAudioLevelIndex];
269
+ // 在未收到渲染事件前,暂时认为摄像头状态未知/关闭,用以显示占位
270
+ // curSpeakerCameraStatus.value = false
271
+ // curSpeakerMicrophoneStatus.value = false
235
272
  }
236
- this.curSpeakers = e;
237
273
  }
238
274
  },
239
275
 
@@ -242,15 +278,61 @@ export default {
242
278
  this.localCameraStatus = e.isCameraEnabled;
243
279
  this.localMicrophoneStatus = e.isMicrophoneEnabled;
244
280
  }
245
- if(e.identity === this.curSpeaker?.identity) {
246
- this.curSpeakerCameraStatus = e.isCameraEnabled;
247
- this.curSpeakerMicrophoneStatus = e.isMicrophoneEnabled;
248
- if(e?.videoTrack) {
249
- this.curSpeakerTrack = e.videoTrack;
281
+ // 利用 videoCallRender 的 remove 标记判断离会
282
+ if (e?.remove === true && e.identity === this.curSpeaker?.identity) {
283
+ this.switchToLocalAsCurrentSpeaker()
284
+ return
285
+ }
286
+ if (e.identity === this.curSpeaker?.identity) {
287
+ // 更新当前讲话者音视频状态
288
+ this.curSpeakerCameraStatus = this.roomMode === 'auto' ? !!e.isCameraEnabled : false
289
+ this.curSpeakerMicrophoneStatus = !!e.isMicrophoneEnabled
290
+
291
+ const videoElm = document.getElementById('video-contain')
292
+
293
+ if (this.roomMode === 'auto' && e.videoTrack && videoElm) {
294
+ // 使用 sid 作为更稳定的身份标识;sid 发布后会保持不变
295
+ const oldTrack = this.curSpeakerTrack
296
+ const newTrack = e.videoTrack
297
+ if (oldTrack) {
298
+ const sameBySid = oldTrack.sid && newTrack?.sid && oldTrack.sid === newTrack.sid
299
+ // 当 sid 不同或对象引用不同(无 sid 时),说明发生实际轨道切换,需要解绑旧轨道
300
+ if (!sameBySid) {
301
+ try { oldTrack.detach(videoElm) } catch (err) {}
302
+ }
303
+ }
304
+ this.curSpeakerTrack = newTrack
305
+ // 重新绑定(同轨道重复 attach 时内部已处理,不影响稳定性)
306
+ try { this.curSpeakerTrack.attach(videoElm) } catch (err) {}
307
+ } else {
308
+ // 摄像头关闭或无轨道,解绑任何已绑定轨道
309
+ if (this.curSpeakerTrack && videoElm) {
310
+ try { this.curSpeakerTrack.detach(videoElm) } catch (err) {}
311
+ }
312
+ this.curSpeakerTrack = null
250
313
  }
251
314
  }
252
315
  },
253
316
 
317
+ // 音频会议渲染事件:不进行任何视频轨道的绑定,仅更新状态并显示占位
318
+ handleAudioCallRender(e) {
319
+ if (e?.remove === true && e.identity === this.curSpeaker?.identity) {
320
+ this.switchToLocalAsCurrentSpeaker()
321
+ return
322
+ }
323
+ if (e.local) {
324
+ this.localMicrophoneStatus = !!e.isMicrophoneEnabled
325
+ }
326
+ if (e.identity === this.curSpeaker?.identity) {
327
+ this.curSpeakerMicrophoneStatus = !!e.isMicrophoneEnabled
328
+ // 音频模式下不显示摄像头画面
329
+ this.curSpeakerCameraStatus = false
330
+ // 确保解绑任何已绑定的视频轨道
331
+ this.detachCurrentTrack()
332
+ this.curSpeakerTrack = null
333
+ }
334
+ },
335
+
254
336
  handleRoomDisconnected() {
255
337
  this.$emit('miniVideoDialogClose');
256
338
  },
@@ -260,27 +342,25 @@ export default {
260
342
  },
261
343
 
262
344
  dispatchLiveClientEvent() {
263
- const liveClient = window['liveClient'];
264
- if(liveClient) {
265
- liveClient.off("localCameraChange", this.handleLocalCameraChange);
266
- liveClient.off("localMicrophoneChange", this.handleLocalMicrophoneChange);
267
- liveClient.off("activeSpeakerChange", this.handleActiveSpeakerChange);
268
- liveClient.off("videoCallRender", this.handleVideoCallRender);
269
- liveClient.off("roomDisconnected", this.handleRoomDisconnected);
345
+ if(this.liveClient) {
346
+ this.liveClient.off("localCameraChange", this.handleLocalCameraChange);
347
+ this.liveClient.off("localMicrophoneChange", this.handleLocalMicrophoneChange);
348
+ this.liveClient.off("activeSpeakerChange", this.handleActiveSpeakerChange);
349
+ this.liveClient.off("videoCallRender", this.handleVideoCallRender);
350
+ this.liveClient.off("audioCallRender", this.handleAudioCallRender)
351
+ this.liveClient.off("roomDisconnected", this.handleRoomDisconnected);
270
352
  }
271
353
  },
272
354
 
273
355
  async changeLocalMicrophoneStatus() {
274
- const liveClient = window['liveClient'];
275
- if(liveClient) {
276
- await liveClient.changeMicrophoneStatus();
356
+ if(this.liveClient) {
357
+ await this.liveClient.changeMicrophoneStatus();
277
358
  }
278
359
  },
279
360
 
280
361
  async changeLocalCameraStatus() {
281
- const liveClient = window['liveClient'];
282
- if(liveClient) {
283
- await liveClient.changeCameraStatus();
362
+ if(this.liveClient) {
363
+ await this.liveClient.changeCameraStatus();
284
364
  }
285
365
  },
286
366
 
@@ -290,6 +370,42 @@ export default {
290
370
  this.footerTimer = null;
291
371
  }
292
372
  },
373
+
374
+ // 工具:解绑当前视频轨道
375
+ detachCurrentTrack() {
376
+ try {
377
+ const videoElm = document.getElementById('video-contain')
378
+ if (this.curSpeakerTrack && videoElm) {
379
+ this.curSpeakerTrack.detach(videoElm)
380
+ }
381
+ } catch (e) {}
382
+ },
383
+ // 工具:切换当前讲话者为本地参会者
384
+ switchToLocalAsCurrentSpeaker() {
385
+ try {
386
+ const lp = this.liveClient?.room?.localParticipant
387
+ this.detachCurrentTrack()
388
+ if (lp) {
389
+ this.curSpeaker = {
390
+ identity: lp.identity,
391
+ name: lp.name,
392
+ isLocal: true,
393
+ }
394
+ this.curSpeakerCameraStatus = this.roomMode === 'auto' ? !!lp.isCameraEnabled : false
395
+ this.curSpeakerMicrophoneStatus = !!lp.isMicrophoneEnabled
396
+ // 立即尝试绑定已知的本地轨道(如存在),后续仍会由 videoCallRender 纠正
397
+ if (this.roomMode === 'auto') {
398
+ const track = this.trackData?.videoTrack
399
+ if (track && this.curSpeakerCameraStatus) {
400
+ this.curSpeakerTrack = track
401
+ this.initVideoAttach()
402
+ }
403
+ } else {
404
+ this.curSpeakerTrack = null
405
+ }
406
+ }
407
+ } catch (e) {}
408
+ }
293
409
  },
294
410
  }
295
411
  </script>
@@ -318,6 +434,25 @@ export default {
318
434
  border-radius: 4px;
319
435
  object-fit: contain;
320
436
  }
437
+ // 与 liveMultipleMeeting 保持一致的缺省板样式
438
+ .board {
439
+ position: absolute;
440
+ width: 100%;
441
+ height: 100%;
442
+ left: 0;
443
+ top: 0;
444
+ z-index: 30;
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ background: var(--meeting-board-bg) no-repeat center / 100% 100%;
449
+ .board-icon {
450
+ width: 20%;
451
+ height: auto;
452
+ aspect-ratio: 1/1;
453
+ background: url("../../assets/image/common/default_avatar.png") no-repeat center / 100% 100%;
454
+ }
455
+ }
321
456
  .describe {
322
457
  position: absolute;
323
458
  bottom: 6px;
@@ -47,6 +47,31 @@
47
47
  </template>
48
48
  </vue-virtual-tree>
49
49
 
50
+ <!-- 会议终端树 -->
51
+ <vue-virtual-tree
52
+ v-show="activeTab === '会议终端'"
53
+ ref="terminalTreeRef"
54
+ :data="terminalTreeData"
55
+ :props="{ children: 'children', label: 'label' }"
56
+ node-key="id"
57
+ show-checkbox
58
+ :height="dynamicTreeHeight"
59
+ :item-height="36"
60
+ @check="handleNodeCheck"
61
+ :filter-node-method="filterNode"
62
+ >
63
+ <template #default="{ node, data }">
64
+ <span class="custom-tree-node">
65
+ <span class="custom-tree-node-label" :title="node.label" v-if="data.isDept">{{
66
+ node.label
67
+ }}</span>
68
+ <span class="custom-tree-node-user" v-else>
69
+ <span :class="['custom-tree-node-icon']"></span>
70
+ <span class="custom-tree-node-label" :title="node.label">{{ node.label }}</span>
71
+ </span>
72
+ </span>
73
+ </template>
74
+ </vue-virtual-tree>
50
75
  <!-- 应用树 -->
51
76
  <vue-virtual-tree
52
77
  v-show="activeTab === '人员'"
@@ -192,6 +217,15 @@
192
217
  <div class="label">
193
218
  <span :class="['custom-item-icon', getInviteItemIconClass(item)]"></span>
194
219
  <span>{{ item.label }}</span>
220
+ <!-- 会议终端:小输入框编辑会议ID,Enter/失焦提交 -->
221
+ <el-input
222
+ v-if="item.source === '会议终端' && item.callMethod == 1"
223
+ v-model="item._callMeetingID"
224
+ class="terminal-id-input"
225
+ placeholder="请输入会议ID"
226
+ @keyup.enter="confirmTerminalMeetingId(item)"
227
+ @blur="confirmTerminalMeetingId(item)"
228
+ />
195
229
  <span :class="getChosenItemPlatformClass(item)"></span>
196
230
  </div>
197
231
  <div class="close" @click="removeInviteItem(item)"></div>
@@ -234,7 +268,7 @@
234
268
 
235
269
  <script>
236
270
  import { debounce } from "lodash-es";
237
- import { mittBus, ShowMessage, deleteCommonGroupAndUser, getAddressOrgTree, getAddressAppTree, getCommonOrgTree, queryEquipmentAddressBook, queryMonitorAddressBook, getVolteAddressBook } from "../../utils/index";
271
+ import { mittBus, ShowMessage, deleteCommonGroupAndUser, getAddressOrgTree, getAddressAppTree, getCommonOrgTree, queryEquipmentAddressBook, queryMonitorAddressBook, getVolteAddressBook, getMeetingRoomAddressBook, updateConfTerminal } from "../../utils/index";
238
272
  import VueVirtualTree from "@fit2cloud-ui/vue-virtual-tree";
239
273
  import CustomGroupDialog from "./customGroupDialog.vue";
240
274
  import EditGroupDialog from "./editGroupDialog.vue";
@@ -263,6 +297,12 @@ export default {
263
297
  type: String,
264
298
  default: "",
265
299
  },
300
+ tabList: {
301
+ type: Array,
302
+ default: () => {
303
+ return ["组织架构", "人员", "会议终端", "volte", "设备", "监控", "常用分组"];
304
+ },
305
+ }
266
306
  },
267
307
  data() {
268
308
  return {
@@ -270,6 +310,7 @@ export default {
270
310
 
271
311
  // 树数据
272
312
  treeData: [],
313
+ terminalTreeData: [],
273
314
  appTreeData: [],
274
315
  volteTreeData: [],
275
316
  groupTreeData: [],
@@ -278,7 +319,6 @@ export default {
278
319
 
279
320
  // 标签页相关
280
321
  activeTab: "组织架构",
281
- tabList: ["组织架构", "人员", "volte", "设备", "监控", "常用分组"],
282
322
 
283
323
  // 搜索相关
284
324
  filterText: "",
@@ -344,17 +384,17 @@ export default {
344
384
 
345
385
  // 监听对话框显示状态
346
386
  isaddressShow: {
347
- async handler(newVal) {
387
+ handler(newVal) {
348
388
  if (newVal) {
349
389
  // 初始化选中状态
350
390
  this.isSyncing = true;
351
391
  this.chosenList = [...this.tempInviteList];
352
- await this.$nextTick();
353
- this.syncTreeSelectionFromChosenList();
354
- // 更新树高度
355
- await this.$nextTick();
356
- this.updateTreeHeight();
357
- this.isSyncing = false;
392
+ this.$nextTick(() => {
393
+ this.syncTreeSelectionFromChosenList();
394
+ // 更新树高度
395
+ this.updateTreeHeight();
396
+ this.isSyncing = false;
397
+ });
358
398
  }
359
399
  }
360
400
  }
@@ -377,7 +417,9 @@ export default {
377
417
  this.$refs.equipmentTreeRef.filter(val);
378
418
  } else if (this.activeTab === "监控" && this.$refs.monitorTreeRef) {
379
419
  this.$refs.monitorTreeRef.filter(val);
380
- }
420
+ } else if (this.activeTab === "会议终端" && this.$refs.terminalTreeRef) {
421
+ this.$refs.terminalTreeRef.filter(val);
422
+ }
381
423
  }, 300);
382
424
 
383
425
  // 监听mitt事件
@@ -460,7 +502,20 @@ export default {
460
502
  this.monitorTreeData = res.data.data;
461
503
  }
462
504
  },
463
-
505
+ // 获取会议终端树数据
506
+ async getTerminalTreeData() {
507
+ try {
508
+ const res = await getMeetingRoomAddressBook({}, { baseUrl: this.baseUrl, token: this.token });
509
+ if (res.data?.code == 200) {
510
+ const data = res.data?.data || [];
511
+ this.terminalTreeData = data.length > 0 ? data : [];
512
+ } else {
513
+ this.terminalTreeData = [];
514
+ }
515
+ } catch (e) {
516
+ this.terminalTreeData = [];
517
+ }
518
+ },
464
519
  // 获取所有树数据
465
520
  async getAllTreeData() {
466
521
  await this.getOrgTreeData();
@@ -469,6 +524,7 @@ export default {
469
524
  await this.getGroupTreeData();
470
525
  await this.getEquipmentTreeData();
471
526
  await this.getMonitorTreeData();
527
+ await this.getTerminalTreeData();
472
528
  },
473
529
 
474
530
  // 过滤树节点的方法 - 虚拟树使用filter-node-method
@@ -496,16 +552,20 @@ export default {
496
552
  // 修改判断条件,只有当isDept为false时,才是叶子节点
497
553
  // isDept为true的节点(即使是空数组)都视为非叶子节点
498
554
  if (!node.isDept) {
499
- leafNodes.push({
500
- ...node,
501
- source: this.activeTab, // 记录节点来源于哪个树
502
- });
555
+ leafNodes.push(this.prepareChosenUser(node, this.activeTab));
503
556
  }
504
557
  });
505
558
 
506
559
  return leafNodes;
507
560
  },
508
-
561
+ // 构建已选用户对象:当来源为“会议终端”时,初始化 _callMeetingID,避免输入框为空
562
+ prepareChosenUser(raw, source) {
563
+ const user = { ...raw, source };
564
+ if (source === '会议终端') {
565
+ user._callMeetingID = raw?.callMeetingID ?? '';
566
+ }
567
+ return user;
568
+ },
509
569
  // 更新chosenList,保留其他树的选中状态
510
570
  updateChosenListWithLeafNodes(newLeafNodes) {
511
571
  // 从chosenList中过滤掉当前激活树的节点
@@ -565,6 +625,8 @@ export default {
565
625
  this.$refs.equipmentTreeRef.setCheckedKeys([]);
566
626
  } else if (this.activeTab === "监控" && this.$refs.monitorTreeRef) {
567
627
  this.$refs.monitorTreeRef.setCheckedKeys([]);
628
+ } else if (this.activeTab === "会议终端" && this.$refs.terminalTreeRef) {
629
+ this.$refs.terminalTreeRef.setCheckedKeys([]);
568
630
  }
569
631
 
570
632
  // 然后根据chosenList设置选中状态
@@ -594,6 +656,10 @@ export default {
594
656
  selectedIds.forEach((id) => {
595
657
  this.$refs.monitorTreeRef.setChecked(id, true, false);
596
658
  });
659
+ } else if (this.activeTab === "会议终端" && this.$refs.terminalTreeRef) {
660
+ selectedIds.forEach((id) => {
661
+ this.$refs.terminalTreeRef.setChecked(id, true, false);
662
+ });
597
663
  }
598
664
  },
599
665
 
@@ -666,11 +732,11 @@ export default {
666
732
  this.showMessage.message("error", "请先选择要添加的人员");
667
733
  return;
668
734
  }
669
- if(this.tempInviteList.some((item) => item.source === '设备' || item.source === '监控')) {
735
+ if(this.tempInviteList.some((item) => item.source === '设备' || item.source === '监控' || item.source === '会议终端')) {
670
736
  this.showMessage.message("info", "设备和监控人员暂不支持添加分组, 将不会被添加到常用分组");
671
737
  }
672
738
  // 确保数据格式正确
673
- const formattedList = this.tempInviteList.filter((item) => item.source !== '设备' && item.source !== '监控').map((item) => {
739
+ const formattedList = this.tempInviteList.filter((item) => item.source !== '设备' && item.source !== '监控' && item.source !== '常用分组' && item.source !== '会议终端').map((item) => {
674
740
  return {
675
741
  id: item.id,
676
742
  label: item.label,
@@ -737,12 +803,33 @@ export default {
737
803
  if( this.$refs.volteTreeRef) {
738
804
  this.$refs.volteTreeRef.setChecked(item.id, false);
739
805
  }
806
+ if (this.$refs.terminalTreeRef) {
807
+ this.$refs.terminalTreeRef.setChecked(item.id, false);
808
+ }
740
809
  this.$nextTick(() => {
741
810
  this.isSyncing = false;
742
811
  });
743
812
  }
744
813
  },
745
814
 
815
+ // 更新会议终端会议ID(Enter/失焦)
816
+ async confirmTerminalMeetingId(user) {
817
+ const newId = (user._callMeetingID || '').toString().trim()
818
+ const oldId = (user.callMeetingID || '').toString().trim()
819
+ if (newId === oldId) return
820
+ try {
821
+ const res = await updateConfTerminal({ id: user.id, callMeetingID: newId }, { baseUrl: props.baseUrl, token: props.token })
822
+ if (res.data?.code == 200) {
823
+ user.callMeetingID = newId
824
+ this.showMessage.message('success', '会议ID已更新')
825
+ } else {
826
+ this.showMessage.message('error', res.data?.msg || '更新会议ID失败')
827
+ }
828
+ } catch (e) {
829
+ this.showMessage.message('error', e.message || '网络异常,更新会议ID失败')
830
+ }
831
+ },
832
+
746
833
  cancel() {
747
834
  this.tempInviteList = [];
748
835
  this.isaddressShow = false;
@@ -750,7 +837,7 @@ export default {
750
837
 
751
838
  confirm() {
752
839
  // 转换数据格式以保持向后兼容
753
- const convertedList = this.tempInviteList.filter((item) => item.source !== '设备' && item.source !== '监控').map((item) => {
840
+ const convertedList = this.tempInviteList.filter((item) => item.source !== '设备' && item.source !== '监控' && item.source !== '会议终端').map((item) => {
754
841
  return {
755
842
  ...item,
756
843
  };
@@ -760,11 +847,17 @@ export default {
760
847
  ...item,
761
848
  };
762
849
  });
763
-
850
+ const convertedTerminalList = this.tempInviteList.filter(item => item.source === '会议终端')
851
+ .map(item => {
852
+ return {
853
+ ...item,
854
+ }
855
+ })
764
856
  this.$emit("inviteListUpdate", convertedList);
765
857
  if (this.isInner) {
766
858
  this.$emit("appendInvitePeople", convertedList);
767
859
  this.$emit("appendInviteDevice", convertedDeviceList);
860
+ this.$emit("appendInviteTerminal", convertedTerminalList);
768
861
  }
769
862
  this.isaddressShow = false;
770
863
  },
@@ -1134,6 +1227,30 @@ export default {
1134
1227
  font-size: 16px;
1135
1228
  color: var(--theme-font-color);
1136
1229
 
1230
+ :deep(.el-input) {
1231
+ margin-left: 8px;
1232
+ width: 100px;
1233
+ line-height: 28px;
1234
+ height: 28px;
1235
+ border: none;
1236
+ .el-input__inner {
1237
+ box-shadow: none;
1238
+ background-color: transparent;
1239
+ border-radius: 0px;
1240
+ border: none;
1241
+ border-bottom: 1px solid var(--input-border-color);
1242
+ padding: 0 6px;
1243
+ color: var(--theme-font-color);
1244
+ font-size: 14px;
1245
+ font-weight: 400;
1246
+ line-height: 28px;
1247
+ &::placeholder {
1248
+ color: var(--input-placeholder-color);
1249
+ font-family: var(--main-font);
1250
+ }
1251
+ }
1252
+ }
1253
+
1137
1254
  .custom-item-icon {
1138
1255
  width: 20px;
1139
1256
  height: 20px;
@@ -123,7 +123,7 @@ export default {
123
123
  .appoint-dialog {
124
124
  width: 220px;
125
125
  position: absolute;
126
- z-index: 100;
126
+ z-index: 3000;
127
127
  background: var(--dialog-bg);
128
128
  box-shadow: 0px 0px 8px 0px var(--dialog-shadow-color);
129
129
  border-radius: 8px;