feat: 改造语音的实现逻辑
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<view class="input-area-wrapper">
|
||||
<view v-if="!visibleWaveBtn" class="area-input">
|
||||
<view
|
||||
class="input-area-wrapper"
|
||||
@touchend="handleVoiceTouchEndFromContainer"
|
||||
@touchcancel="handleVoiceTouchEndFromContainer"
|
||||
>
|
||||
<view class="area-input">
|
||||
<!-- 语音/键盘切换 -->
|
||||
<view
|
||||
v-if="isSpeechRecognitionSupported"
|
||||
@@ -51,11 +55,16 @@
|
||||
<view
|
||||
v-if="isVoiceMode"
|
||||
class="hold-to-talk-button"
|
||||
@longpress="handleVoiceTouchStart"
|
||||
@touchstart="handleVoiceTouchStart"
|
||||
@touchend="handleVoiceTouchEnd"
|
||||
@touchcancel="handleVoiceTouchEnd"
|
||||
>
|
||||
按住 说话
|
||||
<RecordingWaveBtn
|
||||
v-if="visibleWaveBtn"
|
||||
class="recording-wave-inline"
|
||||
ref="recordingWaveBtnRef"
|
||||
/>
|
||||
<text v-else>按住 说话</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -71,12 +80,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 录音按钮 -->
|
||||
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
|
||||
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<yao-asdRealSpeech
|
||||
v-if="isSpeechRecognitionSupported"
|
||||
v-if="isSpeechRecognitionSupported && appSpeechVisible"
|
||||
:key="appSpeechKey"
|
||||
ref="appSpeechRef"
|
||||
:options="appSpeechOptions"
|
||||
@result="handleAppSpeechResult"
|
||||
@@ -98,7 +105,7 @@ import { appSpeechRecognitionOptions } from "@/constant/speech";
|
||||
|
||||
let manager = null;
|
||||
let speechProvider = "";
|
||||
const isSpeechRecognitionEnabled = ref(false);
|
||||
const isSpeechRecognitionEnabled = ref(true);
|
||||
const isSpeechRecognitionSupported = ref(false);
|
||||
const appSpeechOptions = appSpeechRecognitionOptions;
|
||||
|
||||
@@ -134,6 +141,8 @@ const emit = defineEmits([
|
||||
const textareaRef = ref(null);
|
||||
const recordingWaveBtnRef = ref(null);
|
||||
const appSpeechRef = ref(null);
|
||||
const appSpeechKey = ref(0);
|
||||
const appSpeechVisible = ref(true);
|
||||
const placeholder = computed(() => {
|
||||
const config = getCurrentConfig();
|
||||
return `快告诉${config.name}您在想什么~`;
|
||||
@@ -144,7 +153,10 @@ const keyboardHeight = ref(0);
|
||||
const isVoiceMode = ref(false);
|
||||
const visibleWaveBtn = ref(false);
|
||||
const isRecording = ref(false);
|
||||
const isVoicePressing = ref(false);
|
||||
const appRecognizedText = ref("");
|
||||
const isAppSpeechStarting = ref(false);
|
||||
const isAppWaitingToSend = ref(false);
|
||||
let watchDogTimer = null;
|
||||
let appStopFallbackTimer = null;
|
||||
let hasSentAppRecognition = false;
|
||||
@@ -168,7 +180,12 @@ const resetUI = () => {
|
||||
const startWatchDog = (timeout = 10000) => {
|
||||
if (watchDogTimer) clearTimeout(watchDogTimer);
|
||||
watchDogTimer = setTimeout(() => {
|
||||
watchDogTimer = null;
|
||||
console.warn("recording watchdog triggered, forcing UI reset");
|
||||
if (hasActiveVoiceRecognition()) {
|
||||
stopActiveVoiceRecognition({ shouldSend: true });
|
||||
return;
|
||||
}
|
||||
resetUI();
|
||||
}, timeout);
|
||||
};
|
||||
@@ -180,6 +197,48 @@ const clearAppStopFallback = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const showRecordingUI = () => {
|
||||
isRecording.value = true;
|
||||
visibleWaveBtn.value = true;
|
||||
|
||||
nextTick(() => {
|
||||
if (recordingWaveBtnRef.value) {
|
||||
recordingWaveBtnRef.value.startAnimation();
|
||||
}
|
||||
});
|
||||
startWatchDog(10000);
|
||||
};
|
||||
|
||||
const resetAppSpeechComponent = () => {
|
||||
if (speechProvider !== "app") return;
|
||||
|
||||
isAppSpeechStarting.value = false;
|
||||
appSpeechVisible.value = false;
|
||||
nextTick(() => {
|
||||
appSpeechKey.value += 1;
|
||||
appSpeechVisible.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const getPendingAppSpeechText = () => {
|
||||
return (appRecognizedText.value || inputMessage.value || "").trim();
|
||||
};
|
||||
|
||||
const scheduleAppRecognizedTextSend = (timeout = 1200) => {
|
||||
isAppWaitingToSend.value = true;
|
||||
clearAppStopFallback();
|
||||
|
||||
if (getPendingAppSpeechText()) {
|
||||
sendAppRecognizedText();
|
||||
return;
|
||||
}
|
||||
|
||||
appStopFallbackTimer = setTimeout(() => {
|
||||
appStopFallbackTimer = null;
|
||||
sendAppRecognizedText();
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
// 保持和父组件同步
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -199,17 +258,24 @@ watch(inputMessage, (val) => {
|
||||
// 切换语音/文本模式
|
||||
const toggleVoiceMode = () => {
|
||||
if (!isSpeechRecognitionSupported.value) return;
|
||||
if (isVoiceMode.value) {
|
||||
stopActiveVoiceRecognition({ shouldSend: false });
|
||||
}
|
||||
isVoiceMode.value = !isVoiceMode.value;
|
||||
};
|
||||
|
||||
// 处理语音按钮长按开始
|
||||
// 处理语音按钮按下开始
|
||||
const handleVoiceTouchStart = () => {
|
||||
if (!isSpeechRecognitionSupported.value) return;
|
||||
if (isRecording.value || isAppSpeechStarting.value || visibleWaveBtn.value) return;
|
||||
|
||||
isVoicePressing.value = true;
|
||||
|
||||
try {
|
||||
if (speechProvider === "wechat") {
|
||||
if (!manager) return;
|
||||
manager.start({ lang: "zh_CN" });
|
||||
showRecordingUI();
|
||||
} else if (speechProvider === "app") {
|
||||
if (!appSpeechOptions.apikey) {
|
||||
uni.showToast({
|
||||
@@ -220,10 +286,15 @@ const handleVoiceTouchStart = () => {
|
||||
}
|
||||
|
||||
appRecognizedText.value = "";
|
||||
inputMessage.value = "";
|
||||
isAppWaitingToSend.value = false;
|
||||
hasSentAppRecognition = false;
|
||||
clearAppStopFallback();
|
||||
isAppSpeechStarting.value = true;
|
||||
|
||||
const appSpeech = appSpeechRef.value;
|
||||
if (!appSpeech || typeof appSpeech.start !== "function") {
|
||||
isAppSpeechStarting.value = false;
|
||||
uni.showToast({
|
||||
title: "语音组件未初始化",
|
||||
icon: "none",
|
||||
@@ -235,31 +306,65 @@ const handleVoiceTouchStart = () => {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
isRecording.value = true;
|
||||
visibleWaveBtn.value = true;
|
||||
|
||||
// 启动音频条动画
|
||||
nextTick(() => {
|
||||
if (recordingWaveBtnRef.value) {
|
||||
recordingWaveBtnRef.value.startAnimation();
|
||||
}
|
||||
});
|
||||
startWatchDog(10000);
|
||||
} catch (err) {
|
||||
console.error("record start error:", err);
|
||||
isAppSpeechStarting.value = false;
|
||||
// 保底清理
|
||||
resetUI();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理语音按钮长按结束
|
||||
const handleVoiceTouchEnd = () => {
|
||||
const hasActiveVoiceRecognition = () => {
|
||||
return (
|
||||
isVoicePressing.value ||
|
||||
isRecording.value ||
|
||||
isAppSpeechStarting.value ||
|
||||
visibleWaveBtn.value
|
||||
);
|
||||
};
|
||||
|
||||
const stopActiveVoiceRecognition = ({ shouldSend = true } = {}) => {
|
||||
isVoicePressing.value = false;
|
||||
|
||||
if (!isSpeechRecognitionSupported.value) {
|
||||
resetUI();
|
||||
return;
|
||||
}
|
||||
|
||||
if (speechProvider === "app") {
|
||||
const wasStarting = isAppSpeechStarting.value;
|
||||
const wasRecording = isRecording.value;
|
||||
|
||||
if (!wasStarting && !wasRecording) {
|
||||
if (!shouldSend) {
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
resetUI();
|
||||
return false;
|
||||
}
|
||||
|
||||
isAppSpeechStarting.value = false;
|
||||
|
||||
try {
|
||||
appSpeechRef.value?.stop?.();
|
||||
if (wasRecording && shouldSend) {
|
||||
scheduleAppRecognizedTextSend();
|
||||
} else {
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("record stop error:", err);
|
||||
resetAppSpeechComponent();
|
||||
} finally {
|
||||
resetUI();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果本地状态不是录音中,也确保 UI 恢复
|
||||
if (!isRecording.value) {
|
||||
if (recordingWaveBtnRef.value) {
|
||||
@@ -270,26 +375,28 @@ const handleVoiceTouchEnd = () => {
|
||||
clearTimeout(watchDogTimer);
|
||||
watchDogTimer = null;
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (speechProvider === "wechat") {
|
||||
manager?.stop?.();
|
||||
} else if (speechProvider === "app") {
|
||||
appSpeechRef.value?.stop?.();
|
||||
clearAppStopFallback();
|
||||
appStopFallbackTimer = setTimeout(() => {
|
||||
appStopFallbackTimer = null;
|
||||
sendAppRecognizedText();
|
||||
}, 600);
|
||||
}
|
||||
manager?.stop?.();
|
||||
} catch (err) {
|
||||
console.error("record stop error:", err);
|
||||
} finally {
|
||||
// 无论 stop 是否抛错,都保证 UI 恢复
|
||||
resetUI();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 处理语音按钮长按结束
|
||||
const handleVoiceTouchEnd = () => {
|
||||
stopActiveVoiceRecognition({ shouldSend: true });
|
||||
};
|
||||
|
||||
const handleVoiceTouchEndFromContainer = () => {
|
||||
if (!hasActiveVoiceRecognition()) return;
|
||||
stopActiveVoiceRecognition({ shouldSend: true });
|
||||
};
|
||||
|
||||
// 处理发送原语音
|
||||
@@ -336,46 +443,87 @@ const getAppSpeechText = (res) => {
|
||||
};
|
||||
|
||||
const sendAppRecognizedText = () => {
|
||||
if (hasSentAppRecognition) return;
|
||||
if (hasSentAppRecognition) return false;
|
||||
|
||||
const text = appRecognizedText.value.trim();
|
||||
const text = getPendingAppSpeechText();
|
||||
if (!text) {
|
||||
console.log("没有说话");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
hasSentAppRecognition = true;
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
inputMessage.value = text;
|
||||
emit("send", text);
|
||||
appRecognizedText.value = "";
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleAppSpeechResult = (res) => {
|
||||
if (hasSentAppRecognition) return;
|
||||
|
||||
const text = getAppSpeechText(res);
|
||||
if (!text) return;
|
||||
|
||||
appRecognizedText.value = text;
|
||||
inputMessage.value = text;
|
||||
|
||||
if (isAppWaitingToSend.value) {
|
||||
sendAppRecognizedText();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAppSpeechChange = (msg) => {
|
||||
if (!msg || !msg.status) return;
|
||||
|
||||
if (msg.status === "START") {
|
||||
isRecording.value = true;
|
||||
isAppSpeechStarting.value = false;
|
||||
if (!isVoicePressing.value) {
|
||||
appSpeechRef.value?.stop?.();
|
||||
resetAppSpeechComponent();
|
||||
resetUI();
|
||||
return;
|
||||
}
|
||||
|
||||
showRecordingUI();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "STOP") {
|
||||
isAppSpeechStarting.value = false;
|
||||
resetUI();
|
||||
if (!sendAppRecognizedText() && !isAppWaitingToSend.value) {
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "FINISH") {
|
||||
isAppSpeechStarting.value = false;
|
||||
resetUI();
|
||||
sendAppRecognizedText();
|
||||
resetAppSpeechComponent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "CLOSE") {
|
||||
isAppSpeechStarting.value = false;
|
||||
resetUI();
|
||||
if (isAppWaitingToSend.value) {
|
||||
sendAppRecognizedText();
|
||||
}
|
||||
resetAppSpeechComponent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "ERROR") {
|
||||
console.error("app speech recognition error", msg.msg);
|
||||
isAppSpeechStarting.value = false;
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
resetUI();
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -413,6 +561,9 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
clearAppStopFallback();
|
||||
isAppSpeechStarting.value = false;
|
||||
isAppWaitingToSend.value = false;
|
||||
isVoicePressing.value = false;
|
||||
resetUI();
|
||||
});
|
||||
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.recording-wave-inline {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.input-container-send {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user