341 lines
8.7 KiB
Vue
341 lines
8.7 KiB
Vue
<template>
|
|
<div
|
|
:class="{
|
|
'message-input-audio': true,
|
|
'message-input-audio-open': isAudioTouchBarShow,
|
|
}"
|
|
>
|
|
<Icon
|
|
class="audio-message-icon"
|
|
:file="audioIcon"
|
|
:size="'23px'"
|
|
:hotAreaSize="'3px'"
|
|
@onClick="switchAudio"
|
|
/>
|
|
<view
|
|
v-if="props.isEnableAudio"
|
|
class="audio-input-touch-bar"
|
|
@touchstart="handleTouchStart"
|
|
@longpress="handleLongPress"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="handleTouchEnd"
|
|
>
|
|
<span>{{ TUITranslateService.t(`TUIChat.${touchBarText}`) }}</span>
|
|
<view
|
|
v-if="isRecording"
|
|
class="record-modal"
|
|
>
|
|
<div class="red-mask" />
|
|
<view class="float-element moving-slider" />
|
|
<view class="float-element modal-title">
|
|
{{ TUITranslateService.t(`TUIChat.${modalText}`) }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from '../../../adapter-vue';
|
|
import {
|
|
TUIStore,
|
|
StoreName,
|
|
TUIChatService,
|
|
SendMessageParams,
|
|
IConversationModel,
|
|
TUITranslateService,
|
|
} from '@tencentcloud/chat-uikit-engine';
|
|
import { TUIGlobal } from '@tencentcloud/universal-api';
|
|
import Icon from '../../common/Icon.vue';
|
|
import audioIcon from '../../../assets/icon/audio.svg';
|
|
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
|
|
import { throttle } from '../../../utils/lodash';
|
|
import { isEnabledMessageReadReceiptGlobal } from '../utils/utils';
|
|
import { InputDisplayType } from '../../../interface';
|
|
|
|
interface IProps {
|
|
isEnableAudio: boolean;
|
|
}
|
|
|
|
interface IEmits {
|
|
(e: 'changeDisplayType', type: InputDisplayType): void;
|
|
}
|
|
|
|
interface RecordResult {
|
|
tempFilePath: string;
|
|
duration?: number;
|
|
fileSize?: number;
|
|
}
|
|
|
|
type TouchBarText = '按住说话' | '抬起发送' | '抬起取消';
|
|
type ModalText = '正在录音' | '继续上滑可取消' | '松开手指 取消发送';
|
|
|
|
const emits = defineEmits<IEmits>();
|
|
const props = withDefaults(defineProps<IProps>(), {
|
|
isEnableAudio: false,
|
|
});
|
|
|
|
let recordTime: number = 0;
|
|
let isManualCancelBySlide = false;
|
|
let recordTimer: number | undefined;
|
|
let firstTouchPageY: number = -1;
|
|
let isFingerTouchingScreen = false;
|
|
let isFirstAuthrizedRecord = false;
|
|
const recorderManager = TUIGlobal?.getRecorderManager();
|
|
|
|
const isRecording = ref(false);
|
|
const touchBarText = ref<TouchBarText>('按住说话');
|
|
const modalText = ref<ModalText>('正在录音');
|
|
const isAudioTouchBarShow = ref<boolean>(false);
|
|
const currentConversation = ref<IConversationModel>();
|
|
|
|
const recordConfig = {
|
|
// Duration of the recording, in ms, with a maximum value of 600000 (10 minutes)
|
|
duration: 60000,
|
|
// Sampling rate
|
|
sampleRate: 44100,
|
|
// Number of recording channels
|
|
numberOfChannels: 1,
|
|
// Encoding bit rate
|
|
encodeBitRate: 192000,
|
|
// Audio format
|
|
// Select this format to create audio messages that can be interoperable across all chat platforms (Android, iOS, WeChat Mini Programs, and Web).
|
|
format: 'mp3',
|
|
};
|
|
|
|
function switchAudio() {
|
|
emits('changeDisplayType', props.isEnableAudio ? 'editor' : 'audio');
|
|
}
|
|
|
|
onMounted(() => {
|
|
// Register events for the audio recording manager
|
|
recorderManager.onStart(onRecorderStart);
|
|
recorderManager.onStop(onRecorderStop);
|
|
recorderManager.onError(onRecorderError);
|
|
|
|
TUIStore.watch(StoreName.CONV, {
|
|
currentConversation: onCurrentConverstaionUpdated,
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
TUIStore.unwatch(StoreName.CONV, {
|
|
currentConversation: onCurrentConverstaionUpdated,
|
|
});
|
|
});
|
|
|
|
function onCurrentConverstaionUpdated(conversation: IConversationModel) {
|
|
currentConversation.value = conversation;
|
|
}
|
|
|
|
function initRecorder() {
|
|
initRecorderData();
|
|
initRecorderView();
|
|
}
|
|
|
|
function initRecorderView() {
|
|
isRecording.value = false;
|
|
touchBarText.value = '按住说话';
|
|
modalText.value = '正在录音';
|
|
}
|
|
|
|
function initRecorderData(options?: { hasError: boolean }) {
|
|
clearInterval(recordTimer);
|
|
recordTimer = undefined;
|
|
recordTime = 0;
|
|
firstTouchPageY = -1;
|
|
isManualCancelBySlide = false;
|
|
if (!options?.hasError) {
|
|
recorderManager.stop();
|
|
}
|
|
}
|
|
|
|
function handleTouchStart() {
|
|
if (isFingerTouchingScreen) {
|
|
// Compatibility: Ignore the recording generated by the user's first authorization on the APP.
|
|
isFirstAuthrizedRecord = true;
|
|
}
|
|
}
|
|
|
|
function handleLongPress() {
|
|
isFingerTouchingScreen = true;
|
|
recorderManager.start(recordConfig);
|
|
}
|
|
|
|
const handleTouchMove = throttle(function (e) {
|
|
if (isRecording.value) {
|
|
const pageY = e.changedTouches[e.changedTouches.length - 1].pageY;
|
|
if (firstTouchPageY < 0) {
|
|
firstTouchPageY = pageY;
|
|
}
|
|
const offset = (firstTouchPageY as number) - pageY;
|
|
if (offset > 150) {
|
|
touchBarText.value = '抬起取消';
|
|
modalText.value = '松开手指 取消发送';
|
|
isManualCancelBySlide = true;
|
|
} else if (offset > 50) {
|
|
touchBarText.value = '抬起发送';
|
|
modalText.value = '继续上滑可取消';
|
|
isManualCancelBySlide = false;
|
|
} else {
|
|
touchBarText.value = '抬起发送';
|
|
modalText.value = '正在录音';
|
|
isManualCancelBySlide = false;
|
|
}
|
|
}
|
|
}, 100);
|
|
|
|
function handleTouchEnd() {
|
|
isFingerTouchingScreen = false;
|
|
recorderManager.stop();
|
|
}
|
|
|
|
function onRecorderStart() {
|
|
if (!isFingerTouchingScreen) {
|
|
// If recording starts but the finger leaves the screen,
|
|
// it means that the initial authorization popup interrupted the recording and it should be ignored.
|
|
isFirstAuthrizedRecord = true;
|
|
recorderManager.stop();
|
|
return;
|
|
}
|
|
recordTimer = setInterval(() => {
|
|
recordTime += 1;
|
|
}, 1000);
|
|
touchBarText.value = '抬起发送';
|
|
isRecording.value = true;
|
|
}
|
|
|
|
function onRecorderStop(res: RecordResult) {
|
|
if (isFirstAuthrizedRecord) {
|
|
// Compatibility: Ignore the recording generated by the user's first authorization on WeChat. This is not applicable to the APP.
|
|
isFirstAuthrizedRecord = false;
|
|
initRecorder();
|
|
return;
|
|
}
|
|
if (isManualCancelBySlide || !isRecording.value) {
|
|
initRecorder();
|
|
return;
|
|
}
|
|
clearInterval(recordTimer);
|
|
/**
|
|
* Compatible with uniapp for building apps
|
|
* Compatible with uniapp voice messages without duration
|
|
* Duration and fileSize need to be supplemented by the user
|
|
* File size = (Audio bitrate) * Length of time (in seconds) / 8
|
|
* res.tempFilePath stores the temporary path of the recorded audio file
|
|
*/
|
|
const tempFilePath = res.tempFilePath;
|
|
const duration = res.duration ? res.duration : recordTime * 1000;
|
|
const fileSize = res.fileSize ? res.fileSize : ((48 * recordTime) / 8) * 1024;
|
|
|
|
if (duration < 1000) {
|
|
Toast({
|
|
message: '录音时间太短',
|
|
type: TOAST_TYPE.NORMAL,
|
|
duration: 1500,
|
|
});
|
|
} else {
|
|
const options = {
|
|
to:
|
|
currentConversation?.value?.groupProfile?.groupID
|
|
|| currentConversation?.value?.userProfile?.userID,
|
|
conversationType: currentConversation?.value?.type,
|
|
payload: { file: { duration, tempFilePath, fileSize } },
|
|
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
|
|
} as SendMessageParams;
|
|
TUIChatService?.sendAudioMessage(options);
|
|
}
|
|
initRecorder();
|
|
}
|
|
|
|
function onRecorderError() {
|
|
initRecorderData({ hasError: true });
|
|
initRecorderView();
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import "../../../assets/styles/common";
|
|
|
|
.message-input-audio {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
|
|
.audio-message-icon {
|
|
margin-right: 3px;
|
|
}
|
|
|
|
.audio-input-touch-bar {
|
|
height: 39px;
|
|
flex: 1;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background-color: #fff;
|
|
|
|
.record-modal {
|
|
height: 300rpx;
|
|
width: 60vw;
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
position: fixed;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 9999;
|
|
border-radius: 24rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
|
|
.red-mask {
|
|
position: absolute;
|
|
inset: 0;
|
|
background-color: rgba(#ff3e48, 0.5);
|
|
opacity: 0;
|
|
transition: opacity 10ms linear;
|
|
z-index: 1;
|
|
}
|
|
|
|
.moving-slider {
|
|
margin: 10vw;
|
|
width: 40rpx;
|
|
height: 16rpx;
|
|
border-radius: 4rpx;
|
|
background-color: #006fff;
|
|
animation: loading 1s ease-in-out infinite alternate;
|
|
z-index: 2;
|
|
}
|
|
|
|
.float-element {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
}
|
|
|
|
@keyframes loading {
|
|
0% {
|
|
transform: translate(0, 0);
|
|
}
|
|
|
|
100% {
|
|
transform: translate(30vw, 0);
|
|
background-color: #f5634a;
|
|
width: 40px;
|
|
}
|
|
}
|
|
|
|
.modal-title {
|
|
text-align: center;
|
|
color: #fff;
|
|
}
|
|
}
|
|
|
|
&-open {
|
|
flex: 1;
|
|
}
|
|
}
|
|
</style>
|