vue-element-ui-x 0.1.5 → 0.1.7-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 (92) hide show
  1. package/lib/attachments.js +3082 -0
  2. package/lib/bubble-list.js +13840 -0
  3. package/lib/bubble.js +13125 -0
  4. package/lib/components/Attachments/index.js +6 -6
  5. package/lib/components/Bubble/index.js +188 -192
  6. package/lib/components/BubbleList/index.js +189 -194
  7. package/lib/components/Conversations/index.js +6 -6
  8. package/lib/components/FilesCard/index.js +5 -5
  9. package/lib/components/Prompts/index.js +21 -21
  10. package/lib/components/Sender/index.js +4 -4
  11. package/lib/components/Think/index.js +1 -1
  12. package/lib/components/Thinking/index.js +1 -1
  13. package/lib/components/ThoughtChain/index.js +186 -191
  14. package/lib/components/Typewriter/index.js +182 -186
  15. package/lib/components/Welcome/index.js +1 -1
  16. package/lib/conversations.js +18825 -0
  17. package/lib/files-card.js +2471 -0
  18. package/lib/index.common.js +1 -1
  19. package/lib/index.esm.js +1 -1
  20. package/lib/index.js +1707 -1691
  21. package/lib/index.umd.js +1 -1
  22. package/lib/mixins/index.js +2 -2
  23. package/lib/mixins.js +1016 -0
  24. package/lib/prompts.js +832 -0
  25. package/lib/sender.js +1911 -0
  26. package/lib/think.js +799 -0
  27. package/lib/thinking.js +809 -0
  28. package/lib/thought-chain.js +30391 -0
  29. package/lib/typewriter.js +12788 -0
  30. package/lib/welcome.js +755 -0
  31. package/package.json +3 -4
  32. package/src/components/Attachments/index.js +8 -8
  33. package/src/components/Bubble/index.js +6 -6
  34. package/src/components/Bubble/src/main.vue +299 -299
  35. package/src/components/BubbleList/index.js +8 -8
  36. package/src/components/BubbleList/src/loading.vue +75 -75
  37. package/src/components/BubbleList/src/main.vue +466 -466
  38. package/src/components/Conversations/index.js +8 -8
  39. package/src/components/Conversations/src/main.vue +635 -635
  40. package/src/components/FilesCard/index.js +8 -8
  41. package/src/components/FilesCard/src/fileSvg/audio.vue +38 -38
  42. package/src/components/FilesCard/src/fileSvg/code.vue +35 -35
  43. package/src/components/FilesCard/src/fileSvg/database.vue +94 -94
  44. package/src/components/FilesCard/src/fileSvg/excel.vue +38 -38
  45. package/src/components/FilesCard/src/fileSvg/file.vue +40 -40
  46. package/src/components/FilesCard/src/fileSvg/image.vue +40 -40
  47. package/src/components/FilesCard/src/fileSvg/index.js +46 -46
  48. package/src/components/FilesCard/src/fileSvg/link.vue +54 -54
  49. package/src/components/FilesCard/src/fileSvg/mark.vue +38 -38
  50. package/src/components/FilesCard/src/fileSvg/pdf.vue +38 -38
  51. package/src/components/FilesCard/src/fileSvg/ppt.vue +38 -38
  52. package/src/components/FilesCard/src/fileSvg/three.vue +38 -38
  53. package/src/components/FilesCard/src/fileSvg/txt.vue +38 -38
  54. package/src/components/FilesCard/src/fileSvg/unknown.vue +54 -54
  55. package/src/components/FilesCard/src/fileSvg/video.vue +38 -38
  56. package/src/components/FilesCard/src/fileSvg/word.vue +38 -38
  57. package/src/components/FilesCard/src/fileSvg/zip.vue +38 -38
  58. package/src/components/FilesCard/src/options.js +18 -18
  59. package/src/components/Prompts/index.js +8 -8
  60. package/src/components/Prompts/src/main.vue +248 -248
  61. package/src/components/Sender/index.js +8 -8
  62. package/src/components/Sender/src/components/ClearButton.vue +28 -28
  63. package/src/components/Sender/src/components/Loading.vue +53 -53
  64. package/src/components/Sender/src/components/LoadingButton.vue +39 -39
  65. package/src/components/Sender/src/components/SendButton.vue +26 -26
  66. package/src/components/Sender/src/components/SpeechButton.vue +24 -24
  67. package/src/components/Sender/src/components/SpeechLoading.vue +87 -87
  68. package/src/components/Sender/src/components/SpeechLoadingButton.vue +43 -43
  69. package/src/components/Think/index.js +8 -8
  70. package/src/components/Think/src/main.vue +190 -190
  71. package/src/components/Thinking/index.js +8 -8
  72. package/src/components/Thinking/src/main.vue +195 -195
  73. package/src/components/ThoughtChain/index.js +8 -8
  74. package/src/components/ThoughtChain/src/main.vue +293 -293
  75. package/src/components/Typewriter/index.js +8 -8
  76. package/src/components/Welcome/index.js +8 -8
  77. package/src/components/Welcome/src/main.vue +151 -151
  78. package/src/index.js +23 -3
  79. package/src/styles/Attachments.scss +236 -236
  80. package/src/styles/Bubble.scss +157 -157
  81. package/src/styles/BubbleList.scss +148 -148
  82. package/src/styles/Conversations.scss +260 -260
  83. package/src/styles/FilesCard.scss +221 -221
  84. package/src/styles/Prompts.scss +195 -195
  85. package/src/styles/Sender.scss +199 -199
  86. package/src/styles/Think.scss +134 -134
  87. package/src/styles/Thinking.scss +112 -112
  88. package/src/styles/ThoughtChain.scss +113 -113
  89. package/src/styles/Typewriter.scss +66 -66
  90. package/src/styles/button.scss +302 -0
  91. package/src/styles/var.scss +1052 -0
  92. package/src/theme/var.scss +72 -72
@@ -1,635 +1,635 @@
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="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>