Skip to content
On this page

uni-app 实现聊天录音发送功能

介绍

因为最近的项目在做一个聊天的功能,客户需要添加一个录个录音的功能,所以仿照这微信的语音聊天自己捣鼓了一下

功能的实现主要通过 touchstart 、touchmove、touchend这三个事件,以及uni-app中的音频管理器(uni.getRecorderManager),在执行touchstart 记录初始点击位置,然后在touchmove 中计算移动的距离,通过移动的距离来判断用户是否要取消发送

录音管理器

请添加图片描述

项目代码

javascript
<template>
	<view class="">
		<!-- 按住说话按钮 -->
		<view class="h-full w-full flex justify-center items-center" v-if="recordFlag"
						@touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd">
						{{i18n.holdTalk}}
					</view>

		<!-- 遮罩弹窗 -->
		<view class="fixed top-0 right-0 left-0 bottom-0" style="background: rgba(0,0,0,.8);" v-if="popupToggle">
			<view class="h-full w-full relative">
				<view class="absolute h-full w-full flex justify-center items-center pb-12">
					<view class="">
						<view class="flex justify-center items-center rounded-md duration-500" style="height: 100rpx;"
							:style="{width:soundWidth +'rpx',background:canSend?'rgb(238, 243, 243)':'rgb(215, 0, 21)'}">
							<voice-print></voice-print>
						</view>
						<view class="text-sm text-center mt-2" style="color: rgb(238, 243, 243);">
							{{recordTime}}s
						</view>
					</view>
				</view>
				<view class="absolute flex justify-center items-center" style="bottom: 400rpx;left: 0;right: 0;">
					<view class="relative h-full w-full flex justify-center items-center" style="height: 250rpx;">
						<view class="text-white text-xs mb-1 text-center absolute top-0" v-show="!canSend">
							<!-- 松开取消 -->
							{{i18n.releaseCancel}}
						</view>
						<view class="rounded-full flex justify-center items-center duration-500"
							:style="{background:canSend?'rgba(238, 243, 243,.6)':'rgb(215, 0, 21)',width:canSend?'120rpx':'140rpx',height:canSend?'120rpx':'140rpx'}">
							<image :src="chattingImage.close_black" mode="widthFix" style="width: 70rpx;height: auto;"
								v-if="canSend"></image>
							<image :src="chattingImage.close_white" mode="widthFix" style="width: 70rpx;height: auto;"
								v-else>
							</image>
						</view>
					</view>
				</view>
				<view class="absolute bottom-0 left-0 right-0 flex justify-center items-center duration-500"
					style="height: 400rpx;border-top-left-radius: 400rpx;border-top-right-radius: 400rpx; "
					:style="{background:canSend?'rgb(238, 243, 243)':'rgb(184, 188, 202)'}">
					<view class="relative h-full w-full flex justify-center items-center">
						<view class="text-xs text-center  absolute " style="top:60rpx;" v-show="canSend">
							<!-- 松开发送 -->
							{{i18n.releaseSend}}
						</view>
						<view class="flex justify-center items-center">
							<image :src="chattingImage.sound_black" mode="widthFix" style="width: 80rpx;height: auto;"
								v-if="canSend"></image>
							<image :src="chattingImage.sound_white" mode="widthFix" style="width: 80rpx;height: auto;"
								v-else>
							</image>
						</view>
					</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
	import qs from "qs"
	import {
		chattingImage,
		faceImages,
	} from "@/common/images.js"
	import {
		emojiMap,
		emojiUrl
	} from "../base/emojiMap.js"
	import {
		mapState,
		mapGetters
	} from "vuex"
	import {
		parseDate
	} from "@/utils/tools.js";
	import api from "@/api/api.js"

	import VoicePrint from "./voiceprint.vue"
	export default {
		data() {
			return {
				// 录音相关
				startPoint: 0, // 点击的初始点
				popupToggle: false, // 弹窗标识
				isRecording: false,
				canSend: true, // 是否可以发送
				text: '',
				title: '',
				notShow: false,
				isShow: true,
				recordTime: 0,
				recordTimer: null,
				recordFlag: false, // 发送语音的标识
				soundWidth: 200, // 音频框的宽度 动态改变
				originalWidth: 200 // 音频框的初始宽度,不改变
			}
		},
		props: {
			menuIsShow: {
				type: Boolean
			},
			isOs: {
				type: Boolean,
				defalut: false
			},
			paddingBottom: {
				type: Number,
				defalut: 0
			},
			menuCategoryHeight: {
				type: Object,
				default: () => {
					return {
						emoji: 560,
						menu: 400
					}
				}
			}
		},
		computed: {
			...mapState('chat', ['emoticon', 'emojiList', 'chatUserInfo', 'multipleFlag']),
			...mapState('system', ['cardlink']),
			i18n() {
				return this.$t('chat')
			}
		},
		components: {
			VoicePrint
		},
		created() {
			
		},
		beforeMount() {
			// #ifndef H5
			// 加载声音录制管理器
			this.recorderManager = uni.getRecorderManager();
			this.recorderManager.onStop(res => {
				clearInterval(this.recordTimer);
				// 兼容 uniapp 打包app,duration 和 fileSize 需要用户自己补充
				let duration = res.duration ? res.duration : this.recordTime * 1000
				if (this.canSend) {
					if (duration < 1000) {
						uni.showToast({
							title: this.$t('chat').short,
							icon: 'none'
						});
					} else {
						this.uploadSound(res.tempFilePath)
					}
				}
				this.startPoint = 0
				this.popupToggle = false
				this.isRecording = false
				this.canSend = true
				this.title = this.$t('chat').holdTalk
				// this.text = '按住说话'
			});
			// #endif
		},
		beforeDestroy() {
			uni.$off('clearInput')
		},
		methods: {
			// 上传录音
			uploadSound(filePath) {
				let url = api.uploadUrl + '/api/file/upload'
				let lang = uni.getStorageSync("languageKey") ? (uni.getStorageSync("languageKey") == 'en' ? 'en-us' :
					'zh-cn') : 'en-us'
				let params = {
					mime: 'image',
					source: 'message',
					lang
				}
				url = url + "?" + qs.stringify(params)
				uni.showLoading({
					title: this.$t('chat').uploadRecord
				})
				uni.uploadFile({
					url,
					filePath,
					name: 'file',
					header: {
						Authorization: uni.getStorageSync('token')
					},
					success: (uploadFileRes) => {
						uni.hideLoading()
						let res = JSON.parse(uploadFileRes.data).data
						this.send(`img[${res.src}]`, 3);
					},
					fail: (err) => {
						uni.hideLoading()
						showToast({
							title: JSON.stringify(err),
							icon: "none"
						})
					},
				});
			},
			handleTouchStart(e) {
				this.popupToggle = true
				this.recorderManager.start({
					duration: 60000,
					// 录音的时长,单位 ms,最大值 600000(10 分钟)
					sampleRate: 44100,
					// 采样率
					numberOfChannels: 1,
					// 录音通道数
					encodeBitRate: 192000,
					// 编码码率
					format: 'mp3' // 音频格式,选择此格式创建的音频消息,可以在即时通信 IM 全平台(Android、iOS、微信小程序和Web)互通
				});
				this.startPoint = e.touches[0]
				this.title = this.$t('chat').tapeing
				this.notShow = true
				this.isShow = false
				this.isRecording = true
				this.popupToggle = true
				this.recordTime = 0
				this.recordTimer = setInterval(() => {
					this.recordTime++;
					if (this.originalWidth + this.recordTime * 2.5 <= 600) {
						this.soundWidth = this.recordTime * 2.5 + this.originalWidth
					}
				}, 1000);
			},
			// 录音时的手势上划移动距离对应文案变化
			handleTouchMove(e) {
				if (this.isRecording) {
					if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 190) {
						// this.text = '抬起停止'
						this.title = this.$t('chat').cancel
						this.canSend = false
					} else if (this.startPoint.clientY - e.touches[e.touches.length - 1].clientY > 20) {
						// this.text = '抬起停止'
						this.title = this.$t('chat').stroke
						this.canSend = true
					} else {
						// this.text = '抬起停止'
						this.title = this.$t('chat').tapeing
						this.canSend = true
					}
				}
			},

			// 手指离开页面滑动
			handleTouchEnd() {
				this.isRecording = false
				this.popupToggle = false
				uni.hideLoading();
				this.recorderManager.stop();
			},
			openMenu(type) {
				if (this.recordFlag) {
					this.recordFlag = false
				}
				this.menuType = type
				this.$emit("openMenu", type)
			},
			// 发送信息
			send(newContent = "", content_type = 0) {
				let content = newContent || this.inputText;
				if (content.length === 0) return false;
				// content = emoji.unemojify(content);
				const message = {
					id: new Date().getTime(),
					mine: true,
					// time: this.getTime(),
					content,
					content_type,
					to_mid: this.chatUserInfo.to_mid,
					VIEW_ID: "msg" + new Date().getTime(),
					create_time: parseDate(new Date().getTime(), 'full', '-')
				};
				this.$store.dispatch("chat/checkSendMessageLimit", {
					message
				})
			},
			// 录音
			soundFunc() {
				// #ifdef APP-PLUS
				// this.popupToggle = true
				this.menuIsShow && this.openMenu(this.menuType)
				this.recordFlag = !this.recordFlag
				// #endif
				// #ifdef H5
				this.$u.toast(this.$t('chat').notSupport)
				// #endif
			}
		}
	}
