element-ui-x-zzz 1.0.0

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 (99) hide show
  1. package/build/build-components.js +68 -0
  2. package/build/build-locale.js +176 -0
  3. package/build/webpack.common.conf.js +118 -0
  4. package/build/webpack.component.conf.js +85 -0
  5. package/build/webpack.esm.conf.js +90 -0
  6. package/build/webpack.umd.conf.js +72 -0
  7. package/components.json +13 -0
  8. package/package.json +12 -0
  9. package/src/components/Attachments/index.js +8 -0
  10. package/src/components/Attachments/src/main.vue +529 -0
  11. package/src/components/Bubble/index.js +6 -0
  12. package/src/components/Bubble/src/main.vue +288 -0
  13. package/src/components/BubbleList/index.js +8 -0
  14. package/src/components/BubbleList/src/loading.vue +75 -0
  15. package/src/components/BubbleList/src/main.vue +444 -0
  16. package/src/components/Conversations/index.js +8 -0
  17. package/src/components/Conversations/src/components/item.vue +350 -0
  18. package/src/components/Conversations/src/main.vue +587 -0
  19. package/src/components/EditorSender/index.js +8 -0
  20. package/src/components/EditorSender/src/components/ClearButton.vue +35 -0
  21. package/src/components/EditorSender/src/components/Loading.vue +53 -0
  22. package/src/components/EditorSender/src/components/LoadingButton.vue +37 -0
  23. package/src/components/EditorSender/src/components/SendButton.vue +26 -0
  24. package/src/components/EditorSender/src/main.vue +589 -0
  25. package/src/components/FilesCard/index.js +8 -0
  26. package/src/components/FilesCard/src/fileSvg/audio.vue +38 -0
  27. package/src/components/FilesCard/src/fileSvg/changeFileName.bat +18 -0
  28. package/src/components/FilesCard/src/fileSvg/code.vue +35 -0
  29. package/src/components/FilesCard/src/fileSvg/database.vue +94 -0
  30. package/src/components/FilesCard/src/fileSvg/excel.vue +38 -0
  31. package/src/components/FilesCard/src/fileSvg/file.vue +40 -0
  32. package/src/components/FilesCard/src/fileSvg/image.vue +40 -0
  33. package/src/components/FilesCard/src/fileSvg/index.js +46 -0
  34. package/src/components/FilesCard/src/fileSvg/link.vue +54 -0
  35. package/src/components/FilesCard/src/fileSvg/mark.vue +38 -0
  36. package/src/components/FilesCard/src/fileSvg/pdf.vue +38 -0
  37. package/src/components/FilesCard/src/fileSvg/ppt.vue +38 -0
  38. package/src/components/FilesCard/src/fileSvg/three.vue +38 -0
  39. package/src/components/FilesCard/src/fileSvg/txt.vue +38 -0
  40. package/src/components/FilesCard/src/fileSvg/unknown.vue +54 -0
  41. package/src/components/FilesCard/src/fileSvg/video.vue +38 -0
  42. package/src/components/FilesCard/src/fileSvg/word.vue +38 -0
  43. package/src/components/FilesCard/src/fileSvg/zip.vue +38 -0
  44. package/src/components/FilesCard/src/main.vue +403 -0
  45. package/src/components/FilesCard/src/options.js +18 -0
  46. package/src/components/Prompts/index.js +8 -0
  47. package/src/components/Prompts/src/main.vue +248 -0
  48. package/src/components/Sender/index.js +8 -0
  49. package/src/components/Sender/src/components/ClearButton.vue +28 -0
  50. package/src/components/Sender/src/components/Loading.vue +53 -0
  51. package/src/components/Sender/src/components/LoadingButton.vue +37 -0
  52. package/src/components/Sender/src/components/SendButton.vue +26 -0
  53. package/src/components/Sender/src/components/SpeechButton.vue +24 -0
  54. package/src/components/Sender/src/components/SpeechLoading.vue +87 -0
  55. package/src/components/Sender/src/components/SpeechLoadingButton.vue +41 -0
  56. package/src/components/Sender/src/main.vue +803 -0
  57. package/src/components/Thinking/index.js +8 -0
  58. package/src/components/Thinking/src/main.vue +199 -0
  59. package/src/components/ThoughtChain/index.js +8 -0
  60. package/src/components/ThoughtChain/src/main.vue +291 -0
  61. package/src/components/Typewriter/index.js +8 -0
  62. package/src/components/Typewriter/src/main.vue +255 -0
  63. package/src/components/Welcome/index.js +8 -0
  64. package/src/components/Welcome/src/main.vue +151 -0
  65. package/src/index.js +107 -0
  66. package/src/locale/index.js +97 -0
  67. package/src/locale/lang/ar.js +18 -0
  68. package/src/locale/lang/de.js +18 -0
  69. package/src/locale/lang/en.js +18 -0
  70. package/src/locale/lang/es.js +18 -0
  71. package/src/locale/lang/fr.js +18 -0
  72. package/src/locale/lang/index.js +62 -0
  73. package/src/locale/lang/it.js +18 -0
  74. package/src/locale/lang/ja.js +18 -0
  75. package/src/locale/lang/ko.js +18 -0
  76. package/src/locale/lang/pt-br.js +18 -0
  77. package/src/locale/lang/ru-RU.js +18 -0
  78. package/src/locale/lang/zh-CN.js +18 -0
  79. package/src/locale/lang/zh-TW.js +18 -0
  80. package/src/locale/mixin.js +9 -0
  81. package/src/mixins/index.js +49 -0
  82. package/src/mixins/recordMixin.js +117 -0
  83. package/src/mixins/sendMixin.js +448 -0
  84. package/src/mixins/streamMixin.js +497 -0
  85. package/src/styles/Attachments.scss +236 -0
  86. package/src/styles/Bubble.scss +158 -0
  87. package/src/styles/BubbleList.scss +148 -0
  88. package/src/styles/Conversations.scss +283 -0
  89. package/src/styles/EditorSender.scss +183 -0
  90. package/src/styles/FilesCard.scss +222 -0
  91. package/src/styles/Prompts.scss +197 -0
  92. package/src/styles/Sender.scss +211 -0
  93. package/src/styles/Thinking.scss +142 -0
  94. package/src/styles/ThoughtChain.scss +113 -0
  95. package/src/styles/Typewriter.scss +66 -0
  96. package/src/styles/Welcome.scss +283 -0
  97. package/src/theme/var.scss +183 -0
  98. package/src/utils/index.js +199 -0
  99. package/src/utils/scrollDetector.js +34 -0
