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
@@ -14,18 +14,17 @@
14
14
  <div class="custom-layout-simulate-header">
15
15
  <div class="custom-layout-simulate-header-options">
16
16
  <div class="layout-list">
17
- <div
18
- :class="['layout-list-item', item]"
19
- v-for="item of layoutList"
17
+ <div
18
+ :class="['layout-list-item', item]"
19
+ v-for="item of layoutList"
20
20
  :key="item"
21
21
  @click="handleLayoutChange(item)"
22
22
  >
23
23
  <div
24
- :class="
25
- ['layout-list-item-icon',
26
- { 'layout-list-item-icon-active': activeLayout === item }
27
- ]
28
- "
24
+ :class="[
25
+ 'layout-list-item-icon',
26
+ { 'layout-list-item-icon-active': activeLayout === item },
27
+ ]"
29
28
  ></div>
30
29
  </div>
31
30
  </div>
@@ -40,7 +39,7 @@
40
39
  <div :class="['custom-layout-simulate-show', `layout-${activeLayout}`]">
41
40
  <div class="layout-container">
42
41
  <LayoutPlaceholder
43
- v-for="index in placeholderCount"
42
+ v-for="index in placeholderCount"
44
43
  :key="index"
45
44
  :index="index"
46
45
  :member="assignedMembers[index]"
@@ -72,11 +71,14 @@
72
71
  <div class="custom-layout-member-list">
73
72
  <div
74
73
  class="custom-layout-member-list-item"
75
- :class="{ 'is-dragging': isDragging && currentDragMember && currentDragMember.identity === item.identity }"
74
+ :class="{
75
+ 'is-dragging':
76
+ isDragging && currentDragMember && currentDragMember.identity === item.identity,
77
+ }"
76
78
  v-for="item of memberList"
77
79
  :key="item.identity"
78
80
  >
79
- <div
81
+ <div
80
82
  class="custom-layout-member-list-item-drag-icon"
81
83
  @mousedown="handleMouseDown(item, $event)"
82
84
  @touchstart="handleTouchStart(item, $event)"
@@ -87,13 +89,9 @@
87
89
  </div>
88
90
  </div>
89
91
  </div>
90
-
92
+
91
93
  <!-- 拖拽时的浮动元素 -->
92
- <div
93
- v-if="isDragging && currentDragMember"
94
- class="drag-ghost"
95
- :style="dragGhostStyle"
96
- >
94
+ <div v-if="isDragging && currentDragMember" class="drag-ghost" :style="dragGhostStyle">
97
95
  <div class="drag-ghost-avatar"></div>
98
96
  <div class="drag-ghost-name">{{ currentDragMember.name }}</div>
99
97
  </div>
@@ -106,10 +104,12 @@
106
104
  import LayoutPlaceholder from "./LayoutPlaceholder.vue";
107
105
  import { ShowMessage } from "../../utils/index";
108
106
 
