Skip to content

Commit 1641a01

Browse files
author
weilei
committed
feat(examples): 添加录音
1 parent 99975db commit 1641a01

File tree

1 file changed

+178
-8
lines changed

1 file changed

+178
-8
lines changed

examples/src/views/DifyChat/components/ChatInput.vue

Lines changed: 178 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</template>
1818

1919
<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">
2121
<el-button
2222
round
2323
plain
@@ -28,23 +28,43 @@
2828
<i class="el-icon-paperclip"></i>
2929
</el-button>
3030

31-
<template>
31+
<!-- <template>
3232
<div
3333
:class="['thinking', { 'thinking--active': isSelect }]"
3434
@click="$emit('update:is-select', !isSelect)"
3535
>
3636
<i class="el-icon-plus"></i>
3737
<span>深度思考</span>
3838
</div>
39-
</template>
39+
</template> -->
4040
</div>
4141
</template>
4242

4343
<template #action-list>
4444
<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+
4565
<el-button
4666
v-if="loading"
47-
round
67+
circle
4868
class="stop-btn"
4969
style="background: #f56c6c; color: #fff"
5070
size="small"
@@ -54,7 +74,7 @@
5474
</el-button>
5575
<el-button
5676
v-else
57-
round
77+
circle
5878
class="send-btn"
5979
style="background: #626aef; color: #fff"
6080
size="small"
@@ -73,13 +93,13 @@
7393
</template>
7494

7595
<script>
96+
import { recordMixin } from 'vue-element-ui-x';
7697
import SenderHeader from './SenderHeader.vue';
7798
7899
export default {
79100
name: 'ChatInput',
80-
components: {
81-
SenderHeader,
82-
},
101+
components: { SenderHeader },
102+
mixins: [recordMixin],
83103
props: {
84104
value: {
85105
type: String,
@@ -98,7 +118,56 @@
98118
default: () => [],
99119
},
100120
},
121+
data() {
122+
return {
123+
recordError: null,
124+
recordAnimating: false,
125+
};
126+
},
101127
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+
},
102171
watch: {
103172
uploadedFiles: {
104173
handler(newFiles) {
@@ -118,10 +187,42 @@
118187
immediate: true,
119188
},
120189
},
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+
},
121218
};
122219
</script>
123220

124221
<style lang="scss" scoped>
222+
.el-button + .el-button,
223+
.el-checkbox.is-bordered + .el-checkbox.is-bordered {
224+
margin-left: 0 !important;
225+
}
125226
.chat-input {
126227
flex-shrink: 0;
127228
flex-direction: column;
@@ -183,6 +284,75 @@
183284
}
184285
}
185286
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+
186356
@media (max-width: 767px) {
187357
.chat-input {
188358
padding: 0 16px 16px;

0 commit comments

Comments
 (0)