Files
YGChatCS/pages/chat/ChatInputArea.vue
2025-08-07 14:47:18 +08:00

398 lines
9.2 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="area-input">
<!-- 语音/键盘切换 -->
<view class="input-container-voice" @click="toggleVoiceMode">
<image v-if="!isVoiceMode" src='/static/input_voice_icon.png'></image>
<image v-else src='/static/input_keyboard_icon.png'></image>
</view>
<!-- 输入框/语音按钮容器 -->
<view class="input-button-container">
<textarea ref="textareaRef" class="textarea" type="text" :placeholder="placeholder" cursor-spacing="65"
confirm-type='done' v-model="inputMessage" @confirm="sendMessage" @focus="handleFocus" @blur="handleBlur"
@touchstart="handleTouchStart" @touchend="handleTouchEnd" :confirm-hold="true" auto-height
:show-confirm-bar='false' :hold-keyboard="holdKeyboard" :adjust-position="true" maxlength="300" />
<!-- <view
v-if="isVoiceMode"
class="hold-to-talk-button"
@touchstart.stop="startRecording"
@touchend.stop="stopRecording"
@touchmove.stop="handleTouchMove"
>
按住说话
</view> -->
<view v-if="isVoiceMode" class="hold-to-talk-button" @click.stop="startRecording">
按住说话
</view>
</view>
<view class="input-container-send">
<view class="input-container-send-btn" @click="sendMessage">
<image v-if="props.isSessionActive" src='/static/input_stop_icon.png'></image>
<image v-else src='/static/input_send_icon.png'></image>
</view>
</view>
</view>
<!-- 使用封装的弹窗组件 -->
<RecordingPopup ref="recordingPopupRef" :is-slide-to-text="isSlideToText" @cancel="handleRecordingCancel" />
<VoiceResultPopup ref="voiceResultPopupRef" :voice-text="voiceText" @cancel="cancelVoice" @sendVoice="handleSendVoice"
@sendText="handleSendText" />
</template>
<script setup>
import { ref, watch, nextTick, onMounted } from 'vue'
import RecordingPopup from '@/components/Speech/RecordingPopup.vue'
import VoiceResultPopup from '@/components/Speech/VoiceResultPopup.vue'
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 placeholder = ref('快告诉朵朵您在想什么~')
const inputMessage = ref(props.modelValue || '')
const isFocused = ref(false)
const keyboardHeight = ref(0)
const isVoiceMode = ref(false)
const isRecording = ref(false)
const recordingTime = ref(0)
const recordingTimer = ref(null)
const voiceText = ref('')
const showVoiceResult = ref(false)
const isSlideToText = ref(false)
const recordingPopupRef = ref(null)
const voiceResultPopupRef = ref(null)
// 保持和父组件同步
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 startRecording = () => {
console.log('startRecording')
isRecording.value = true
recordingTime.value = 0
// 启动录音计时器
recordingTimer.value = setInterval(() => {
recordingTime.value += 1
}, 1000)
// 打开录音弹窗
if (recordingPopupRef.value) {
recordingPopupRef.value.open()
}
// 调用uni-app录音API
uni.startRecord({
success: (res) => {
// 录音成功,处理录音文件
const tempFilePath = res.tempFilePath
// 这里可以添加语音转文字的逻辑
// 模拟语音转文字
setTimeout(() => {
voiceText.value = '这是语音转文字的结果'
showVoiceResult.value = true
// 打开语音结果弹窗
if (voiceResultPopupRef.value) {
voiceResultPopupRef.value.open()
}
}, 1000)
},
fail: (err) => {
console.error('录音失败:', err)
isRecording.value = false
clearInterval(recordingTimer.value)
if (recordingPopupRef.value) {
recordingPopupRef.value.close()
}
}
})
}
// 处理录音弹窗取消
const handleRecordingCancel = () => {
isRecording.value = false
clearInterval(recordingTimer.value)
uni.stopRecord()
}
// 结束录音
const stopRecording = () => {
isRecording.value = false
clearInterval(recordingTimer.value)
// 关闭录音弹窗
if (recordingPopupRef.value) {
recordingPopupRef.value.close()
}
// 如果录音时间小于1秒取消录音
if (recordingTime.value < 1) {
uni.stopRecord()
return
}
// 停止录音
uni.stopRecord()
}
// 处理发送原语音
const handleSendVoice = (data) => {
// 发送语音逻辑
emit('sendVoice', {
text: data.text,
// 可以添加语音文件路径等信息
})
showVoiceResult.value = false
isVoiceMode.value = false
}
// 处理发送文本
const handleSendText = (data) => {
// 发送文本逻辑
emit('sendVoice', {
text: data.text,
// 可以添加语音文件路径等信息
})
showVoiceResult.value = false
isVoiceMode.value = false
}
// 处理滑动事件
const handleTouchMove = (e) => {
// 检测滑动位置,判断是否需要转文字
// 这里只是示例逻辑需要根据实际UI调整
const touchY = e.touches[0].clientY
// 假设滑动到某个位置以下转为文字
isSlideToText.value = touchY < 200
}
// 取消语音
const cancelVoice = () => {
showVoiceResult.value = false
isVoiceMode.value = false
}
// 测试弹窗方法
const testPopup = () => {
// 模拟开始录音,打开录音弹窗
isRecording.value = true
if (recordingPopupRef.value) {
recordingPopupRef.value.open()
}
// 2秒后关闭录音弹窗打开语音结果弹窗
setTimeout(() => {
if (recordingPopupRef.value) {
recordingPopupRef.value.close()
}
voiceText.value = '测试语音转文字结果'
showVoiceResult.value = true
if (voiceResultPopupRef.value) {
voiceResultPopupRef.value.open()
}
}, 2000)
}
// 监听键盘高度变化
onMounted(() => {
// 监听键盘弹起
uni.onKeyboardHeightChange((res) => {
keyboardHeight.value = res.height
if (res.height > 0) {
emit('keyboardShow', res.height)
} else {
emit('keyboardHide')
}
})
})
const sendMessage = () => {
if (isVoiceMode.value) {
testPopup()
return
}
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,
blurInput,
isFocused,
toggleVoiceMode
})
</script>
<style scoped lang="scss">
.area-input {
display: flex;
align-items: center;
border-radius: 22px;
background-color: #FFFFFF;
box-shadow: 0px 0px 20px 0px rgba(52, 25, 204, 0.05);
margin: 0 12px;
/* 确保输入框在安全区域内 */
margin-bottom: 8px;
.input-container-voice {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
flex-shrink: 0;
align-self: flex-end;
cursor: pointer;
image {
width: 22px;
height: 22px;
}
}
.input-button-container {
flex: 1;
position: relative;
height: 44px;
}
.hold-to-talk-button {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: #333333;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
background-color: #FFFFFF;
}
.input-container-send {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
flex-shrink: 0;
align-self: flex-end;
.input-container-send-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
image {
width: 28px;
height: 28px;
}
}
.textarea {
flex: 1;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
max-height: 92px;
min-height: 44px;
font-size: 16px;
line-height: 22px;
padding: 3px 0 0;
align-items: center;
justify-content: center;
}
/* 确保textarea在iOS上的样式正常 */
.textarea::-webkit-input-placeholder {
color: #CCCCCC;
}
.textarea:focus {
outline: none;
}
}
</style>