vue-element-ui-x 0.1.8-beta → 0.1.10-beta

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 (97) hide show
  1. package/lib/components/Attachments/index.js +229 -237
  2. package/lib/components/Bubble/index.js +47 -34
  3. package/lib/components/BubbleList/index.js +57 -49
  4. package/lib/components/Conversations/index.js +32 -69
  5. package/lib/components/FilesCard/index.js +207 -211
  6. package/lib/components/Prompts/index.js +21 -21
  7. package/lib/components/Sender/index.js +18 -18
  8. package/lib/components/Think/index.js +1 -1
  9. package/lib/components/Thinking/index.js +1 -1
  10. package/lib/components/ThoughtChain/index.js +60 -47
  11. package/lib/components/Typewriter/index.js +46 -33
  12. package/lib/components/Welcome/index.js +1 -1
  13. package/lib/index.common.js +1 -1
  14. package/lib/index.esm.js +1 -1
  15. package/lib/index.js +479 -427
  16. package/lib/index.umd.js +1 -1
  17. package/lib/mixins/index.js +105 -16
  18. package/package.json +1 -1
  19. package/src/components/Attachments/index.js +8 -8
  20. package/src/components/Attachments/src/main.vue +10 -10
  21. package/src/components/Bubble/index.js +6 -6
  22. package/src/components/Bubble/src/main.vue +299 -299
  23. package/src/components/BubbleList/index.js +8 -8
  24. package/src/components/BubbleList/src/loading.vue +75 -75
  25. package/src/components/BubbleList/src/main.vue +461 -466
  26. package/src/components/Conversations/index.js +8 -8
  27. package/src/components/Conversations/src/components/item.vue +13 -34
  28. package/src/components/Conversations/src/main.vue +622 -635
  29. package/src/components/FilesCard/index.js +8 -8
  30. package/src/components/FilesCard/src/fileSvg/audio.vue +38 -38
  31. package/src/components/FilesCard/src/fileSvg/code.vue +35 -35
  32. package/src/components/FilesCard/src/fileSvg/database.vue +94 -94
  33. package/src/components/FilesCard/src/fileSvg/excel.vue +38 -38
  34. package/src/components/FilesCard/src/fileSvg/file.vue +40 -40
  35. package/src/components/FilesCard/src/fileSvg/image.vue +40 -40
  36. package/src/components/FilesCard/src/fileSvg/index.js +46 -46
  37. package/src/components/FilesCard/src/fileSvg/link.vue +54 -54
  38. package/src/components/FilesCard/src/fileSvg/mark.vue +38 -38
  39. package/src/components/FilesCard/src/fileSvg/pdf.vue +38 -38
  40. package/src/components/FilesCard/src/fileSvg/ppt.vue +38 -38
  41. package/src/components/FilesCard/src/fileSvg/three.vue +38 -38
  42. package/src/components/FilesCard/src/fileSvg/txt.vue +38 -38
  43. package/src/components/FilesCard/src/fileSvg/unknown.vue +54 -54
  44. package/src/components/FilesCard/src/fileSvg/video.vue +38 -38
  45. package/src/components/FilesCard/src/fileSvg/word.vue +38 -38
  46. package/src/components/FilesCard/src/fileSvg/zip.vue +38 -38
  47. package/src/components/FilesCard/src/main.vue +4 -8
  48. package/src/components/FilesCard/src/options.js +18 -18
  49. package/src/components/Prompts/index.js +8 -8
  50. package/src/components/Prompts/src/main.vue +248 -248
  51. package/src/components/Sender/index.js +8 -8
  52. package/src/components/Sender/src/components/ClearButton.vue +28 -28
  53. package/src/components/Sender/src/components/Loading.vue +53 -53
  54. package/src/components/Sender/src/components/LoadingButton.vue +39 -39
  55. package/src/components/Sender/src/components/SendButton.vue +26 -26
  56. package/src/components/Sender/src/components/SpeechButton.vue +24 -24
  57. package/src/components/Sender/src/components/SpeechLoading.vue +87 -87
  58. package/src/components/Sender/src/components/SpeechLoadingButton.vue +43 -43
  59. package/src/components/Sender/src/main.vue +4 -3
  60. package/src/components/Think/index.js +8 -8
  61. package/src/components/Think/src/main.vue +190 -190
  62. package/src/components/Thinking/index.js +8 -8
  63. package/src/components/ThoughtChain/index.js +8 -8
  64. package/src/components/ThoughtChain/src/main.vue +293 -293
  65. package/src/components/Typewriter/index.js +8 -8
  66. package/src/components/Typewriter/src/main.vue +10 -2
  67. package/src/components/Welcome/index.js +8 -8
  68. package/src/components/Welcome/src/main.vue +151 -151
  69. package/src/mixins/recordMixin.js +0 -1
  70. package/src/mixins/sendMixin.js +104 -11
  71. package/src/mixins/streamMixin.js +3 -5
  72. package/src/styles/Attachments.scss +236 -236
  73. package/src/styles/Bubble.scss +157 -157
  74. package/src/styles/BubbleList.scss +148 -148
  75. package/src/styles/Conversations.scss +260 -260
  76. package/src/styles/FilesCard.scss +221 -221
  77. package/src/styles/Prompts.scss +195 -195
  78. package/src/styles/Sender.scss +199 -199
  79. package/src/styles/Think.scss +134 -134
  80. package/src/styles/ThoughtChain.scss +113 -113
  81. package/src/styles/Typewriter.scss +66 -66
  82. package/src/theme/var.scss +72 -72
  83. package/lib/attachments.js +0 -3082
  84. package/lib/bubble-list.js +0 -13840
  85. package/lib/bubble.js +0 -13125
  86. package/lib/conversations.js +0 -18825
  87. package/lib/files-card.js +0 -2471
  88. package/lib/mixins.js +0 -1016
  89. package/lib/prompts.js +0 -832
  90. package/lib/sender.js +0 -1911
  91. package/lib/think.js +0 -799
  92. package/lib/thinking.js +0 -809
  93. package/lib/thought-chain.js +0 -30391
  94. package/lib/typewriter.js +0 -12788
  95. package/lib/welcome.js +0 -755
  96. package/src/styles/button.scss +0 -302
  97. package/src/styles/var.scss +0 -1052
