Files
YGChatCS/pages/chat/ChatInputArea.vue
2025-08-06 16:04:39 +08:00

451 lines
9.6 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>
<view class="input-container-send">
<view v-if="!isVoiceMode" 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>
<!-- 录音界面 -->
<uni-popup v-model:show="isRecording" position="center" :mask-click-able="false">
<view class="recording-popup">
<view class="recording-wave">
<!-- 波形动画 -->
<view class="wave-animation"></view>
</view>
<view class="recording-text">
{{ isSlideToText ? '松开发送 转文字' : '松开发送' }}
</view>
<view class="recording-cancel">
取消
</view>
</view>
</uni-popup>
<!-- 语音结果界面 -->
<uni-popup v-model:show="showVoiceResult" position="center" :mask-click-able="false">
<view class="voice-result-popup">
<view class="voice-result-bubble">
{{ voiceText }}
</view>
<view class="voice-result-actions">
<view class="action-button cancel" @click="cancelVoice">取消</view>
<view class="action-button voice">发送原语音</view>
<view class="action-button send" @click="sendVoiceMessage">发送</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
const props = defineProps({
inputMessage: String,
holdKeyboard: Boolean,
isSessionActive: Boolean,
stopRequest: Function
})
const emit = defineEmits(['update:inputMessage', 'send', 'noHideKeyboard', 'keyboardShow', 'keyboardHide', 'sendVoice'])
const textareaRef = ref(null)
const placeholder = ref('快告诉朵朵您在想什么~')
const inputMessage = ref(props.inputMessage || '')
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)
// 保持和父组件同步
watch(() => props.inputMessage, (val) => {
inputMessage.value = val
})
// 切换语音/文本模式
const toggleVoiceMode = () => {
isVoiceMode.value = !isVoiceMode.value
}
// 开始录音
const startRecording = () => {
isRecording.value = true
return
recordingTime.value = 0
// 启动录音计时器
recordingTimer.value = setInterval(() => {
recordingTime.value += 1
}, 1000)
// 调用uni-app录音API
uni.startRecord({
success: (res) => {
// 录音成功,处理录音文件
const tempFilePath = res.tempFilePath
// 这里可以添加语音转文字的逻辑
// 模拟语音转文字
setTimeout(() => {
voiceText.value = '这是语音转文字的结果'
showVoiceResult.value = true
}, 1000)
},
fail: (err) => {
console.error('录音失败:', err)
isRecording.value = false
clearInterval(recordingTimer.value)
}
})
}
// 结束录音
const stopRecording = () => {
isRecording.value = false
return
clearInterval(recordingTimer.value)
// 如果录音时间小于1秒取消录音
if (recordingTime.value < 1) {
uni.stopRecord()
return
}
// 停止录音
uni.stopRecord()
}
// 处理滑动事件
const handleTouchMove = (e) => {
// 检测滑动位置,判断是否需要转文字
// 这里只是示例逻辑需要根据实际UI调整
const touchY = e.touches[0].clientY
// 假设滑动到某个位置以下转为文字
isSlideToText.value = touchY < 200
}
// 发送语音
const sendVoiceMessage = () => {
// 发送语音逻辑
emit('sendVoice', {
text: voiceText.value,
// 可以添加语音文件路径等信息
})
showVoiceResult.value = false
isVoiceMode.value = false
}
// 取消语音
const cancelVoice = () => {
showVoiceResult.value = false
isVoiceMode.value = false
}
// 监听键盘高度变化
onMounted(() => {
// 监听键盘弹起
uni.onKeyboardHeightChange((res) => {
keyboardHeight.value = res.height
if (res.height > 0) {
emit('keyboardShow', res.height)
} else {
emit('keyboardHide')
}
})
})
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,
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;
}
}
/* 录音弹窗样式 */
.recording-popup {
width: 280px;
height: 280px;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
}
.recording-wave {
width: 160px;
height: 160px;
border-radius: 50%;
background-color: rgba(76, 217, 100, 0.3);
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.wave-animation {
width: 100px;
height: 100px;
/* 这里可以添加波形动画 */
background-image: url('/static/wave_icon.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.recording-text {
font-size: 16px;
margin-bottom: 20px;
}
.recording-cancel {
font-size: 14px;
color: #CCCCCC;
}
/* 语音结果弹窗样式 */
.voice-result-popup {
width: 300px;
background-color: white;
border-radius: 16px;
padding: 20px;
}
.voice-result-bubble {
background-color: #4CD964;
color: white;
padding: 15px;
border-radius: 18px;
border-top-left-radius: 4px;
margin-bottom: 20px;
min-height: 60px;
font-size: 16px;
}
.voice-result-actions {
display: flex;
justify-content: space-between;
}
.action-button {
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
}
.cancel {
color: #666666;
background-color: #F5F5F5;
}
.voice {
color: #007AFF;
background-color: #E8F0FE;
}
.send {
color: white;
background-color: #007AFF;
}
</style>