提交发送语音

This commit is contained in:
2025-07-04 22:34:07 +08:00
parent 2cf13f673d
commit ad33895b6d
10 changed files with 571 additions and 94 deletions

View File

@@ -4,59 +4,119 @@
<view class="chat_footer">
<view class="footer_action_area">
<view class="input_content">
<CustomEditor
class="custom_editor"
ref="customEditor"
@ready="editorReady"
@focus="editorFocus"
@blur="editorBlur"
@input="editorInput"
<image v-if="inputBox"
@click.prevent="updateInput"
src="@/static/images/chating_footer_recording.png"
alt=""
srcset=""
/>
<image v-if="!inputBox"
@click.prevent="updateInput"
src="@/static/images/chating_footer_audio_recording.png"
alt=""
srcset=""
/>
</view>
<view class="input_content" v-if="inputBox">
<CustomEditor class="custom_editor" ref="customEditor" @ready="editorReady" @focus="editorFocus"
@blur="editorBlur" @input="editorInput"/>
</view>
<view class="input_content"
v-if="!inputBox"
style="position:relative;"
>
<!-- 录音提示区域 -->
<view v-if="isRecording"
style="position:absolute;left:50%;top:-120rpx;transform:translateX(-50%);z-index:10;display:flex;flex-direction:column;align-items:center;">
<view :style="{
background: willCancel ? 'rgba(255,80,80,0.95)' : 'rgba(0,0,0,0.85)',
color: '#fff',
borderRadius: '12rpx',
padding: '18rpx 32rpx',
fontSize: '28rpx',
minWidth: '260rpx',
textAlign: 'center',
marginBottom: '10rpx',
}">
<text v-if="willCancel">松开手指取消发送</text>
<text v-else>正在讲话已录制 {{recordingTime}} 松开发送</text>
</view>
</view>
<!-- 蒙版 -->
<view v-if="isRecording" style="position:absolute;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.08);z-index:5;border-radius:16rpx;"></view>
<button
class="record_btn"
@touchstart="startRecord"
@touchend="stopRecord"
@touchcancel="cancelRecord"
@touchmove="moveRecord"
style="width:100%;height:100%;background:transparent;border:none;outline:none;display:flex;align-items:center;justify-content:center;z-index:20;position:relative;"
>
<text v-if="!isRecording">按住说话</text>
<text v-else :style="willCancel ? 'color:#ff5050;' : 'color:#4a9cfc;'">{{ willCancel ? '松开手指,取消发送' : '正在说话,松开发送' }}</text>
</button>
</view>
<view class="footer_action_area">
<!-- <image-->
<!-- v-show="!hasContent"-->
<!-- @click.prevent="updateActionBar"-->
<!-- src="@/static/images/chating_footer_add.png"-->
<!-- alt=""-->
<!-- srcset=""-->
<!-- />-->
<image
v-show="hasContent"
@touchend.prevent="sendTextMessage"
src="@/static/images/send_btn.png"
class="emoji_action"
v-if="inputBox && !showEmojiPicker"
@click.prevent="toggleEmojiPicker"
src="@/static/images/chating_footer_emoji.png"
alt=""
srcset=""
/>
<image
class="emoji_action"
v-if="inputBox && showEmojiPicker"
@click.prevent="closeEmojiAndFocusInput"
src="@/static/images/chating_footer_audio_recording.png"
alt=""
srcset=""
/>
<image
v-show="!hasContent"
@click.prevent="updateActionBar"
src="@/static/images/chating_footer_add.png"
alt=""
srcset=""
/>
<image v-show="hasContent" @touchend.prevent="sendTextMessage" src="@/static/images/send_btn.png"
alt="" srcset=""/>
</view>
</view>
<chating-action-bar
@sendMessage="sendMessage"
@prepareMediaMessage="prepareMediaMessage"
v-show="actionBarVisible"
/>
<u-action-sheet
:safeAreaInsetBottom="true"
round="12"
:actions="actionSheetMenu"
@select="selectClick"
:closeOnClickOverlay="true"
:closeOnClickAction="true"
:show="showActionSheet"
@close="showActionSheet = false"
>
<chating-action-bar @sendMessage="sendMessage" @prepareMediaMessage="prepareMediaMessage"
v-show="actionBarVisible"/>
<u-action-sheet :safeAreaInsetBottom="true" round="12" :actions="actionSheetMenu" @select="selectClick"
:closeOnClickOverlay="true" :closeOnClickAction="true" :show="showActionSheet"
@close="showActionSheet = false">
</u-action-sheet>
<!-- Emoji 选择面板 -->
<view v-if="showEmojiPicker">
<view class="emoji-mask" @touchstart="showEmojiPicker = false" style="position:fixed;left:0;top:0;width:100vw;height:100vh;z-index:998;"></view>
<view class="emoji-picker" style="position:relative;z-index:999;">
<EmojiPicker @select="onEmojiSelect" />
</view>
</view>
</view>
</view>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import { getPurePath, html2Text } from "@/util/common";
import { offlinePushInfo } from "@/util/imCommon";
import {
mapGetters,
mapActions
} from "vuex";
import {
getPurePath,
html2Text
} from "@/util/common";
import {
offlinePushInfo
} from "@/util/imCommon";
import {
ChatingFooterActionTypes,
UpdateMessageTypes,
@@ -69,15 +129,15 @@ import IMSDK, {
import UParse from "@/components/gaoyia-parse/parse.vue";
import CustomEditor from "./CustomEditor.vue";
import ChatingActionBar from "./ChatingActionBar.vue";
import EmojiPicker from '@/components/EmojiPicker/index.vue';
const needClearTypes = [MessageType.TextMessage];
const albumChoose = [
{
name: "图片",
type: ChatingFooterActionTypes.Album,
idx: 0,
},
const albumChoose = [{
name: "图片",
type: ChatingFooterActionTypes.Album,
idx: 0,
},
{
name: "拍照",
type: ChatingFooterActionTypes.Camera,
@@ -90,18 +150,28 @@ export default {
CustomEditor,
ChatingActionBar,
UParse,
EmojiPicker,
},
props: {
footerOutsideFlag: Number,
},
data() {
return {
inputBox: true,
customEditorCtx: null,
inputHtml: "",
actionBarVisible: false,
isInputFocus: false,
actionSheetMenu: [],
showActionSheet: false,
isRecording: false,
willCancel: false,
recordingTime: 0,
recordingTimer: null,
showEmojiPicker: false,
recorderManager: null,
recordFilePath: '',
recordFileDuration: 0,
};
},
computed: {
@@ -121,21 +191,77 @@ export default {
},
mounted() {
this.setKeyboardListener();
document.addEventListener('plusready', function() {
// plus 相关代码
});
},
beforeDestroy() {
this.disposeKeyboardListener();
},
methods: {
updateInput() {
this.inputBox = !this.inputBox;
this.showEmojiPicker = false;
},
startRecord(e) {
this.isRecording = true;
this.willCancel = false;
this.recordingTime = 0;
if (this.recordingTimer) clearInterval(this.recordingTimer);
this.recordingTimer = setInterval(() => {
this.recordingTime++;
}, 1000);
// 录音开始
if (!this.recorderManager) {
this.recorderManager = uni.getRecorderManager();
this.recorderManager.onStop(this.onRecordStop);
}
this.recordFilePath = '';
this.recordFileDuration = 0;
this.recorderManager.start({ format: 'mp3' });
},
stopRecord(e) {
if (this.recordingTimer) clearInterval(this.recordingTimer);
this.recordingTimer = null;
this.isRecording = false;
if (this.recorderManager) {
this.recorderManager.stop();
}
this._startY = null;
},
cancelRecord(e) {
if (this.recordingTimer) clearInterval(this.recordingTimer);
this.recordingTimer = null;
this.isRecording = false;
this.willCancel = false;
this.recordingTime = 0;
if (this.recorderManager) {
this.recorderManager.stop();
}
this._startY = null;
},
moveRecord(e) {
// 判断手指是否上滑通常通过e.touches[0].clientY与起始Y比较
if (!this.isRecording) return;
const touch = e.touches[0];
if (!this._startY && touch) {
this._startY = touch.clientY;
}
if (touch && this._startY) {
// 上滑距离大于40px则判定为取消
this.willCancel = (this._startY - touch.clientY) > 40;
}
},
...mapActions("message", ["pushNewMessage", "updateOneMessage"]),
async createTextMessage() {
let message = "";
const text = html2Text(this.inputHtml);
message = await IMSDK.asyncApi(
IMMethods.CreateTextMessage,
IMSDK.uuid(),
text
);
console.log(message);
IMMethods.CreateTextMessage,
IMSDK.uuid(),
text
);
return message;
},
async sendTextMessage() {
@@ -155,28 +281,33 @@ export default {
message,
offlinePushInfo,
})
.then(({ data }) => {
this.updateOneMessage({
message: data,
isSuccess: true,
});
})
.catch(({ data, errCode, errMsg }) => {
this.updateOneMessage({
message: data,
type: UpdateMessageTypes.KeyWords,
keyWords: [
{
.then(({
data
}) => {
this.updateOneMessage({
message: data,
isSuccess: true,
});
})
.catch(({
data,
errCode,
errMsg
}) => {
this.updateOneMessage({
message: data,
type: UpdateMessageTypes.KeyWords,
keyWords: [{
key: "status",
value: MessageStatus.Failed,
},
{
key: "errCode",
value: errCode,
},
],
{
key: "errCode",
value: errCode,
},
],
});
});
});
},
// action
@@ -187,6 +318,7 @@ export default {
},
updateActionBar() {
this.actionBarVisible = !this.actionBarVisible;
this.showEmojiPicker = false;
},
editorReady(e) {
this.customEditorCtx = e.context;
@@ -194,6 +326,7 @@ export default {
},
editorFocus() {
this.isInputFocus = true;
this.showEmojiPicker = false;
this.$emit("scrollToBottom");
},
editorBlur() {
@@ -205,29 +338,67 @@ export default {
prepareMediaMessage(type) {
if (type === ChatingFooterActionTypes.Album) {
this.actionSheetMenu = [...albumChoose];
this.showActionSheet = true;
} else if (type === ChatingFooterActionTypes.File) {
this.chooseFileAndSend();
}
this.showActionSheet = true;
},
async chooseFileAndSend() {
if (!uni.chooseMessageFile) {
return;
}
uni.chooseMessageFile({
count: 1,
type: 'file',
success: async (res) => {
const file = res.tempFiles[0];
try {
const nameIdx = file.name.lastIndexOf("/") + 1;
const fileName = file.name.slice(nameIdx);
const typeIdx = file.name.lastIndexOf(".") + 1;
const fileType = file.name.slice(typeIdx);
const { data: { url } } = await IMSDK.asyncApi(IMMethods.UploadFile, IMSDK.uuid(), {
filepath: file.path,
name: fileName,
contentType: fileType,
uuid: IMSDK.uuid(),
});
// 创建文件消息
const message = await IMSDK.asyncApi(IMMethods.CreateFileMessage, IMSDK.uuid(), {
fileName: fileName,
fileSize: file.size,
sourceUrl: url,
});
this.sendMessage(message);
} catch (e) {
}
},
fail: function (err) {
},
});
},
// from comp
batchCreateImageMesage(paths) {
paths.forEach(async (path) => {
const message = await IMSDK.asyncApi(
IMMethods.CreateImageMessageFromFullPath,
IMSDK.uuid(),
getPurePath(path)
IMMethods.CreateImageMessageFromFullPath,
IMSDK.uuid(),
getPurePath(path)
);
this.sendMessage(message);
});
},
selectClick({ idx }) {
selectClick({
idx
}) {
if (idx === 0) {
this.chooseOrShotImage(["album"]).then((paths) =>
this.batchCreateImageMesage(paths)
this.batchCreateImageMesage(paths)
);
} else {
this.chooseOrShotImage(["camera"]).then((paths) =>
this.batchCreateImageMesage(paths)
this.batchCreateImageMesage(paths)
);
}
},
@@ -237,11 +408,12 @@ export default {
count: 9,
sizeType: ["compressed"],
sourceType,
success: function ({ tempFilePaths }) {
success: function ({
tempFilePaths
}) {
resolve(tempFilePaths);
},
fail: function (err) {
console.log(err);
reject(err);
},
});
@@ -249,7 +421,9 @@ export default {
},
// keyboard
keyboardChangeHander({ height }) {
keyboardChangeHander({
height
}) {
if (height > 0) {
if (this.actionBarVisible) {
this.actionBarVisible = false;
@@ -262,6 +436,116 @@ export default {
disposeKeyboardListener() {
uni.offKeyboardHeightChange(this.keyboardChangeHander);
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
if (!this.showEmojiPicker && this.$refs.customEditor && this.$refs.customEditor.editorCtx) {
// 关闭emoji面板时再focus
this.$refs.customEditor.editorCtx.focus();
}
},
closeEmojiAndFocusInput() {
this.showEmojiPicker = false;
// 只在这里主动focus
if (this.$refs.customEditor && this.$refs.customEditor.editorCtx) {
this.$refs.customEditor.editorCtx.focus();
}
},
onEmojiSelect(emoji) {
this.inputHtml += emoji;
if (this.$refs.customEditor && this.$refs.customEditor.editorCtx) {
this.$refs.customEditor.editorCtx.setContents({
html: this.inputHtml
});
}
// 不做任何focus/blur
},
async onRecordStop(res) {
let tempFilePath = res.tempFilePath;
let fileName = tempFilePath.split('/').pop();
let fileType = fileName.split('.').pop();
let uuid = IMSDK.uuid();
let duration = this.recordingTime;
if (!duration || isNaN(duration) || duration < 1) {
this.recordingTime = 0;
return;
}
// 先保存到本地
uni.saveFile({
tempFilePath,
success: (saveRes) => {
let savedFilePath = saveRes.savedFilePath;
// 用 plus.io 转换为原生绝对路径
if (typeof plus !== 'undefined') {
plus.io.resolveLocalFileSystemURL(savedFilePath, (entry) => {
IMSDK.asyncApi('uploadFile', uuid, {
name: fileName,
contentType: fileType,
uuid: uuid,
filepath: entry.fullPath
})
.then(({ data }) => {
let sourceUrl = data.url;
IMSDK.asyncApi('createSoundMessageByURL', IMSDK.uuid(), {
uuid: uuid,
soundPath: entry.fullPath,
sourceUrl: sourceUrl,
dataSize: res.fileSize || 0,
duration: duration,
soundType: fileType
})
.then((message) => {
IMSDK.asyncApi('sendMessageNotOss', IMSDK.uuid(), {
recvID: this.storeCurrentConversation.userID,
groupID: this.storeCurrentConversation.groupID,
message: message,
offlinePushInfo: {
title: '你有新消息',
desc: '新消息',
ex: '',
iOSPushSound: '+1',
iOSBadgeCount: true,
}
})
.then(({ data }) => {
// 1. 本地新增消息
this.pushNewMessage(message);
// 2. 更新消息状态
this.updateOneMessage({
message: data,
isSuccess: true,
});
this.$emit("scrollToBottom");
})
.catch(({ errCode, errMsg }) => {
// 发送失败时也要更新消息状态为失败
this.updateOneMessage({
message: message,
type: UpdateMessageTypes.KeyWords,
keyWords: [
{ key: "status", value: MessageStatus.Failed },
{ key: "errCode", value: errCode },
],
});
});
})
.catch(() => {
// 忽略错误提示
});
})
.catch(() => {
// 忽略错误提示
});
}, () => {
// 忽略路径转换失败
});
}
},
fail: () => {
// 忽略保存失败
}
});
this.recordingTime = 0;
},
},
};
</script>
@@ -298,7 +582,7 @@ export default {
min-height: 30px;
max-height: 120px;
margin: 0 24rpx;
border-radius: 8rpx;
border-radius: 16rpx;
position: relative;
.record_btn {
@@ -350,4 +634,18 @@ export default {
color: #fff;
}
}
</style>
.emoji-mask {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: transparent;
z-index: 998;
}
.emoji-picker {
position: relative;
z-index: 999;
}
</style>