</script>

<style lang="scss" scoped>

</style>

VoicePrint组件

javascript
<template>
	<view class="com__box">
		<!-- loading -->
		<view class="loading">
			<view></view>
			<view></view>
			<view></view>
			<view></view>
			<view></view>
		</view>
	</view>
</template>

<style lang="scss" scoped>
	.loading,
	.loading>view {
		position: relative;
		box-sizing: border-box;
	}

	.loading {
		display: block;
		font-size: 0;
		color: #000;
	}

	.loading.la-dark {
		color: #333;
	}

	.loading>view {
		display: inline-block;
		float: none;
		background-color: currentColor;
		border: 0 solid currentColor;
	}

	.loading {
		width: 40px;
		height: 32px;
	}

	.loading>view {
		width: 4px;
		height: 32px;
		margin: 2px;
		margin-top: 0;
		margin-bottom: 0;
		border-radius: 0;
		animation: line-scale-pulse-out 0.9s infinite cubic-bezier(0.85, 0.25, 0.37, 0.85);
	}

	.loading>view:nth-child(3) {
		animation-delay: -0.9s;
	}

	.loading>view:nth-child(2),
	.loading>view:nth-child(4) {
		animation-delay: -0.7s;
	}

	.loading>view:nth-child(1),
	.loading>view:nth-child(5) {
		animation-delay: -0.5s;
	}

	.loading.la-sm {
		width: 20px;
		height: 16px;
	}

	.loading.la-sm>view {
		width: 2px;
		height: 16px;
		margin: 1px;
		margin-top: 0;
		margin-bottom: 0;
	}

	.loading.la-2x {
		width: 80px;
		height: 64px;
	}

	.loading.la-2x>view {
		width: 8px;
		height: 64px;
		margin: 4px;
		margin-top: 0;
		margin-bottom: 0;
	}

	.loading.la-3x {
		width: 120px;
		height: 96px;
	}

	.loading.la-3x>view {
		width: 12px;
		height: 96px;
		margin: 6px;
		margin-top: 0;
		margin-bottom: 0;
	}

	@keyframes line-scale-pulse-out {
		0% {
			transform: scaley(1);
		}

		50% {
			transform: scaley(0.3);
		}

		100% {
			transform: scaley(1);
		}
	}
</style>