|
17 | 17 | </template> |
18 | 18 |
|
19 | 19 | <template #prefix> |
20 | | - <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"> |
| 20 | + <div style="display: flex; align-items: center; flex-wrap: wrap"> |
21 | 21 | <el-button |
22 | 22 | round |
23 | 23 | plain |
|
28 | 28 | <i class="el-icon-paperclip"></i> |
29 | 29 | </el-button> |
30 | 30 |
|
31 | | - <template> |
| 31 | + <!-- <template> |
32 | 32 | <div |
33 | 33 | :class="['thinking', { 'thinking--active': isSelect }]" |
34 | 34 | @click="$emit('update:is-select', !isSelect)" |
35 | 35 | > |
36 | 36 | <i class="el-icon-plus"></i> |
37 | 37 | <span>深度思考</span> |
38 | 38 | </div> |
39 | | - </template> |
| 39 | + </template> --> |
40 | 40 | </div> |
41 | 41 | </template> |
42 | 42 |
|
43 | 43 | <template #action-list> |
44 | 44 | <div style="display: flex; align-items: center; gap: 8px"> |
| 45 | + <!-- 录音按钮 --> |
| 46 | + |
| 47 | + <el-button |
| 48 | + circle |
| 49 | + :class="[ |
| 50 | + 'record-btn', |
| 51 | + { 'record-btn--active': recordLoading, 'record-btn--error': recordError }, |
| 52 | + ]" |
| 53 | + :style=" |
| 54 | + recordLoading |
| 55 | + ? 'background: #ff6b6b; color: #fff' |
| 56 | + : 'background: #52c41a; color: #fff' |
| 57 | + " |
| 58 | + size="small" |
| 59 | + @click="handleRecord" |
| 60 | + :disabled="loading" |
| 61 | + > |
| 62 | + <i :class="recordLoading ? 'el-icon-close' : 'el-icon-microphone'"></i> |
| 63 | + </el-button> |
| 64 | + |
45 | 65 | <el-button |
46 | 66 | v-if="loading" |
47 | | - round |
| 67 | + circle |
48 | 68 | class="stop-btn" |
49 | 69 | style="background: #f56c6c; color: #fff" |
50 | 70 | size="small" |
|
54 | 74 | </el-button> |
55 | 75 | <el-button |
56 | 76 | v-else |
57 | | - round |
| 77 | + circle |
58 | 78 | class="send-btn" |
59 | 79 | style="background: #626aef; color: #fff" |
60 | 80 | size="small" |
|
73 | 93 | </template> |
74 | 94 |
|
75 | 95 | <script> |
| 96 | + import { recordMixin } from 'vue-element-ui-x'; |
76 | 97 | import SenderHeader from './SenderHeader.vue'; |
77 | 98 |
|
78 | 99 | export default { |
79 | 100 | name: 'ChatInput', |
80 | | - components: { |
81 | | - SenderHeader, |
82 | | - }, |
| 101 | + components: { SenderHeader }, |
| 102 | + mixins: [recordMixin], |
83 | 103 | props: { |
84 | 104 | value: { |
85 | 105 | type: String, |
|
98 | 118 | default: () => [], |
99 | 119 | }, |
100 | 120 | }, |
| 121 | + data() { |
| 122 | + return { |
| 123 | + recordError: null, |
| 124 | + recordAnimating: false, |
| 125 | + }; |
| 126 | + }, |
101 | 127 | emits: ['input', 'send', 'stop', 'file-upload', 'delete-file', 'update:is-select'], |
| 128 | + methods: { |
| 129 | + handleRecord() { |
| 130 | + if (this.recordLoading) { |
| 131 | + this.stopRecord(); |
| 132 | + } else { |
| 133 | + this.recordError = null; |
| 134 | + this.startRecord(); |
| 135 | + } |
| 136 | + }, |
| 137 | +
|
| 138 | + handleRecordResult(value) { |
| 139 | + if (value && value.trim()) { |
| 140 | + // 如果输入框已有内容,追加到末尾,否则直接设置 |
| 141 | + const currentValue = this.value || ''; |
| 142 | + const newValue = currentValue ? `${currentValue} ${value}` : value; |
| 143 | + this.$emit('input', newValue); |
| 144 | + } |
| 145 | + }, |
| 146 | +
|
| 147 | + handleRecordError(error) { |
| 148 | + this.recordError = error; |
| 149 | + this.recordAnimating = true; |
| 150 | + setTimeout(() => { |
| 151 | + this.recordAnimating = false; |
| 152 | + }, 500); |
| 153 | +
|
| 154 | + // 显示用户友好的错误信息 |
| 155 | + let errorMessage = '录音失败'; |
| 156 | + if (error.message) { |
| 157 | + if (error.message.includes('not-allowed')) { |
| 158 | + errorMessage = '请允许麦克风权限后重试'; |
| 159 | + } else if (error.message.includes('network')) { |
| 160 | + errorMessage = '网络错误,请检查网络连接'; |
| 161 | + } else if (error.message.includes('no-speech')) { |
| 162 | + errorMessage = '未检测到语音,请重试'; |
| 163 | + } else { |
| 164 | + errorMessage = error.message; |
| 165 | + } |
| 166 | + } |
| 167 | +
|
| 168 | + this.$message && this.$message.error(errorMessage); |
| 169 | + }, |
| 170 | + }, |
102 | 171 | watch: { |
103 | 172 | uploadedFiles: { |
104 | 173 | handler(newFiles) { |
|
118 | 187 | immediate: true, |
119 | 188 | }, |
120 | 189 | }, |
| 190 | + mounted() { |
| 191 | + // 初始化录音功能 |
| 192 | + this.initRecord({ |
| 193 | + onStart: () => { |
| 194 | + this.recordAnimating = true; |
| 195 | + console.log('开始录音'); |
| 196 | + }, |
| 197 | + onEnd: value => { |
| 198 | + this.recordAnimating = false; |
| 199 | + this.handleRecordResult(value); |
| 200 | + console.log('录音结束', value); |
| 201 | + }, |
| 202 | + onError: error => { |
| 203 | + this.handleRecordError(error); |
| 204 | + console.error('录音错误', error); |
| 205 | + }, |
| 206 | + onResult: result => { |
| 207 | + // 实时识别结果处理(可选) |
| 208 | + console.log('实时识别结果:', result); |
| 209 | + }, |
| 210 | + }); |
| 211 | + }, |
| 212 | + beforeDestroy() { |
| 213 | + // 组件销毁时清理录音资源 |
| 214 | + if (this.cleanupRecord) { |
| 215 | + this.cleanupRecord(); |
| 216 | + } |
| 217 | + }, |
121 | 218 | }; |
122 | 219 | </script> |
123 | 220 |
|
124 | 221 | <style lang="scss" scoped> |
| 222 | + .el-button + .el-button, |
| 223 | + .el-checkbox.is-bordered + .el-checkbox.is-bordered { |
| 224 | + margin-left: 0 !important; |
| 225 | + } |
125 | 226 | .chat-input { |
126 | 227 | flex-shrink: 0; |
127 | 228 | flex-direction: column; |
|
183 | 284 | } |
184 | 285 | } |
185 | 286 |
|
| 287 | + // 录音按钮样式 |
| 288 | + .record-btn { |
| 289 | + position: relative; |
| 290 | + transition: all 0.3s ease; |
| 291 | +
|
| 292 | + &:hover { |
| 293 | + transform: scale(1.05); |
| 294 | + } |
| 295 | +
|
| 296 | + // 录音激活状态 - 呼吸灯效果 |
| 297 | + &--active { |
| 298 | + animation: recordPulse 1.5s ease-in-out infinite; |
| 299 | +
|
| 300 | + &::after { |
| 301 | + content: ''; |
| 302 | + position: absolute; |
| 303 | + top: -2px; |
| 304 | + left: -2px; |
| 305 | + right: -2px; |
| 306 | + bottom: -2px; |
| 307 | + border-radius: 50%; |
| 308 | + background: rgba(255, 107, 107, 0.3); |
| 309 | + animation: recordRipple 1.5s ease-out infinite; |
| 310 | + } |
| 311 | + } |
| 312 | +
|
| 313 | + // 错误状态 |
| 314 | + &--error { |
| 315 | + animation: recordShake 0.5s ease-in-out; |
| 316 | + } |
| 317 | + } |
| 318 | +
|
| 319 | + // 录音按钮呼吸灯动画 |
| 320 | + @keyframes recordPulse { |
| 321 | + 0%, |
| 322 | + 100% { |
| 323 | + box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.7); |
| 324 | + } |
| 325 | + 50% { |
| 326 | + box-shadow: 0 0 0 8px rgba(255, 107, 107, 0.1); |
| 327 | + } |
| 328 | + } |
| 329 | +
|
| 330 | + // 录音按钮波纹动画 |
| 331 | + @keyframes recordRipple { |
| 332 | + 0% { |
| 333 | + transform: scale(1); |
| 334 | + opacity: 0.6; |
| 335 | + } |
| 336 | + 100% { |
| 337 | + transform: scale(1.5); |
| 338 | + opacity: 0; |
| 339 | + } |
| 340 | + } |
| 341 | +
|
| 342 | + // 错误震动动画 |
| 343 | + @keyframes recordShake { |
| 344 | + 0%, |
| 345 | + 100% { |
| 346 | + transform: translateX(0); |
| 347 | + } |
| 348 | + 25% { |
| 349 | + transform: translateX(-3px); |
| 350 | + } |
| 351 | + 75% { |
| 352 | + transform: translateX(3px); |
| 353 | + } |
| 354 | + } |
| 355 | +
|
186 | 356 | @media (max-width: 767px) { |
187 | 357 | .chat-input { |
188 | 358 | padding: 0 16px 16px; |
|
0 commit comments