107
+ const STORAGE_PREFIX = "customLayoutState";
108
+ let reconcileTimer = null;
109
109
  export default {
110
110
  name: "CustomLayout",
111
111
  components: {
112
- LayoutPlaceholder
112
+ LayoutPlaceholder,
113
113
  },
114
114
  props: {
115
115
  participantList: {
@@ -126,7 +126,7 @@ export default {
126
126
  searchVal: "",
127
127
  memberList: [],
128
128
  autoFill: true,
129
- layoutList: ["grid4", "grid9", "ring", "topSide", "rightSide"],
129
+ layoutList: ["grid4", "grid9", "ring", "rightSide"],
130
130
  activeLayout: "grid4",
131
131
  // 拖拽相关状态
132
132
  isDragging: false,
@@ -138,7 +138,9 @@ export default {
138
138
  hoverPlaceholderIndex: null,
139
139
  currentMousePosition: { x: 0, y: 0 },
140
140
  liveClient: null,
141
- showMessage: new ShowMessage()
141
+ showMessage: new ShowMessage(),
142
+ // 参会者变化对齐延迟标记,避免拖拽过程被打断
143
+ pendingReconcile: false,
142
144
  };
143
145
  },
144
146
  computed: {
@@ -151,19 +153,44 @@ export default {
151
153
  // 计算不同布局下的占位元素数量
152
154
  placeholderCount() {
153
155
  return this.getLayoutPlaceholderCount(this.activeLayout);
154
- }
156
+ },
157
+ // 监听参会者身份集合变化(不仅仅是数量),防抖处理
158
+ participantIdentitySignature() {
159
+ return (this.participantList || [])
160
+ .map((m) => m && m.identity)
161
+ .filter(Boolean)
162
+ .sort()
163
+ .join(",");
164
+ },
155
165
  },
156
166
  watch: {
157
167
  searchVal(newVal) {
158
- this.getMemberList(newVal);
168
+ this.$nextTick(() => {
169
+ this.getMemberList(newVal);
170
+ });
159
171
  },
160
172
  participantNum(newVal) {
161
173
  this.getMemberList();
162
- }
174
+ },
175
+ isDragging(newVal) {
176
+ if (!newVal && this.pendingReconcile) {
177
+ // 拖拽结束后再处理未完成的对齐
178
+ this.pendingReconcile = false;
179
+ Promise.resolve().then(() => this.reconcileAssignedMembers());
180
+ }
181
+ },
182
+ participantIdentitySignature(newVal) {
183
+ if (reconcileTimer) clearTimeout(reconcileTimer);
184
+ reconcileTimer = setTimeout(() => {
185
+ this.reconcileAssignedMembers();
186
+ }, 120);
187
+ },
163
188
  },
164
189
  mounted() {
165
190
  this.getMemberList();
166
191
  this.liveClient = window["liveClient"];
192
+ // 打开弹窗时尝试恢复上一次应用的布局
193
+ this.restoreLayoutState();
167
194
  },
168
195
  beforeDestroy() {
169
196
  this.liveClient = null;
@@ -175,8 +202,7 @@ export default {
175
202
  grid4: 4,
176
203
  grid9: 9,
177
204
  ring: 8,
178
- topSide: 4,
179
- rightSide: 4
205
+ rightSide: 4,
180
206
  };
181
207
  return layoutCountMap[layout] || 4;
182
208
  },
@@ -184,95 +210,96 @@ export default {
184
210
  // 长按开始拖拽 - 鼠标事件
185
211
  handleMouseDown(member, event) {
186
212
  event.preventDefault();
187
-
213
+
188
214
  // 使用固定偏移量,让浮动元素在鼠标右下角
189
215
  this.dragOffset = { x: 10, y: 10 };
190
-
216
+
191
217
  // 设置初始鼠标位置
192
218
  this.currentMousePosition = { x: event.clientX, y: event.clientY };
193
-
219
+
194
220
  this.startDragTimer(member);
195
-
221
+
196
222
  const startX = event.clientX;
197
223
  const startY = event.clientY;
198
-
224
+
199
225
  const handleMouseMove = (e) => {
200
226
  // 保存鼠标实际位置
201
227
  this.currentMousePosition = { x: e.clientX, y: e.clientY };
202
-
228
+
203
229
  // 如果鼠标移动超过5px,清除长按定时器
204
230
  const deltaX = Math.abs(e.clientX - startX);
205
231
  const deltaY = Math.abs(e.clientY - startY);
206
232
  if ((deltaX > 5 || deltaY > 5) && !this.isDragging) {
207
233
  this.clearDragTimer();
208
234
  }
209
-
235
+
210
236
  if (this.isDragging) {
211
237
  this.updateDragPosition(e.clientX, e.clientY);
212
238
  }
213
239
  };
214
-
240
+
215
241
  const handleMouseUp = () => {
216
242
  this.clearDragTimer();
217
243
  this.endDrag();
218
- document.removeEventListener('mousemove', handleMouseMove);
219
- document.removeEventListener('mouseup', handleMouseUp);
244
+ document.removeEventListener("mousemove", handleMouseMove);
245
+ document.removeEventListener("mouseup", handleMouseUp);
220
246
  };
221
-
222
- document.addEventListener('mousemove', handleMouseMove);
223
- document.addEventListener('mouseup', handleMouseUp);
247
+
248
+ document.addEventListener("mousemove", handleMouseMove);
249
+ document.addEventListener("mouseup", handleMouseUp);
224
250
  },
225
251
 
226
252
  // 长按开始拖拽 - 触摸事件
227
253
  handleTouchStart(member, event) {
228
254
  event.preventDefault();
229
255
  const touch = event.touches[0];
230
-
256
+
231
257
  // 使用固定偏移量,让浮动元素在触摸点右下角
232
258
  this.dragOffset = { x: 10, y: 10 };
233
-
259
+
234
260
  // 设置初始触摸位置
235
261
  this.currentMousePosition = { x: touch.clientX, y: touch.clientY };
236
-
262
+
237
263
  this.startDragTimer(member);
238
-
264
+
239
265
  const startX = touch.clientX;
240
266
  const startY = touch.clientY;
241
-
267
+
242
268
  const handleTouchMove = (e) => {
243
269
  const touch = e.touches[0];
244
270
  // 保存触摸实际位置
245
271
  this.currentMousePosition = { x: touch.clientX, y: touch.clientY };
246
-
272
+
247
273
  // 如果触摸移动超过5px,清除长按定时器
248
274
  const deltaX = Math.abs(touch.clientX - startX);
249
275
  const deltaY = Math.abs(touch.clientY - startY);
250
276
  if ((deltaX > 5 || deltaY > 5) && !this.isDragging) {
251
277
  this.clearDragTimer();
252
278
  }
253
-
279
+
254
280
  if (this.isDragging) {
255
281
  this.updateDragPosition(touch.clientX, touch.clientY);
256
282
  }
257
283
  };
258
-
284
+
259
285
  const handleTouchEnd = () => {
260
286
  this.clearDragTimer();
261
287
  this.endDrag();
262
- document.removeEventListener('touchmove', handleTouchMove);
263
- document.removeEventListener('touchend', handleTouchEnd);
288
+ document.removeEventListener("touchmove", handleTouchMove);
289
+ document.removeEventListener("touchend", handleTouchEnd);
264
290
  };
265
-
266
- document.addEventListener('touchmove', handleTouchMove);
267
- document.addEventListener('touchend', handleTouchEnd);
291
+
292
+ document.addEventListener("touchmove", handleTouchMove);
293
+ document.addEventListener("touchend", handleTouchEnd);
268
294
  },
269
295
 
270
296
  // 开始长按计时器
271
297
  startDragTimer(member) {
272
298
  this.clearDragTimer();
273
- this.dragStartTimer = setTimeout(() => {
274
- this.startDrag(member);
275
- }, 500);
299
+ // this.dragStartTimer = setTimeout(() => {
300
+ // this.startDrag(member);
301
+ // }, 500);
302
+ this.startDrag(member);
276
303
  },
277
304
 
278
305
  // 清除长按计时器
@@ -287,24 +314,24 @@ export default {
287
314
  startDrag(member) {
288
315
  this.isDragging = true;
289
316
  this.currentDragMember = member;
290
-
317
+
291
318
  // 设置初始浮动元素位置
292
319
  const { x, y } = this.currentMousePosition;
293
320
  this.updateDragPosition(x, y);
294
-
295
- console.log('开始拖拽:', member.name);
321
+
322
+ console.log("开始拖拽:", member.name);
296
323
  },
297
324
 
298
325
  // 更新拖拽位置
299
326
  updateDragPosition(clientX, clientY) {
300
327
  this.dragGhostStyle = {
301
- position: 'fixed',
328
+ position: "fixed",
302
329
  left: `${clientX + this.dragOffset.x}px`,
303
330
  top: `${clientY + this.dragOffset.y}px`,
304
- pointerEvents: 'none',
305
- zIndex: 9999
331
+ pointerEvents: "none",
332
+ zIndex: 9999,
306
333
  };
307
-
334
+
308
335
  // 检测是否悬停在占位符上
309
336
  this.checkHoverTarget(clientX, clientY);
310
337
  },
@@ -313,8 +340,8 @@ export default {
313
340
  checkHoverTarget(clientX, clientY) {
314
341
  // 使用鼠标的实际位置来检测下方元素
315
342
  const elementBelow = document.elementFromPoint(clientX, clientY);
316
- const placeholder = elementBelow?.closest('.layout-placeholder');
317
-
343
+ const placeholder = elementBelow?.closest(".layout-placeholder");
344
+
318
345
  if (placeholder) {
319
346
  const index = Array.from(placeholder.parentNode.children).indexOf(placeholder) + 1;
320
347
  if (index > 0 && index <= this.placeholderCount) {
@@ -332,7 +359,7 @@ export default {
332
359
  if (this.isDragging) {
333
360
  // 检查是否拖拽到了占位符上
334
361
  this.checkDropTarget();
335
-
362
+
336
363
  this.isDragging = false;
337
364
  this.currentDragMember = null;
338
365
  this.dragGhostStyle = {};
@@ -345,10 +372,10 @@ export default {
345
372
  checkDropTarget() {
346
373
  // 使用保存的鼠标位置来检测拖拽目标
347
374
  const { x, y } = this.currentMousePosition;
348
-
375
+
349
376
  const elementBelow = document.elementFromPoint(x, y);
350
- const placeholder = elementBelow?.closest('.layout-placeholder');
351
-
377
+ const placeholder = elementBelow?.closest(".layout-placeholder");
378
+
352
379
  if (placeholder) {
353
380
  const index = Array.from(placeholder.parentNode.children).indexOf(placeholder) + 1;
354
381
  if (index > 0 && index <= this.placeholderCount) {
@@ -362,17 +389,20 @@ export default {
362
389
  if (draggedMember) {
363
390
  // 1. 清除该成员在其他位置的显示
364
391
  this.clearMemberFromOtherPositions(draggedMember.identity);
365
-
392
+
366
393
  // 2. 将成员分配到新位置(直接覆盖原有成员)
367
394
  const previousMember = this.assignedMembers[index];
368
395
  this.assignedMembers[index] = draggedMember;
369
-
396
+
370
397
  if (previousMember) {
371
- console.log('位置替换:', `位置 ${index} 的 ${previousMember.name} 被 ${draggedMember.name} 替换`);
398
+ console.log(
399
+ "位置替换:",
400
+ `位置 ${index} 的 ${previousMember.name} 被 ${draggedMember.name} 替换`
401
+ );
372
402
  } else {
373
- console.log('拖拽成功:', draggedMember.name, '已分配到位置', index);
403
+ console.log("拖拽成功:", draggedMember.name, "已分配到位置", index);
374
404
  }
375
-
405
+
376
406
  // 可以在这里添加成功提示
377
407
  // ElMessage.success(`${draggedMember.name} 已分配到位置 ${index}`);
378
408
  }
@@ -388,7 +418,7 @@ export default {
388
418
  }
389
419
  }
390
420
  if (clearedPositions.length > 0) {
391
- console.log('清除重复显示:', `成员在位置 [${clearedPositions.join(', ')}] 的显示已清除`);
421
+ console.log("清除重复显示:", `成员在位置 [${clearedPositions.join(", ")}] 的显示已清除`);
392
422
  }
393
423
  },
394
424
 
@@ -397,32 +427,32 @@ export default {
397
427
  const member = this.assignedMembers[index];
398
428
  if (member) {
399
429
  delete this.assignedMembers[index];
400
- console.log('手动清空:', `位置 ${index} 的 ${member.name} 已清空`);
430
+ console.log("手动清空:", `位置 ${index} 的 ${member.name} 已清空`);
401
431
  }
402
432
  },
403
433
 
404
434
  handleLayoutChange(layout) {
405
435
  const oldLayout = this.activeLayout;
406
436
  this.activeLayout = layout;
407
-
437
+
408
438
  // 获取新布局的占位符数量
409
439
  const newPlaceholderCount = this.getLayoutPlaceholderCount(layout);
410
-
440
+
411
441
  // 特殊处理:切换到topSide布局时的成员重排
412
- if (layout === 'topSide' && oldLayout !== 'topSide') {
442
+ if (layout === "topSide" && oldLayout !== "topSide") {
413
443
  this.handleTopSideLayoutTransition();
414
444
  }
415
-
445
+
416
446
  // 特殊处理:切换到rightSide布局时的成员重排
417
- if (layout === 'rightSide' && oldLayout !== 'rightSide') {
447
+ if (layout === "rightSide" && oldLayout !== "rightSide") {
418
448
  this.handleRightSideLayoutTransition();
419
449
  }
420
-
450
+
421
451
  // 特殊处理:切换到ring布局时的成员重排
422
- if (layout === 'ring' && oldLayout !== 'ring') {
452
+ if (layout === "ring" && oldLayout !== "ring") {
423
453
  this.handleRingLayoutTransition();
424
454
  }
425
-
455
+
426
456
  // 清空超出新布局占位符数量的成员
427
457
  const clearedMembers = [];
428
458
  for (const key in this.assignedMembers) {
@@ -433,68 +463,76 @@ export default {
433
463
  delete this.assignedMembers[key];
434
464
  }
435
465
  }
436
-
466
+
437
467
  if (clearedMembers.length > 0) {
438
- console.log(`布局切换:清空超出范围的成员 [${clearedMembers.join(', ')}]`);
468
+ console.log(`布局切换:清空超出范围的成员 [${clearedMembers.join(", ")}]`);
439
469
  }
440
-
441
- console.log(`布局切换:${oldLayout} -> ${layout},保留了 ${Object.keys(this.assignedMembers).length} 个已分配成员`);
470
+
471
+ console.log(
472
+ `布局切换:${oldLayout} -> ${layout},保留了 ${
473
+ Object.keys(this.assignedMembers).length
474
+ } 个已分配成员`
475
+ );
442
476
  },
443
477
 
444
478
  // 处理切换到topSide布局时的成员重排
445
479
  handleTopSideLayoutTransition() {
446
480
  // 在topSide布局中,位置1是焦点位置,位置2-4是上排小窗口
447
481
  // 如果位置1已有成员,则无需调整;如果位置1没有成员但其他位置有,可以考虑智能重排
448
-
482
+
449
483
  if (!this.assignedMembers[1]) {
450
484
  // 如果焦点位置(位置1)为空,尝试将最重要的成员移到焦点位置
451
485
  for (let i = 2; i <= 4; i++) {
452
486
  if (this.assignedMembers[i]) {
453
- console.log(`topSide布局优化:将位置${i}的${this.assignedMembers[i].name}移动到焦点位置`);
487
+ console.log(
488
+ `topSide布局优化:将位置${i}的${this.assignedMembers[i].name}移动到焦点位置`
489
+ );
454
490
  this.assignedMembers[1] = this.assignedMembers[i];
455
491
  delete this.assignedMembers[i];
456
492
  break;
457
493
  }
458
494
  }
459
495
  }
460
-
461
- console.log('topSide布局转换完成:位置1为焦点位置,位置2-4为上排小窗口');
496
+
497
+ console.log("topSide布局转换完成:位置1为焦点位置,位置2-4为上排小窗口");
462
498
  },
463
499
 
464
500
  // 处理切换到rightSide布局时的成员重排
465
501
  handleRightSideLayoutTransition() {
466
502
  // 在rightSide布局中,位置1是焦点位置,位置2-4是右侧纵向排列
467
503
  // 如果位置1已有成员,则无需调整;如果位置1没有成员但其他位置有,可以考虑智能重排
468
-
504
+
469
505
  if (!this.assignedMembers[1]) {
470
506
  // 如果焦点位置(位置1)为空,尝试将最重要的成员移到焦点位置
471
507
  for (let i = 2; i <= 4; i++) {
472
508
  if (this.assignedMembers[i]) {
473
- console.log(`rightSide布局优化:将位置${i}的${this.assignedMembers[i].name}移动到焦点位置`);
509
+ console.log(
510
+ `rightSide布局优化:将位置${i}的${this.assignedMembers[i].name}移动到焦点位置`
511
+ );
474
512
  this.assignedMembers[1] = this.assignedMembers[i];
475
513
  delete this.assignedMembers[i];
476
514
  break;
477
515
  }
478
516
  }
479
517
  }
480
-
481
- console.log('rightSide布局转换完成:位置1为焦点位置,位置2-4为右侧纵向排列');
518
+
519
+ console.log("rightSide布局转换完成:位置1为焦点位置,位置2-4为右侧纵向排列");
482
520
  },
483
521
 
484
522
  // 处理切换到ring布局时的成员重排
485
523
  handleRingLayoutTransition() {
486
524
  // 在ring布局中,位置1是焦点位置(左上角大窗口),位置2-8是环状排列
487
525
  // 如果位置1已有成员,则无需调整;如果位置1没有成员但其他位置有,按优先级智能重排
488
-
526
+
489
527
  if (!this.assignedMembers[1]) {
490
528
  // 如果焦点位置(位置1)为空,按优先级顺序选择成员移到焦点位置
491
529
  // 优先级:位置2 > 位置3 > 位置4 > 位置5 > 位置6 > 位置7 > 位置8
492
530
  // 这样可以确保最重要或最早分配的成员优先显示在焦点位置
493
-
531
+
494
532
  const priorityOrder = [2, 3, 4, 5, 6, 7, 8];
495
533
  let movedMember = null;
496
534
  let fromPosition = null;
497
-
535
+
498
536
  for (const position of priorityOrder) {
499
537
  if (this.assignedMembers[position]) {
500
538
  movedMember = this.assignedMembers[position];
@@ -502,22 +540,22 @@ export default {
502
540
  break;
503
541
  }
504
542
  }
505
-
543
+
506
544
  if (movedMember && fromPosition) {
507
545
  console.log(`ring布局优化:将位置${fromPosition}的${movedMember.name}移动到焦点位置`);
508
546
  this.assignedMembers[1] = movedMember;
509
547
  delete this.assignedMembers[fromPosition];
510
-
548
+
511
549
  // 启用环状分布优化,让剩余成员排列更加平衡
512
550
  this.optimizeRingMemberDistribution();
513
551
  } else {
514
- console.log('ring布局:没有成员需要移动到焦点位置');
552
+ console.log("ring布局:没有成员需要移动到焦点位置");
515
553
  }
516
554
  } else {
517
555
  console.log(`ring布局:焦点位置已有成员 ${this.assignedMembers[1].name},无需重排`);
518
556
  }
519
-
520
- console.log('ring布局转换完成:位置1为焦点位置,位置2-8为环状排列');
557
+
558
+ console.log("ring布局转换完成:位置1为焦点位置,位置2-8为环状排列");
521
559
  },
522
560
 
523
561
  // 优化ring布局的成员分布
@@ -528,180 +566,193 @@ export default {
528
566
  if (this.assignedMembers[i]) {
529
567
  ringMembers.push({
530
568
  member: this.assignedMembers[i],
531
- originalPosition: i
569
+ originalPosition: i,
532
570
  });
533
571
  delete this.assignedMembers[i];
534
572
  }
535
573
  }
536
-
574
+
537
575
  if (ringMembers.length === 0) {
538
- console.log('ring布局优化:没有环状位置的成员需要重新排列');
576
+ console.log("ring布局优化:没有环状位置的成员需要重新排列");
539
577
  return;
540
578
  }
541
-
579
+
542
580
  // 重新分配到环状位置,优先使用前面的位置以保持视觉平衡
543
581
  // 这样可以避免成员分布过于分散,让界面看起来更整洁
544
582
  const ringPositions = [2, 3, 4, 5, 6, 7, 8];
545
583
  let optimizedCount = 0;
546
-
584
+
547
585
  for (let i = 0; i < ringMembers.length && i < ringPositions.length; i++) {
548
586
  const newPosition = ringPositions[i];
549
587
  const memberInfo = ringMembers[i];
550
-
588
+
551
589
  this.assignedMembers[newPosition] = memberInfo.member;
552
-
590
+
553
591
  if (memberInfo.originalPosition !== newPosition) {
554
592
  optimizedCount++;
555
593
  }
556
594
  }
557
-
595
+
558
596
  if (optimizedCount > 0) {
559
597
  console.log(`ring布局优化:重新排列了${optimizedCount}个成员,使环状分布更加平衡`);
560
598
  } else {
561
- console.log('ring布局优化:成员分布已经是最优状态,无需调整');
599
+ console.log("ring布局优化:成员分布已经是最优状态,无需调整");
562
600
  }
563
601
  },
564
602
 
565
603
  handleReset() {
566
- this.activeLayout = "grid4";
567
- this.autoFill = true;
568
- this.assignedMembers = {};
604
+ this.liveClient.resetVolteLayout(this.meetingNum).then((res) => {
605
+ if (res.code == 200) {
606
+ this.activeLayout = "grid4";
607
+ this.autoFill = true;
608
+ this.assignedMembers = {};
609
+ // 清空保存的状态
610
+ this.clearLayoutState();
611
+ } else {
612
+ this.showMessage.error(res?.msg || "volte融屏重置布局失败");
613
+ }
614
+ });
569
615
  },
570
616
 
571
617
  handleApply() {
572
618
  // 获取当前布局的占位符数量
573
619
  const currentPlaceholderCount = this.getLayoutPlaceholderCount(this.activeLayout);
574
-
620
+
575
621
  // 生成布局映射数组
576
622
  const layoutArray = this.generateLayoutArray(currentPlaceholderCount);
577
-
623
+
578
624
  // 验证布局数组
579
625
  const validation = this.validateLayoutArray(layoutArray);
580
-
626
+
581
627
  if (!validation.isValid) {
582
- console.error('布局验证失败:', validation.errors);
628
+ console.error("布局验证失败:", validation.errors);
583
629
  // 可以在这里显示错误提示给用户
584
630
  // ElMessage.error('布局配置有误,请检查后重试');
585
631
  return null;
586
632
  }
587
-
633
+
588
634
  if (validation.warnings.length > 0) {
589
- console.warn('布局验证警告:', validation.warnings);
635
+ console.warn("布局验证警告:", validation.warnings);
590
636
  // 可以在这里显示警告提示给用户
591
637
  // ElMessage.warning('布局配置存在一些问题,建议检查');
592
638
  }
593
-
639
+
594
640
  // 这里可以发送应用布局的事件或API调用
595
- console.log('应用布局成功:', {
641
+ console.log("应用布局成功:", {
596
642
  layout: this.activeLayout,
597
643
  autoFill: this.autoFill,
598
644
  placeholderCount: currentPlaceholderCount,
599
645
  layoutArray: layoutArray,
600
646
  assignedMembers: this.assignedMembers,
601
- validation: validation
647
+ validation: validation,
602
648
  });
603
-
649
+
604
650
  const handleCurrentLayout = () => {
605
- let layout = ""
606
- switch(this.activeLayout) {
651
+ let layout = "";
652
+ switch (this.activeLayout) {
607
653
  case "grid4":
608
- layout = "four_grids"
654
+ layout = "four_grids";
609
655
  break;
610
656
  case "grid9":
611
- layout = "nine_grids"
657
+ layout = "nine_grids";
612
658
  break;
613
659
  case "ring":
614
- layout = "custom1"
660
+ layout = "custom1";
615
661
  break;
616
662
  case "topSide":
617
- layout = "custom2"
663
+ layout = "custom2";
618
664
  break;
619
665
  case "rightSide":
620
- layout = "custom3"
666
+ layout = "custom3";
621
667
  break;
622
668
  default:
623
- layout = "four_grids"
669
+ layout = "four_grids";
624
670
  break;
625
671
  }
626
- return layout
672
+ return layout;
627
673
  };
628
-
629
- this.liveClient.modifyVolteLayout({
630
- roomNum: this.meetingNum,
631
- layout: handleCurrentLayout(),
632
- identitys: layoutArray.map(item => item && item?.identity ? item.identity : ""),
633
- autoFill: this.autoFill
634
- }).then(res => {
635
- if(res.code == 200) {
636
- this.showMessage.success("volte融屏应用布局成功")
637
- this.closeCustomLayout()
638
- } else {
639
- this.showMessage.error(res?.msg || "volte融屏应用布局失败")
640
- }
641
- }).catch(err => {
642
- this.showMessage.error(err?.message || "volte融屏应用布局失败")
643
- });
674
+
675
+ this.liveClient
676
+ .modifyVolteLayout({
677
+ roomNum: this.meetingNum,
678
+ layout: handleCurrentLayout(),
679
+ identitys: layoutArray.map((item) => (item && item?.identity ? item.identity : "")),
680
+ autoFill: this.autoFill,
681
+ })
682
+ .then((res) => {
683
+ if (res.code == 200) {
684
+ this.showMessage.success("volte融屏应用布局成功");
685
+ // 成功后保存当前布局状态,便于下次打开还原
686
+ this.saveLayoutState();
687
+ this.closeCustomLayout();
688
+ } else {
689
+ this.showMessage.error(res?.msg || "volte融屏应用布局失败");
690
+ }
691
+ })
692
+ .catch((err) => {
693
+ this.showMessage.error(err?.message || "volte融屏应用布局失败");
694
+ });
644
695
  },
645
696
 
646
697
  // 生成布局映射数组
647
698
  generateLayoutArray(placeholderCount) {
648
699
  const layoutArray = [];
649
-
700
+
650
701
  // 遍历每个位置,构建数组
651
702
  for (let i = 1; i <= placeholderCount; i++) {
652
703
  const member = this.assignedMembers[i];
653
-
704
+
654
705
  if (member) {
655
706
  // 如果该位置有与会者,添加成员信息
656
707
  layoutArray.push(member);
657
708
  } else {
658
709
  // 如果该位置是空占位符,添加空字符串
659
- layoutArray.push('');
710
+ layoutArray.push("");
660
711
  }
661
712
  }
662
-
713
+
663
714
  // 输出详细信息
664
715
  const statistics = this.getLayoutArrayStatistics(layoutArray);
665
- console.log('生成布局数组:', {
716
+ console.log("生成布局数组:", {
666
717
  layout: this.activeLayout,
667
718
  placeholderCount: placeholderCount,
668
719
  ...statistics,
669
- array: layoutArray
720
+ array: layoutArray,
670
721
  });
671
-
722
+
672
723
  // 输出格式化的数组显示
673
- console.log('布局数组格式化显示:', this.formatLayoutArrayDisplay(layoutArray));
674
-
724
+ console.log("布局数组格式化显示:", this.formatLayoutArrayDisplay(layoutArray));
725
+
675
726
  return layoutArray;
676
727
  },
677
728
 
678
729
  // 获取布局数组统计信息
679
730
  getLayoutArrayStatistics(layoutArray) {
680
- const memberCount = layoutArray.filter(item => item !== '').length;
681
- const emptyCount = layoutArray.filter(item => item === '').length;
682
- const memberNames = layoutArray
683
- .filter(item => item !== '')
684
- .map(member => member.name);
685
-
731
+ const memberCount = layoutArray.filter((item) => item !== "").length;
732
+ const emptyCount = layoutArray.filter((item) => item === "").length;
733
+ const memberNames = layoutArray.filter((item) => item !== "").map((member) => member.name);
734
+
686
735
  return {
687
736
  arrayLength: layoutArray.length,
688
737
  memberCount: memberCount,
689
738
  emptyCount: emptyCount,
690
739
  memberNames: memberNames,
691
- utilizationRate: Math.round((memberCount / layoutArray.length) * 100) + '%'
740
+ utilizationRate: Math.round((memberCount / layoutArray.length) * 100) + "%",
692
741
  };
693
742
  },
694
743
 
695
744
  // 格式化布局数组显示
696
745
  formatLayoutArrayDisplay(layoutArray) {
697
- return layoutArray.map((item, index) => {
698
- const position = index + 1;
699
- if (item === '') {
700
- return `位置${position}: [空]`;
701
- } else {
702
- return `位置${position}: ${item.name}`;
703
- }
704
- }).join('\n');
746
+ return layoutArray
747
+ .map((item, index) => {
748
+ const position = index + 1;
749
+ if (item === "") {
750
+ return `位置${position}: [空]`;
751
+ } else {
752
+ return `位置${position}: ${item.name}`;
753
+ }
754
+ })
755
+ .join("\n");
705
756
  },
706
757
 
707
758
  // 验证布局数组
@@ -709,32 +760,32 @@ export default {
709
760
  const validation = {
710
761
  isValid: true,
711
762
  errors: [],
712
- warnings: []
763
+ warnings: [],
713
764
  };
714
-
765
+
715
766
  // 检查数组长度
716
767
  const expectedLength = this.getLayoutPlaceholderCount(this.activeLayout);
717
768
  if (layoutArray.length !== expectedLength) {
718
769
  validation.isValid = false;
719
770
  validation.errors.push(`数组长度不匹配:期望${expectedLength},实际${layoutArray.length}`);
720
771
  }
721
-
772
+
722
773
  // 检查重复成员
723
- const members = layoutArray.filter(item => item !== '');
724
- const memberIds = members.map(member => member.identity);
774
+ const members = layoutArray.filter((item) => item !== "");
775
+ const memberIds = members.map((member) => member.identity);
725
776
  const uniqueIds = [...new Set(memberIds)];
726
-
777
+
727
778
  if (memberIds.length !== uniqueIds.length) {
728
779
  validation.isValid = false;
729
- validation.errors.push('检测到重复的成员分配');
780
+ validation.errors.push("检测到重复的成员分配");
730
781
  }
731
-
782
+
732
783
  // 检查成员利用率
733
784
  const utilizationRate = (members.length / layoutArray.length) * 100;
734
785
  if (utilizationRate < 50) {
735
786
  validation.warnings.push(`成员利用率较低:${Math.round(utilizationRate)}%`);
736
787
  }
737
-
788
+
738
789
  return validation;
739
790
  },
740
791
 
@@ -743,15 +794,129 @@ export default {
743
794
  val ? item.name.includes(val) : item.name.includes(this.searchVal)
744
795
  );
745
796
  },
746
-
797
+
747
798
  refreshMemberList() {
748
799
  this.getMemberList();
749
800
  },
750
-
801
+
751
802
  closeCustomLayout() {
752
803
  this.$emit("closeCustomLayout");
753
- }
754
- }
804
+ },
805
+ // =============================
806
+ // 布局记忆:存/取/清/还原
807
+ // =============================
808
+ getStorageKey() {
809
+ const meeting = this.meetingNum || "default";
810
+ return `${STORAGE_PREFIX}:${meeting}`;
811
+ },
812
+ saveLayoutState() {
813
+ try {
814
+ const layout = this.activeLayout;
815
+ const count = this.getLayoutPlaceholderCount(layout);
816
+ const slots = [];
817
+ for (let i = 1; i <= count; i++) {
818
+ const m = this.assignedMembers[i];
819
+ slots.push(m && m.identity ? m.identity : "");
820
+ }
821
+ const state = {
822
+ version: 1,
823
+ layout,
824
+ autoFill: !!this.autoFill,
825
+ slots,
826
+ updatedAt: Date.now(),
827
+ };
828
+ localStorage.setItem(this.getStorageKey(), JSON.stringify(state));
829
+ } catch (e) {
830
+ console.warn("保存布局状态失败:", e);
831
+ }
832
+ },
833
+ loadLayoutState() {
834
+ try {
835
+ const raw = localStorage.getItem(this.getStorageKey());
836
+ if (!raw) return null;
837
+ const state = JSON.parse(raw);
838
+ if (!state || !Array.isArray(state.slots) || typeof state.layout !== "string") return null;
839
+ return state;
840
+ } catch (e) {
841
+ console.warn("读取布局状态失败:", e);
842
+ return null;
843
+ }
844
+ },
845
+ clearLayoutState() {
846
+ try {
847
+ localStorage.removeItem(this.getStorageKey());
848
+ } catch (e) {
849
+ console.warn("清理布局状态失败:", e);
850
+ }
851
+ },
852
+ restoreLayoutState() {
853
+ const state = this.loadLayoutState();
854
+ if (!state) return;
855
+
856
+ const savedLayout = state.layout;
857
+ const slotCount = this.getLayoutPlaceholderCount(savedLayout);
858
+ this.activeLayout = savedLayout;
859
+ this.autoFill = !!state.autoFill;
860
+
861
+ // 根据当前参会者映射还原
862
+ const identityToMember = new Map();
863
+ (this.participantList || []).forEach((m) => {
864
+ if (m && m.identity) identityToMember.set(m.identity, m);
865
+ });
866
+ const nextAssigned = {};
867
+ for (let i = 0; i < Math.min(state.slots.length, slotCount); i++) {
868
+ const id = state.slots[i];
869
+ if (id && identityToMember.has(id)) {
870
+ nextAssigned[i + 1] = identityToMember.get(id);
871
+ }
872
+ }
873
+ this.assignedMembers = nextAssigned;
874
+ },
875
+ // 当参会者变化时,移除已离开的与会者,同时更新已保存的状态
876
+ reconcileAssignedMembers() {
877
+ const currentIds = new Set((this.participantList || []).map((m) => m.identity));
878
+
879
+ // 拖拽期间不打断,延迟处理
880
+ if (this.isDragging) {
881
+ this.pendingReconcile = true;
882
+ return;
883
+ }
884
+
885
+ // 1) 清理界面中的离会成员
886
+ let removed = false;
887
+ const nextAssigned = { ...this.assignedMembers };
888
+ Object.keys(nextAssigned).forEach((k) => {
889
+ const mem = nextAssigned[k];
890
+ if (mem && mem.identity && !currentIds.has(mem.identity)) {
891
+ delete nextAssigned[k];
892
+ removed = true;
893
+ }
894
+ });
895
+ if (removed) {
896
+ this.assignedMembers = nextAssigned;
897
+ }
898
+
899
+ // 2) 同步本地保存的 slots
900
+ const saved = this.loadLayoutState();
901
+ if (saved && Array.isArray(saved.slots)) {
902
+ let changed = false;
903
+ for (let i = 0; i < saved.slots.length; i++) {
904
+ const id = saved.slots[i];
905
+ if (id && !currentIds.has(id)) {
906
+ saved.slots[i] = "";
907
+ changed = true;
908
+ }
909
+ }
910
+ if (changed) {
911
+ try {
912
+ localStorage.setItem(this.getStorageKey(), JSON.stringify(saved));
913
+ } catch (e) {
914
+ console.warn("更新保存的布局状态失败:", e);
915
+ }
916
+ }
917
+ }
918
+ },
919
+ },
755
920
  };
756
921
  </script>
757
922
 
@@ -763,7 +928,7 @@ export default {
763
928
  position: absolute;
764
929
  right: 0;
765
930
  top: 0;
766
- z-index: 2000;
931
+ z-index: 3000;
767
932
  overflow: hidden;
768
933
  color: var(--theme-font-color);
769
934
  background: var(--dialog-bg);
@@ -911,7 +1076,7 @@ export default {
911
1076
  align-items: center;
912
1077
  justify-content: center;
913
1078
  padding: 16px;
914
-
1079
+
915
1080
  .layout-container {
916
1081
  width: 100%;
917
1082
  max-width: 100%;
@@ -919,44 +1084,44 @@ export default {
919
1084
  display: grid;
920
1085
  gap: 8px;
921
1086
  }
922
-
1087
+
923
1088
  // 4宫格布局 - 2x2网格
924
1089
  &.layout-grid4 .layout-container {
925
1090
  grid-template-columns: 1fr 1fr;
926
1091
  grid-template-rows: 1fr 1fr;
927
1092
  }
928
-
1093
+
929
1094
  // 9宫格布局 - 3x3网格
930
1095
  &.layout-grid9 .layout-container {
931
1096
  grid-template-columns: repeat(3, 1fr);
932
1097
  grid-template-rows: repeat(3, 1fr);
933
1098
  }
934
-
1099
+
935
1100
  // 环状布局 - 左上角大窗口 + 右侧纵向 + 下侧横向 + 右下角
936
1101
  &.layout-ring .layout-container {
937
1102
  grid-template-columns: repeat(4, 1fr);
938
1103
  grid-template-rows: repeat(4, 1fr);
939
- grid-template-areas:
1104
+ grid-template-areas:
940
1105
  "main main main slot1"
941
1106
  "main main main slot2"
942
1107
  "main main main slot3"
943
1108
  "slot4 slot5 slot6 slot7";
944
1109
  }
945
-
1110
+
946
1111
  // 下焦点布局 - 上排3个小窗口,下方1个大窗口
947
1112
  &.layout-topSide .layout-container {
948
1113
  grid-template-columns: repeat(3, 1fr);
949
1114
  grid-template-rows: 1fr 2fr;
950
- grid-template-areas:
1115
+ grid-template-areas:
951
1116
  "slot2 slot3 slot4"
952
1117
  "main main main";
953
1118
  }
954
-
1119
+
955
1120
  // 左焦点布局 - 左侧1个大窗口,右侧3个纵向排列
956
1121
  &.layout-rightSide .layout-container {
957
1122
  grid-template-columns: 2fr 1fr;
958
1123
  grid-template-rows: repeat(3, 1fr);
959
- grid-template-areas:
1124
+ grid-template-areas:
960
1125
  "main slot2"
961
1126
  "main slot3"
962
1127
  "main slot4";
@@ -1014,7 +1179,8 @@ export default {
1014
1179
  .input-suffix-icon {
1015
1180
  width: 20px;
1016
1181
  height: 20px;
1017
- background: url("../../assets/image/common/input_search_icon.png") no-repeat center / 100% 100%;
1182
+ background: url("../../assets/image/common/input_search_icon.png") no-repeat center / 100%
1183
+ 100%;
1018
1184
  }
1019
1185
  }
1020
1186
  &-list {
@@ -1027,14 +1193,14 @@ export default {
1027
1193
  display: flex;
1028
1194
  align-items: center;
1029
1195
  transition: all 0.2s ease;
1030
-
1196
+
1031
1197
  &.is-dragging {
1032
1198
  opacity: 0.5;
1033
1199
  background: rgba(255, 255, 255, 0.1);
1034
1200
  border-radius: 4px;
1035
1201
  transform: scale(0.95);
1036
1202
  }
1037
-
1203
+
1038
1204
  &-drag-icon {
1039
1205
  width: 16px;
1040
1206
  height: 16px;
@@ -1042,7 +1208,7 @@ export default {
1042
1208
  cursor: pointer;
1043
1209
  margin-right: 12px;
1044
1210
  transition: opacity 0.2s ease;
1045
-
1211
+
1046
1212
  &:hover {
1047
1213
  opacity: 0.8;
1048
1214
  }
@@ -1089,7 +1255,7 @@ export default {
1089
1255
  pointer-events: none;
1090
1256
  user-select: none;
1091
1257
  max-width: 200px;
1092
-
1258
+
1093
1259
  &-avatar {
1094
1260
  width: 20px;
1095
1261
  height: 20px;
@@ -1098,7 +1264,7 @@ export default {
1098
1264
  margin-right: 6px;
1099
1265
  flex-shrink: 0;
1100
1266
  }
1101
-
1267
+
1102
1268
  &-name {
1103
1269
  color: var(--theme-font-color);
1104
1270
  font-size: 11px;