You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1136 lines
36 KiB

4 weeks ago
<template>
<!-- 模板完全保持不变与原来一致 -->
<view class=" chat-box" id="chatBox">
<!-- <nav-bar more @more="onShowMore">{{ title }}</nav-bar> -->
<view class="chat-main-box" :style="{height: chatMainHeight+'px'}" >
<view class="chat-message" @click="switchChatTabBox('none')" :style="{ paddingBottom: bottomPadding + 'px' }" >
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-' + scrollMsgIdx" >
<view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item :ref="'message'+msgInfo.id" v-if="idx >= showMinIdx"
:headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)" :showName="showName(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @copy="onCopyMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile"
@audioStateChange="onAudioStateChange" :id="'chat-item-' + idx" :msgInfo="msgInfo"
:groupMembers="groupMembers">
</chat-message-item>
</view>
</scroll-view>
<view v-if="!isInBottom" class="scroll-to-bottom" @click="onClickToBottom">
{{ newMessageSize > 0 ? newMessageSize+'条新消息' :'回到底部'}}
</view>
</view>
<!-- <view v-if="atUserIds.length > 0" class="chat-at-bar" @click="openAtBox()">-->
<!-- <view class="iconfont icon-at">&nbsp;</view>-->
<!-- <scroll-view v-if="atUserIds.length > 0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">-->
<!-- <view class="chat-at-items">-->
<!-- <view v-for="m in atUserItems" class="chat-at-item" :key="m.userId">-->
<!-- <head-image :name="m.showNickName" :url="m.headImage" size="minier"></head-image>-->
<!-- </view>-->
<!-- </view>-->
<!-- </scroll-view>-->
<!-- </view>-->
<submit ref="submitRef" @inputs="onInputs" @send-message="onSendMessage" @heights="onHeights" />
</view>
</view>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { myCache } from '../../utils/utils.js';
import submit from '@/components/chat/submit.vue';
import myVideo from "@/components/myVideo.vue";
//音频播放
const innerAudioContext = uni.createInnerAudioContext();
export default {
components: {
submit,
myVideo
},
data() {
return {
chat: {},
userInfo: {},
group: {},
groupMembers: [],
isReceipt: true,
scrollMsgIdx: 0,
chatTabBox: 'none',
showRecord: false,
chatMainHeight: 0,
bottomPadding: 50,
keyboardHeight: 290,
windowHeight: 1000,
initHeight: 1000,
atUserIds: [],
needScrollToBottom: false,
showMinIdx: 0,
reqQueue: [],
isSending: false,
isShowKeyBoard: false,
editorCtx: null,
isEmpty: true,
isFocus: false,
isReadOnly: false,
playingAudio: null,
isInBottom: true,
newMessageSize: 0
};
},
computed: {
// 映射 Vuex state
...mapState('chatStore', ['chats']),
// ...mapState('userStore', ['userInfo']),
// ...mapState('configStore', ['config']),
// 映射 Vuex getters
...mapGetters('friendStore', ['findFriend']),
...mapGetters('chatStore', ['findChatIdx']),
// 当前用户信息(兼容原 `mine`
mine() {
return myCache('user') || {};
},
// 好友信息
friend() {
if (!this.userInfo.id) return null;
return this.findFriend(this.userInfo.id);
},
// 会话标题
title() {
if (!this.chat) return '';
let title = this.chat.showName;
if (this.chat.type === 'GROUP' ) {
const size = this.groupMembers.filter(m => !m.quit).length;
title += `(${size})`;
}
return title;
},
// 消息发送接口 URL
messageAction() {
return `/api/message/${this.chat.type.toLowerCase()}/send`;
},
// 消息数量
messageSize() {
return this.chat?.messages?.length || 0;
},
// 未读消息数
unreadCount() {
return this.chat?.unreadCount || 0;
},
// 是否被封禁
isBanned() {
return (this.chat.type === 'PRIVATE' && this.userInfo.isBanned) ||
(this.chat.type === 'GROUP' && this.group.isBanned);
},
// @ 成员列表
atUserItems() {
const items = [];
this.atUserIds.forEach(id => {
if (id === -1) {
items.push({ id: -1, showNickName: '全体成员' });
} else {
const member = this.groupMembers.find(m => m.userId === id);
if (member) items.push(member);
}
});
return items;
},
// 群成员数
memberSize() {
return this.groupMembers.filter(m => !m.quit).length;
},
// 配置中的 webrtc 最大通道数(用于模板)
webrtcMaxChannel() {
// return this.config?.webrtc?.maxChannel || 0;
}
},
watch: {
messageSize(newSize, oldSize) {
if (newSize > oldSize) {
if (this.isInBottom) {
this.scrollToBottom();
} else {
this.newMessageSize++;
}
}
},
unreadCount(newCount) {
if (newCount > 0) {
this.toReadedMessage();
}
}
},
onLoad(options) {
// 从 Vuex state 中获取当前会话
this.chat = this.chats[options.chatIdx];
const size = this.messageSize;
this.showMinIdx = size > 20 ? size - 20 : 0;
this.toReadedMessage();
if (this.chat.type === 'GROUP') {
this.loadGroup(this.chat.targetId);
uni.setNavigationBarTitle({
title: this.chat.showName
});
} else {
this.loadFriend(this.chat.targetId);
this.loadReaded(this.chat.targetId);
uni.setNavigationBarTitle({
title: this.chat.showName
});
}
// 激活当前会话action
this.activeChat(options.chatIdx);
this.isReceipt = true;
this.listenKeyBoard();
this.$nextTick(() => {
this.windowHeight = uni.getSystemInfoSync().windowHeight;
this.reCalChatMainHeight();
this.scrollToBottom();
// #ifdef H5
this.initHeight = window.innerHeight;
const chatBox = document.getElementById('chatBox');
chatBox.addEventListener('touchmove', e => e.preventDefault(), { passive: false });
// #endif
});
},
onUnload() {
this.unListenKeyboard();
},
onShow() {
if (this.needScrollToBottom) {
this.scrollToBottom();
this.needScrollToBottom = false;
}
},
methods: {
// 映射 Vuex actions
...mapActions('chatStore', [
'insertMessage',
'deleteMessage',
'recallMessage',
'moveTop',
'resetUnreadCount',
'updateChatFromGroup',
'updateChatFromFriend',
'updateChatFromUser',
'readedMessage',
'activeChat'
]),
...mapActions('friendStore', ['updateFriend']),
...mapActions('groupStore', ['updateGroup']),
// 以下是原业务方法,只需将 `this.chatStore.xxx` 替换为映射后的方法名
onRecorderInput() {
this.showRecord = true;
this.switchChatTabBox('none');
},
onKeyboardInput() {
this.showRecord = false;
this.switchChatTabBox('none');
},
onSendRecord(data) {
if (this.isBanned) {
this.showBannedTip();
return;
}
const msgInfo = {
content: JSON.stringify(data),
type: this.$enums.MESSAGE_TYPE.AUDIO,
receipt: this.isReceipt
};
this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then(res => {
var m=res.data.data;
m.selfSend = true;
this.insertMessage({ msgInfo: m, chatInfo: this.chat });
this.moveChatToTop();
this.scrollToBottom();
this.isReceipt = true;
});
},
onInputs(data) {
if (this.isBanned) {
this.showBannedTip();
return;
}
const { message, type } = data;
if (type === 'txt') {
// 文本消息直接发送
const msgInfo = {
content: message,
type: this.$enums.MESSAGE_TYPE.TEXT,
receipt: true, // 可根据需要调整
};
this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then(m => {
m.selfSend = true;
this.insertMessage({ msgInfo: m, chatInfo: this.chat });
this.moveChatToTop();
this.scrollToBottom();
});
} else if (type === 'image' || type === 'video' || type === 'audio') {
// 图片/视频/语音需要先上传,这里直接调用现有的上传逻辑
// 因为 submit 组件已经做了文件选择,只需用 message 路径
this.uploadAndSend(message, type);
} else {
// 其他类型(如定位)可按需扩展
console.warn('未处理的消息类型:', type);
}
},
/**
* 接收 submit 组件发出的高度变化事件
* @param {number} height - submit 组件总高度px
*/
onHeights(height) {
this.bottomPadding = height;
// 若需同步滚动到底部,可在此调用
this.scrollToBottom();
},
// 扩展的上传发送方法(类似 submit 中原有的 uploadFile 逻辑)
uploadAndSend(filePath, type) {
// 复用原有上传逻辑,例如 onUploadImageBefore 等
// 此处简化,实际应调用统一的发送流程
// ...
},
onRtCall(msgInfo) {
if (msgInfo.type === this.$enums.MESSAGE_TYPE.ACT_RT_VOICE) {
this.onPriviteVoice();
} else if (msgInfo.type === this.$enums.MESSAGE_TYPE.ACT_RT_VIDEO) {
this.onPriviteVideo();
}
},
onPriviteVideo() {
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-private-video?mode=video&friend=${friendInfo}&isHost=true`
});
},
onPriviteVoice() {
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-private-video?mode=voice&friend=${friendInfo}&isHost=true`
});
},
onGroupVideo() {
const ids = [this.info.userid];
this.$refs.selBox.init(ids, ids, []);
this.$refs.selBox.open();
},
onInviteOk(ids) {
if (ids.length < 2) return;
const users = ids.map(id => {
const m = this.groupMembers.find(m => m.userId === id);
return {
id: m.userId,
nickName: m.showNickName,
headImage: m.headImage,
isCamera: false,
isMicroPhone: true
};
});
const groupId = this.group.id;
const inviterId = this.info.userid;
const userInfos = encodeURIComponent(JSON.stringify(users));
uni.navigateTo({
url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=true&inviterId=${inviterId}&userInfos=${userInfos}`
});
},
moveChatToTop() {
const idx = this.findChatIdx(this.chat);
if (idx >= 0) {
this.moveTop(idx);
}
},
switchReceipt() {
this.isReceipt = !this.isReceipt;
},
openAtBox() {
this.$refs.atBox.init(this.atUserIds);
this.$refs.atBox.open();
},
onAtComplete(atUserIds) {
this.atUserIds = atUserIds;
},
onLongPressHead(msgInfo) {
if (!msgInfo.selfSend && this.chat.type === 'GROUP' && this.atUserIds.indexOf(msgInfo.sendId) < 0) {
this.atUserIds.push(msgInfo.sendId);
}
},
headImage(msgInfo) {
if (this.chat.type === 'GROUP') {
const member = this.groupMembers.find(m => m.userId === msgInfo.sendId);
return member ? member.headImage : '';
} else {
return msgInfo.selfSend ? this.mine.avatar : this.chat.headImage;
}
},
showName(msgInfo) {
if (this.chat.type === 'GROUP') {
const member = this.groupMembers.find(m => m.userId === msgInfo.sendId);
return member ? member.showNickName : '';
} else {
return msgInfo.selfSend ? this.mine.nickName : this.chat.showName;
}
},
onSendMessage(payload) {
if (this.isBanned) {
this.showBannedTip();
return;
}
const { content, type } = payload;
// 文本消息直接发送
if (type === 'txt') {
// 仿照 sendTextMessage 构造 msgInfo但无需从 editor 获取内容
// 注意:这里需要处理 @ 和回执,可从当前实例获取 this.atUserIds 和 this.isReceipt
let sendText = content.trim();
if (!sendText && this.atUserIds.length === 0) {
return uni.showToast({ title: '不能发送空白信息', icon: 'none' });
}
// const receiptText = this.isReceipt ? '【回执消息】' : '';
const atText = this.createAtText(); // 沿用已有的 createAtText 方法
const msgInfo = {
content: this.html2Escape(sendText) + atText,
atUserIds: this.atUserIds,
receipt: this.isReceipt,
type: this.$enums.MESSAGE_TYPE.TEXT
};
// 清空 @ 和回执状态
this.atUserIds = [];
this.isReceipt = true;
this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then(m => {
m.selfSend = true;
this.insertMessage({ msgInfo: m, chatInfo: this.chat });
this.moveChatToTop();
this.scrollToBottom();
});
}
// 图片消息
else if (type === 'image') {
// 直接使用现有的图片上传发送逻辑,可调用 onUploadImageBefore 的流程
// 但 onUploadImageBefore 需要 file 对象,这里 content 是文件路径
// 可以模拟一个 file 对象,或直接调用发送图片的封装方法
this.uploadAndSendImage(content);
}
// 视频消息
else if (type === 'video') {
this.uploadAndSendVideo(content);
}
// 音频消息
else if (type === 'audio') {
// content 为 { voice, time }
this.uploadAndSendAudio(content);
}
// 其他类型可根据需要扩展
else {
uni.showToast({ title: '暂不支持该类型', icon: 'none' });
}
},
/**
* 上传并发送图片
* @param {string} filePath - 图片本地路径
*/
uploadAndSendImage(filePath) {
if (this.isBanned) {
this.showBannedTip();
return;
}
// 构造临时消息(模拟文件对象)
const data = { originUrl: filePath, thumbUrl: filePath };
const msgInfo = {
id: 0,
tmpId: this.generateId(),
sendId: this.mine.userid,
content: JSON.stringify(data),
sendTime: Date.now(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.IMAGE,
readedCount: 0,
loadStatus: 'loading',
status: this.$enums.MESSAGE_STATUS.UNSEND,
receipt: this.isReceipt
};
this.fillTargetId(msgInfo, this.chat.targetId);
// 先插入消息(占位)
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.moveChatToTop();
this.scrollToBottom();
// 开始上传
const token = uni.getStorageSync('token');
uni.uploadFile({
url: uni.$http.baseUrl + '/api/image/upload',
filePath: filePath,
name: 'file',
header: { Authorization: token },
success: (res) => {
const result = JSON.parse(res.data);
if (result.data && result.data.originUrl) {
// 更新消息内容
msgInfo.content = JSON.stringify(result.data);
msgInfo.receipt = this.isReceipt;
// 发送消息请求
this.sendMessageRequest(msgInfo).then(m => {
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
// 再次插入更新消息
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.isReceipt = true; // 重置回执状态(可根据需求调整)
});
} else {
// 上传失败
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: this.chat });
uni.showToast({ title: result.msg || '图片上传失败', icon: 'none' });
}
},
fail: (err) => {
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: this.chat });
uni.showToast({ title: '图片上传失败', icon: 'none' });
}
});
},
/**
* 上传并发送视频
* @param {string} filePath - 视频本地路径
*/
uploadAndSendVideo(filePath) {
// 视频上传逻辑与图片类似,但接口可能不同,此处假设使用 /api/file/upload
// 消息类型为 MESSAGE_TYPE.VIDEO
const data = { url: filePath };
const msgInfo = {
id: 0,
tmpId: this.generateId(),
sendId: this.mine.userid,
content: JSON.stringify(data),
sendTime: Date.now(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.VIDEO,
loadStatus: 'loading',
status: this.$enums.MESSAGE_STATUS.UNSEND,
receipt: this.isReceipt
};
this.fillTargetId(msgInfo, this.chat.targetId);
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.moveChatToTop();
this.scrollToBottom();
const token = uni.getStorageSync('token');
uni.uploadFile({
url: uni.$http.baseUrl + '/api/file/upload',
filePath: filePath,
name: 'file',
header: { Authorization: token },
success: (res) => {
const result = JSON.parse(res.data);
if (result.data) {
msgInfo.content = JSON.stringify({ url: result.data });
msgInfo.receipt = this.isReceipt;
this.sendMessageRequest(msgInfo).then(m => {
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.isReceipt = true;
});
} else {
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: this.chat });
uni.showToast({ title: result.msg || '视频上传失败', icon: 'none' });
}
},
fail: () => {
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: this.chat });
uni.showToast({ title: '视频上传失败', icon: 'none' });
}
});
},
/**
* 上传并发送音频
* @param {Object} audioData - { voice: 文件路径, time: 秒数 }
*/
uploadAndSendAudio(audioData) {
const { voice, time } = audioData;
const data = { url: voice, duration: time };
const msgInfo = {
id: 0,
tmpId: this.generateId(),
sendId: this.mine.userid,
content: JSON.stringify(data),
sendTime: Date.now(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.AUDIO,
loadStatus: 'loading',
status: this.$enums.MESSAGE_STATUS.UNSEND,
receipt: this.isReceipt
};
this.fillTargetId(msgInfo, this.chat.targetId);
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.moveChatToTop();
this.scrollToBottom();
const token = uni.getStorageSync('token');
uni.uploadFile({
url: uni.$http.baseUrl + '/api/file/upload',
filePath: voice,
name: 'file',
header: { Authorization: token },
success: (res) => {
const result = JSON.parse(res.data);
if (result.data) {
msgInfo.content = JSON.stringify({ url: result.data, duration: time });
msgInfo.receipt = this.isReceipt;
this.sendMessageRequest(msgInfo).then(m => {
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.isReceipt = true;
});
} else {
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: this.chat });
uni.showToast({ title: result.msg || '音频上传失败', icon: 'none' });
}
},
fail: () => {
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: this.chat });
uni.showToast({ title: '音频上传失败', icon: 'none' });
}
});
},
sendTextMessage() {
this.editorCtx.getContents({
success: e => {
this.editorCtx.clear();
if (this.isBanned) {
this.showBannedTip();
return;
}
let sendText = '';
e.delta.ops.forEach(op => {
if (op.insert.image) {
sendText += `#${op.attributes.alt};`;
} else {
sendText += op.insert;
}
});
if (!sendText.trim() && this.atUserIds.length === 0) {
return uni.showToast({ title: '不能发送空白信息', icon: 'none' });
}
// const receiptText = this.isReceipt ? '【回执消息】' : '';
const atText = this.createAtText();
const msgInfo = {
content: this.html2Escape(sendText) + atText,
atUserIds: this.atUserIds,
receipt: this.isReceipt,
type: 0
};
this.atUserIds = [];
this.isReceipt = true;
this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then(m=> {
m.selfSend = true;
this.insertMessage({ msgInfo: m, chatInfo: this.chat });
this.moveChatToTop();
}).finally(() => {
this.scrollToBottom();
});
}
});
},
createAtText() {
let text = '';
this.atUserIds.forEach(id => {
if (id === -1) {
text += ' @全体成员';
} else {
const member = this.groupMembers.find(m => m.userId === id);
if (member) text += ` @${member.showNickName}`;
}
});
return text;
},
fillTargetId(msgInfo, targetId) {
if (this.chat.type === 'GROUP') {
msgInfo.groupId = targetId;
} else {
msgInfo.recvId = targetId;
}
},
scrollToBottom() {
const size = this.messageSize;
if (size > 0) {
this.scrollToMsgIdx(size - 1);
}
},
scrollToMsgIdx(idx) {
if (idx === this.scrollMsgIdx && idx > 0) {
this.$nextTick(() => {
this.scrollMsgIdx = idx - 1;
this.scrollToMsgIdx(idx);
});
return;
}
this.$nextTick(() => {
this.scrollMsgIdx = idx;
});
},
onShowEmoChatTab() {
this.showRecord = false;
this.switchChatTabBox('emo');
},
onShowToolsChatTab() {
this.showRecord = false;
this.switchChatTabBox('tools');
},
switchChatTabBox(type) {
this.chatTabBox = type;
this.reCalChatMainHeight();
uni.hideKeyboard();
// 通知 submit 组件关闭其内部面板
if (this.$refs.submitRef) {
this.$refs.submitRef.closeAll();
}
// if (type !== 'tools' && this.$refs.fileUpload) {
// this.$refs.fileUpload.hide();
// }
},
selectEmoji(emoText) {
const path = this.$emo.textToPath(emoText);
this.isReadOnly = true;
this.isEmpty = false;
this.$nextTick(() => {
this.editorCtx.insertImage({
src: path,
alt: emoText,
extClass: 'emoji-small',
nowrap: true,
complete: () => {
this.isReadOnly = false;
this.editorCtx.blur();
}
});
});
},
onUploadImageBefore(file) {
if (this.isBanned) {
this.showBannedTip();
return false;
}
const data = { originUrl: file.path, thumbUrl: file.path };
const msgInfo = {
id: 0,
tmpId: this.generateId(),
fileId: file.uid,
sendId: this.info.userid,
content: JSON.stringify(data),
sendTime: Date.now(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.IMAGE,
readedCount: 0,
loadStatus: 'loading',
status: this.$enums.MESSAGE_STATUS.UNSEND
};
this.fillTargetId(msgInfo, this.chat.targetId);
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.moveChatToTop();
file.msgInfo = msgInfo;
file.chat = this.chat;
this.scrollToBottom();
return true;
},
onUploadImageSuccess(file, res) {
const msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(res.data);
msgInfo.receipt = this.isReceipt;
this.sendMessageRequest(msgInfo).then( res=> {
var m =res.data.data
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
this.isReceipt = true;
this.insertMessage({ msgInfo, chatInfo: file.chat });
});
},
onUploadImageFail(file) {
const msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: file.chat });
},
onUploadFileBefore(file) {
if (this.isBanned) {
this.showBannedTip();
return false;
}
const data = { name: file.name, size: file.size, url: file.path };
const msgInfo = {
id: 0,
tmpId: this.generateId(),
sendId: this.info.userid,
content: JSON.stringify(data),
sendTime: Date.now(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.FILE,
readedCount: 0,
loadStatus: 'loading',
status: this.$enums.MESSAGE_STATUS.UNSEND
};
this.fillTargetId(msgInfo, this.chat.targetId);
this.insertMessage({ msgInfo, chatInfo: this.chat });
this.moveChatToTop();
file.msgInfo = msgInfo;
file.chat = this.chat;
this.scrollToBottom();
return true;
},
onUploadFileSuccess(file, res) {
const data = { name: file.name, size: file.size, url: res.data };
const msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data);
msgInfo.receipt = this.isReceipt;
this.sendMessageRequest(msgInfo).then(res => {
var m=res.data.data
msgInfo.loadStatus = 'ok';
msgInfo.id = m.id;
this.isReceipt = true;
this.insertMessage({ msgInfo, chatInfo: file.chat });
});
},
onUploadFileFail(file) {
const msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail';
this.insertMessage({ msgInfo, chatInfo: file.chat });
},
onDeleteMessage(msgInfo) {
uni.showModal({
title: '删除消息',
content: '确认删除消息?',
success: res => {
if (!res.cancel) {
this.deleteMessage({ msgInfo, chatInfo: this.chat });
uni.showToast({ title: '删除成功', icon: 'none' });
}
}
});
},
async onRecallMessage(msgInfo) {
uni.showModal({
title: '撤回消息',
content: '确认撤回消息?',
success: res => {
if (!res.cancel) {
// const url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`;
// uni.$http.delete(url).then(m => {
// m.selfSend = true;
// this.recallMessage({ msgInfo: m, chatInfo: this.chat });
// });
}
}
});
},
onCopyMessage(msgInfo) {
uni.setClipboardData({
data: msgInfo.content,
success: () => uni.showToast({ title: '复制成功' }),
fail: () => uni.showToast({ title: '复制失败', icon: 'none' })
});
},
onDownloadFile(msgInfo) {
const url = JSON.parse(msgInfo.content).url;
uni.downloadFile({
url,
success(res) {
if (res.statusCode === 200) {
const filePath = encodeURI(res.tempFilePath);
uni.openDocument({ filePath, showMenu: true });
}
},
fail() {
uni.showToast({ title: '文件下载失败', icon: 'none' });
}
});
},
onClickToBottom() {
this.scrollToBottom();
setTimeout(() => {
this.isInBottom = true;
this.newMessageSize = 0;
}, 100);
},
onScrollToTop() {
if (this.showMinIdx > 0) {
// #ifndef H5
this.scrollToMsgIdx(this.showMinIdx);
// #endif
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
}
this.isInBottom = false;
},
onScrollToBottom() {
this.isInBottom = true;
this.newMessageSize = 0;
},
onShowMore() {
if (this.chat.type === 'GROUP') {
uni.navigateTo({ url: `/pages/group/group-info?id=${this.group.id}` });
} else {
uni.navigateTo({ url: `/pages/common/user-info?id=${this.userInfo.id}` });
}
},
onTextInput(e) {
this.isEmpty = e.detail.html === '<p><br></p>';
},
onEditorReady() {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query.select('#editor').context(res => {
this.editorCtx = res.context;
}).exec();
});
},
onEditorFocus() {
this.isFocus = true;
this.scrollToBottom();
this.switchChatTabBox('none');
},
onEditorBlur() {
this.isFocus = false;
},
onAudioStateChange(state, msgInfo) {
const playingAudio = this.$refs['message' + msgInfo.id]?.[0];
if (state === 'PLAYING' && playingAudio !== this.playingAudio) {
this.playingAudio?.stopPlayAudio();
this.playingAudio = playingAudio;
}
},
//获取最大已读id
async loadReaded(fid) {
await uni.$http.get( `/api/message/private/maxReadedId?friendId=${fid}`).then(res => {
//获取最大已读id
this.readedMessage({ friendId: fid, maxId: res.data.data });
});
},
async toReadedMessage() {
if (this.unreadCount === 0) return;
const url = this.chat.type === 'GROUP'
? `/api/message/group/readed?groupId=${this.chat.targetId}`
: `/api/message/private/readed?friendId=${this.chat.targetId}`;
await uni.$http.put(url).then(() => {
this.resetUnreadCount(this.chat);
});
},
async loadGroup(groupId) {
await uni.$http.get(`/api/group/find/${groupId}`).then(res => {
var group =res.data.data
this.group = group;
this.updateChatFromGroup(group);
this.updateGroup(group);
});
await uni.$http.get(`/api/group/members/${groupId}`).then(members => {
this.groupMembers = members.data.data;
});
},
updateFriendInfo() {
if (this.friend) {
const friend = JSON.parse(JSON.stringify(this.friend));
friend.headImage = this.userInfo.headImageThumb;
friend.nickName = this.userInfo.nickName;
friend.showNickName = friend.remarkNickName || friend.nickName;
this.updateFriend(friend);
this.updateChatFromFriend(friend);
} else {
this.updateChatFromUser(this.userInfo);
}
},
async loadFriend(friendId) {
await uni.$http.get( `/api/friend/find/${friendId}`).then(userInfo => {
this.userInfo = userInfo.data.data;
this.updateFriendInfo();
});
},
rpxTopx(rpx) {
const info = uni.getSystemInfoSync();
return Math.floor(info.windowWidth * rpx / 750);
},
html2Escape(str) {
return str.replace(/[<>&"]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c]));
},
sendMessageRequest(msgInfo) {
return new Promise((resolve, reject) => {
this.reqQueue.push({ msgInfo, resolve, reject });
this.processReqQueue();
});
},
async processReqQueue() {
if (this.reqQueue.length && !this.isSending) {
this.isSending = true;
const reqData = this.reqQueue.shift();
await uni.$http.post( this.messageAction,reqData.msgInfo)
.then(res => reqData.resolve(res.data.data))
.catch(e => reqData.reject(e))
.finally(() => {
this.isSending = false;
this.processReqQueue();
});
}
},
reCalChatMainHeight() {
setTimeout(() => {
let h = this.windowHeight;
// 减去标题栏高度
h -= 0;
// 减去键盘高度
// if (this.isShowKeyBoard || this.chatTabBox !== 'none') {
// h -= this.keyboardHeight;
// this.scrollToBottom();
// }
// #ifndef H5
// h5需要减去状态栏高度
h -= uni.getSystemInfoSync().statusBarHeight;
// #endif
this.chatMainHeight = h;
if (this.isShowKeyBoard || this.chatTabBox !== 'none') {
this.scrollToBottom();
}
// #ifdef H5
if (uni.getSystemInfoSync().platform === 'ios') {
[50, 100, 500].forEach(delay => {
setTimeout(() => {
uni.pageScrollTo({ scrollTop: 0, duration: 10 });
}, delay);
});
}
// #endif
}, 30);
},
listenKeyBoard() {
// #ifdef H5
if (navigator.platform === 'Win32') {
console.log('navigator.platform:', navigator.platform);
return;
}
if (uni.getSystemInfoSync().platform === 'ios') {
window.addEventListener('focusin', this.focusInListener);
window.addEventListener('focusout', this.focusOutListener);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', this.resizeListener);
}
} else {
window.addEventListener('resize', this.resizeListener);
}
// #endif
// #ifndef H5
uni.onKeyboardHeightChange(this.keyBoardListener);
// #endif
},
unListenKeyboard() {
// #ifdef H5
window.removeEventListener('resize', this.resizeListener);
window.removeEventListener('focusin', this.focusInListener);
window.removeEventListener('focusout', this.focusOutListener);
// #endif
// #ifndef H5
uni.offKeyboardHeightChange(this.keyBoardListener);
// #endif
},
keyBoardListener(res) {
this.isShowKeyBoard = res.height > 0;
if (this.isShowKeyBoard) this.keyboardHeight = res.height;
this.reCalChatMainHeight();
},
resizeListener() {
let height = this.initHeight - window.innerHeight;
if (window.visualViewport && uni.getSystemInfoSync().platform === 'ios') {
height = this.initHeight - window.visualViewport.height;
}
console.log('resizeListener:', window.visualViewport?.height);
this.isShowKeyBoard = height > 150;
if (this.isShowKeyBoard) this.keyboardHeight = height;
this.reCalChatMainHeight();
},
focusInListener() {
this.isShowKeyBoard = true;
this.reCalChatMainHeight();
},
focusOutListener() {
this.isShowKeyBoard = false;
this.reCalChatMainHeight();
},
showBannedTip() {
const msgInfo = {
tmpId: this.generateId(),
sendId: this.mine.userid,
sendTime: Date.now(),
type: this.$enums.MESSAGE_TYPE.TIP_TEXT
};
if (this.chat.type === 'PRIVATE') {
msgInfo.recvId = this.mine.userid;
msgInfo.content = `该用户已被管理员封禁,原因:${this.userInfo.reason}`;
} else {
msgInfo.groupId = this.group.id;
msgInfo.content = `本群聊已被管理员封禁,原因:${this.group.reason}`;
}
this.insertMessage({ msgInfo, chatInfo: this.chat });
},
generateId() {
return String(Date.now()) + String(Math.floor(Math.random() * 1000));
}
}
};
</script>
<style lang="scss">
.chat-box {
$icon-color: rgba(0, 0, 0, 0.88);
position: relative;
background-color: rgba(244, 244, 244, 1);
.chat-main-box {
position: fixed;
width: 100%;
display: flex;
flex-direction: column;
z-index: 2;
.chat-message {
flex: 1;
padding: 0;
overflow: hidden;
position: relative;
background-color: white;
.scroll-box {
height: 100%;
}
.scroll-to-bottom {
//position: absolute;
//
//right: 30rpx;
//bottom: 30rpx;
font-size: $im-font-size;
color: $im-color-primary;
font-weight: 600;
background: white;
//padding: 10rpx 30rpx;
border-radius: 25rpx;
//box-shadow: $im-box-shadow-dark;
}
}
}
}
</style>