Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
水玉婷
ai-wechat
Commits
f4859448
Commit
f4859448
authored
Dec 02, 2025
by
水玉婷
Browse files
feat:更新语音组件交互
parent
54a022a9
Changes
1
Hide whitespace changes
Inline
Side-by-side
src/views/components/VoiceRecognition.vue
View file @
f4859448
...
@@ -10,11 +10,11 @@
...
@@ -10,11 +10,11 @@
'recording': isRecording,
'recording': isRecording,
'disabled': disabled
'disabled': disabled
}"
}"
@click="
toggleFullScreen
"
@click="
handleClick
"
@mousedown="
startRecording
"
@mousedown="
handleMouseDown
"
@mouseup="stopRecording"
@mouseup="stopRecording"
@mouseleave="stopRecording"
@mouseleave="stopRecording"
@touchstart="
startRecording
"
@touchstart="
handleTouchStart
"
@touchend="stopRecording"
@touchend="stopRecording"
@touchcancel="stopRecording"
@touchcancel="stopRecording"
:disabled="disabled"
:disabled="disabled"
...
@@ -91,6 +91,57 @@ const mediaRecorder = ref<MediaRecorder | null>(null)
...
@@ -91,6 +91,57 @@ const mediaRecorder = ref<MediaRecorder | null>(null)
const
audioChunks
=
ref
<
Blob
[]
>
([])
const
audioChunks
=
ref
<
Blob
[]
>
([])
const
audioStream
=
ref
<
MediaStream
|
null
>
(
null
)
const
audioStream
=
ref
<
MediaStream
|
null
>
(
null
)
// 全局音频流缓存(单例模式)
let
globalAudioStream
:
MediaStream
|
null
=
null
// 获取或创建音频流(单例模式)
const
getOrCreateAudioStream
=
async
():
Promise
<
MediaStream
>
=>
{
// 检查是否有活跃的音频流
if
(
globalAudioStream
)
{
// 检查流是否仍然活跃
const
activeTracks
=
globalAudioStream
.
getAudioTracks
().
filter
(
track
=>
track
.
readyState
===
'
live
'
)
if
(
activeTracks
.
length
>
0
)
{
if
(
props
.
debug
)
{
console
.
log
(
'
复用现有音频流
'
)
}
return
globalAudioStream
}
else
{
// 流已失效,清理缓存
globalAudioStream
=
null
}
}
// 创建新的音频流
if
(
props
.
debug
)
{
console
.
log
(
'
创建新的音频流
'
)
}
const
stream
=
await
navigator
.
mediaDevices
.
getUserMedia
({
audio
:
{
echoCancellation
:
true
,
noiseSuppression
:
true
,
sampleRate
:
44100
}
}
)
// 缓存音频流
globalAudioStream
=
stream
// 监听轨道结束事件,清理缓存
stream
.
getAudioTracks
().
forEach
(
track
=>
{
track
.
addEventListener
(
'
ended
'
,
()
=>
{
if
(
globalAudioStream
===
stream
)
{
globalAudioStream
=
null
if
(
props
.
debug
)
{
console
.
log
(
'
音频流已结束,清理缓存
'
)
}
}
}
)
}
)
return
stream
}
// 格式化录音时长显示(分:秒)
// 格式化录音时长显示(分:秒)
const
formatDuration
=
(
seconds
:
number
)
=>
{
const
formatDuration
=
(
seconds
:
number
)
=>
{
const
minutes
=
Math
.
floor
(
seconds
/
60
)
const
minutes
=
Math
.
floor
(
seconds
/
60
)
...
@@ -134,6 +185,67 @@ const exitFullScreen = () => {
...
@@ -134,6 +185,67 @@ const exitFullScreen = () => {
isFullScreen
.
value
=
false
isFullScreen
.
value
=
false
}
}
// 点击事件处理
const
handleClick
=
()
=>
{
if
(
props
.
disabled
||
isRecording
.
value
)
return
// 如果不在全屏模式,进入全屏模式
if
(
!
isFullScreen
.
value
)
{
isFullScreen
.
value
=
true
// 在企业微信中,进入全屏时异步预加载音频流
// 这样用户点击"按住说话"时就不会再弹出授权对话框
if
(
props
.
debug
)
{
console
.
log
(
'
进入全屏模式,异步预加载音频流
'
)
}
// 异步预加载音频流,不阻塞UI
setTimeout
(
async
()
=>
{
try
{
// 预加载音频流,但不开始录音
const
stream
=
await
getOrCreateAudioStream
()
if
(
props
.
debug
)
{
console
.
log
(
'
音频流预加载完成
'
)
}
// 预加载成功后,立即停止音频轨道,避免自动开始录音
// 但保持流缓存,这样用户点击"按住说话"时可以直接复用
stream
.
getAudioTracks
().
forEach
(
track
=>
{
track
.
stop
()
}
)
// 清理缓存,因为轨道已停止
globalAudioStream
=
null
if
(
props
.
debug
)
{
console
.
log
(
'
音频流预加载完成,轨道已停止,缓存已清理
'
)
}
}
catch
(
error
)
{
if
(
props
.
debug
)
{
console
.
warn
(
'
音频流预加载失败:
'
,
error
)
}
// 预加载失败不影响后续操作,用户点击"按住说话"时会重新尝试
}
}
,
0
)
}
}
// 处理鼠标按下事件
const
handleMouseDown
=
(
event
:
MouseEvent
)
=>
{
if
(
props
.
debug
)
{
console
.
log
(
'
鼠标按下事件触发
'
)
}
startRecording
()
}
// 处理触摸开始事件
const
handleTouchStart
=
(
event
:
TouchEvent
)
=>
{
if
(
props
.
debug
)
{
console
.
log
(
'
触摸开始事件触发
'
)
}
startRecording
()
}
// 检查浏览器是否支持MediaRecorder
// 检查浏览器是否支持MediaRecorder
const
isMediaRecorderSupported
=
()
=>
{
const
isMediaRecorderSupported
=
()
=>
{
const
supported
=
'
MediaRecorder
'
in
window
;
const
supported
=
'
MediaRecorder
'
in
window
;
...
@@ -186,14 +298,8 @@ const startRecording = async () => {
...
@@ -186,14 +298,8 @@ const startRecording = async () => {
}
}
try
{
try
{
// 获取麦克风权限
// 使用缓存音频流(单例模式)
const
stream
=
await
navigator
.
mediaDevices
.
getUserMedia
({
const
stream
=
await
getOrCreateAudioStream
();
audio
:
{
echoCancellation
:
true
,
noiseSuppression
:
true
,
sampleRate
:
44100
}
}
);
audioStream
.
value
=
stream
;
audioStream
.
value
=
stream
;
audioChunks
.
value
=
[];
audioChunks
.
value
=
[];
...
@@ -275,8 +381,8 @@ const stopRecording = async () => {
...
@@ -275,8 +381,8 @@ const stopRecording = async () => {
// 停止录音计时器
// 停止录音计时器
stopRecordingTimer
();
stopRecordingTimer
();
// 停止
所有
音频轨道
// 停止音频轨道
,释放麦克风权限
if
(
audioStream
.
value
)
{
if
(
audioStream
.
value
)
{
audioStream
.
value
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
audioStream
.
value
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
audioStream
.
value
=
null
;
audioStream
.
value
=
null
;
...
@@ -315,7 +421,7 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
...
@@ -315,7 +421,7 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
// 解析响应体为JSON
// 解析响应体为JSON
const
result
=
await
response
.
json
();
const
result
=
await
response
.
json
();
if
(
result
.
data
.
code
===
0
)
{
if
(
result
.
code
===
0
)
{
const
filePath
=
result
.
data
.
filePath
;
const
filePath
=
result
.
data
.
filePath
;
// 计算音频时长(秒),四舍五入取整
// 计算音频时长(秒),四舍五入取整
const
durationTime
=
recordingDuration
.
value
;
const
durationTime
=
recordingDuration
.
value
;
...
@@ -331,6 +437,17 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
...
@@ -331,6 +437,17 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
// 发送录制的音频
// 发送录制的音频
const
sendRecordedAudio
=
async
()
=>
{
const
sendRecordedAudio
=
async
()
=>
{
// 防误触检查:只有录音时长超过0.5秒才发送
if
(
recordingDuration
.
value
<
0.5
)
{
if
(
props
.
debug
)
{
console
.
log
(
'
录音时长过短,视为误触,不发送数据。时长:
'
,
recordingDuration
.
value
,
'
秒
'
);
}
// 清理录音数据并退出全屏
audioChunks
.
value
=
[];
exitFullScreen
();
return
;
}
if
(
audioChunks
.
value
.
length
===
0
)
{
if
(
audioChunks
.
value
.
length
===
0
)
{
emit
(
'
error
'
,
'
录音数据为空
'
);
emit
(
'
error
'
,
'
录音数据为空
'
);
exitFullScreen
();
// 数据为空时退出全屏
exitFullScreen
();
// 数据为空时退出全屏
...
@@ -358,6 +475,19 @@ const sendRecordedAudio = async () => {
...
@@ -358,6 +475,19 @@ const sendRecordedAudio = async () => {
}
finally
{
}
finally
{
// 清理录音数据并退出全屏
// 清理录音数据并退出全屏
audioChunks
.
value
=
[];
audioChunks
.
value
=
[];
// 清理音频流,释放麦克风权限(重要:解决企业微信中麦克风一直开启的问题)
// 但保留全局缓存,以便后续录音可以复用
if
(
audioStream
.
value
)
{
// 只停止轨道,不清理全局缓存,以便后续复用
audioStream
.
value
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
audioStream
.
value
=
null
;
if
(
props
.
debug
)
{
console
.
log
(
'
消息发送完成:音频流轨道已停止,麦克风权限已释放
'
);
}
}
exitFullScreen
();
exitFullScreen
();
}
}
}
}
...
@@ -368,10 +498,21 @@ onUnmounted(() => {
...
@@ -368,10 +498,21 @@ onUnmounted(() => {
stopRecording
();
stopRecording
();
}
}
// 清理当前录音音频流
if
(
audioStream
.
value
)
{
if
(
audioStream
.
value
)
{
audioStream
.
value
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
audioStream
.
value
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
}
}
// 清理全局缓存音频流(重要:防止权限占用)
if
(
globalAudioStream
)
{
globalAudioStream
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
globalAudioStream
=
null
;
if
(
props
.
debug
)
{
console
.
log
(
'
组件卸载:全局音频流已清理
'
);
}
}
// 清理计时器
// 清理计时器
stopRecordingTimer
();
stopRecordingTimer
();
}
)
}
)
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment