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