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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>