@@ -0,0 +1,803 @@
1
+ <template>
2
+ <div
3
+ class="el-x-sender-wrap"
4
+ :style="{
5
+ display: 'block',
6
+ cursor: disabled ? 'not-allowed' : 'default',
7
+ '--el-x-sender-trigger-popover-width': triggerPopoverWidth,
8
+ '--el-x-sender-trigger-popover-left': triggerPopoverLeft,
9
+ }"
10
+ >
11
+ <div
12
+ ref="senderRef"
13
+ class="el-x-sender"
14
+ :style="{
15
+ '--el-x-sender-box-shadow-tertiary':
16
+ '0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02)',
17
+ '--el-x-sender-input-font-size': '14px',
18
+ '--el-x-sender-header-animation-duration': `${headerAnimationTimer}ms`,
19
+ }"
20
+ :class="{
21
+ 'el-x-sender-disabled': disabled,
22
+ }"
23
+ >
24
+ <!-- 头部容器 -->
25
+ <transition name="slide">
26
+ <div
27
+ v-if="visiableHeader"
28
+ class="el-x-sender-header-wrap"
29
+ >
30
+ <div
31
+ v-if="$slots.header"
32
+ class="el-x-sender-header"
33
+ >
34
+ <slot name="header"></slot>
35
+ </div>
36
+ </div>
37
+ </transition>
38
+ <!-- 内容容器 内置变体逻辑 -->
39
+ <div
40
+ class="el-x-sender-content"
41
+ :class="{ 'content-variant-updown': variant === 'updown' }"
42
+ @mousedown="onContentMouseDown"
43
+ >
44
+ <!-- Prefix 前缀 -->
45
+ <div
46
+ v-if="$slots.prefix && variant === 'default'"
47
+ class="el-x-sender-prefix"
48
+ >
49
+ <slot name="prefix"></slot>
50
+ </div>
51
+ <!-- 输入框 -->
52
+ <el-input
53
+ ref="inputRef"
54
+ v-model="internalValue"
55
+ class="el-x-sender-input"
56
+ :rows="1"
57
+ :autosize="computedAutoSize"
58
+ type="textarea"
59
+ :validate-event="false"
60
+ :placeholder="computedPlaceholder"
61
+ :readonly="readOnly || disabled"
62
+ :disabled="disabled"
63
+ @keydown.native="handleKeyDown"
64
+ @compositionstart="handleCompositionStart"
65
+ @compositionend="handleCompositionEnd"
66
+ />
67
+ <!-- 操作列表 -->
68
+ <div
69
+ v-if="variant === 'default'"
70
+ class="el-x-sender-action-list"
71
+ >
72
+ <slot name="action-list">
73
+ <div class="el-x-sender-action-list-presets">
74
+ <send-button
75
+ v-if="!loading"
76
+ :disabled="isSubmitDisabled"
77
+ @submit="submit"
78
+ ></send-button>
79
+
80
+ <loading-button
81
+ v-if="loading"
82
+ @cancel="cancel"
83
+ ></loading-button>
84
+
85
+ <speech-button
86
+ v-if="!speechLoading && allowSpeech"
87
+ @click="startRecognition"
88
+ ></speech-button>
89
+
90
+ <speech-loading-button
91
+ v-if="speechLoading && allowSpeech"
92
+ @click="stopRecognition"
93
+ ></speech-loading-button>
94
+
95
+ <clear-button
96
+ v-if="clearable"
97
+ @clear="clear"
98
+ ></clear-button>
99
+ </div>
100
+ </slot>
101
+ </div>
102
+
103
+ <!-- 变体样式 -->
104
+ <div
105
+ v-if="variant === 'updown' && showUpdown"
106
+ class="el-x-sender-updown-wrap"
107
+ >
108
+ <!-- 变体 updown: Prefix 前缀 -->
109
+ <div
110
+ v-if="$slots.prefix"
111
+ class="el-x-sender-prefix"
112
+ >
113
+ <slot name="prefix"></slot>
114
+ </div>
115
+
116
+ <!-- 变体 updown:操作列表 -->
117
+ <div class="el-x-sender-action-list">
118
+ <slot name="action-list">
119
+ <div class="el-x-sender-action-list-presets">
120
+ <send-button
121
+ v-if="!loading"
122
+ :disabled="isSubmitDisabled"
123
+ @submit="submit"
124
+ ></send-button>
125
+
126
+ <loading-button
127
+ v-if="loading"
128
+ @cancel="cancel"
129
+ ></loading-button>
130
+
131
+ <speech-button
132
+ v-if="!speechLoading && allowSpeech"
133
+ @click="startRecognition"
134
+ ></speech-button>
135
+
136
+ <speech-loading-button
137
+ v-if="speechLoading && allowSpeech"
138
+ @click="stopRecognition"
139
+ ></speech-loading-button>
140
+
141
+ <clear-button
142
+ v-if="clearable"
143
+ @clear="clear"
144
+ ></clear-button>
145
+ </div>
146
+ </slot>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- 底部容器 -->
152
+ <transition name="slide">
153
+ <div
154
+ v-if="$slots.footer"
155
+ class="el-x-sender-footer"
156
+ >
157
+ <slot name="footer"></slot>
158
+ </div>
159
+ </transition>
160
+
161
+ <!-- popover 直接附加到发送器 -->
162
+ <el-popover
163
+ ref="popoverRef"
164
+ v-model="popoverVisible"
165
+ :disabled="disabled"
166
+ :visible-arrow="false"
167
+ :appendToBody="false"
168
+ :placement="triggerPopoverPlacement"
169
+ :offset="triggerPopoverOffset"
170
+ popper-class="el-x-sender-trigger-popover"
171
+ trigger="manual"
172
+ @show="onPopoverShow"
173
+ >
174
+ <template slot="default">
175
+ <slot
176
+ name="trigger-popover"
177
+ :trigger-string="triggerString"
178
+ :readonly="readOnly"
179
+ >
180
+ 当前触发的字符为:{{ `${triggerString}` }} 在这里定义的内容,但注意这里的回车事件将会被
181
+ 输入框 覆盖
182
+ </slot>
183
+ </template>
184
+ </el-popover>
185
+ </div>
186
+ </div>
187
+ </template>
188
+
189
+ <script>
190
+ import Locale from '../../../locale/mixin';
191
+ import ClearButton from './components/ClearButton.vue';
192
+ import LoadingButton from './components/LoadingButton.vue';
193
+ import SendButton from './components/SendButton.vue';
194
+ import SpeechButton from './components/SpeechButton.vue';
195
+ import SpeechLoadingButton from './components/SpeechLoadingButton.vue';
196
+
197
+ export default {
198
+ name: 'ElXSender',
199
+ mixins: [Locale],
200
+
201
+ components: {
202
+ ClearButton,
203
+ LoadingButton,
204
+ SendButton,
205
+ SpeechButton,
206
+ SpeechLoadingButton,
207
+ },
208
+
209
+ props: {
210
+ value: {
211
+ type: String,
212
+ default: '',
213
+ },
214
+ placeholder: {
215
+ type: String,
216
+ default: '',
217
+ },
218
+ autoSize: {
219
+ type: Object,
220
+ default: () => ({
221
+ minRows: 1,
222
+ maxRows: 6,
223
+ }),
224
+ },
225
+ readOnly: Boolean,
226
+ disabled: Boolean,
227
+ loading: Boolean,
228
+ clearable: Boolean,
229
+ allowSpeech: Boolean,
230
+ submitType: {
231
+ type: String,
232
+ default: 'enter',
233
+ validator: value => ['enter', 'shiftEnter'].includes(value),
234
+ },
235
+ headerAnimationTimer: {
236
+ type: Number,
237
+ default: 300,
238
+ },
239
+ inputWidth: {
240
+ type: String,
241
+ default: '100%',
242
+ },
243
+
244
+ // 变体属性
245
+ variant: {
246
+ type: String,
247
+ default: 'default',
248
+ validator: value => ['default', 'updown'].includes(value),
249
+ },
250
+ showUpdown: {
251
+ type: Boolean,
252
+ default: true,
253
+ },
254
+ submitBtnDisabled: Boolean,
255
+
256
+ inputStyle: {
257
+ type: Object,
258
+ default: () => ({}),
259
+ },
260
+
261
+ // 新增 el-popover 样式透传
262
+ triggerStrings: {
263
+ type: Array,
264
+ default: () => [],
265
+ },
266
+ triggerPopoverVisible: {
267
+ type: Boolean,
268
+ default: false,
269
+ },
270
+ triggerPopoverWidth: {
271
+ type: String,
272
+ default: 'fit-content',
273
+ },
274
+ triggerPopoverLeft: {
275
+ type: String,
276
+ default: '0px',
277
+ },
278
+ triggerPopoverOffset: {
279
+ type: Number,
280
+ default: 0,
281
+ },
282
+ triggerPopoverPlacement: {
283
+ type: String,
284
+ default: 'top-start',
285
+ validator: value =>
286
+ [
287
+ 'top',
288
+ 'top-start',
289
+ 'top-end',
290
+ 'bottom',
291
+ 'bottom-start',
292
+ 'bottom-end',
293
+ 'left',
294
+ 'left-start',
295
+ 'left-end',
296
+ 'right',
297
+ 'right-start',
298
+ 'right-end',
299
+ ].includes(value),
300
+ },
301
+ },
302
+
303
+ data() {
304
+ return {
305
+ senderRef: null,
306
+ inputRef: null,
307
+ popoverVisible: this.triggerPopoverVisible,
308
+ internalValue: this.value,
309
+ isComposing: false,
310
+ popoverRef: null,
311
+ triggerString: '',
312
+ visiableHeader: false,
313
+ recognition: null,
314
+ speechLoading: false,
315
+ triggerDebounce: false,
316
+ };
317
+ },
318
+
319
+ computed: {
320
+ // 判断是否存在 recordingChange 监听器
321
+ hasOnRecordingChangeListener() {
322
+ return !!(this.$listeners && this.$listeners.recordingChange);
323
+ },
324
+
325
+ // 判断是否存在 trigger 监听器
326
+ hasOnTriggerListener() {
327
+ return !!(this.$listeners && this.$listeners.trigger);
328
+ },
329
+
330
+ // 计算提交按钮禁用状态
331
+ isSubmitDisabled() {
332
+ // 用户显式设置了 submitBtnDisabled 时优先使用
333
+ if (typeof this.submitBtnDisabled === 'boolean') {
334
+ return this.submitBtnDisabled;
335
+ }
336
+ // 否则保持默认逻辑:无内容时禁用
337
+ return !this.internalValue;
338
+ },
339
+
340
+ // 根据字体大小动态计算 autoSize
341
+ computedAutoSize() {
342
+ // 如果用户提供了autoSize,则优先使用
343
+ if (this.autoSize) return this.autoSize;
344
+
345
+ // 否则返回默认值
346
+ return {
347
+ minRows: 1,
348
+ maxRows: 6,
349
+ };
350
+ },
351
+
352
+ // 计算 placeholder
353
+ computedPlaceholder() {
354
+ return this.placeholder || this.elXt('el_x.sender.placeholder');
355
+ },
356
+ },
357
+
358
+ watch: {
359
+ value(val) {
360
+ this.internalValue = val;
361
+ },
362
+ // 监听样式变化
363
+ inputStyle: {
364
+ handler() {
365
+ this.$nextTick(() => {
366
+ this.applyInputStyles();
367
+ });
368
+ },
369
+ deep: true,
370
+ },
371
+ inputWidth() {
372
+ this.$nextTick(() => {
373
+ this.applyInputStyles();
374
+ });
375
+ },
376
+ // 监听外部传入的 triggerPopoverVisible 变化
377
+ triggerPopoverVisible(val) {
378
+ // 仅在值不同时更新,避免循环触发
379
+ if (this.popoverVisible !== val) {
380
+ this.popoverVisible = val;
381
+ }
382
+ },
383
+ // 监听内部 popoverVisible 变化,向外同步
384
+ popoverVisible(val) {
385
+ if (val !== this.triggerPopoverVisible) {
386
+ this.$emit('update:triggerPopoverVisible', val);
387
+ }
388
+
389
+ // 新增:当弹窗关闭时,设置短时间内的防抖状态
390
+ if (val === false) {
391
+ this.triggerDebounce = true;
392
+ setTimeout(() => {
393
+ this.triggerDebounce = false;
394
+ }, 300); // 300ms 防抖时间,防止频繁触发
395
+ }
396
+ },
397
+ internalValue(newVal, oldVal) {
398
+ this.$emit('input', newVal);
399
+
400
+ if (this.isComposing) return;
401
+ if (this.triggerDebounce) return;
402
+
403
+ const triggerStrings = this.triggerStrings || []; // 如果为 undefined,就使用空数组
404
+
405
+ if (this.inputRef && triggerStrings.length > 0) {
406
+ const textArea = this.inputRef.$el.querySelector('textarea');
407
+ if (textArea) {
408
+ const cursorPosition = textArea.selectionStart;
409
+ if (cursorPosition > 0 && newVal.length > oldVal.length) {
410
+ const lastChar = newVal.charAt(cursorPosition - 1);
411
+ if (triggerStrings.includes(lastChar)) {
412
+ this.triggerString = lastChar;
413
+ if (this.hasOnTriggerListener) {
414
+ this.$emit('trigger', {
415
+ oldValue: oldVal,
416
+ newValue: newVal,
417
+ triggerString: lastChar,
418
+ isOpen: true,
419
+ cursorPosition: cursorPosition,
420
+ });
421
+ }
422
+ this.popoverVisible = true;
423
+ return;
424
+ }
425
+ }
426
+ }
427
+ }
428
+ // V2.7X 兼容问题
429
+ // 原有的处理逻辑,用于向后兼容
430
+ const validOldVal = typeof oldVal === 'string' ? oldVal : '';
431
+ const wasOldValTrigger = triggerStrings.includes(validOldVal);
432
+ const isNewValTrigger = triggerStrings.includes(newVal);
433
+
434
+ // 触发显示:从空变为触发字符
435
+ if (oldVal === '' && isNewValTrigger) {
436
+ this.triggerString = newVal;
437
+ if (this.hasOnTriggerListener) {
438
+ this.$emit('trigger', {
439
+ oldValue: oldVal,
440
+ newValue: newVal,
441
+ triggerString: newVal,
442
+ isOpen: true,
443
+ });
444
+ }
445
+ this.popoverVisible = true;
446
+ }
447
+ // 关闭:从触发字符变为非触发字符
448
+ else if (!isNewValTrigger && wasOldValTrigger) {
449
+ if (this.hasOnTriggerListener) {
450
+ this.$emit('trigger', {
451
+ oldValue: oldVal,
452
+ newValue: newVal,
453
+ triggerString: undefined,
454
+ isOpen: false,
455
+ });
456
+ }
457
+ this.popoverVisible = false;
458
+ }
459
+ // 触发显示:从非空且非触发字符变为触发字符
460
+ else if (oldVal !== '' && isNewValTrigger && !wasOldValTrigger) {
461
+ this.triggerString = newVal;
462
+ if (this.hasOnTriggerListener) {
463
+ this.$emit('trigger', {
464
+ oldValue: oldVal,
465
+ newValue: newVal,
466
+ triggerString: newVal,
467
+ isOpen: true,
468
+ });
469
+ }
470
+ this.popoverVisible = true;
471
+ }
472
+ },
473
+ },
474
+
475
+ methods: {
476
+ /* 确保光标在可视区域内 */
477
+ ensureCursorVisible() {
478
+ if (!this.inputRef) return;
479
+
480
+ const textareaEl = this.inputRef.$el.querySelector('textarea');
481
+ if (!textareaEl) return;
482
+
483
+ this.$nextTick(() => {
484
+ // 获取光标位置信息
485
+ const cursorPosition = textareaEl.selectionStart;
486
+ const textBeforeCursor = this.internalValue.substring(0, cursorPosition);
487
+ const linesBeforeCursor = textBeforeCursor.split('\n').length;
488
+
489
+ const lineHeight = parseInt(window.getComputedStyle(textareaEl).lineHeight) || 20;
490
+ const maxVisibleHeight = textareaEl.offsetHeight;
491
+ const maxVisibleLines = Math.floor(maxVisibleHeight / lineHeight);
492
+
493
+ // 如果光标超出可视范围,滚动到光标位置
494
+ if (linesBeforeCursor > maxVisibleLines) {
495
+ const targetScrollTop = (linesBeforeCursor - maxVisibleLines) * lineHeight;
496
+ textareaEl.scrollTop = targetScrollTop;
497
+ }
498
+ });
499
+ },
500
+
501
+ /* 直接应用输入框样式 */
502
+ applyInputStyles() {
503
+ if (!this.inputRef) return;
504
+
505
+ const textareaEl = this.inputRef.$el.querySelector('textarea');
506
+ if (!textareaEl) return;
507
+
508
+ // 设置默认基础样式(当启用autosize时,不设置固定高度)
509
+ const defaultStyles = {
510
+ width: this.inputWidth || '100%',
511
+ maxHeight: '176px',
512
+ boxSizing: 'border-box',
513
+ };
514
+
515
+ // 只有在未启用autosize时才设置固定高度
516
+ if (!this.computedAutoSize) {
517
+ defaultStyles.height = '24px';
518
+ }
519
+
520
+ // 应用默认样式
521
+ Object.keys(defaultStyles).forEach(key => {
522
+ textareaEl.style[key] = defaultStyles[key];
523
+ });
524
+
525
+ // 如果用户传入了样式对象,则应用覆盖默认样式
526
+ if (this.inputStyle && typeof this.inputStyle === 'object') {
527
+ Object.keys(this.inputStyle).forEach(key => {
528
+ // 当启用autosize时,避免覆盖height相关属性
529
+ if (this.computedAutoSize && (key === 'height' || key === 'minHeight')) {
530
+ return;
531
+ }
532
+ textareaEl.style[key] = this.inputStyle[key];
533
+ });
534
+
535
+ // 如果用户设置了字体大小,需要调整高度(仅在未启用autosize时)
536
+ if (this.inputStyle.fontSize && !this.computedAutoSize) {
537
+ // 确保高度能完全容纳当前字体大小
538
+ const computedFontSize = window.getComputedStyle(textareaEl).fontSize;
539
+ const fontSize = parseInt(computedFontSize);
540
+ const minHeight = Math.max(fontSize * 1.5, 24) + 'px';
541
+ textareaEl.style.minHeight = minHeight;
542
+
543
+ // 重新触发 autosize
544
+ this.$nextTick(() => {
545
+ // 在某些情况下需要手动触发Element UI的autosize更新
546
+ const event = document.createEvent('Event');
547
+ event.initEvent('autosize:update', true, false);
548
+ textareaEl.dispatchEvent(event);
549
+ });
550
+ }
551
+ }
552
+ },
553
+
554
+ /* 手动更新 popover 位置 */
555
+ onPopoverShow() {
556
+ if (this.$refs.popoverRef) {
557
+ this.$nextTick(() => {
558
+ this.$refs.popoverRef.referenceElm = this.$refs.senderRef;
559
+ this.$refs.popoverRef.doDestroy();
560
+ this.$refs.popoverRef.updatePopper();
561
+ });
562
+ }
563
+ },
564
+ /* 内容容器聚焦 开始 */
565
+ onContentMouseDown(e) {
566
+ // 点击容器后设置输入框的聚焦,会触发 &:focus-within 样式
567
+ if (e.target !== this.$el.querySelector(`.el-textarea__inner`)) {
568
+ e.preventDefault();
569
+ }
570
+ this.inputRef.focus();
571
+ },
572
+ /* 内容容器聚焦 结束 */
573
+
574
+ /* 头部显示隐藏 开始 */
575
+ openHeader() {
576
+ if (!this.$slots.header) return false;
577
+ if (this.readOnly) return false;
578
+ this.visiableHeader = true;
579
+ },
580
+
581
+ closeHeader() {
582
+ if (!this.$slots.header) return;
583
+ if (this.readOnly) return;
584
+ this.visiableHeader = false;
585
+ },
586
+ /* 头部显示隐藏 结束 */
587
+
588
+ /* 使用浏览器自带的语音转文字功能 开始 */
589
+ startRecognition() {
590
+ if (this.readOnly || this.disabled) return; // 直接返回,不执行后续逻辑
591
+
592
+ if (this.hasOnRecordingChangeListener) {
593
+ this.speechLoading = true;
594
+ this.$emit('recording-change', true);
595
+ return;
596
+ }
597
+
598
+ // 检查浏览器支持的 SpeechRecognition API
599
+ const SpeechRecognition =
600
+ window.SpeechRecognition ||
601
+ window.webkitSpeechRecognition ||
602
+ window.mozSpeechRecognition ||
603
+ window.msSpeechRecognition;
604
+
605
+ if (SpeechRecognition) {
606
+ try {
607
+ this.recognition = new SpeechRecognition();
608
+ this.recognition.continuous = true;
609
+ this.recognition.interimResults = true;
610
+ this.recognition.lang = 'zh-CN';
611
+ this.recognition.onresult = event => {
612
+ let results = '';
613
+ for (let i = 0; i <= event.resultIndex; i++) {
614
+ results += event.results[i][0].transcript;
615
+ }
616
+ if (!this.readOnly) {
617
+ this.internalValue = results;
618
+ }
619
+ };
620
+ this.recognition.onstart = () => {
621
+ this.speechLoading = true;
622
+ console.log('语音识别已启动');
623
+ };
624
+ this.recognition.onend = () => {
625
+ this.speechLoading = false;
626
+ console.log('语音识别已结束');
627
+ };
628
+ this.recognition.onerror = event => {
629
+ console.error('语音识别出错:', event.error);
630
+ this.speechLoading = false;
631
+ // 可以添加用户友好提示
632
+ if (event.error === 'not-allowed') {
633
+ console.error('用户拒绝了麦克风访问权限');
634
+ // 这里可以显示提示
635
+ }
636
+ };
637
+ this.recognition.start();
638
+ } catch (error) {
639
+ console.error('启动语音识别失败:', error);
640
+ this.speechLoading = false;
641
+ }
642
+ } else {
643
+ console.error('浏览器不支持 Web Speech API');
644
+ this.speechLoading = false;
645
+ }
646
+ },
647
+
648
+ stopRecognition() {
649
+ // 如果有自定义处理函数
650
+ if (this.hasOnRecordingChangeListener) {
651
+ this.speechLoading = false;
652
+ this.$emit('recordingChange', false);
653
+ return;
654
+ }
655
+
656
+ if (this.recognition) {
657
+ this.recognition.stop();
658
+ this.speechLoading = false;
659
+ }
660
+ },
661
+ /* 使用浏览器自带的语音转文字功能 结束 */
662
+
663
+ /* 输入框事件 开始 */
664
+ submit() {
665
+ if (this.readOnly || this.loading || this.disabled || this.isSubmitDisabled) return;
666
+ this.$emit('submit', this.internalValue);
667
+ },
668
+
669
+ // 取消按钮
670
+ cancel() {
671
+ if (this.readOnly) return;
672
+ this.$emit('cancel', this.internalValue);
673
+ },
674
+
675
+ clear() {
676
+ if (this.readOnly) return; // 直接返回,不执行后续逻辑
677
+ this.inputRef.clear();
678
+ this.internalValue = '';
679
+ },
680
+
681
+ // 在这判断组合键的回车键 (目前支持两种模式)
682
+ handleKeyDown(e) {
683
+ if (this.readOnly) return; // 直接返回,不执行后续逻辑
684
+
685
+ if (this.submitType === 'enter') {
686
+ // 判断是否按下了 Shift + 回车键
687
+ if (e.shiftKey && e.keyCode === 13) {
688
+ e.preventDefault();
689
+ const cursorPosition = e.target.selectionStart; // 获取光标位置
690
+ const textBeforeCursor = this.internalValue.slice(0, cursorPosition); // 光标前的文本
691
+ const textAfterCursor = this.internalValue.slice(cursorPosition); // 光标后的文本
692
+ this.internalValue = `${textBeforeCursor}\n${textAfterCursor}`; // 插入换行符
693
+ this.$nextTick(() => {
694
+ e.target.setSelectionRange(cursorPosition + 1, cursorPosition + 1); // 更新光标位置
695
+ // 确保光标在可视区域内
696
+ this.ensureCursorVisible();
697
+ });
698
+ } else if (e.keyCode === 13 && !e.shiftKey) {
699
+ // 阻止掉 Enter 的默认换行行为
700
+ e.preventDefault();
701
+ // 触发提交功能
702
+ this.submit();
703
+ }
704
+ } else if (this.submitType === 'shiftEnter') {
705
+ // 判断是否按下了 Shift + 回车键
706
+ if (e.shiftKey && e.keyCode === 13) {
707
+ // 阻止掉 Enter 的默认换行行为
708
+ e.preventDefault();
709
+ // 触发提交功能
710
+ this.submit();
711
+ } else if (e.keyCode === 13 && !e.shiftKey) {
712
+ e.preventDefault();
713
+ const cursorPosition = e.target.selectionStart; // 获取光标位置
714
+ const textBeforeCursor = this.internalValue.slice(0, cursorPosition); // 光标前的文本
715
+ const textAfterCursor = this.internalValue.slice(cursorPosition); // 光标后的文本
716
+ this.internalValue = `${textBeforeCursor}\n${textAfterCursor}`; // 插入换行符
717
+ this.$nextTick(() => {
718
+ e.target.setSelectionRange(cursorPosition + 1, cursorPosition + 1); // 更新光标位置
719
+ // 确保光标在可视区域内
720
+ this.ensureCursorVisible();
721
+ });
722
+ }
723
+ }
724
+ },
725
+ /* 输入框事件 结束 */
726
+
727
+ /* 焦点 事件 开始 */
728
+ blur() {
729
+ if (this.readOnly) return false;
730
+ this.inputRef.blur();
731
+ },
732
+
733
+ focus(type = 'all') {
734
+ if (this.readOnly) return false;
735
+
736
+ if (type === 'all') {
737
+ this.inputRef.select();
738
+ } else if (type === 'start') {
739
+ this.focusToStart();
740
+ } else if (type === 'end') {
741
+ this.focusToEnd();
742
+ }
743
+ },
744
+
745
+ // 聚焦到文本最前方
746
+ focusToStart() {
747
+ if (this.inputRef) {
748
+ // 获取底层的 textarea DOM 元素
749
+ const textarea = this.inputRef.$el.querySelector('textarea');
750
+ if (textarea) {
751
+ textarea.focus(); // 聚焦到输入框
752
+ textarea.setSelectionRange(0, 0); // 设置光标到最前方
753
+ }
754
+ }
755
+ },
756
+
757
+ // 聚焦到文本最后方
758
+ focusToEnd() {
759
+ if (this.inputRef) {
760
+ // 获取底层的 textarea DOM 元素
761
+ const textarea = this.inputRef.$el.querySelector('textarea');
762
+ if (textarea) {
763
+ textarea.focus(); // 聚焦到输入框
764
+ textarea.setSelectionRange(this.internalValue.length, this.internalValue.length); // 设置光标到最后方
765
+ }
766
+ }
767
+ },
768
+ /* 焦点 事件 结束 */
769
+
770
+ // 处理输入法开始/结束 (此方法是拼音输入法的时候用)
771
+ handleCompositionStart() {
772
+ this.isComposing = true;
773
+ },
774
+
775
+ handleCompositionEnd() {
776
+ this.isComposing = false;
777
+ },
778
+ },
779
+
780
+ mounted() {
781
+ // 应用样式
782
+ this.$nextTick(() => {
783
+ // 获取组件引用
784
+ this.senderRef = this.$refs.senderRef;
785
+ this.inputRef = this.$refs.inputRef;
786
+ this.popoverRef = this.$refs.popoverRef;
787
+ this.applyInputStyles();
788
+ });
789
+ },
790
+
791
+ updated() {
792
+ // 只在特定条件下重新应用样式,避免干扰autosize功能
793
+ // 当启用autosize时,减少样式重新应用的频率
794
+ if (!this.computedAutoSize) {
795
+ this.applyInputStyles();
796
+ }
797
+ },
798
+ };
799
+ </script>
800
+
801
+ <style lang="scss" scoped>
802
+ @import '../../../styles/Sender.scss';
803
+ </style>