<template> <s-layout class="chat-wrap" title="语音转文字"> <view class="chat-container"> <view class="messages" :style="{height:messagesHeight+'px'}" id="messages"> <view v-for="(message, index) in messages" :key="index" class="message" @click="playRecording(index)"> <view class="bubble"> <text class="duration">{{ message.duration }}" </text><text class="ss-m-l-10"> <image src="@/static/icon/audioPaly.png" class="audioPaly" /> </text> </view> <view class="text" v-if="message.transcription != null && message.transcription != ''"> {{message.transcription}} </view> </view> </view> <view class="input-area"> <button @mousedown="startRecording" @mouseup="stopRecording" @mouseleave="cancelRecording" @touchstart="startRecording" @touchend="stopRecording" @touchcancel="cancelRecording" @touchmove="handleTouchMove"> 按住 说话 </button> <view v-if="isRecording" class="recording-overlay"> <view>{{ recordingDuration }}</view> <view>上滑至此取消</view> </view> </view> </view> </s-layout> </template> <script setup> import { ref, nextTick, onMounted } from 'vue'; import sheep from '@/sheep'; import VoiceApi from '@/sheep/api/system/voice'; const { safeAreaInsets, safeArea } = sheep.$platform.device; const sysNavBar = sheep.$platform.navbar; const messagesHeight = (safeArea.height) - sysNavBar - 20 - 60; const messages = ref([]); const isRecording = ref(false); let startTime = ref(null); const recordingDuration = ref(''); const minRecordingTime = 500; // 设置最小录音时间为500毫秒 let intervalId = null; const cancelOffset = 280; // 上滑取消的距离阈值 const startTouchY = ref(0); let mediaRecorder = null; let audioChunks = []; let isCancelled = ref(false); // 初始化录音功能 onMounted(() => { navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { mediaRecorder = new MediaRecorder(stream); mediaRecorder.ondataavailable = (event) => { audioChunks.push(event.data); }; mediaRecorder.onstop = () => { if (!isCancelled.value) { sendDuration() } audioChunks = []; }; }) .catch(error => { console.error("Error accessing media devices.", error); }); }); async function sendAudioToServer(audioBlob,audioUrl,duration,messageIndex) { const formData = new FormData(); formData.append('audio_file', audioBlob); try { const response = await fetch(import.meta.env.SHOPRO_BASE_URL + '/voice2text/', { method: 'POST', body: formData, }); const data = await response.json(); if(data.success){ messages.value[messageIndex].transcription = data.transcription; } console.log('Server response:', data); } catch (error) { console.error('Error sending audio file:', error); } } // 发送录音 const sendDuration = () => { const audioBlob = new Blob(audioChunks, { type: 'audio/mpeg' }); const audioUrl = URL.createObjectURL(audioBlob); const duration = Math.max(Math.floor((new Date() - startTime.value) / 1000), 1); // 先添加消息,但不包含转写文本 const messageIndex = messages.value.push({ duration, audioUrl, transcription: '' // 初始为空 }) - 1; sendAudioToServer(audioBlob,audioUrl,duration,messageIndex); nextTick(() => { let messagesElement = document.getElementById('messages'); messagesElement.scrollTop = messagesElement.scrollHeight; }); } // 更新录音 const updateDuration = () => { const currentDuration = Math.floor((new Date() - startTime.value) / 1000); recordingDuration.value = currentDuration + 's'; if (currentDuration >= 60) { // 如果到达60秒自动停止录音 stopRecording(new Event('mouseup')); } }; // 开始录音 const startRecording = (event) => { if (!isRecording.value && (event.type === 'mousedown' || event.type === 'touchstart')) { startTime.value = new Date(); isRecording.value = true; intervalId = setInterval(updateDuration, 1000); // 每秒更新一次时间 recordingDuration.value = '1s'; // 开始时显示1秒 // console.log('开始录音...'); event.preventDefault(); startTouchY.value = event.touches ? event.touches[0].clientY : 0; audioChunks = []; isCancelled.value = false; // 确保开始录音时取消标志为假 mediaRecorder.start(); } }; // 停止录音 const stopRecording = (event) => { if (isRecording.value && (event.type === 'mouseup' || event.type === 'touchend' || event.type === 'mouseleave' || event.type === 'touchcancel')) { clearInterval(intervalId); // 停止定时器 isRecording.value = false; recordingDuration.value = ''; // 清空显示的时间 if (new Date() - startTime.value >= minRecordingTime) { mediaRecorder.stop(); } else { console.log('录音时间太短,不保存'); } } }; // 取消录音 const cancelRecording = () => { if (isRecording.value) { clearInterval(intervalId); console.log('录音取消'); isCancelled.value = true; // 设置取消标志为真 mediaRecorder.stop(); resetRecording(); } }; // 重置录音 const resetRecording = () => { isRecording.value = false; recordingDuration.value = 0; startTime.value = null; audioChunks = []; }; // 处理触摸移动事件 const handleTouchMove = (event) => { // 此处应添加逻辑来检测触摸位置,如果滑动到取消区域,调用 cancelRecording const currentTouchY = event.touches[0].clientY; if (startTouchY.value - currentTouchY > cancelOffset) { // 检查是否上滑超过阈值 cancelRecording(); } }; // 播放录音 const playRecording = (index) => { const message = messages.value[index]; const audio = new Audio(message.audioUrl); audio.play(); }; </script> <style scoped> .chat-container { display: flex; flex-direction: column; overflow-y: auto; background-color: #f0f0f0; padding: 10px; } .messages { flex-grow: 1; overflow-y: auto; } .message { display: flex; justify-content: flex-end; align-items: flex-end; margin-bottom: 10px; flex-direction: column; } .text{ width: 70%; background: white; border-radius: 5px; padding: 10px; margin: 5px 0 10px; box-sizing: border-box; font-size: 16px; } .bubble { width:100px; padding: 10px 20px 10px 20px; justify-content: flex-end; margin-right: 10px; background-color: rgb(131 235 96); border-radius: 5px; display: flex; align-items: right; position: relative; } .bubble::after { content: ""; width: 0px; height: 0px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 5px solid #83eb60; position: absolute; top: 14px; right: -5px; } .duration { color: #000; font-size: 16px; } .audioPaly { width: 12px; height: 12px; } .input-area { position: fixed; bottom: 0; left: 0; width: 100%; background-color: rgb(245, 245, 245); display: flex; justify-content: center; padding: 10px; box-sizing: border-box; } .input-area>button { display: block; width: 100%; background-color: #FFFFFF; border: none; padding: 3px 0; border-radius: 5px; color: #333333; font-size: 16px; font-weight: bold; text-align: center; outline: none; cursor: pointer; } .input-area>.button-hover { background-color: #f0f0f0 !important; } .input-area>button:after { border: none; } .recording-overlay { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 20px; border-radius: 10px; text-align: center; } .recording-overlay view { margin: 5px 0; } </style>