vue-element-ui-x 0.1.9-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 (80) hide show
  1. package/lib/components/Attachments/index.js +2 -2
  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 +27 -64
  5. package/lib/components/FilesCard/index.js +1 -1
  6. package/lib/components/Prompts/index.js +21 -21
  7. package/lib/components/Sender/index.js +4 -4
  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 +47 -34
  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 +220 -160
  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/Bubble/index.js +6 -6
  21. package/src/components/Bubble/src/main.vue +299 -299
  22. package/src/components/BubbleList/index.js +8 -8
  23. package/src/components/BubbleList/src/loading.vue +75 -75
  24. package/src/components/BubbleList/src/main.vue +461 -466
  25. package/src/components/Conversations/index.js +8 -8
  26. package/src/components/Conversations/src/components/item.vue +13 -34
  27. package/src/components/Conversations/src/main.vue +4 -18
  28. package/src/components/FilesCard/index.js +8 -8
  29. package/src/components/FilesCard/src/fileSvg/audio.vue +38 -38
  30. package/src/components/FilesCard/src/fileSvg/code.vue +35 -35
  31. package/src/components/FilesCard/src/fileSvg/database.vue +94 -94
  32. package/src/components/FilesCard/src/fileSvg/excel.vue +38 -38
  33. package/src/components/FilesCard/src/fileSvg/file.vue +40 -40
  34. package/src/components/FilesCard/src/fileSvg/image.vue +40 -40
  35. package/src/components/FilesCard/src/fileSvg/index.js +46 -46
  36. package/src/components/FilesCard/src/fileSvg/link.vue +54 -54
  37. package/src/components/FilesCard/src/fileSvg/mark.vue +38 -38
  38. package/src/components/FilesCard/src/fileSvg/pdf.vue +38 -38
  39. package/src/components/FilesCard/src/fileSvg/ppt.vue +38 -38
  40. package/src/components/FilesCard/src/fileSvg/three.vue +38 -38
  41. package/src/components/FilesCard/src/fileSvg/txt.vue +38 -38
  42. package/src/components/FilesCard/src/fileSvg/unknown.vue +54 -54
  43. package/src/components/FilesCard/src/fileSvg/video.vue +38 -38
  44. package/src/components/FilesCard/src/fileSvg/word.vue +38 -38
  45. package/src/components/FilesCard/src/fileSvg/zip.vue +38 -38
  46. package/src/components/FilesCard/src/options.js +18 -18
  47. package/src/components/Prompts/index.js +8 -8
  48. package/src/components/Prompts/src/main.vue +248 -248
  49. package/src/components/Sender/index.js +8 -8
  50. package/src/components/Sender/src/components/ClearButton.vue +28 -28
  51. package/src/components/Sender/src/components/Loading.vue +53 -53
  52. package/src/components/Sender/src/components/LoadingButton.vue +39 -39
  53. package/src/components/Sender/src/components/SendButton.vue +26 -26
  54. package/src/components/Sender/src/components/SpeechButton.vue +24 -24
  55. package/src/components/Sender/src/components/SpeechLoading.vue +87 -87
  56. package/src/components/Sender/src/components/SpeechLoadingButton.vue +43 -43
  57. package/src/components/Think/index.js +8 -8
  58. package/src/components/Think/src/main.vue +190 -190
  59. package/src/components/Thinking/index.js +8 -8
  60. package/src/components/ThoughtChain/index.js +8 -8
  61. package/src/components/Typewriter/index.js +8 -8
  62. package/src/components/Typewriter/src/main.vue +10 -2
  63. package/src/components/Welcome/index.js +8 -8
  64. package/src/components/Welcome/src/main.vue +151 -151
  65. package/src/mixins/recordMixin.js +0 -1
  66. package/src/mixins/sendMixin.js +104 -11
  67. package/src/mixins/streamMixin.js +3 -5
  68. package/src/styles/Attachments.scss +236 -236
  69. package/src/styles/Bubble.scss +157 -157
  70. package/src/styles/BubbleList.scss +148 -148
  71. package/src/styles/Conversations.scss +260 -260
  72. package/src/styles/FilesCard.scss +221 -221
  73. package/src/styles/Prompts.scss +195 -195
  74. package/src/styles/Sender.scss +199 -199
  75. package/src/styles/Think.scss +134 -134
  76. package/src/styles/ThoughtChain.scss +113 -113
  77. package/src/styles/Typewriter.scss +66 -66
  78. package/src/theme/var.scss +72 -72
  79. package/src/styles/button.scss +0 -302
  80. package/src/styles/var.scss +0 -1052
