mdm-client 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/App.vue +72 -67
  3. package/src/assets/image/common/layout-active16.png +0 -0
  4. package/src/assets/image/common/layout-active25.png +0 -0
  5. package/src/assets/image/common/layout-active3.png +0 -0
  6. package/src/assets/image/common/layout-active9.png +0 -0
  7. package/src/assets/image/common/layout16.png +0 -0
  8. package/src/assets/image/common/layout25.png +0 -0
  9. package/src/assets/image/common/layout3.png +0 -0
  10. package/src/assets/image/common/layout9.png +0 -0
  11. package/src/assets/image/common/mirror.png +0 -0
  12. package/src/assets/image/common/rotate_icon1.png +0 -0
  13. package/src/assets/image/common/rotate_icon2.png +0 -0
  14. package/src/assets/image/common/rotate_icon3.png +0 -0
  15. package/src/assets/image/common/rotate_icon4.png +0 -0
  16. package/src/assets/style/base.scss +5 -0
  17. package/src/components/LiveMulti/LiveMulti.vue +27 -6
  18. package/src/components/LiveMultipleMeeting/LiveMultipleMeeting.vue +1063 -98
  19. package/src/components/LiveMultipleMeeting/style/index.scss +145 -14
  20. package/src/components/LivePoint/LivePoint.vue +49 -211
  21. package/src/components/LivePointMeeting/LivePointMeeting.vue +159 -10
  22. package/src/components/LivePointMeeting/style/index.scss +35 -0
  23. package/src/components/MeetingReadyDialog/MeetingReadyDialog.vue +96 -14
  24. package/src/components/other/addressBook.vue +137 -20
  25. package/src/components/other/appointDialog.vue +1 -1
  26. package/src/components/other/customLayout.vue +368 -202
  27. package/src/components/other/layoutSwitch.vue +253 -37
  28. package/src/components/other/leadershipFocus.vue +422 -0
  29. package/src/components/other/leaveOptionDialog.vue +1 -1
  30. package/src/components/other/moreOptionDialog.vue +17 -1
  31. package/src/components/other/selectDialog.vue +1 -1
  32. package/src/components/other/selectSpecialDialog.vue +1 -1
  33. package/src/utils/api.js +19 -0
  34. package/src/utils/livekit/live-client-esm.js +1 -1
