123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- <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 { throttle } from 'lodash';
- import Icon from '../../common/Icon.vue';
- import audioIcon from '../../../assets/icon/audio.svg';
- import { Toast, TOAST_TYPE } from '../../common/Toast/index';
- 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>
|