Files
YGChatCS/src/pages/index/components/chat/ChatInputArea/index.vue

316 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="input-area-wrapper">
<view v-if="!visibleWaveBtn" class="area-input">
<!-- 语音/键盘切换 -->
<view class="input-container-voice" @click="toggleVoiceMode">
<image
class="voice-icon"
v-if="!isVoiceMode"
src="https://oss.nianxx.cn/mp/static/version_101/home/input_voice_icon.png"
/>
<image
class="voice-icon"
v-else
src="https://oss.nianxx.cn/mp/static/version_101/home/input_keyboard_icon.png"
/>
</view>
<!-- 输入框/语音按钮容器 -->
<view class="input-button-container">
<textarea
ref="textareaRef"
v-if="!isVoiceMode"
class="textarea"
type="text"
cursor-spacing="20"
confirm-type="send"
v-model="inputMessage"
auto-height
:confirm-hold="true"
:placeholder="placeholder"
:show-confirm-bar="false"
:hold-keyboard="holdKeyboard"
:adjust-position="true"
:disable-default-padding="true"
maxlength="300"
@confirm="sendMessage"
@focus="handleFocus"
@blur="handleBlur"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
/>
<view
v-if="isVoiceMode"
class="hold-to-talk-button"
@longpress="handleVoiceTouchStart"
@touchend="handleVoiceTouchEnd"
@touchcancel="handleVoiceTouchEnd"
>
按住 说话
</view>
</view>
<view class="input-container-send">
<view class="input-container-send-btn" @click="sendMessage">
<view v-if="props.isSessionActive" class="send-stop"> </view>
<image
v-else
class="send-icon"
src="https://oss.nianxx.cn/mp/static/version_101/home/input_send_icon.png"
/>
</view>
</view>
</view>
<!-- 录音按钮 -->
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
<view class="color-99A0AE font-size-9 text-center text-gray-400">
内容由AI大模型生成请仔细鉴别
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted, defineExpose, onUnmounted } from "vue";
import RecordingWaveBtn from "@/components/Speech/RecordingWaveBtn.vue";
import { getCurrentConfig } from "@/constant/base";
const plugin = requirePlugin("WechatSI");
const manager = plugin.getRecordRecognitionManager();
const props = defineProps({
modelValue: String,
holdKeyboard: Boolean,
isSessionActive: Boolean,
stopRequest: Function,
});
const emit = defineEmits([
"update:modelValue",
"send",
"noHideKeyboard",
"keyboardShow",
"keyboardHide",
"sendVoice",
]);
const textareaRef = ref(null);
const recordingWaveBtnRef = ref(null);
const placeholder = computed(() => getCurrentConfig().placeholder);
const inputMessage = ref(props.modelValue || "");
const isFocused = ref(false);
const keyboardHeight = ref(0);
const isVoiceMode = ref(false);
const visibleWaveBtn = ref(false);
const isRecording = ref(false);
let watchDogTimer = null;
const resetUI = () => {
isRecording.value = false;
visibleWaveBtn.value = false;
try {
if (recordingWaveBtnRef.value) {
recordingWaveBtnRef.value.stopAnimation();
}
} catch (e) {
console.error("resetUI stopAnimation error", e);
}
if (watchDogTimer) {
clearTimeout(watchDogTimer);
watchDogTimer = null;
}
};
const startWatchDog = (timeout = 10000) => {
if (watchDogTimer) clearTimeout(watchDogTimer);
watchDogTimer = setTimeout(() => {
console.warn("recording watchdog triggered, forcing UI reset");
resetUI();
}, timeout);
};
// 保持和父组件同步
watch(
() => props.modelValue,
(val) => {
inputMessage.value = val;
}
);
// 当子组件的 inputMessage 变化时,通知父组件(但要避免循环更新)
watch(inputMessage, (val) => {
// 只有当值真正不同时才emit避免循环更新
if (val !== props.modelValue) {
emit("update:modelValue", val);
}
});
// 切换语音/文本模式
const toggleVoiceMode = () => {
isVoiceMode.value = !isVoiceMode.value;
};
// 处理语音按钮长按开始
const handleVoiceTouchStart = () => {
try {
manager.start({ lang: "zh_CN" });
isRecording.value = true;
visibleWaveBtn.value = true;
// 启动音频条动画
nextTick(() => {
if (recordingWaveBtnRef.value) {
recordingWaveBtnRef.value.startAnimation();
}
});
startWatchDog(10000);
} catch (err) {
console.error("record start error:", err);
// 保底清理
resetUI();
}
};
// 处理语音按钮长按结束
const handleVoiceTouchEnd = () => {
// 如果本地状态不是录音中,也确保 UI 恢复
if (!isRecording.value) {
if (recordingWaveBtnRef.value) {
recordingWaveBtnRef.value.stopAnimation();
}
visibleWaveBtn.value = false;
if (watchDogTimer) {
clearTimeout(watchDogTimer);
watchDogTimer = null;
}
return;
}
try {
manager.stop();
} catch (err) {
console.error("record stop error:", err);
} finally {
// 无论 stop 是否抛错,都保证 UI 恢复
resetUI();
}
};
// 处理发送原语音
const initRecord = () => {
manager.onRecognize = (res) => {
let text = res.result || "";
inputMessage.value = text;
};
// 识别结束事件
manager.onStop = (res) => {
console.log(res, 37);
let text = (res && res.result) || "";
// 保证 UI 恢复(防止未走 handleVoiceTouchEnd
resetUI();
if (text === "") {
console.log("没有说话");
return;
}
inputMessage.value = text;
// 在语音识别完成后发送消息
emit("send", text);
};
// 错误处理,确保 UI 重置
manager.onError = (err) => {
console.error("record manager error", err);
resetUI();
};
};
// 监听键盘高度变化
onMounted(() => {
// 监听键盘弹起
uni.onKeyboardHeightChange((res) => {
keyboardHeight.value = res.height;
if (res.height) {
emit("keyboardShow", res.height);
} else {
emit("keyboardHide");
}
});
initRecord();
});
onUnmounted(() => {
try {
manager.stop && manager.stop();
} catch (e) {
// ignore
}
manager.onRecognize = null;
manager.onStop = null;
manager.onError = null;
resetUI();
});
const sendMessage = () => {
if (props.isSessionActive) {
// 如果会话进行中,调用停止请求函数
if (props.stopRequest) {
props.stopRequest();
}
} else {
// 否则发送新消息
if (!inputMessage.value.trim()) return;
emit("send", inputMessage.value);
// 发送后保持焦点(可选)
if (props.holdKeyboard && textareaRef.value) {
nextTick(() => {
textareaRef.value.focus();
});
}
}
};
const handleFocus = () => {
isFocused.value = true;
emit("noHideKeyboard");
};
const handleBlur = () => {
isFocused.value = false;
};
const handleTouchStart = () => {
emit("noHideKeyboard");
};
const handleTouchEnd = () => {
emit("noHideKeyboard");
};
// 手动聚焦输入框
const focusInput = () => {
if (textareaRef.value) {
textareaRef.value.focus();
}
};
// 手动失焦输入框
const blurInput = () => {
if (textareaRef.value) {
textareaRef.value.blur();
}
};
// 暴露方法给父组件
defineExpose({ focusInput });
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>