1140 lines
32 KiB
Vue
1140 lines
32 KiB
Vue
<template>
|
||
<view>
|
||
<view>
|
||
<view class="chat_footer">
|
||
|
||
<view class="footer_action_area">
|
||
<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
|
||
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">
|
||
</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 v-if="showFileSelectModal" class="file-select-mask" @touchstart="showFileSelectModal = false">
|
||
<view class="file-select-container" @touchstart.stop>
|
||
<view class="file-select-header">
|
||
<text class="file-select-title">选择文件</text>
|
||
<text class="file-select-close" @click="showFileSelectModal = false">×</text>
|
||
</view>
|
||
<view class="file-select-tabs">
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: currentFileTab === 'chat' }"
|
||
@click="currentFileTab = 'chat'"
|
||
>
|
||
聊天文件
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: currentFileTab === 'system' }"
|
||
@click="currentFileTab = 'system'"
|
||
>
|
||
系统文件
|
||
</view>
|
||
</view>
|
||
<view class="file-select-content">
|
||
<view v-if="currentFileTab === 'chat'" class="chat-files">
|
||
<view v-if="chatFiles.length === 0" class="empty-state">
|
||
<text class="empty-text">暂无聊天文件</text>
|
||
</view>
|
||
<view v-else class="file-list">
|
||
<view
|
||
v-for="file in chatFiles"
|
||
:key="file.clientMsgID"
|
||
class="file-item"
|
||
@click="selectChatFile(file)"
|
||
>
|
||
<image :src="getFileIcon(file)" class="file-icon" mode="aspectFit" />
|
||
<view class="file-info">
|
||
<text class="file-name">{{ getFileName(file) }}</text>
|
||
<text class="file-size">{{ getFileSize(file) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else-if="currentFileTab === 'system'" class="system-files">
|
||
<view class="file-options">
|
||
<view class="option-item" @click="chooseSystemFile('document')">
|
||
<image src="/static/images/file_message/file_pdf.png" class="option-icon" mode="aspectFit" />
|
||
<text class="option-text">文档</text>
|
||
</view>
|
||
<view class="option-item" @click="chooseSystemFile('image')">
|
||
<image src="/static/images/file_message/file_image.png" class="option-icon" mode="aspectFit" />
|
||
<text class="option-text">图片</text>
|
||
</view>
|
||
<view class="option-item" @click="chooseSystemFile('video')">
|
||
<image src="/static/images/file_message/file_unknown.png" class="option-icon" mode="aspectFit" />
|
||
<text class="option-text">视频</text>
|
||
</view>
|
||
<view class="option-item" @click="chooseSystemFile('audio')">
|
||
<image src="/static/images/file_message/file_unknown.png" class="option-icon" mode="aspectFit" />
|
||
<text class="option-text">音频</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
mapGetters,
|
||
mapActions
|
||
} from "vuex";
|
||
import {
|
||
getPurePath,
|
||
html2Text
|
||
} from "@/util/common";
|
||
import {
|
||
offlinePushInfo
|
||
} from "@/util/imCommon";
|
||
import {
|
||
ChatingFooterActionTypes,
|
||
UpdateMessageTypes,
|
||
} from "@/constant";
|
||
import IMSDK, {
|
||
IMMethods,
|
||
MessageStatus,
|
||
MessageType,
|
||
} from "openim-uniapp-polyfill";
|
||
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,
|
||
},
|
||
{
|
||
name: "拍照",
|
||
type: ChatingFooterActionTypes.Camera,
|
||
idx: 1,
|
||
},
|
||
];
|
||
|
||
export default {
|
||
components: {
|
||
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,
|
||
showFileSelectModal: false,
|
||
currentFileTab: 'chat',
|
||
chatFiles: [],
|
||
recorderManager: null,
|
||
recordFilePath: '',
|
||
recordFileDuration: 0,
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters([
|
||
"storeCurrentConversation",
|
||
"storeCurrentGroup",
|
||
"storeBlackList",
|
||
]),
|
||
hasContent() {
|
||
return html2Text(this.inputHtml) !== "";
|
||
},
|
||
},
|
||
watch: {
|
||
footerOutsideFlag(newVal) {
|
||
this.onClickActionBarOutside();
|
||
},
|
||
showFileSelectModal(newVal) {
|
||
if (newVal) {
|
||
// 当弹窗显示时,获取聊天文件列表
|
||
this.getChatFiles();
|
||
}
|
||
},
|
||
},
|
||
mounted() {
|
||
this.setKeyboardListener();
|
||
// 只在浏览器环境下添加事件监听
|
||
if (typeof document !== 'undefined') {
|
||
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
|
||
);
|
||
return message;
|
||
},
|
||
async sendTextMessage() {
|
||
if (!this.hasContent) return;
|
||
const message = await this.createTextMessage();
|
||
this.sendMessage(message);
|
||
},
|
||
sendMessage(message) {
|
||
this.pushNewMessage(message);
|
||
if (needClearTypes.includes(message.contentType)) {
|
||
this.customEditorCtx.clear();
|
||
}
|
||
this.$emit("scrollToBottom");
|
||
IMSDK.asyncApi(IMMethods.SendMessage, IMSDK.uuid(), {
|
||
recvID: this.storeCurrentConversation.userID,
|
||
groupID: this.storeCurrentConversation.groupID,
|
||
message,
|
||
offlinePushInfo,
|
||
})
|
||
.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,
|
||
},
|
||
],
|
||
});
|
||
});
|
||
},
|
||
|
||
// action
|
||
onClickActionBarOutside() {
|
||
if (this.actionBarVisible) {
|
||
this.actionBarVisible = false;
|
||
}
|
||
},
|
||
updateActionBar() {
|
||
this.actionBarVisible = !this.actionBarVisible;
|
||
this.showEmojiPicker = false;
|
||
},
|
||
editorReady(e) {
|
||
this.customEditorCtx = e.context;
|
||
this.customEditorCtx.clear();
|
||
},
|
||
editorFocus() {
|
||
this.isInputFocus = true;
|
||
this.showEmojiPicker = false;
|
||
this.$emit("scrollToBottom");
|
||
},
|
||
editorBlur() {
|
||
this.isInputFocus = false;
|
||
},
|
||
editorInput(e) {
|
||
this.inputHtml = e.detail.html;
|
||
},
|
||
prepareMediaMessage(type) {
|
||
if (type === ChatingFooterActionTypes.Album) {
|
||
this.actionSheetMenu = [...albumChoose];
|
||
this.showActionSheet = true;
|
||
} else if (type === ChatingFooterActionTypes.File) {
|
||
this.chooseFileAndSend();
|
||
}
|
||
},
|
||
async chooseFileAndSend() {
|
||
// 显示文件选择弹窗,类似微信的文件选择
|
||
this.showFileSelectModal = true;
|
||
},
|
||
|
||
// 获取聊天文件列表
|
||
getChatFiles() {
|
||
// 从store中获取当前会话的消息列表,筛选出文件消息
|
||
const messages = this.$store.getters.storeHistoryMessageList || [];
|
||
this.chatFiles = messages.filter(msg => {
|
||
return msg.contentType === 105; // 文件消息类型
|
||
}).slice(-20); // 只显示最近20个文件
|
||
},
|
||
|
||
// 选择聊天文件
|
||
selectChatFile(file) {
|
||
// 直接转发这个文件消息
|
||
this.forwardFileMessage(file);
|
||
this.showFileSelectModal = false;
|
||
},
|
||
|
||
// 转发文件消息
|
||
async forwardFileMessage(file) {
|
||
try {
|
||
const uuid = IMSDK.uuid();
|
||
const fileElem = file.fileElem;
|
||
|
||
// 创建新的文件消息
|
||
const message = await IMSDK.asyncApi('createFileMessageByURL', IMSDK.uuid(), {
|
||
filePath: fileElem.filePath || '',
|
||
fileName: fileElem.fileName,
|
||
uuid: uuid,
|
||
sourceUrl: fileElem.sourceUrl,
|
||
fileSize: fileElem.fileSize,
|
||
fileType: fileElem.fileType,
|
||
});
|
||
|
||
// 发送消息
|
||
this.sendMessage(message);
|
||
} catch (e) {
|
||
console.log('转发文件失败:', e);
|
||
uni.showToast({
|
||
title: '转发文件失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 获取文件图标
|
||
getFileIcon(file) {
|
||
const fileName = file.fileElem?.fileName?.toLowerCase() || '';
|
||
if (fileName.endsWith('.pdf')) {
|
||
return '/static/images/file_message/file_pdf.png';
|
||
} else if (fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
|
||
return '/static/images/file_message/file_word.png';
|
||
} else if (fileName.endsWith('.xls') || fileName.endsWith('.xlsx')) {
|
||
return '/static/images/file_message/file_excel.png';
|
||
} else if (fileName.endsWith('.ppt') || fileName.endsWith('.pptx')) {
|
||
return '/static/images/file_message/file_ppt.png';
|
||
} else if (fileName.endsWith('.zip') || fileName.endsWith('.rar') || fileName.endsWith('.7z')) {
|
||
return '/static/images/file_message/file_zip.png';
|
||
} else if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png') || fileName.endsWith('.gif')) {
|
||
return '/static/images/file_message/file_image.png';
|
||
} else {
|
||
return '/static/images/file_message/file_unknown.png';
|
||
}
|
||
},
|
||
|
||
// 获取文件名
|
||
getFileName(file) {
|
||
return file.fileElem?.fileName || '未知文件';
|
||
},
|
||
|
||
// 获取文件大小
|
||
getFileSize(file) {
|
||
const size = file.fileElem?.fileSize;
|
||
if (!size) return '';
|
||
if (size < 1024) {
|
||
return size + ' B';
|
||
} else if (size < 1024 * 1024) {
|
||
return (size / 1024).toFixed(1) + ' KB';
|
||
} else {
|
||
return (size / (1024 * 1024)).toFixed(1) + ' MB';
|
||
}
|
||
},
|
||
|
||
// 选择系统文件
|
||
chooseSystemFile(type) {
|
||
switch (type) {
|
||
case 'document':
|
||
// 选择文档
|
||
uni.chooseMessageFile({
|
||
count: 1,
|
||
type: 'file',
|
||
success: async (res) => {
|
||
const file = res.tempFiles[0];
|
||
await this.processFileAndSend(file);
|
||
},
|
||
fail: () => {
|
||
uni.showToast({
|
||
title: '请从聊天记录中选择文档',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
break;
|
||
case 'image':
|
||
// 选择图片
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sourceType: ['album'],
|
||
success: async (res) => {
|
||
const file = {
|
||
name: 'image.jpg',
|
||
size: 0,
|
||
path: res.tempFilePaths[0],
|
||
type: 'image'
|
||
};
|
||
await this.processFileAndSend(file);
|
||
}
|
||
});
|
||
break;
|
||
case 'video':
|
||
// 选择视频
|
||
uni.chooseVideo({
|
||
sourceType: ['album'],
|
||
success: async (res) => {
|
||
const file = {
|
||
name: 'video.mp4',
|
||
size: res.size || 0,
|
||
path: res.tempFilePath,
|
||
type: 'video'
|
||
};
|
||
await this.processFileAndSend(file);
|
||
}
|
||
});
|
||
break;
|
||
case 'audio':
|
||
// 选择音频
|
||
uni.chooseMessageFile({
|
||
count: 1,
|
||
type: 'file',
|
||
success: async (res) => {
|
||
const file = res.tempFiles[0];
|
||
await this.processFileAndSend(file);
|
||
},
|
||
fail: () => {
|
||
uni.showToast({
|
||
title: '请从聊天记录中选择音频文件',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
break;
|
||
}
|
||
this.showFileSelectModal = false;
|
||
},
|
||
|
||
|
||
|
||
|
||
|
||
async processFileAndSend(file) {
|
||
try {
|
||
// 处理文件名
|
||
let fileName = file.name;
|
||
if (!fileName) {
|
||
const pathParts = file.path.split('/');
|
||
fileName = pathParts[pathParts.length - 1] || 'unknown';
|
||
}
|
||
|
||
// 处理文件类型
|
||
let fileType = file.type;
|
||
if (!fileType) {
|
||
const nameIdx = fileName.lastIndexOf(".") + 1;
|
||
fileType = fileName.slice(nameIdx) || 'unknown';
|
||
}
|
||
|
||
const uuid = IMSDK.uuid();
|
||
|
||
// 如果是图片类型,直接创建图片消息
|
||
if (fileType.toLowerCase().includes('image') || fileType.toLowerCase().includes('jpg') || fileType.toLowerCase().includes('jpeg') || fileType.toLowerCase().includes('png') || fileType.toLowerCase().includes('gif')) {
|
||
// 创建图片消息
|
||
const message = await IMSDK.asyncApi(
|
||
IMMethods.CreateImageMessageFromFullPath,
|
||
IMSDK.uuid(),
|
||
file.path
|
||
);
|
||
this.sendMessage(message);
|
||
return;
|
||
}
|
||
|
||
// 先保存到本地
|
||
uni.saveFile({
|
||
tempFilePath: file.path,
|
||
success: (saveRes) => {
|
||
let savedFilePath = saveRes.savedFilePath;
|
||
// 用 plus.io 转换为原生绝对路径
|
||
if (typeof plus !== 'undefined') {
|
||
plus.io.resolveLocalFileSystemURL(savedFilePath, (entry) => {
|
||
IMSDK.asyncApi(IMMethods.UploadFile, uuid, {
|
||
name: fileName,
|
||
contentType: fileType,
|
||
uuid: uuid,
|
||
filepath: entry.fullPath
|
||
})
|
||
.then(({ data }) => {
|
||
let sourceUrl = data.url;
|
||
IMSDK.asyncApi('createFileMessageByURL', IMSDK.uuid(), {
|
||
filePath: entry.fullPath,
|
||
fileName: fileName,
|
||
uuid: uuid,
|
||
sourceUrl: sourceUrl,
|
||
fileSize: file.size || 0,
|
||
fileType: 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: () => {
|
||
// 忽略保存失败
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.log('处理文件失败:', e);
|
||
}
|
||
},
|
||
|
||
// from comp
|
||
batchCreateImageMesage(paths) {
|
||
paths.forEach(async (path) => {
|
||
const message = await IMSDK.asyncApi(
|
||
IMMethods.CreateImageMessageFromFullPath,
|
||
IMSDK.uuid(),
|
||
getPurePath(path)
|
||
);
|
||
this.sendMessage(message);
|
||
});
|
||
},
|
||
selectClick({
|
||
idx
|
||
}) {
|
||
if (idx === 0) {
|
||
this.chooseOrShotImage(["album"]).then((paths) =>
|
||
this.batchCreateImageMesage(paths)
|
||
);
|
||
} else {
|
||
this.chooseOrShotImage(["camera"]).then((paths) =>
|
||
this.batchCreateImageMesage(paths)
|
||
);
|
||
}
|
||
},
|
||
chooseOrShotImage(sourceType) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.chooseImage({
|
||
count: 9,
|
||
sizeType: ["compressed"],
|
||
sourceType,
|
||
success: function ({
|
||
tempFilePaths
|
||
}) {
|
||
resolve(tempFilePaths);
|
||
},
|
||
fail: function (err) {
|
||
reject(err);
|
||
},
|
||
});
|
||
});
|
||
},
|
||
|
||
// keyboard
|
||
keyboardChangeHander({
|
||
height
|
||
}) {
|
||
if (height > 0) {
|
||
if (this.actionBarVisible) {
|
||
this.actionBarVisible = false;
|
||
}
|
||
}
|
||
},
|
||
setKeyboardListener() {
|
||
uni.onKeyboardHeightChange(this.keyboardChangeHander);
|
||
},
|
||
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>
|
||
|
||
<style lang="scss" scoped>
|
||
.custom_editor {
|
||
img {
|
||
vertical-align: sub;
|
||
}
|
||
}
|
||
|
||
.forbidden_footer {
|
||
width: 100%;
|
||
height: 112rpx;
|
||
color: #8e9ab0;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background: #f0f2f6;
|
||
}
|
||
|
||
.chat_footer {
|
||
display: flex;
|
||
align-items: center;
|
||
// background-color: #e9f4ff;
|
||
background: #f0f2f6;
|
||
// height: 50px;
|
||
max-height: 120px;
|
||
padding: 24rpx 20rpx;
|
||
|
||
.input_content {
|
||
flex: 1;
|
||
min-height: 30px;
|
||
max-height: 120px;
|
||
margin: 0 24rpx;
|
||
border-radius: 16rpx;
|
||
position: relative;
|
||
|
||
.record_btn {
|
||
// background-color: #3c9cff;
|
||
background: #fff;
|
||
color: black;
|
||
height: 30px;
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
|
||
.quote_message {
|
||
@include vCenterBox();
|
||
justify-content: space-between;
|
||
margin-top: 12rpx;
|
||
padding: 8rpx;
|
||
// padding-top: 20rpx;
|
||
border-radius: 6rpx;
|
||
background-color: #fff;
|
||
color: #666;
|
||
|
||
.content {
|
||
/deep/ uni-view {
|
||
@include ellipsisWithLine(2);
|
||
}
|
||
}
|
||
}
|
||
|
||
.footer_action_area {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.emoji_action {
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
image {
|
||
width: 26px;
|
||
height: 26px;
|
||
}
|
||
}
|
||
|
||
.send_btn {
|
||
height: 30px;
|
||
line-height: 30px;
|
||
background-color: #4a9cfc;
|
||
padding: 0 8px;
|
||
border-radius: 4px;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.emoji-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: transparent;
|
||
z-index: 998;
|
||
}
|
||
.emoji-picker {
|
||
position: relative;
|
||
z-index: 999;
|
||
}
|
||
|
||
.file-select-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
z-index: 998;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.file-select-container {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
max-height: 80vh;
|
||
overflow: hidden;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.file-select-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.file-select-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.file-select-close {
|
||
font-size: 24px;
|
||
color: #999;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
}
|
||
|
||
.file-select-tabs {
|
||
display: flex;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 16px;
|
||
font-size: 16px;
|
||
color: #666;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
border-bottom: 2px solid transparent;
|
||
}
|
||
|
||
.tab-item.active {
|
||
color: #4a9cfc;
|
||
border-bottom-color: #4a9cfc;
|
||
background: rgba(74, 156, 252, 0.05);
|
||
}
|
||
|
||
.file-select-content {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.chat-files {
|
||
padding: 16px;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.empty-text {
|
||
color: #999;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.file-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
background: #f8f9fa;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.file-item:hover {
|
||
background: #e9ecef;
|
||
}
|
||
|
||
.file-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.file-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.file-name {
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
margin-bottom: 4px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.system-files {
|
||
padding: 24px;
|
||
}
|
||
|
||
.file-options {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.option-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
background: #f8f9fa;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.option-item:hover {
|
||
background: #e9ecef;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.option-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.option-text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
|
||
</style> |