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>