@@ -1,466 +1,461 @@
1
- <template>
2
- <div
3
- ref="scrollContainer"
4
- class="el-x-bubble-list"
5
- :class="{ 'always-scrollbar': alwaysShowScrollbar }"
6
- :style="{
7
- '--el-x-bubble-list-max-height': `${maxHeight}`,
8
- '--el-x-bubble-list-btn-size': `${btnIconSize}px`,
9
- }"
10
- @scroll="handleScroll"
11
- >
12
- <Bubble
13
- v-for="(item, index) in list"
14
- :key="index"
15
- :content="item.content"
16
- :placement="defaultPlacement || item.placement"
17
- :loading="defaultLoading !== undefined ? defaultLoading : item.loading"
18
- :shape="defaultShape || item.shape"
19
- :variant="defaultVariant || item.variant"
20
- :is-markdown="defaultIsMarkdown !== undefined ? defaultIsMarkdown : item.isMarkdown"
21
- :is-fog="
22
- defaultPlacement === 'start' || item.placement === 'start'
23
- ? defaultIsFog !== undefined
24
- ? defaultIsFog
25
- : item.isFog
26
- : false
27
- "
28
- :typing="
29
- defaultPlacement === 'start' || item.placement === 'start'
30
- ? defaultTyping !== undefined
31
- ? defaultTyping
32
- : item.typing
33
- : false
34
- "
35
- :max-width="defaultMaxWidth || item.maxWidth"
36
- :avatar="defaultAvatar || item.avatar"
37
- :avatar-size="defaultAvatarSize || item.avatarSize"
38
- :avatar-gap="defaultAvatarGap || item.avatarGap"
39
- :avatar-shape="defaultAvatarShape || item.avatarShape"
40
- :avatar-icon="defaultAvatarIcon || item.avatarIcon"
41
- :avatar-src-set="defaultAvatarSrcSet || item.avatarSrcSet"
42
- :avatar-alt="defaultAvatarAlt || item.avatarAlt"
43
- :avatar-fit="defaultAvatarFit || item.avatarFit"
44
- :no-style="defaultNoStyle !== undefined ? defaultNoStyle : item.noStyle"
45
- @finish="instance => handleBubbleComplete(index, instance)"
46
- >
47
- <template slot="avatar">
48
- <slot
49
- name="avatar"
50
- :item="item"
51
- >
52
- <template v-if="defaultAvatar || item.avatar">
53
- <el-avatar
54
- :size="defaultAvatarSize || item.avatarSize || 40"
55
- :src="defaultAvatar || item.avatar"
56
- :shape="defaultAvatarShape || item.avatarShape || 'circle'"
57
- :icon="defaultAvatarIcon || item.avatarIcon"
58
- :src-set="defaultAvatarSrcSet || item.avatarSrcSet"
59
- :alt="defaultAvatarAlt || item.avatarAlt"
60
- :fit="defaultAvatarFit || item.avatarFit || 'cover'"
61
- />
62
- </template>
63
- </slot>
64
- </template>
65
- <template
66
- v-if="$scopedSlots.header || $slots.header"
67
- slot="header"
68
- >
69
- <slot
70
- name="header"
71
- :item="item"
72
- />
73
- </template>
74
- <template
75
- v-if="$scopedSlots.content || $slots.content"
76
- slot="content"
77
- >
78
- <slot
79
- name="content"
80
- :item="item"
81
- />
82
- </template>
83
- <template
84
- v-if="$scopedSlots.footer || $slots.footer"
85
- slot="footer"
86
- >
87
- <slot
88
- name="footer"
89
- :item="item"
90
- />
91
- </template>
92
- <template
93
- v-if="$scopedSlots.loading || $slots.loading"
94
- slot="loading"
95
- >
96
- <slot
97
- name="loading"
98
- :item="item"
99
- />
100
- </template>
101
- </Bubble>
102
-
103
- <!-- 保持原有的返回底部按钮代码不变 -->
104
- <div
105
- v-if="showBackToBottom && hasVertical"
106
- class="el-x-bubble-list-default-back-button"
107
- :class="{
108
- 'el-x-bubble-list-back-to-bottom-solt': $scopedSlots.backToBottom || $slots.backToBottom,
109
- }"
110
- :style="{
111
- bottom: backButtonPosition.bottom,
112
- left: backButtonPosition.left,
113
- }"
114
- @click="scrollToBottom"
115
- >
116
- <slot name="backToBottom">
117
- <i
118
- class="el-icon-bottom el-x-bubble-list-back-to-bottom-icon"
119
- :style="{ color: btnColor }"
120
- ></i>
121
- <loadingBg
122
- v-if="btnLoading"
123
- class="back-to-bottom-loading-svg-bg"
124
- :style="{ color: btnColor }"
125
- />
126
- </slot>
127
- </div>
128
- </div>
129
- </template>
130
-
131
- <script>
132
- import Bubble from '../../Bubble/index.js';
133
- import loadingBg from './loading.vue';
134
- import createScrollDetector from '../../../utils/scrollDetector';
135
-
136
- export default {
137
- name: 'ElXBubbleList',
138
- components: {
139
- Bubble,
140
- loadingBg,
141
- },
142
- props: {
143
- list: {
144
- type: Array,
145
- default: () => [],
146
- },
147
- maxHeight: {
148
- type: String,
149
- default: '500px',
150
- },
151
- triggerIndices: {
152
- type: [String, Array],
153
- default: 'only-last',
154
- },
155
- alwaysShowScrollbar: {
156
- type: Boolean,
157
- default: false,
158
- },
159
- backButtonThreshold: {
160
- type: Number,
161
- default: 80,
162
- },
163
- showBackButton: {
164
- type: Boolean,
165
- default: true,
166
- },
167
- backButtonPosition: {
168
- type: Object,
169
- default: () => ({
170
- bottom: '20px',
171
- left: 'calc(50% - 19px)',
172
- }),
173
- },
174
- btnLoading: {
175
- type: Boolean,
176
- default: true,
177
- },
178
- btnColor: {
179
- type: String,
180
- default: '#409EFF',
181
- },
182
- btnIconSize: {
183
- type: Number,
184
- default: 24,
185
- },
186
- // 新增全局默认属性
187
- defaultPlacement: {
188
- type: String,
189
- default: '',
190
- },
191
- defaultLoading: {
192
- type: Boolean,
193
- default: undefined,
194
- },
195
- defaultShape: {
196
- type: String,
197
- default: '',
198
- },
199
- defaultVariant: {
200
- type: String,
201
- default: '',
202
- },
203
- defaultIsMarkdown: {
204
- type: Boolean,
205
- default: true,
206
- },
207
- defaultIsFog: {
208
- type: Boolean,
209
- default: false,
210
- },
211
- defaultTyping: {
212
- type: [Boolean, Object],
213
- default: undefined,
214
- },
215
- defaultMaxWidth: {
216
- type: String,
217
- default: '',
218
- },
219
- defaultAvatar: {
220
- type: String,
221
- default: '',
222
- },
223
- defaultAvatarSize: {
224
- type: Number,
225
- default: undefined,
226
- },
227
- defaultAvatarGap: {
228
- type: Number,
229
- default: undefined,
230
- },
231
- defaultAvatarShape: {
232
- type: String,
233
- default: '',
234
- },
235
- defaultAvatarIcon: {
236
- type: String,
237
- default: '',
238
- },
239
- defaultAvatarSrcSet: {
240
- type: String,
241
- default: '',
242
- },
243
- defaultAvatarAlt: {
244
- type: String,
245
- default: '',
246
- },
247
- defaultAvatarFit: {
248
- type: String,
249
- default: '',
250
- },
251
- defaultNoStyle: {
252
- type: Boolean,
253
- default: undefined,
254
- },
255
- },
256
- data() {
257
- return {
258
- scrollContainer: null,
259
- stopAutoScrollToBottom: false,
260
- lastScrollTop: 0,
261
- accumulatedScrollUpDistance: 0,
262
- resizeObserver: null,
263
- containerResizeObserver: null, // 新增容器尺寸监听器引用
264
- showBackToBottom: false,
265
- hasVertical: false,
266
- backButtonTimer: null, // 新增计时器变量
267
- };
268
- },
269
- computed: {
270
- effectiveTriggerIndices() {
271
- if (this.triggerIndices === 'only-last') {
272
- const triggerIndices = this.list.filter(item => item.typing).map((_, index) => index);
273
- return triggerIndices.length > 0 ? [triggerIndices[triggerIndices.length - 1]] : [];
274
- } else if (this.triggerIndices === 'all') {
275
- return this.list.map((_, index) => index);
276
- } else if (Array.isArray(this.triggerIndices)) {
277
- const validIndices = this.getValidIndices(this.list, this.triggerIndices);
278
- return validIndices.length > 0 ? [validIndices[validIndices.length - 1]] : [];
279
- }
280
- return [];
281
- },
282
- },
283
- watch: {
284
- list: {
285
- handler() {
286
- if (this.list && this.list.length > 0) {
287
- this.$nextTick(() => {
288
- this.autoScroll();
289
- });
290
- }
291
- },
292
- immediate: true,
293
- },
294
- },
295
- mounted() {
296
- this.scrollDetector = createScrollDetector(this.$refs.scrollContainer);
297
- this.scrollDetector.init();
298
- this.hasVertical = this.scrollDetector.state.hasVertical;
299
-
300
- // 添加容器尺寸变化监听
301
- const containerResizeObserver = new ResizeObserver(() => {
302
- this.hasVertical =
303
- this.$refs.scrollContainer.scrollHeight > this.$refs.scrollContainer.clientHeight;
304
- });
305
- containerResizeObserver.observe(this.$refs.scrollContainer);
306
- this.containerResizeObserver = containerResizeObserver;
307
- },
308
- beforeDestroy() {
309
- if (this.resizeObserver) {
310
- this.resizeObserver.disconnect();
311
- }
312
- if (this.containerResizeObserver) {
313
- this.containerResizeObserver.disconnect();
314
- }
315
- if (this.scrollDetector) {
316
- this.scrollDetector.destroy();
317
- }
318
- },
319
- beforeDestroy() {
320
- if (this.resizeObserver) {
321
- this.resizeObserver.disconnect();
322
- }
323
- },
324
- methods: {
325
- getValidIndices(list, indices) {
326
- const validIndices = [];
327
- const invalidIndices = [];
328
- for (let i = 0; i < indices.length; i++) {
329
- const index = indices[i];
330
- if (index < 0 || index >= list.length || !list[index].typing) {
331
- invalidIndices.push(index);
332
- } else {
333
- validIndices.push(index);
334
- }
335
- }
336
- if (invalidIndices.length > 0) {
337
- console.warn(`无效索引 ${invalidIndices}`);
338
- }
339
- return validIndices;
340
- },
341
- scrollToTop() {
342
- this.stopAutoScrollToBottom = true;
343
- this.$nextTick(() => {
344
- this.$refs.scrollContainer.scrollTop = 0;
345
- });
346
- },
347
- scrollToBottom() {
348
- try {
349
- if (this.$refs.scrollContainer && this.$refs.scrollContainer.scrollHeight) {
350
- this.$nextTick(() => {
351
- this.$refs.scrollContainer.scrollTop = this.$refs.scrollContainer.scrollHeight;
352
- this.stopAutoScrollToBottom = false;
353
- });
354
- }
355
- } catch (error) {
356
- console.warn('[BubbleList error]: ', error);
357
- }
358
- },
359
- scrollToBubble(index) {
360
- const container = this.$refs.scrollContainer;
361
- if (!container) return;
362
-
363
- const bubbles = container.querySelectorAll('.el-x-bubble');
364
- if (index >= bubbles.length) return;
365
-
366
- this.stopAutoScrollToBottom = true;
367
- const targetBubble = bubbles[index];
368
-
369
- const containerRect = container.getBoundingClientRect();
370
- const bubbleRect = targetBubble.getBoundingClientRect();
371
-
372
- const scrollPosition = bubbleRect.top - containerRect.top + container.scrollTop;
373
-
374
- container.scrollTo({
375
- top: scrollPosition,
376
- behavior: 'smooth',
377
- });
378
- },
379
- autoScroll() {
380
- if (this.$refs.scrollContainer) {
381
- const listBubbles = this.$refs.scrollContainer.querySelectorAll(
382
- '.el-x-bubble-content-wrapper',
383
- );
384
- if (this.resizeObserver) {
385
- this.resizeObserver.disconnect();
386
- }
387
- const lastItem = listBubbles[listBubbles.length - 1];
388
- if (lastItem) {
389
- this.resizeObserver = new ResizeObserver(() => {
390
- if (!this.stopAutoScrollToBottom) {
391
- this.scrollToBottom();
392
- }
393
- });
394
- this.resizeObserver.observe(lastItem);
395
- }
396
- }
397
- },
398
- handleBubbleComplete(index, instance) {
399
- if (this.effectiveTriggerIndices.includes(index)) {
400
- this.$emit('complete', instance, index);
401
- }
402
- },
403
- handleScroll() {
404
- if (this.$refs.scrollContainer) {
405
- const { scrollTop, scrollHeight, clientHeight } = this.$refs.scrollContainer;
406
-
407
- const distanceToBottom = scrollHeight - (scrollTop + clientHeight);
408
-
409
- // 更新 hasVertical 状态
410
- this.hasVertical = scrollHeight > clientHeight;
411
-
412
- // 使用延迟逻辑处理按钮显示
413
- if (this.showBackButton && distanceToBottom > this.backButtonThreshold) {
414
- // 如果应该显示按钮,但还没有设置计时器
415
- if (!this.backButtonTimer) {
416
- this.backButtonTimer = setTimeout(() => {
417
- // 再次检查条件,确保 500ms 后仍然需要显示按钮
418
- const { scrollTop, scrollHeight, clientHeight } = this.$refs.scrollContainer;
419
- const newDistanceToBottom = scrollHeight - (scrollTop + clientHeight);
420
- if (newDistanceToBottom > this.backButtonThreshold) {
421
- this.showBackToBottom = true;
422
- }
423
- this.backButtonTimer = null;
424
- }, 200); // 200ms 延迟
425
- }
426
- } else {
427
- // 如果不应该显示按钮
428
- if (this.backButtonTimer) {
429
- clearTimeout(this.backButtonTimer);
430
- this.backButtonTimer = null;
431
- }
432
- this.showBackToBottom = false;
433
- }
434
-
435
- const isCloseToBottom = scrollTop + clientHeight >= scrollHeight - 30;
436
- const isScrollingUp = this.lastScrollTop > scrollTop;
437
- const isScrollingDown = this.lastScrollTop < scrollTop;
438
- const scrollDelta = this.lastScrollTop - scrollTop;
439
- this.lastScrollTop = scrollTop;
440
-
441
- if (isScrollingUp) {
442
- this.accumulatedScrollUpDistance += scrollDelta;
443
- if (this.accumulatedScrollUpDistance >= 20) {
444
- if (!this.stopAutoScrollToBottom) {
445
- this.stopAutoScrollToBottom = true;
446
- }
447
- this.accumulatedScrollUpDistance = 0;
448
- }
449
- } else {
450
- this.accumulatedScrollUpDistance = 0;
451
- }
452
-
453
- if (isScrollingDown && isCloseToBottom) {
454
- if (this.stopAutoScrollToBottom) {
455
- this.stopAutoScrollToBottom = false;
456
- }
457
- }
458
- }
459
- },
460
- },
461
- };
462
- </script>
463
-
464
- <style lang="scss" scoped>
465
- @import '../../../styles/BubbleList.scss';
466
- </style>
1
+ <template>
2
+ <div
3
+ ref="scrollContainer"
4
+ class="el-x-bubble-list"
5
+ :class="{ 'always-scrollbar': alwaysShowScrollbar }"
6
+ :style="{
7
+ '--el-x-bubble-list-max-height': `${maxHeight}`,
8
+ '--el-x-bubble-list-btn-size': `${btnIconSize}px`,
9
+ }"
10
+ @scroll="handleScroll"
11
+ >
12
+ <Bubble
13
+ v-for="(item, index) in list"
14
+ :key="index"
15
+ :content="item.content"
16
+ :placement="defaultPlacement || item.placement"
17
+ :loading="defaultLoading !== undefined ? defaultLoading : item.loading"
18
+ :shape="defaultShape || item.shape"
19
+ :variant="defaultVariant || item.variant"
20
+ :is-markdown="defaultIsMarkdown !== undefined ? defaultIsMarkdown : item.isMarkdown"
21
+ :is-fog="
22
+ defaultPlacement === 'start' || item.placement === 'start'
23
+ ? defaultIsFog !== undefined
24
+ ? defaultIsFog
25
+ : item.isFog
26
+ : false
27
+ "
28
+ :typing="
29
+ defaultPlacement === 'start' || item.placement === 'start'
30
+ ? defaultTyping !== undefined
31
+ ? defaultTyping
32
+ : item.typing
33
+ : false
34
+ "
35
+ :max-width="defaultMaxWidth || item.maxWidth"
36
+ :avatar="defaultAvatar || item.avatar"
37
+ :avatar-size="defaultAvatarSize || item.avatarSize"
38
+ :avatar-gap="defaultAvatarGap || item.avatarGap"
39
+ :avatar-shape="defaultAvatarShape || item.avatarShape"
40
+ :avatar-icon="defaultAvatarIcon || item.avatarIcon"
41
+ :avatar-src-set="defaultAvatarSrcSet || item.avatarSrcSet"
42
+ :avatar-alt="defaultAvatarAlt || item.avatarAlt"
43
+ :avatar-fit="defaultAvatarFit || item.avatarFit"
44
+ :no-style="defaultNoStyle !== undefined ? defaultNoStyle : item.noStyle"
45
+ @finish="instance => handleBubbleComplete(index, instance)"
46
+ >
47
+ <template slot="avatar">
48
+ <slot
49
+ name="avatar"
50
+ :item="item"
51
+ >
52
+ <template v-if="defaultAvatar || item.avatar">
53
+ <el-avatar
54
+ :size="defaultAvatarSize || item.avatarSize || 40"
55
+ :src="defaultAvatar || item.avatar"
56
+ :shape="defaultAvatarShape || item.avatarShape || 'circle'"
57
+ :icon="defaultAvatarIcon || item.avatarIcon"
58
+ :src-set="defaultAvatarSrcSet || item.avatarSrcSet"
59
+ :alt="defaultAvatarAlt || item.avatarAlt"
60
+ :fit="defaultAvatarFit || item.avatarFit || 'cover'"
61
+ />
62
+ </template>
63
+ </slot>
64
+ </template>
65
+ <template
66
+ v-if="$scopedSlots.header || $slots.header"
67
+ slot="header"
68
+ >
69
+ <slot
70
+ name="header"
71
+ :item="item"
72
+ />
73
+ </template>
74
+ <template
75
+ v-if="$scopedSlots.content || $slots.content"
76
+ slot="content"
77
+ >
78
+ <slot
79
+ name="content"
80
+ :item="item"
81
+ />
82
+ </template>
83
+ <template
84
+ v-if="$scopedSlots.footer || $slots.footer"
85
+ slot="footer"
86
+ >
87
+ <slot
88
+ name="footer"
89
+ :item="item"
90
+ />
91
+ </template>
92
+ <template
93
+ v-if="$scopedSlots.loading || $slots.loading"
94
+ slot="loading"
95
+ >
96
+ <slot
97
+ name="loading"
98
+ :item="item"
99
+ />
100
+ </template>
101
+ </Bubble>
102
+
103
+ <!-- 保持原有的返回底部按钮代码不变 -->
104
+ <div
105
+ v-if="showBackToBottom && hasVertical"
106
+ class="el-x-bubble-list-default-back-button"
107
+ :class="{
108
+ 'el-x-bubble-list-back-to-bottom-solt': $scopedSlots.backToBottom || $slots.backToBottom,
109
+ }"
110
+ :style="{
111
+ bottom: backButtonPosition.bottom,
112
+ left: backButtonPosition.left,
113
+ }"
114
+ @click="scrollToBottom"
115
+ >
116
+ <slot name="backToBottom">
117
+ <i
118
+ class="el-icon-bottom el-x-bubble-list-back-to-bottom-icon"
119
+ :style="{ color: btnColor }"
120
+ ></i>
121
+ <loadingBg
122
+ v-if="btnLoading"
123
+ class="back-to-bottom-loading-svg-bg"
124
+ :style="{ color: btnColor }"
125
+ />
126
+ </slot>
127
+ </div>
128
+ </div>
129
+ </template>
130
+
131
+ <script>
132
+ import Bubble from '../../Bubble/index.js';
133
+ import loadingBg from './loading.vue';
134
+ import createScrollDetector from '../../../utils/scrollDetector';
135
+
136
+ export default {
137
+ name: 'ElXBubbleList',
138
+ components: {
139
+ Bubble,
140
+ loadingBg,
141
+ },
142
+ props: {
143
+ list: {
144
+ type: Array,
145
+ default: () => [],
146
+ },
147
+ maxHeight: {
148
+ type: String,
149
+ default: '500px',
150
+ },
151
+ triggerIndices: {
152
+ type: [String, Array],
153
+ default: 'only-last',
154
+ },
155
+ alwaysShowScrollbar: {
156
+ type: Boolean,
157
+ default: false,
158
+ },
159
+ backButtonThreshold: {
160
+ type: Number,
161
+ default: 80,
162
+ },
163
+ showBackButton: {
164
+ type: Boolean,
165
+ default: true,
166
+ },
167
+ backButtonPosition: {
168
+ type: Object,
169
+ default: () => ({
170
+ bottom: '20px',
171
+ left: 'calc(50% - 19px)',
172
+ }),
173
+ },
174
+ btnLoading: {
175
+ type: Boolean,
176
+ default: true,
177
+ },
178
+ btnColor: {
179
+ type: String,
180
+ default: '#409EFF',
181
+ },
182
+ btnIconSize: {
183
+ type: Number,
184
+ default: 24,
185
+ },
186
+ // 新增全局默认属性
187
+ defaultPlacement: {
188
+ type: String,
189
+ default: '',
190
+ },
191
+ defaultLoading: {
192
+ type: Boolean,
193
+ default: undefined,
194
+ },
195
+ defaultShape: {
196
+ type: String,
197
+ default: '',
198
+ },
199
+ defaultVariant: {
200
+ type: String,
201
+ default: '',
202
+ },
203
+ defaultIsMarkdown: {
204
+ type: Boolean,
205
+ default: true,
206
+ },
207
+ defaultIsFog: {
208
+ type: Boolean,
209
+ default: false,
210
+ },
211
+ defaultTyping: {
212
+ type: [Boolean, Object],
213
+ default: undefined,
214
+ },
215
+ defaultMaxWidth: {
216
+ type: String,
217
+ default: '',
218
+ },
219
+ defaultAvatar: {
220
+ type: String,
221
+ default: '',
222
+ },
223
+ defaultAvatarSize: {
224
+ type: Number,
225
+ default: undefined,
226
+ },
227
+ defaultAvatarGap: {
228
+ type: Number,
229
+ default: undefined,
230
+ },
231
+ defaultAvatarShape: {
232
+ type: String,
233
+ default: '',
234
+ },
235
+ defaultAvatarIcon: {
236
+ type: String,
237
+ default: '',
238
+ },
239
+ defaultAvatarSrcSet: {
240
+ type: String,
241
+ default: '',
242
+ },
243
+ defaultAvatarAlt: {
244
+ type: String,
245
+ default: '',
246
+ },
247
+ defaultAvatarFit: {
248
+ type: String,
249
+ default: '',
250
+ },
251
+ defaultNoStyle: {
252
+ type: Boolean,
253
+ default: undefined,
254
+ },
255
+ },
256
+ data() {
257
+ return {
258
+ scrollContainer: null,
259
+ stopAutoScrollToBottom: false,
260
+ lastScrollTop: 0,
261
+ accumulatedScrollUpDistance: 0,
262
+ resizeObserver: null,
263
+ containerResizeObserver: null, // 新增容器尺寸监听器引用
264
+ showBackToBottom: false,
265
+ hasVertical: false,
266
+ backButtonTimer: null, // 新增计时器变量
267
+ };
268
+ },
269
+ computed: {
270
+ effectiveTriggerIndices() {
271
+ if (this.triggerIndices === 'only-last') {
272
+ const triggerIndices = this.list.filter(item => item.typing).map((_, index) => index);
273
+ return triggerIndices.length > 0 ? [triggerIndices[triggerIndices.length - 1]] : [];
274
+ } else if (this.triggerIndices === 'all') {
275
+ return this.list.map((_, index) => index);
276
+ } else if (Array.isArray(this.triggerIndices)) {
277
+ const validIndices = this.getValidIndices(this.list, this.triggerIndices);
278
+ return validIndices.length > 0 ? [validIndices[validIndices.length - 1]] : [];
279
+ }
280
+ return [];
281
+ },
282
+ },
283
+ watch: {
284
+ list: {
285
+ handler() {
286
+ if (this.list && this.list.length > 0) {
287
+ this.$nextTick(() => {
288
+ this.autoScroll();
289
+ });
290
+ }
291
+ },
292
+ immediate: true,
293
+ },
294
+ },
295
+ mounted() {
296
+ this.scrollDetector = createScrollDetector(this.$refs.scrollContainer);
297
+ this.scrollDetector.init();
298
+ this.hasVertical = this.scrollDetector.state.hasVertical;
299
+
300
+ // 添加容器尺寸变化监听
301
+ const containerResizeObserver = new ResizeObserver(() => {
302
+ this.hasVertical =
303
+ this.$refs.scrollContainer.scrollHeight > this.$refs.scrollContainer.clientHeight;
304
+ });
305
+ containerResizeObserver.observe(this.$refs.scrollContainer);
306
+ this.containerResizeObserver = containerResizeObserver;
307
+ },
308
+ beforeDestroy() {
309
+ if (this.resizeObserver) {
310
+ this.resizeObserver.disconnect();
311
+ }
312
+ if (this.containerResizeObserver) {
313
+ this.containerResizeObserver.disconnect();
314
+ }
315
+ if (this.scrollDetector) {
316
+ this.scrollDetector.destroy();
317
+ }
318
+ },
319
+ methods: {
320
+ getValidIndices(list, indices) {
321
+ const validIndices = [];
322
+ const invalidIndices = [];
323
+ for (let i = 0; i < indices.length; i++) {
324
+ const index = indices[i];
325
+ if (index < 0 || index >= list.length || !list[index].typing) {
326
+ invalidIndices.push(index);
327
+ } else {
328
+ validIndices.push(index);
329
+ }
330
+ }
331
+ if (invalidIndices.length > 0) {
332
+ console.warn(`无效索引 ${invalidIndices}`);
333
+ }
334
+ return validIndices;
335
+ },
336
+ scrollToTop() {
337
+ this.stopAutoScrollToBottom = true;
338
+ this.$nextTick(() => {
339
+ this.$refs.scrollContainer.scrollTop = 0;
340
+ });
341
+ },
342
+ scrollToBottom() {
343
+ try {
344
+ if (this.$refs.scrollContainer && this.$refs.scrollContainer.scrollHeight) {
345
+ this.$nextTick(() => {
346
+ this.$refs.scrollContainer.scrollTop = this.$refs.scrollContainer.scrollHeight;
347
+ this.stopAutoScrollToBottom = false;
348
+ });
349
+ }
350
+ } catch (error) {
351
+ console.warn('[BubbleList error]: ', error);
352
+ }
353
+ },
354
+ scrollToBubble(index) {
355
+ const container = this.$refs.scrollContainer;
356
+ if (!container) return;
357
+
358
+ const bubbles = container.querySelectorAll('.el-x-bubble');
359
+ if (index >= bubbles.length) return;
360
+
361
+ this.stopAutoScrollToBottom = true;
362
+ const targetBubble = bubbles[index];
363
+
364
+ const containerRect = container.getBoundingClientRect();
365
+ const bubbleRect = targetBubble.getBoundingClientRect();
366
+
367
+ const scrollPosition = bubbleRect.top - containerRect.top + container.scrollTop;
368
+
369
+ container.scrollTo({
370
+ top: scrollPosition,
371
+ behavior: 'smooth',
372
+ });
373
+ },
374
+ autoScroll() {
375
+ if (this.$refs.scrollContainer) {
376
+ const listBubbles = this.$refs.scrollContainer.querySelectorAll(
377
+ '.el-x-bubble-content-wrapper',
378
+ );
379
+ if (this.resizeObserver) {
380
+ this.resizeObserver.disconnect();
381
+ }
382
+ const lastItem = listBubbles[listBubbles.length - 1];
383
+ if (lastItem) {
384
+ this.resizeObserver = new ResizeObserver(() => {
385
+ if (!this.stopAutoScrollToBottom) {
386
+ this.scrollToBottom();
387
+ }
388
+ });
389
+ this.resizeObserver.observe(lastItem);
390
+ }
391
+ }
392
+ },
393
+ handleBubbleComplete(index, instance) {
394
+ if (this.effectiveTriggerIndices.includes(index)) {
395
+ this.$emit('complete', instance, index);
396
+ }
397
+ },
398
+ handleScroll() {
399
+ if (this.$refs.scrollContainer) {
400
+ const { scrollTop, scrollHeight, clientHeight } = this.$refs.scrollContainer;
401
+
402
+ const distanceToBottom = scrollHeight - (scrollTop + clientHeight);
403
+
404
+ // 更新 hasVertical 状态
405
+ this.hasVertical = scrollHeight > clientHeight;
406
+
407
+ // 使用延迟逻辑处理按钮显示
408
+ if (this.showBackButton && distanceToBottom > this.backButtonThreshold) {
409
+ // 如果应该显示按钮,但还没有设置计时器
410
+ if (!this.backButtonTimer) {
411
+ this.backButtonTimer = setTimeout(() => {
412
+ // 再次检查条件,确保 500ms 后仍然需要显示按钮
413
+ const { scrollTop, scrollHeight, clientHeight } = this.$refs.scrollContainer;
414
+ const newDistanceToBottom = scrollHeight - (scrollTop + clientHeight);
415
+ if (newDistanceToBottom > this.backButtonThreshold) {
416
+ this.showBackToBottom = true;
417
+ }
418
+ this.backButtonTimer = null;
419
+ }, 200); // 200ms 延迟
420
+ }
421
+ } else {
422
+ // 如果不应该显示按钮
423
+ if (this.backButtonTimer) {
424
+ clearTimeout(this.backButtonTimer);
425
+ this.backButtonTimer = null;
426
+ }
427
+ this.showBackToBottom = false;
428
+ }
429
+
430
+ const isCloseToBottom = scrollTop + clientHeight >= scrollHeight - 30;
431
+ const isScrollingUp = this.lastScrollTop > scrollTop;
432
+ const isScrollingDown = this.lastScrollTop < scrollTop;
433
+ const scrollDelta = this.lastScrollTop - scrollTop;
434
+ this.lastScrollTop = scrollTop;
435
+
436
+ if (isScrollingUp) {
437
+ this.accumulatedScrollUpDistance += scrollDelta;
438
+ if (this.accumulatedScrollUpDistance >= 20) {
439
+ if (!this.stopAutoScrollToBottom) {
440
+ this.stopAutoScrollToBottom = true;
441
+ }
442
+ this.accumulatedScrollUpDistance = 0;
443
+ }
444
+ } else {
445
+ this.accumulatedScrollUpDistance = 0;
446
+ }
447
+
448
+ if (isScrollingDown && isCloseToBottom) {
449
+ if (this.stopAutoScrollToBottom) {
450
+ this.stopAutoScrollToBottom = false;
451
+ }
452
+ }
453
+ }
454
+ },
455
+ },
456
+ };
457
+ </script>
458
+
459
+ <style lang="scss" scoped>
460
+ @import '../../../styles/BubbleList.scss';
461
+ </style>