Files
im-uniapp/pages/conversation/chating/components/ChatingFooter/index.vue

1140 lines
32 KiB
Vue
Raw Normal View History

2025-07-04 16:18:58 +08:00
<template>
<view>
<view>
<view class="chat_footer">
<view class="footer_action_area">
2025-07-04 22:34:07 +08:00
<image v-if="inputBox"
@click.prevent="updateInput"
src="@/static/images/chating_footer_recording.png"
alt=""
srcset=""
/>
2025-07-04 16:18:58 +08:00
2025-07-04 22:34:07 +08:00
<image v-if="!inputBox"
@click.prevent="updateInput"
src="@/static/images/chating_footer_audio_recording.png"
alt=""
srcset=""
2025-07-04 16:18:58 +08:00
/>
</view>
2025-07-04 22:34:07 +08:00
<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>
2025-07-04 16:18:58 +08:00
<view class="footer_action_area">
<image
2025-07-04 22:34:07 +08:00
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"
2025-07-04 16:18:58 +08:00
alt=""
srcset=""
/>
2025-07-04 22:34:07 +08:00
<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=""/>
2025-07-04 16:18:58 +08:00
</view>
</view>
2025-07-04 22:34:07 +08:00
<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">
2025-07-04 16:18:58 +08:00
</u-action-sheet>
<!-- Emoji 选择面板 -->
2025-07-04 22:34:07 +08:00
<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>
2025-07-04 16:18:58 +08:00
</view>
</view>
</template>
<script>
2025-07-04 22:34:07 +08:00
import {
mapGetters,
mapActions
} from "vuex";
import {
getPurePath,
html2Text
} from "@/util/common";
import {
offlinePushInfo
} from "@/util/imCommon";
2025-07-04 16:18:58 +08:00
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";
2025-07-04 22:34:07 +08:00
import EmojiPicker from '@/components/EmojiPicker/index.vue';
2025-07-04 16:18:58 +08:00
const needClearTypes = [MessageType.TextMessage];
2025-07-04 22:34:07 +08:00
const albumChoose = [{
name: "图片",
type: ChatingFooterActionTypes.Album,
idx: 0,
},
2025-07-04 16:18:58 +08:00
{
name: "拍照",
type: ChatingFooterActionTypes.Camera,
idx: 1,
},
];
export default {
components: {
CustomEditor,
ChatingActionBar,
UParse,
2025-07-04 22:34:07 +08:00
EmojiPicker,
2025-07-04 16:18:58 +08:00
},
props: {
footerOutsideFlag: Number,
},
data() {
return {
2025-07-04 22:34:07 +08:00
inputBox: true,
2025-07-04 16:18:58 +08:00
customEditorCtx: null,
inputHtml: "",
actionBarVisible: false,
isInputFocus: false,
actionSheetMenu: [],
showActionSheet: false,
2025-07-04 22:34:07 +08:00
isRecording: false,
willCancel: false,
recordingTime: 0,
recordingTimer: null,
showEmojiPicker: false,
showFileSelectModal: false,
currentFileTab: 'chat',
chatFiles: [],
2025-07-04 22:34:07 +08:00
recorderManager: null,
recordFilePath: '',
recordFileDuration: 0,
2025-07-04 16:18:58 +08:00
};
},
computed: {
...mapGetters([
"storeCurrentConversation",
"storeCurrentGroup",
"storeBlackList",
]),
hasContent() {
return html2Text(this.inputHtml) !== "";
},
},
watch: {
footerOutsideFlag(newVal) {
this.onClickActionBarOutside();
},
showFileSelectModal(newVal) {
if (newVal) {
// 当弹窗显示时,获取聊天文件列表
this.getChatFiles();
}
},
2025-07-04 16:18:58 +08:00
},
mounted() {
this.setKeyboardListener();
// 只在浏览器环境下添加事件监听
if (typeof document !== 'undefined') {
document.addEventListener('plusready', function() {
// plus 相关代码
});
}
2025-07-04 16:18:58 +08:00
},
beforeDestroy() {
this.disposeKeyboardListener();
},
methods: {
2025-07-04 22:34:07 +08:00
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;
}
},
2025-07-04 16:18:58 +08:00
...mapActions("message", ["pushNewMessage", "updateOneMessage"]),
async createTextMessage() {
let message = "";
const text = html2Text(this.inputHtml);
message = await IMSDK.asyncApi(
2025-07-04 22:34:07 +08:00
IMMethods.CreateTextMessage,
IMSDK.uuid(),
text
);
2025-07-04 16:18:58 +08:00
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,
})
2025-07-04 22:34:07 +08:00
.then(({
data
}) => {
this.updateOneMessage({
message: data,
isSuccess: true,
});
})
.catch(({
data,
errCode,
errMsg
}) => {
this.updateOneMessage({
message: data,
type: UpdateMessageTypes.KeyWords,
keyWords: [{
2025-07-04 16:18:58 +08:00
key: "status",
value: MessageStatus.Failed,
},
2025-07-04 22:34:07 +08:00
{
key: "errCode",
value: errCode,
},
],
});
2025-07-04 16:18:58 +08:00
});
},
// action
onClickActionBarOutside() {
if (this.actionBarVisible) {
this.actionBarVisible = false;
}
},
updateActionBar() {
this.actionBarVisible = !this.actionBarVisible;
2025-07-04 22:34:07 +08:00
this.showEmojiPicker = false;
2025-07-04 16:18:58 +08:00
},
editorReady(e) {
this.customEditorCtx = e.context;
this.customEditorCtx.clear();
},
editorFocus() {
this.isInputFocus = true;
2025-07-04 22:34:07 +08:00
this.showEmojiPicker = false;
2025-07-04 16:18:58 +08:00
this.$emit("scrollToBottom");
},
editorBlur() {
this.isInputFocus = false;
},
editorInput(e) {
this.inputHtml = e.detail.html;
},
prepareMediaMessage(type) {
if (type === ChatingFooterActionTypes.Album) {
this.actionSheetMenu = [...albumChoose];
2025-07-04 22:34:07 +08:00
this.showActionSheet = true;
} else if (type === ChatingFooterActionTypes.File) {
this.chooseFileAndSend();
2025-07-04 16:18:58 +08:00
}
2025-07-04 22:34:07 +08:00
},
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'
});
2025-07-04 22:34:07 +08:00
}
},
// 获取文件图标
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: () => {
// 忽略保存失败
2025-07-04 22:34:07 +08:00
}
});
} catch (e) {
console.log('处理文件失败:', e);
}
2025-07-04 16:18:58 +08:00
},
// from comp
batchCreateImageMesage(paths) {
paths.forEach(async (path) => {
const message = await IMSDK.asyncApi(
2025-07-04 22:34:07 +08:00
IMMethods.CreateImageMessageFromFullPath,
IMSDK.uuid(),
getPurePath(path)
2025-07-04 16:18:58 +08:00
);
this.sendMessage(message);
});
},
2025-07-04 22:34:07 +08:00
selectClick({
idx
}) {
2025-07-04 16:18:58 +08:00
if (idx === 0) {
this.chooseOrShotImage(["album"]).then((paths) =>
2025-07-04 22:34:07 +08:00
this.batchCreateImageMesage(paths)
2025-07-04 16:18:58 +08:00
);
} else {
this.chooseOrShotImage(["camera"]).then((paths) =>
2025-07-04 22:34:07 +08:00
this.batchCreateImageMesage(paths)
2025-07-04 16:18:58 +08:00
);
}
},
chooseOrShotImage(sourceType) {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 9,
sizeType: ["compressed"],
sourceType,
2025-07-04 22:34:07 +08:00
success: function ({
tempFilePaths
}) {
2025-07-04 16:18:58 +08:00
resolve(tempFilePaths);
},
fail: function (err) {
reject(err);
},
});
});
},
// keyboard
2025-07-04 22:34:07 +08:00
keyboardChangeHander({
height
}) {
2025-07-04 16:18:58 +08:00
if (height > 0) {
if (this.actionBarVisible) {
this.actionBarVisible = false;
}
}
},
setKeyboardListener() {
uni.onKeyboardHeightChange(this.keyboardChangeHander);
},
disposeKeyboardListener() {
uni.offKeyboardHeightChange(this.keyboardChangeHander);
},
2025-07-04 22:34:07 +08:00
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;
},
2025-07-04 16:18:58 +08:00
},
};
</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;
2025-07-04 22:34:07 +08:00
border-radius: 16rpx;
2025-07-04 16:18:58 +08:00
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;
}
}
2025-07-04 22:34:07 +08:00
.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;
}
2025-07-04 22:34:07 +08:00
</style>