| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 | <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>
 |