@@ -0,0 +1,422 @@
1
+ <template>
2
+ <div
3
+ class="leadership"
4
+ id="leadership"
5
+ @mouseenter="handleMouseEnter"
6
+ @mouseleave="handleMouseLeave"
7
+ >
8
+ <div class="leadership-container">
9
+ <!-- 视频元素 -->
10
+ <video
11
+ class="p-video"
12
+ id="video-leadership"
13
+ autoplay
14
+ webkit-playsinline
15
+ playsinline
16
+ x5-video-player-type="h5"
17
+ ref="videoRef"
18
+ ></video>
19
+ <!-- 屏幕共享提示 -->
20
+ <div
21
+ v-if="participant.isScreenShareEnabled"
22
+ id="screenshare-leadership"
23
+ class="screen-share-suspension"
24
+ >
25
+ {{ participant.name }} 正在共享屏幕
26
+ </div>
27
+ <!-- 摄像头关闭时的头像展板 -->
28
+ <div
29
+ v-if="!participant.isCameraEnabled && !participant.isScreenShareEnabled"
30
+ id="board-leadership"
31
+ class="board"
32
+ >
33
+ <div class="board-icon"></div>
34
+ </div>
35
+ <!-- 网络状态图标 -->
36
+ <div
37
+ v-if="!participant.isScreenShareEnabled"
38
+ id="signal-leadership"
39
+ :class="['signal-icon', getSignalClass]"
40
+ :style="{ visibility: isHovered ? 'visible' : 'hidden' }"
41
+ />
42
+ <!-- 更多操作按钮 -->
43
+ <div
44
+ v-if="!participant.isScreenShareEnabled && showMoreButton"
45
+ id="more-leadership"
46
+ class="more-icon"
47
+ :style="{ visibility: isHovered ? 'visible' : 'hidden' }"
48
+ @click="handleMoreClick"
49
+ />
50
+ <!-- 旋转按钮(仅平台ID为4显示) -->
51
+ <div
52
+ v-if="showRotateButton"
53
+ id="rotate-leadership"
54
+ class="rotate-icon rotate-icon1"
55
+ :style="{ visibility: isHovered ? 'visible' : 'hidden' }"
56
+ @click="handleRotateClick"
57
+ />
58
+ <!-- 底部信息栏 -->
59
+ <div class="describe">
60
+ <div
61
+ id="microphone-leadership"
62
+ :class="getMicrophoneClass"
63
+ @click="handleMicrophoneClick"
64
+ />
65
+ <div class="identity">
66
+ {{ participant.isLocal ? participant.name + "(我)" : participant.name }}
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <script>
74
+ export default {
75
+ name: 'LeadershipFocus',
76
+ props: {
77
+ participant: {
78
+ type: Object,
79
+ required: true,
80
+ },
81
+ localMetadata: {
82
+ type: Object,
83
+ default: () => ({}),
84
+ },
85
+ },
86
+ data() {
87
+ return {
88
+ isHovered: false,
89
+ currentRotateDeg: 0,
90
+ };
91
+ },
92
+ injected: ['rotateDegreeMap'],
93
+ // Vue2 风格的侦听器:监听 videoTrack 的变化并绑定/解绑到 video 元素
94
+ watch: {
95
+ 'participant.videoTrack': {
96
+ handler(newTrack, oldTrack) {
97
+ // 等待 DOM 更新,确保 $refs 可用
98
+ this.$nextTick(() => {
99
+ const videoElm = this.$refs.videoRef
100
+ try {
101
+ if (oldTrack && videoElm && typeof oldTrack.detach === 'function') {
102
+ oldTrack.detach(videoElm)
103
+ }
104
+ } catch (err) {
105
+ console.warn('failed to detach oldTrack', err)
106
+ }
107
+ try {
108
+ if (newTrack && videoElm && typeof newTrack.attach === 'function') {
109
+ newTrack.attach(videoElm)
110
+ }
111
+ } catch (err) {
112
+ console.warn('failed to attach newTrack', err)
113
+ }
114
+ })
115
+ }
116
+ },
117
+ participant: {
118
+ handler(newParticipant) {
119
+ try {
120
+ this.applyVideoObjectFit(newParticipant)
121
+ // 元素尺寸/适配变化后,重新应用旋转
122
+ const deg = this.rotateDegreeMap?.get(newParticipant?.identity) || 0
123
+ this.currentRotateDeg = deg
124
+ console.log('props.participant changed', deg, this.rotateDegreeMap)
125
+ this.applyLeadershipRotation(deg)
126
+ } catch (err) {
127
+ console.error('participant watch handler error', err)
128
+ }
129
+ },
130
+ }
131
+ },
132
+ computed: {
133
+ getSignalClass() {
134
+ const quality = this.participant.connectionQuality;
135
+ return quality === "excellent" || quality === "good" ? "signal-icon-good" : "signal-icon-poor";
136
+ },
137
+ getMicrophoneClass() {
138
+ return this.participant.isMicrophoneEnabled
139
+ ? "microphone microphone-active"
140
+ : "microphone microphone-inactive";
141
+ },
142
+ showMoreButton() {
143
+ return this.localMetadata?.isOwner || this.localMetadata?.isCoHost;
144
+ },
145
+ // 是否展示旋转按钮:仅当平台为4且非屏幕共享时显示(与多画面一致)
146
+ showRotateButton() {
147
+ try {
148
+ return !this.participant.isScreenShareEnabled && this.participant?.metadata?.platformID === 4
149
+ } catch (e) {
150
+ return false
151
+ }
152
+ }
153
+ },
154
+ methods: {
155
+ handleMouseEnter() {
156
+ this.isHovered = true;
157
+ },
158
+ handleMouseLeave() {
159
+ this.isHovered = false;
160
+ },
161
+ handleMoreClick(event) {
162
+ this.$emit("moreClick", {
163
+ event,
164
+ identity: this.participant.identity,
165
+ });
166
+ },
167
+ handleMicrophoneClick() {
168
+ this.$emit("microphoneClick", {
169
+ identity: this.participant.identity,
170
+ isMuted: this.participant.isMicrophoneEnabled,
171
+ });
172
+ },
173
+ // 旋转按钮点击:顺时针旋转90°
174
+ handleRotateClick() {
175
+ if (!this.participant?.identity) return
176
+ const cur = this.rotateDegreeMap?.get(this.participant.identity) || 0
177
+ const next = (cur + 90) % 360
178
+ this.rotateDegreeMap?.set(this.participant.identity, next)
179
+ console.log('afterClick', this.rotateDegreeMap);
180
+ this.currentRotateDeg = next
181
+ this.applyLeadershipRotation(next)
182
+ },
183
+ // 将旋转角度应用到领导聚焦的视频元素
184
+ applyLeadershipRotation(degree) {
185
+ try {
186
+ const norm = ((degree % 360) + 360) % 360
187
+ const videoElm = this.$refs.videoRef
188
+ if (!videoElm) return
189
+
190
+ videoElm.style.transformOrigin = 'center center'
191
+ // 针对90/270度,居中并以高度为基准适配
192
+ if (norm === 90 || norm === 270) {
193
+ const parent = videoElm.parentElement
194
+ if (parent) {
195
+ const rect = parent.getBoundingClientRect()
196
+ videoElm.style.top = '50%'
197
+ videoElm.style.left = '50%'
198
+ videoElm.style.width = `${rect.height}px`
199
+ videoElm.style.height = `${rect.width}px`
200
+ videoElm.style.transform = `translate(-50%, -50%) rotate(${norm}deg)`
201
+ videoElm.style.objectFit = 'cover'
202
+ }
203
+ } else {
204
+ // 0/180度直接占满容器
205
+ videoElm.style.top = '0'
206
+ videoElm.style.left = '0'
207
+ videoElm.style.width = '100%'
208
+ videoElm.style.height = '100%'
209
+ videoElm.style.transform = `rotate(${norm}deg)`
210
+ // 恢复原有objectFit策略
211
+ this.applyVideoObjectFit(this.participant)
212
+ }
213
+ // 更新本组件旋转图标的外观(1..4)
214
+ const rotateElm = document.getElementById('rotate-leadership')
215
+ if (rotateElm) {
216
+ let clsIndex = 1
217
+ if (norm === 0) clsIndex = 1
218
+ else if (norm === 90) clsIndex = 2
219
+ else if (norm === 180) clsIndex = 3
220
+ else if (norm === 270) clsIndex = 4
221
+ rotateElm.className = `rotate-icon rotate-icon${clsIndex}`
222
+ }
223
+ } catch (err) {
224
+ console.error('applyLeadershipRotation error', err)
225
+ }
226
+ },
227
+ applyVideoObjectFit(videoItem) {
228
+ this.$nextTick(() => {
229
+ if(videoItem.metadata?.platformID == 1 || videoItem.metadata?.platformID == 4 || videoItem.metadata?.platformID == 7) {
230
+ this.$refs.videoRef && (this.$refs.videoRef.style.objectFit = "contain");
231
+ } else {
232
+ if(videoItem?.source == "camera") {
233
+ this.$refs.videoRef && (this.$refs.videoRef.style.objectFit = "cover");
234
+ } else {
235
+ this.$refs.videoRef && (this.$refs.videoRef.style.objectFit = "contain");
236
+ }
237
+ }
238
+ })
239
+ },
240
+ },
241
+ mounted() {
242
+ const videoRef = this.$refs.videoRef
243
+ if(this.participant.videoTrack && videoRef) {
244
+ console.log('testLeadershipFocus', this.participant.videoTrack, videoRef);
245
+ this.participant.videoTrack.attach(videoRef)
246
+ this.applyVideoObjectFit(this.participant)
247
+ // 初始应用旋转角度
248
+ const deg = this.rotateDegreeMap?.get(this.participant.identity) || 0
249
+ console.log('initialRotateDeg', deg, this.rotateDegreeMap);
250
+ this.currentRotateDeg = deg
251
+ this.applyLeadershipRotation(deg)
252
+ }
253
+ },
254
+ beforeDestroy() {
255
+ if (this.participant.videoTrack && this.$refs.videoRef) {
256
+ this.participant.videoTrack.detach(this.$refs.videoRef);
257
+ }
258
+ }
259
+ }
260
+ </script>
261
+
262
+ <style lang="scss" scoped>
263
+ .leadership {
264
+ position: absolute;
265
+ width: 100%;
266
+ height: 100%;
267
+ padding: 5px;
268
+ background: var(--meeting-bg);
269
+ z-index: 2000;
270
+ left: 0;
271
+ top: 0;
272
+ &-container {
273
+ background: var(--meeting-board-bg) no-repeat center / 100% 100%;
274
+ position: relative;
275
+ // border: 1px solid #798BA4;
276
+ border-radius: 5px;
277
+ width: 100%;
278
+ height: 100%;
279
+ overflow: hidden;
280
+ // margin: 5px;
281
+ .p-video {
282
+ position: absolute;
283
+ width: 100%;
284
+ height: 100%;
285
+ left: 0;
286
+ top: 0;
287
+ z-index: 10;
288
+ object-fit: contain;
289
+ border-radius: 5px;
290
+ backface-visibility: hidden; // 避免3D旋转引发的锯齿闪烁
291
+ will-change: transform; // 提示浏览器优化旋转
292
+ }
293
+ .board {
294
+ position: absolute;
295
+ width: 100%;
296
+ height: 100%;
297
+ left: 0;
298
+ top: 0;
299
+ z-index: 30;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ background: var(--meeting-board-bg) no-repeat center / 100% 100%;
304
+ .board-icon {
305
+ width: 20%;
306
+ height: auto;
307
+ aspect-ratio: 1/1;
308
+ background: var(--default-avatar) no-repeat center / 100% 100%;
309
+ }
310
+ }
311
+ .screen-share-suspension {
312
+ &::before {
313
+ content: "";
314
+ position: absolute;
315
+ left: 11px;
316
+ top: 10px;
317
+ width: 20px;
318
+ height: 20px;
319
+ background: var(--meeting-share-icon) no-repeat center / 100% 100%;
320
+ }
321
+ & {
322
+ position: absolute;
323
+ left: 24px;
324
+ top: 20px;
325
+ z-index: 50;
326
+ padding: 9px 13px 9px 41px;
327
+ background: rgba(28,36,47,0.6);
328
+ border-radius: 8px;
329
+ white-space: nowrap;
330
+ font-weight: 500;
331
+ font-size: 16px;
332
+ color: var(--theme-font-color);
333
+ }
334
+ }
335
+ .describe {
336
+ position: absolute;
337
+ left: 30px;
338
+ bottom: 6px;
339
+ z-index: 50;
340
+ height: 24px;
341
+ display: flex;
342
+ align-items: center;
343
+ flex-wrap: nowrap;
344
+ padding: 0 10px;
345
+ background: var(--call-audio-text-bg);
346
+ border-radius: 4px;
347
+ .microphone {
348
+ width: 11px;
349
+ height: 16px;
350
+ cursor: pointer;
351
+ margin-right: 6px;
352
+ &-active {
353
+ background: var(--call-user-mic-on-icon) no-repeat center / 100% 100%;
354
+ }
355
+ &-inactive {
356
+ background: var(--call-user-mic-off-icon) no-repeat center / 100% 100%;
357
+ }
358
+ }
359
+ .identity {
360
+ white-space: nowrap;
361
+ font-weight: 400;
362
+ font-size: 14px;
363
+ color: #fff;
364
+ }
365
+ }
366
+ .signal-icon {
367
+ position: absolute;
368
+ top: 10px;
369
+ left: 14px;
370
+ z-index: 50;
371
+ width: auto;
372
+ height: 5%;
373
+ min-height: 30px;
374
+ aspect-ratio: 1/1;
375
+ visibility: hidden;
376
+ &-good {
377
+ background: var(--signal-good-icon) no-repeat center / 100% 100%;
378
+ }
379
+ &-poor {
380
+ background: var(--signal-poor-icon) no-repeat center / 100% 100%;
381
+ }
382
+ }
383
+ .more-icon {
384
+ visibility: hidden;
385
+ cursor: pointer;
386
+ position: absolute;
387
+ top: 10px;
388
+ right: 14px;
389
+ z-index: 50;
390
+ aspect-ratio: 1/1;
391
+ width: auto;
392
+ height: 5%;
393
+ min-height: 30px;
394
+ background: var(--more-icon) no-repeat center / 100% 100%;
395
+ }
396
+ .rotate-icon {
397
+ visibility: hidden;
398
+ cursor: pointer;
399
+ position: absolute;
400
+ bottom: 10px;
401
+ right: 14px;
402
+ z-index: 50;
403
+ aspect-ratio: 1/1;
404
+ width: auto;
405
+ height: 5%;
406
+ min-height: 30px;
407
+ &1 {
408
+ background: var(--rotate-icon1) no-repeat center / 100% 100%;
409
+ }
410
+ &2 {
411
+ background: var(--rotate-icon2) no-repeat center / 100% 100%;
412
+ }
413
+ &3 {
414
+ background: var(--rotate-icon3) no-repeat center / 100% 100%;
415
+ }
416
+ &4 {
417
+ background: var(--rotate-icon4) no-repeat center / 100% 100%;
418
+ }
419
+ }
420
+ }
421
+ }
422
+ </style>
@@ -57,7 +57,7 @@ export default {
57
57
  .check-dialog {
58
58
  width: 160px;
59
59
  position: absolute;
60
- z-index: 100;
60
+ z-index: 2100;
61
61
  &-inner {
62
62
  width: 100%;
63
63
  background: var(--dialog-bg);
@@ -41,6 +41,10 @@ export default {
41
41
  },
42
42
  meetingCoHost: {
43
43
  type: Array,
44
+ },
45
+ leaderShipFocus: {
46
+ type: String,
47
+ default: "",
44
48
  }
45
49
  },
46
50
  computed: {
@@ -59,6 +63,7 @@ export default {
59
63
  this.curBlur === this.identity ? "取消设为主屏" : "设为主屏",
60
64
  participantItem.isMicrophoneEnabled ? "关闭麦克风" : "打开麦克风",
61
65
  participantItem.isCameraEnabled ? "关闭摄像头" : "打开摄像头",
66
+ this.leaderShipFocus ? this.leaderShipFocus === this.identity ? "取消聚焦画面" : "聚焦画面" : "聚焦画面"
62
67
  ];
63
68
  // 如果不是本地与会者,添加主持人权限操作
64
69
  if (!isLocalParticipant) {
@@ -77,6 +82,7 @@ export default {
77
82
  this.curBlur === this.identity ? "取消设为主屏" : "设为主屏",
78
83
  participantItem.isMicrophoneEnabled ? "关闭麦克风" : "打开麦克风",
79
84
  participantItem.isCameraEnabled ? "关闭摄像头" : "打开摄像头",
85
+ this.leaderShipFocus ? this.leaderShipFocus === this.identity ? "取消聚焦画面" : "聚焦画面" : "聚焦画面"
80
86
  ];
81
87
  if (!isLocalParticipant) {
82
88
  options.push(
@@ -95,6 +101,7 @@ export default {
95
101
  this.curBlur === this.identity ? "取消设为主屏" : "设为主屏",
96
102
  participantItem.isMicrophoneEnabled ? "关闭麦克风" : "打开麦克风",
97
103
  participantItem.isCameraEnabled ? "关闭摄像头" : "打开摄像头",
104
+ this.leaderShipFocus ? this.leaderShipFocus === this.identity ? "取消聚焦画面" : "聚焦画面" : "聚焦画面"
98
105
  ];
99
106
  if (!isLocalParticipant) {
100
107
  options.push(
@@ -118,6 +125,7 @@ export default {
118
125
  this.curBlur === this.identity ? "取消设为主屏" : "设为主屏",
119
126
  participantItem.isMicrophoneEnabled ? "关闭麦克风" : "打开麦克风",
120
127
  participantItem.isCameraEnabled ? "关闭摄像头" : "打开摄像头",
128
+ this.leaderShipFocus ? this.leaderShipFocus === this.identity ? "取消聚焦画面" : "聚焦画面" : "聚焦画面"
121
129
  ];
122
130
 
123
131
  // 如果不是本地与会者,添加联席主持人权限操作
@@ -137,6 +145,7 @@ export default {
137
145
  this.curBlur === this.identity ? "取消设为主屏" : "设为主屏",
138
146
  participantItem.isMicrophoneEnabled ? "关闭麦克风" : "打开麦克风",
139
147
  participantItem.isCameraEnabled ? "关闭摄像头" : "打开摄像头",
148
+ this.leaderShipFocus ? this.leaderShipFocus === this.identity ? "取消聚焦画面" : "聚焦画面" : "聚焦画面"
140
149
  ];
141
150
  // 如果不是本地与会者,添加联席主持人权限操作
142
151
  if (!isLocalParticipant) {
@@ -156,6 +165,7 @@ export default {
156
165
  this.curBlur === this.identity ? "取消设为主屏" : "设为主屏",
157
166
  participantItem.isMicrophoneEnabled ? "关闭麦克风" : "打开麦克风",
158
167
  participantItem.isCameraEnabled ? "关闭摄像头" : "打开摄像头",
168
+ this.leaderShipFocus ? this.leaderShipFocus === this.identity ? "取消聚焦画面" : "聚焦画面" : "聚焦画面"
159
169
  ];
160
170
 
161
171
  // 如果不是本地与会者,添加联席主持人权限操作
@@ -248,6 +258,12 @@ export default {
248
258
  case "移出会议":
249
259
  this.$emit("remove", this.identity);
250
260
  break;
261
+ case "聚焦画面":
262
+ this.$emit("openLeadershipFocus", this.identity);
263
+ break;
264
+ case "取消聚焦画面":
265
+ this.$emit("closeLeadershipFocus", this.identity);
266
+ break;
251
267
  }
252
268
  this.$emit("close");
253
269
  }
@@ -265,7 +281,7 @@ export default {
265
281
  border: 1px solid var(--dialog-border-color);
266
282
  padding: 4px 6px;
267
283
  position: absolute;
268
- z-index: 100;
284
+ z-index: 2100;
269
285
  &-item {
270
286
  width: 100%;
271
287
  height: 36px;
@@ -200,7 +200,7 @@ export default {
200
200
  width: 340px;
201
201
  padding-bottom: 10px;
202
202
  position: absolute;
203
- z-index: 1000;
203
+ z-index: 2100;
204
204
  &-mini {
205
205
  width: 110px;
206
206
  .control-list-item-value {
@@ -165,7 +165,7 @@ export default {
165
165
  width: 206px;
166
166
  padding-bottom: 10px;
167
167
  position: absolute;
168
- z-index: 1000;
168
+ z-index: 2100;
169
169
  &-inner {
170
170
  width: 100%;
171
171
  background: var(--dialog-bg);
package/src/utils/api.js CHANGED
@@ -89,3 +89,22 @@ export const getVolteAddressBook = (params = {}, { baseUrl, token } = {}) => {
89
89
  },
90
90
  });
91
91
  }
92
+ // 获取会议终端通讯录树
93
+ export const getMeetingRoomAddressBook = (params = {}, { baseUrl, token } = {}) => {
94
+ return http.post("/api/v1/meeting/queryMeetingRoomAddressBook", params, {
95
+ baseURL: baseUrl,
96
+ headers: {
97
+ satoken: token,
98
+ },
99
+ });
100
+ };
101
+ // 更新会议终端会议号
102
+ export const updateConfTerminal = (params = {}, { baseUrl, token } = {}) => {
103
+ // 参数:{ id: number, callMeetingID: string }
104
+ return http.post("/api/v1/meeting/updateConfTerminal", params, {
105
+ baseURL: baseUrl,
106
+ headers: {
107
+ satoken: token,
108
+ },
109
+ });
110
+ };