@@ -1,635 +1,622 @@
1
- <template>
2
- <div
3
- class="el-x-conversations-container"
4
- :style="{
5
- '--conversation-label-height': `${labelHeight}px`,
6
- '--conversation-list-auto-bg-color': mergedStyle.backgroundColor,
7
- }"
8
- >
9
- <slot name="header"></slot>
10
- <ul
11
- class="el-x-conversations-list"
12
- :style="mergedStyle"
13
- >
14
- <!-- 滚动区域容器 -->
15
- <li class="el-x-conversations-scroll-wrapper">
16
- <div
17
- ref="scrollContainer"
18
- class="el-x-conversations-scrollbar"
19
- @scroll="handleScroll"
20
- >
21
- <div class="scroll-content">
22
- <template v-if="shouldUseGrouping">
23
- <!-- 分组显示 -->
24
- <div
25
- v-for="group in groups"
26
- :key="group.key"
27
- :ref="el => bindGroupRef(el, group)"
28
- class="el-x-conversation-group"
29
- >
30
- <div
31
- class="el-x-conversation-group-title sticky-title"
32
- :class="{ 'active-sticky': stickyGroupKeys.has(group.key) }"
33
- >
34
- <slot
35
- name="groupTitle"
36
- :group="group"
37
- >{{ group.title }}</slot
38
- >
39
- </div>
40
- <div class="el-x-conversation-group-items">
41
- <conversations-item
42
- v-for="item in group.children"
43
- :key="item.uniqueKey"
44
- :item="item"
45
- :active="item.uniqueKey === active"
46
- :items-style="itemsStyle"
47
- :items-hover-style="itemsHoverStyle"
48
- :items-active-style="itemsActiveStyle"
49
- :items-menu-opened-style="itemsMenuOpenedStyle"
50
- :prefix-icon="item.prefixIcon"
51
- :show-tooltip="showTooltip"
52
- :tooltip-placement="tooltipPlacement"
53
- :tooltip-offset="tooltipOffset"
54
- :suffix-icon="item.suffixIcon"
55
- :active-key="active || ''"
56
- :label-max-width="labelMaxWidth"
57
- :menu="menu"
58
- :show-built-in-menu="showBuiltInMenu"
59
- :menu-placement="menuPlacement"
60
- :menu-offset="menuOffset"
61
- :menu-max-height="menuMaxHeight"
62
- :menu-style="menuStyle"
63
- :menu-show-arrow="menuShowArrow"
64
- :menu-class-name="menuClassName"
65
- :menu-teleported="menuTeleported"
66
- @click="handleClick(item)"
67
- @menu-command="handleMenuItemClick"
68
- >
69
- <!-- 传递插槽 -->
70
- <template
71
- v-if="$scopedSlots.label"
72
- #label
73
- >
74
- <slot
75
- name="label"
76
- :item="item"
77
- ></slot>
78
- </template>
79
-
80
- <template
81
- v-if="$scopedSlots['more-filled']"
82
- #more-filled="moreFilledSoltProps"
83
- >
84
- <slot
85
- name="more-filled"
86
- v-bind="moreFilledSoltProps"
87
- ></slot>
88
- </template>
89
-
90
- <template
91
- v-if="$scopedSlots.menu"
92
- #menu
93
- >
94
- <slot
95
- name="menu"
96
- :item="item"
97
- ></slot>
98
- </template>
99
- </conversations-item>
100
- </div>
101
- </div>
102
- </template>
103
-
104
- <template v-else>
105
- <conversations-item
106
- v-for="item in filteredItems"
107
- :key="item.uniqueKey"
108
- :item="item"
109
- :items-style="itemsStyle"
110
- :items-hover-style="itemsHoverStyle"
111
- :items-active-style="itemsActiveStyle"
112
- :active="item.uniqueKey === active"
113
- :items-menu-opened-style="itemsMenuOpenedStyle"
114
- :prefix-icon="item.prefixIcon"
115
- :show-tooltip="showTooltip"
116
- :tooltip-placement="tooltipPlacement"
117
- :tooltip-offset="tooltipOffset"
118
- :suffix-icon="item.suffixIcon"
119
- :active-key="active || ''"
120
- :label-max-width="labelMaxWidth"
121
- :menu="menu"
122
- :show-built-in-menu="showBuiltInMenu"
123
- :menu-placement="menuPlacement"
124
- :menu-offset="menuOffset"
125
- :menu-max-height="menuMaxHeight"
126
- :menu-style="menuStyle"
127
- :menu-show-arrow="menuShowArrow"
128
- :menu-class-name="menuClassName"
129
- :menu-teleported="menuTeleported"
130
- @click="handleClick(item)"
131
- @menu-command="handleMenuItemClick"
132
- >
133
- <!-- 传递插槽 -->
134
- <template
135
- v-if="$scopedSlots.label"
136
- #label
137
- >
138
- <slot
139
- name="label"
140
- :item="item"
141
- ></slot>
142
- </template>
143
-
144
- <template
145
- v-if="$scopedSlots['more-filled']"
146
- #more-filled="moreFilledSoltProps"
147
- >
148
- <slot
149
- name="more-filled"
150
- v-bind="moreFilledSoltProps"
151
- ></slot>
152
- </template>
153
-
154
- <template
155
- v-if="$scopedSlots.menu"
156
- #menu
157
- >
158
- <slot
159
- name="menu"
160
- :item="item"
161
- ></slot>
162
- </template>
163
- </conversations-item>
164
- </template>
165
-
166
- <!-- 加载更多 -->
167
- <div
168
- v-if="loadMoreLoading"
169
- class="el-x-conversations-load-more"
170
- >
171
- <slot name="load-more">
172
- <i class="el-icon-loading el-x-conversations-load-more-is-loading"></i>
173
- <span>加载更多...</span>
174
- </slot>
175
- </div>
176
- </div>
177
- </div>
178
- </li>
179
- </ul>
180
- <slot name="footer"></slot>
181
- <!-- 滚动到顶部按钮 -->
182
- <el-button
183
- v-show="showScrollTop && showToTopBtn"
184
- class="scroll-to-top-btn"
185
- size="small"
186
- circle
187
- :type="toTopBtnType"
188
- :style="toTopBtnStyle"
189
- @click="scrollToTop"
190
- >
191
- <i class="el-icon-top"></i>
192
- </el-button>
193
- </div>
194
- </template>
195
-
196
- <script>
197
- import ConversationsItem from './components/item.vue';
198
- import { get } from 'lodash';
199
-
200
- export default {
201
- name: 'ElXConversations',
202
-
203
- components: {
204
- ConversationsItem,
205
- },
206
-
207
- props: {
208
- items: {
209
- type: Array,
210
- default: () => [],
211
- },
212
- itemsStyle: {
213
- type: Object,
214
- default: () => ({}),
215
- },
216
- itemsHoverStyle: {
217
- type: Object,
218
- default: () => ({}),
219
- },
220
- itemsActiveStyle: {
221
- type: Object,
222
- default: () => ({}),
223
- },
224
- itemsMenuOpenedStyle: {
225
- type: Object,
226
- default: () => ({}),
227
- },
228
- styleConfig: {
229
- type: Object,
230
- default: () => ({}),
231
- },
232
- showTooltip: {
233
- type: Boolean,
234
- default: false,
235
- },
236
- groupable: {
237
- type: [Boolean, Object],
238
- default: false,
239
- },
240
- labelMaxWidth: {
241
- type: Number,
242
- default: undefined,
243
- },
244
- labelHeight: {
245
- type: Number,
246
- default: 20,
247
- },
248
- showBuiltInMenu: {
249
- type: Boolean,
250
- default: false,
251
- },
252
- menu: {
253
- type: Array,
254
- default: () => [
255
- {
256
- label: '重命名',
257
- key: 'rename',
258
- icon: 'el-icon-edit',
259
- command: 'rename',
260
- },
261
- {
262
- label: '删除',
263
- key: 'delete',
264
- icon: 'el-icon-delete',
265
- command: 'delete',
266
- menuItemHoverStyle: {
267
- color: 'red',
268
- backgroundColor: 'rgba(255, 0, 0, 0.1)',
269
- },
270
- },
271
- ],
272
- },
273
- ungroupedTitle: {
274
- type: String,
275
- default: '未分组',
276
- },
277
- tooltipPlacement: {
278
- type: String,
279
- default: 'top',
280
- },
281
- tooltipOffset: {
282
- type: Number,
283
- default: 12,
284
- },
285
- menuPlacement: {
286
- type: String,
287
- default: 'bottom-start',
288
- },
289
- menuOffset: {
290
- type: Number,
291
- default: 50,
292
- },
293
- menuShowArrow: {
294
- type: Boolean,
295
- default: false,
296
- },
297
- menuClassName: {
298
- type: String,
299
- default: '',
300
- },
301
- menuTeleported: {
302
- type: Boolean,
303
- default: true,
304
- },
305
- menuStyle: {
306
- type: Object,
307
- default: () => ({}),
308
- },
309
- menuMaxHeight: Number,
310
- loadMoreLoading: {
311
- type: Boolean,
312
- default: false,
313
- },
314
- showToTopBtn: {
315
- type: Boolean,
316
- default: false,
317
- },
318
- toTopBtnType: {
319
- type: String,
320
- default: 'primary',
321
- validator: value =>
322
- ['primary', 'success', 'warning', 'danger', 'info', 'text'].includes(value),
323
- },
324
- toTopBtnStyle: {
325
- type: Object,
326
- default: () => ({}),
327
- },
328
- labelKey: {
329
- type: String,
330
- default: 'label',
331
- },
332
- rowKey: {
333
- type: String,
334
- default: 'id',
335
- },
336
- active: {
337
- type: [String, Number, Boolean],
338
- default: '',
339
- },
340
- loadMore: {
341
- type: Function,
342
- default: null,
343
- },
344
- },
345
-
346
- data() {
347
- return {
348
- showScrollTop: false,
349
- groupRefs: {},
350
- stickyGroupKeys: new Set(),
351
- };
352
- },
353
-
354
- computed: {
355
- itemsUse() {
356
- return this.items.map((item, index) => ({
357
- ...item,
358
- uniqueKey: this.rowKey ? get(item, this.rowKey) : index.toString(),
359
- label: get(item, this.labelKey),
360
- }));
361
- },
362
-
363
- mergedStyle() {
364
- const defaultStyle = {
365
- padding: '10px 0 10px 10px',
366
- backgroundColor: '#fff',
367
- borderRadius: '8px',
368
- width: '280px',
369
- height: '0',
370
- };
371
- return {
372
- ...defaultStyle,
373
- ...this.styleConfig,
374
- };
375
- },
376
-
377
- shouldUseGrouping() {
378
- return !!this.groupable;
379
- },
380
-
381
- filteredItems() {
382
- return this.itemsUse;
383
- },
384
-
385
- groups() {
386
- // 如果不需要分组,则返回空数组
387
- if (!this.shouldUseGrouping) return [];
388
-
389
- // 检查filteredItems是否有值
390
- if (!this.filteredItems || this.filteredItems.length === 0) {
391
- return [];
392
- }
393
-
394
- // 用于存储每个组的项目
395
- const groupMap = {};
396
-
397
- // 使用过滤后的项目进行分组
398
- this.filteredItems.forEach(item => {
399
- let groupName = null;
400
-
401
- // 优先使用item中的group字段
402
- if (item.group) {
403
- groupName = item.group;
404
- }
405
- // 如果没有找到分组,使用未分组
406
- const finalGroupName = groupName || this.ungroupedTitle;
407
-
408
- // 若该组尚未创建,则创建一个新组
409
- if (!groupMap[finalGroupName]) {
410
- groupMap[finalGroupName] = {
411
- title: finalGroupName,
412
- key: finalGroupName,
413
- children: [],
414
- isUngrouped: !groupName, // 如果没有找到组名,则标记为未分组
415
- };
416
- }
417
-
418
- // 将项目添加到相应的组中
419
- groupMap[finalGroupName].children.push(item);
420
- });
421
-
422
- // 将分组转换为数组
423
- const groupArray = Object.values(groupMap);
424
-
425
- // 如果有自定义排序函数,使用它排序
426
- if (typeof this.groupable === 'object' && this.groupable.sort) {
427
- return groupArray.sort((a, b) => {
428
- // 确保未分组总是在最后
429
- if (a.isUngrouped) return 1;
430
- if (b.isUngrouped) return -1;
431
-
432
- const sortFn = this.groupable.sort;
433
- return sortFn ? sortFn(a.key, b.key) : 0;
434
- });
435
- }
436
-
437
- // 否则只确保未分组在最后,不做其他排序
438
- return groupArray.sort((a, b) => {
439
- // 确保未分组总是在最后
440
- if (a.isUngrouped) return 1;
441
- if (b.isUngrouped) return -1;
442
-
443
- // 不做其他排序
444
- return 0;
445
- });
446
- },
447
- },
448
-
449
- mounted() {
450
- // 如果有分组,默认将第一个分组设置为吸顶状态
451
- if (this.shouldUseGrouping && this.groups.length > 0) {
452
- // 添加第一个组的key到吸顶状态集合中
453
- this.stickyGroupKeys.add(this.groups[0].key);
454
- }
455
- },
456
-
457
- methods: {
458
- handleClick(item) {
459
- // 如果是disabled状态,则不允许选中
460
- if (item.disabled) return;
461
- this.$emit('change', item);
462
- },
463
-
464
- handleScroll(e) {
465
- // 获取滚动容器
466
- const scrollContainer = this.$refs.scrollContainer;
467
- if (!scrollContainer) return;
468
-
469
- const scrollTop = scrollContainer.scrollTop;
470
-
471
- // 显示/隐藏回到顶部按钮
472
- this.showScrollTop = scrollTop > 200;
473
-
474
- // 检查是否需要加载更多
475
- const bottomOffset = 20;
476
- const scrollHeight = scrollContainer.scrollHeight;
477
- const clientHeight = scrollContainer.clientHeight;
478
-
479
- // 计算是否接近底部
480
- const isNearBottom = scrollHeight - scrollTop - clientHeight < bottomOffset;
481
-
482
- if (isNearBottom) {
483
- this.loadMoreData();
484
- }
485
-
486
- // 更新吸顶状态
487
- this.updateStickyStatus();
488
- },
489
-
490
- updateStickyStatus() {
491
- if (!this.shouldUseGrouping || this.groups.length === 0) return;
492
-
493
- // 先清空当前的吸顶组
494
- this.stickyGroupKeys.clear();
495
-
496
- // 获取滚动容器
497
- const scrollContainer = this.$refs.scrollContainer;
498
- if (!scrollContainer) return;
499
-
500
- // 如果只有一个分组,直接设置为吸顶状态
501
- if (this.groups.length === 1) {
502
- this.stickyGroupKeys.add(this.groups[0].key);
503
- return;
504
- }
505
-
506
- const scrollContainerTop = scrollContainer.getBoundingClientRect().top;
507
- const containerHeight = scrollContainer.clientHeight;
508
- const scrollHeight = scrollContainer.scrollHeight;
509
- const scrollTop = scrollContainer.scrollTop;
510
-
511
- // 判断是否已经滚动到底部
512
- const isNearBottom = scrollHeight - scrollTop - containerHeight < 20;
513
-
514
- // 如果已接近底部,直接使最后一个分组吸顶
515
- if (isNearBottom && this.groups.length > 0) {
516
- this.stickyGroupKeys.add(this.groups[this.groups.length - 1].key);
517
- return;
518
- }
519
-
520
- // 检查每个分组的位置
521
- const visibleGroups = [];
522
-
523
- // 收集所有可见的分组
524
- for (const group of this.groups) {
525
- const groupElement = this.groupRefs[group.key];
526
- if (groupElement) {
527
- const groupRect = groupElement.getBoundingClientRect();
528
- const relativeTop = groupRect.top - scrollContainerTop;
529
-
530
- // 分组至少部分可见
531
- if (relativeTop < containerHeight && relativeTop + groupRect.height > 0) {
532
- visibleGroups.push({
533
- group,
534
- relativeTop,
535
- height: groupRect.height,
536
- });
537
- }
538
- }
539
- }
540
-
541
- // 对可见分组按相对位置排序
542
- visibleGroups.sort((a, b) => a.relativeTop - b.relativeTop);
543
-
544
- // 如果有可见分组
545
- if (visibleGroups.length > 0) {
546
- // 寻找第一个完全进入视口的分组
547
- const fullyVisibleGroup = visibleGroups.find(g => g.relativeTop >= 0);
548
-
549
- if (fullyVisibleGroup) {
550
- // 如果有完全进入视口的分组,选择它
551
- this.stickyGroupKeys.add(fullyVisibleGroup.group.key);
552
- } else {
553
- // 否则选择第一个部分可见的分组(通常是标题已经滚出但内容还可见的)
554
- this.stickyGroupKeys.add(visibleGroups[0].group.key);
555
- }
556
- } else if (this.groups.length > 0) {
557
- // 如果没有可见分组,则选择第一个分组
558
- this.stickyGroupKeys.add(this.groups[0].key);
559
- }
560
- },
561
-
562
- loadMoreData() {
563
- if (!this.loadMore) return;
564
- this.loadMore();
565
- },
566
-
567
- scrollToTop() {
568
- if (this.$refs.scrollContainer) {
569
- this.$refs.scrollContainer.scrollTop = 0;
570
- }
571
-
572
- // 确保吸顶组状态也被重置
573
- if (this.shouldUseGrouping && this.groups.length > 0) {
574
- this.stickyGroupKeys.clear();
575
- this.stickyGroupKeys.add(this.groups[0].key);
576
- }
577
- },
578
-
579
- handleMenuItemClick(command, item) {
580
- this.$emit('menuCommand', command, item);
581
- },
582
-
583
- bindGroupRef(el, item) {
584
- if (el) {
585
- this.groupRefs[item.key] = el;
586
- }
587
- },
588
- },
589
- };
590
- </script>
591
-
592
- <style lang="scss">
593
- // 引入外部样式文件
594
- @import '../../../styles/Conversations.scss';
595
-
596
- /* 自定义滚动条样式 */
597
- .el-x-conversations-scrollbar {
598
- height: 100%;
599
- overflow-y: auto;
600
-
601
- /* 隐藏默认滚动条 */
602
- &::-webkit-scrollbar {
603
- width: 6px;
604
- }
605
-
606
- &::-webkit-scrollbar-thumb {
607
- background-color: transparent;
608
- border-radius: 3px;
609
- transition: background-color 0.3s ease;
610
- }
611
-
612
- &::-webkit-scrollbar-track {
613
- background-color: transparent;
614
- }
615
-
616
- /* 鼠标悬停时显示滚动条 */
617
- &:hover {
618
- &::-webkit-scrollbar-thumb {
619
- background-color: #e0e0e0;
620
- }
621
- }
622
- }
623
-
624
- /* 为Firefox添加滚动条样式 */
625
- @supports (scrollbar-width: thin) {
626
- .el-x-conversations-scrollbar {
627
- scrollbar-width: thin;
628
- scrollbar-color: transparent transparent;
629
-
630
- &:hover {
631
- scrollbar-color: #e0e0e0 transparent;
632
- }
633
- }
634
- }
635
- </style>
1
+ <template>
2
+ <div
3
+ class="el-x-conversations-container"
4
+ :style="{
5
+ '--conversation-label-height': `${labelHeight}px`,
6
+ '--conversation-list-auto-bg-color': mergedStyle.backgroundColor,
7
+ }"
8
+ >
9
+ <slot name="header"></slot>
10
+ <ul
11
+ class="el-x-conversations-list"
12
+ :style="mergedStyle"
13
+ >
14
+ <!-- 滚动区域容器 -->
15
+ <li class="el-x-conversations-scroll-wrapper">
16
+ <div
17
+ ref="scrollContainer"
18
+ class="el-x-conversations-scrollbar"
19
+ @scroll="handleScroll"
20
+ >
21
+ <div class="scroll-content">
22
+ <template v-if="shouldUseGrouping">
23
+ <!-- 分组显示 -->
24
+ <div
25
+ v-for="group in groups"
26
+ :key="group.key"
27
+ :ref="el => bindGroupRef(el, group)"
28
+ class="el-x-conversation-group"
29
+ >
30
+ <div
31
+ class="el-x-conversation-group-title sticky-title"
32
+ :class="{ 'active-sticky': stickyGroupKeys.has(group.key) }"
33
+ >
34
+ <slot
35
+ name="group-title"
36
+ :group="group"
37
+ >
38
+ {{ group.title }}
39
+ </slot>
40
+ </div>
41
+ <div class="el-x-conversation-group-items">
42
+ <conversations-item
43
+ v-for="item in group.children"
44
+ :key="item.uniqueKey"
45
+ :item="item"
46
+ :active="item.uniqueKey === active"
47
+ :items-style="itemsStyle"
48
+ :items-hover-style="itemsHoverStyle"
49
+ :items-active-style="itemsActiveStyle"
50
+ :items-menu-opened-style="itemsMenuOpenedStyle"
51
+ :prefix-icon="item.prefixIcon"
52
+ :show-tooltip="showTooltip"
53
+ :tooltip-placement="tooltipPlacement"
54
+ :tooltip-offset="tooltipOffset"
55
+ :suffix-icon="item.suffixIcon"
56
+ :active-key="active || ''"
57
+ :label-max-width="labelMaxWidth"
58
+ :menu="menu"
59
+ :show-built-in-menu="showBuiltInMenu"
60
+ :menu-placement="menuPlacement"
61
+ :menu-style="menuStyle"
62
+ :menu-show-arrow="menuShowArrow"
63
+ @click="handleClick(item)"
64
+ @menu-command="handleMenuItemClick"
65
+ >
66
+ <!-- 传递插槽 -->
67
+ <template
68
+ v-if="$scopedSlots.label"
69
+ #label
70
+ >
71
+ <slot
72
+ name="label"
73
+ :item="item"
74
+ ></slot>
75
+ </template>
76
+
77
+ <template
78
+ v-if="$scopedSlots['more-filled']"
79
+ #more-filled="moreFilledSoltProps"
80
+ >
81
+ <slot
82
+ name="more-filled"
83
+ v-bind="moreFilledSoltProps"
84
+ ></slot>
85
+ </template>
86
+
87
+ <template
88
+ v-if="$scopedSlots.menu"
89
+ #menu
90
+ >
91
+ <slot
92
+ name="menu"
93
+ :item="item"
94
+ ></slot>
95
+ </template>
96
+ </conversations-item>
97
+ </div>
98
+ </div>
99
+ </template>
100
+
101
+ <template v-else>
102
+ <conversations-item
103
+ v-for="item in filteredItems"
104
+ :key="item.uniqueKey"
105
+ :item="item"
106
+ :items-style="itemsStyle"
107
+ :items-hover-style="itemsHoverStyle"
108
+ :items-active-style="itemsActiveStyle"
109
+ :active="item.uniqueKey === active"
110
+ :items-menu-opened-style="itemsMenuOpenedStyle"
111
+ :prefix-icon="item.prefixIcon"
112
+ :show-tooltip="showTooltip"
113
+ :tooltip-placement="tooltipPlacement"
114
+ :tooltip-offset="tooltipOffset"
115
+ :suffix-icon="item.suffixIcon"
116
+ :active-key="active || ''"
117
+ :label-max-width="labelMaxWidth"
118
+ :menu="menu"
119
+ :show-built-in-menu="showBuiltInMenu"
120
+ :menu-placement="menuPlacement"
121
+ :menu-style="menuStyle"
122
+ :menu-show-arrow="menuShowArrow"
123
+ @click="handleClick(item)"
124
+ @menu-command="handleMenuItemClick"
125
+ >
126
+ <!-- 传递插槽 -->
127
+ <template
128
+ v-if="$scopedSlots.label"
129
+ #label
130
+ >
131
+ <slot
132
+ name="label"
133
+ :item="item"
134
+ ></slot>
135
+ </template>
136
+
137
+ <template
138
+ v-if="$scopedSlots['more-filled']"
139
+ #more-filled="moreFilledSoltProps"
140
+ >
141
+ <slot
142
+ name="more-filled"
143
+ v-bind="moreFilledSoltProps"
144
+ ></slot>
145
+ </template>
146
+
147
+ <template
148
+ v-if="$scopedSlots.menu"
149
+ #menu
150
+ >
151
+ <slot
152
+ name="menu"
153
+ :item="item"
154
+ ></slot>
155
+ </template>
156
+ </conversations-item>
157
+ </template>
158
+
159
+ <!-- 加载更多 -->
160
+ <div
161
+ v-if="loadMoreLoading"
162
+ class="el-x-conversations-load-more"
163
+ >
164
+ <slot name="load-more">
165
+ <i class="el-icon-loading el-x-conversations-load-more-is-loading"></i>
166
+ <span>加载更多...</span>
167
+ </slot>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </li>
172
+ </ul>
173
+ <slot name="footer"></slot>
174
+ <!-- 滚动到顶部按钮 -->
175
+ <el-button
176
+ v-show="showScrollTop && showToTopBtn"
177
+ class="scroll-to-top-btn"
178
+ size="small"
179
+ circle
180
+ :type="toTopBtnType"
181
+ :style="toTopBtnStyle"
182
+ @click="scrollToTop"
183
+ >
184
+ <i class="el-icon-top"></i>
185
+ </el-button>
186
+ </div>
187
+ </template>
188
+
189
+ <script>
190
+ import { get } from 'lodash';
191
+ import ConversationsItem from './components/item.vue';
192
+
193
+ export default {
194
+ name: 'ElXConversations',
195
+
196
+ components: {
197
+ ConversationsItem,
198
+ },
199
+
200
+ props: {
201
+ items: {
202
+ type: Array,
203
+ default: () => [],
204
+ },
205
+ itemsStyle: {
206
+ type: Object,
207
+ default: () => ({}),
208
+ },
209
+ itemsHoverStyle: {
210
+ type: Object,
211
+ default: () => ({}),
212
+ },
213
+ itemsActiveStyle: {
214
+ type: Object,
215
+ default: () => ({}),
216
+ },
217
+ itemsMenuOpenedStyle: {
218
+ type: Object,
219
+ default: () => ({}),
220
+ },
221
+ styleConfig: {
222
+ type: Object,
223
+ default: () => ({}),
224
+ },
225
+ showTooltip: {
226
+ type: Boolean,
227
+ default: false,
228
+ },
229
+ groupable: {
230
+ type: [Boolean, Object],
231
+ default: false,
232
+ },
233
+ labelMaxWidth: {
234
+ type: Number,
235
+ default: undefined,
236
+ },
237
+ labelHeight: {
238
+ type: Number,
239
+ default: 20,
240
+ },
241
+ showBuiltInMenu: {
242
+ type: Boolean,
243
+ default: false,
244
+ },
245
+ menu: {
246
+ type: Array,
247
+ default: () => [
248
+ {
249
+ label: '重命名',
250
+ key: 'rename',
251
+ icon: 'el-icon-edit',
252
+ command: 'rename',
253
+ },
254
+ {
255
+ label: '删除',
256
+ key: 'delete',
257
+ icon: 'el-icon-delete',
258
+ command: 'delete',
259
+ menuItemHoverStyle: {
260
+ color: 'red',
261
+ backgroundColor: 'rgba(255, 0, 0, 0.1)',
262
+ },
263
+ },
264
+ ],
265
+ },
266
+ ungroupedTitle: {
267
+ type: String,
268
+ default: '未分组',
269
+ },
270
+ tooltipPlacement: {
271
+ type: String,
272
+ default: 'top',
273
+ },
274
+ tooltipOffset: {
275
+ type: Number,
276
+ default: 12,
277
+ },
278
+ menuPlacement: {
279
+ type: String,
280
+ default: 'bottom-start',
281
+ },
282
+
283
+ menuShowArrow: {
284
+ type: Boolean,
285
+ default: false,
286
+ },
287
+ menuClassName: {
288
+ type: String,
289
+ default: '',
290
+ },
291
+
292
+ menuStyle: {
293
+ type: Object,
294
+ default: () => ({}),
295
+ },
296
+
297
+ loadMoreLoading: {
298
+ type: Boolean,
299
+ default: false,
300
+ },
301
+ showToTopBtn: {
302
+ type: Boolean,
303
+ default: false,
304
+ },
305
+ toTopBtnType: {
306
+ type: String,
307
+ default: 'primary',
308
+ validator: value =>
309
+ ['primary', 'success', 'warning', 'danger', 'info', 'text'].includes(value),
310
+ },
311
+ toTopBtnStyle: {
312
+ type: Object,
313
+ default: () => ({}),
314
+ },
315
+ labelKey: {
316
+ type: String,
317
+ default: 'label',
318
+ },
319
+ rowKey: {
320
+ type: String,
321
+ default: 'id',
322
+ },
323
+ active: {
324
+ type: [String, Number, Boolean],
325
+ default: '',
326
+ },
327
+ loadMore: {
328
+ type: Function,
329
+ default: null,
330
+ },
331
+ },
332
+
333
+ data() {
334
+ return {
335
+ showScrollTop: false,
336
+ groupRefs: {},
337
+ stickyGroupKeys: new Set(),
338
+ };
339
+ },
340
+
341
+ computed: {
342
+ itemsUse() {
343
+ return this.items.map((item, index) => ({
344
+ ...item,
345
+ uniqueKey: this.rowKey ? get(item, this.rowKey) : index.toString(),
346
+ label: get(item, this.labelKey),
347
+ }));
348
+ },
349
+
350
+ mergedStyle() {
351
+ const defaultStyle = {
352
+ padding: '10px 0 10px 10px',
353
+ backgroundColor: '#fff',
354
+ borderRadius: '8px',
355
+ width: '280px',
356
+ height: '0',
357
+ };
358
+ return {
359
+ ...defaultStyle,
360
+ ...this.styleConfig,
361
+ };
362
+ },
363
+
364
+ shouldUseGrouping() {
365
+ return !!this.groupable;
366
+ },
367
+
368
+ filteredItems() {
369
+ return this.itemsUse;
370
+ },
371
+
372
+ groups() {
373
+ // 如果不需要分组,则返回空数组
374
+ if (!this.shouldUseGrouping) return [];
375
+
376
+ // 检查filteredItems是否有值
377
+ if (!this.filteredItems || this.filteredItems.length === 0) {
378
+ return [];
379
+ }
380
+
381
+ // 用于存储每个组的项目
382
+ const groupMap = {};
383
+
384
+ // 使用过滤后的项目进行分组
385
+ this.filteredItems.forEach(item => {
386
+ let groupName = null;
387
+
388
+ // 优先使用item中的group字段
389
+ if (item.group) {
390
+ groupName = item.group;
391
+ }
392
+ // 如果没有找到分组,使用未分组
393
+ const finalGroupName = groupName || this.ungroupedTitle;
394
+
395
+ // 若该组尚未创建,则创建一个新组
396
+ if (!groupMap[finalGroupName]) {
397
+ groupMap[finalGroupName] = {
398
+ title: finalGroupName,
399
+ key: finalGroupName,
400
+ children: [],
401
+ isUngrouped: !groupName, // 如果没有找到组名,则标记为未分组
402
+ };
403
+ }
404
+
405
+ // 将项目添加到相应的组中
406
+ groupMap[finalGroupName].children.push(item);
407
+ });
408
+
409
+ // 将分组转换为数组
410
+ const groupArray = Object.values(groupMap);
411
+
412
+ // 如果有自定义排序函数,使用它排序
413
+ if (typeof this.groupable === 'object' && this.groupable.sort) {
414
+ return groupArray.sort((a, b) => {
415
+ // 确保未分组总是在最后
416
+ if (a.isUngrouped) return 1;
417
+ if (b.isUngrouped) return -1;
418
+
419
+ const sortFn = this.groupable.sort;
420
+ return sortFn ? sortFn(a.key, b.key) : 0;
421
+ });
422
+ }
423
+
424
+ // 否则只确保未分组在最后,不做其他排序
425
+ return groupArray.sort((a, b) => {
426
+ // 确保未分组总是在最后
427
+ if (a.isUngrouped) return 1;
428
+ if (b.isUngrouped) return -1;
429
+
430
+ // 不做其他排序
431
+ return 0;
432
+ });
433
+ },
434
+ },
435
+
436
+ mounted() {
437
+ // 如果有分组,默认将第一个分组设置为吸顶状态
438
+ if (this.shouldUseGrouping && this.groups.length > 0) {
439
+ // 添加第一个组的key到吸顶状态集合中
440
+ this.stickyGroupKeys.add(this.groups[0].key);
441
+ }
442
+ },
443
+
444
+ methods: {
445
+ handleClick(item) {
446
+ // 如果是disabled状态,则不允许选中
447
+ if (item.disabled) return;
448
+ this.$emit('change', item);
449
+ },
450
+
451
+ handleScroll(e) {
452
+ // 获取滚动容器
453
+ const scrollContainer = this.$refs.scrollContainer;
454
+ if (!scrollContainer) return;
455
+
456
+ const scrollTop = scrollContainer.scrollTop;
457
+
458
+ // 显示/隐藏回到顶部按钮
459
+ this.showScrollTop = scrollTop > 200;
460
+
461
+ // 检查是否需要加载更多
462
+ const bottomOffset = 20;
463
+ const scrollHeight = scrollContainer.scrollHeight;
464
+ const clientHeight = scrollContainer.clientHeight;
465
+
466
+ // 计算是否接近底部
467
+ const isNearBottom = scrollHeight - scrollTop - clientHeight < bottomOffset;
468
+
469
+ if (isNearBottom) {
470
+ this.loadMoreData();
471
+ }
472
+
473
+ // 更新吸顶状态
474
+ this.updateStickyStatus();
475
+ },
476
+
477
+ updateStickyStatus() {
478
+ if (!this.shouldUseGrouping || this.groups.length === 0) return;
479
+
480
+ // 先清空当前的吸顶组
481
+ this.stickyGroupKeys.clear();
482
+
483
+ // 获取滚动容器
484
+ const scrollContainer = this.$refs.scrollContainer;
485
+ if (!scrollContainer) return;
486
+
487
+ // 如果只有一个分组,直接设置为吸顶状态
488
+ if (this.groups.length === 1) {
489
+ this.stickyGroupKeys.add(this.groups[0].key);
490
+ return;
491
+ }
492
+
493
+ const scrollContainerTop = scrollContainer.getBoundingClientRect().top;
494
+ const containerHeight = scrollContainer.clientHeight;
495
+ const scrollHeight = scrollContainer.scrollHeight;
496
+ const scrollTop = scrollContainer.scrollTop;
497
+
498
+ // 判断是否已经滚动到底部
499
+ const isNearBottom = scrollHeight - scrollTop - containerHeight < 20;
500
+
501
+ // 如果已接近底部,直接使最后一个分组吸顶
502
+ if (isNearBottom && this.groups.length > 0) {
503
+ this.stickyGroupKeys.add(this.groups[this.groups.length - 1].key);
504
+ return;
505
+ }
506
+
507
+ // 检查每个分组的位置
508
+ const visibleGroups = [];
509
+
510
+ // 收集所有可见的分组
511
+ for (const group of this.groups) {
512
+ const groupElement = this.groupRefs[group.key];
513
+ if (groupElement) {
514
+ const groupRect = groupElement.getBoundingClientRect();
515
+ const relativeTop = groupRect.top - scrollContainerTop;
516
+
517
+ // 分组至少部分可见
518
+ if (relativeTop < containerHeight && relativeTop + groupRect.height > 0) {
519
+ visibleGroups.push({
520
+ group,
521
+ relativeTop,
522
+ height: groupRect.height,
523
+ });
524
+ }
525
+ }
526
+ }
527
+
528
+ // 对可见分组按相对位置排序
529
+ visibleGroups.sort((a, b) => a.relativeTop - b.relativeTop);
530
+
531
+ // 如果有可见分组
532
+ if (visibleGroups.length > 0) {
533
+ // 寻找第一个完全进入视口的分组
534
+ const fullyVisibleGroup = visibleGroups.find(g => g.relativeTop >= 0);
535
+
536
+ if (fullyVisibleGroup) {
537
+ // 如果有完全进入视口的分组,选择它
538
+ this.stickyGroupKeys.add(fullyVisibleGroup.group.key);
539
+ } else {
540
+ // 否则选择第一个部分可见的分组(通常是标题已经滚出但内容还可见的)
541
+ this.stickyGroupKeys.add(visibleGroups[0].group.key);
542
+ }
543
+ } else if (this.groups.length > 0) {
544
+ // 如果没有可见分组,则选择第一个分组
545
+ this.stickyGroupKeys.add(this.groups[0].key);
546
+ }
547
+ },
548
+
549
+ loadMoreData() {
550
+ if (!this.loadMore) return;
551
+ this.loadMore();
552
+ },
553
+
554
+ scrollToTop() {
555
+ if (this.$refs.scrollContainer) {
556
+ this.$refs.scrollContainer.scrollTop = 0;
557
+ }
558
+
559
+ // 确保吸顶组状态也被重置
560
+ if (this.shouldUseGrouping && this.groups.length > 0) {
561
+ this.stickyGroupKeys.clear();
562
+ this.stickyGroupKeys.add(this.groups[0].key);
563
+ }
564
+ },
565
+
566
+ handleMenuItemClick(command, item) {
567
+ this.$emit('menu-command', command, item);
568
+ },
569
+
570
+ bindGroupRef(el, item) {
571
+ if (el) {
572
+ this.groupRefs[item.key] = el;
573
+ }
574
+ },
575
+ },
576
+ };
577
+ </script>
578
+
579
+ <style lang="scss">
580
+ // 引入外部样式文件
581
+ @import '../../../styles/Conversations.scss';
582
+
583
+ /* 自定义滚动条样式 */
584
+ .el-x-conversations-scrollbar {
585
+ height: 100%;
586
+ overflow-y: auto;
587
+
588
+ /* 隐藏默认滚动条 */
589
+ &::-webkit-scrollbar {
590
+ width: 6px;
591
+ }
592
+
593
+ &::-webkit-scrollbar-thumb {
594
+ background-color: transparent;
595
+ border-radius: 3px;
596
+ transition: background-color 0.3s ease;
597
+ }
598
+
599
+ &::-webkit-scrollbar-track {
600
+ background-color: transparent;
601
+ }
602
+
603
+ /* 鼠标悬停时显示滚动条 */
604
+ &:hover {
605
+ &::-webkit-scrollbar-thumb {
606
+ background-color: #e0e0e0;
607
+ }
608
+ }
609
+ }
610
+
611
+ /* 为Firefox添加滚动条样式 */
612
+ @supports (scrollbar-width: thin) {
613
+ .el-x-conversations-scrollbar {
614
+ scrollbar-width: thin;
615
+ scrollbar-color: transparent transparent;
616
+
617
+ &:hover {
618
+ scrollbar-color: #e0e0e0 transparent;
619
+ }
620
+ }
621
+ }
622
+ </style>