|
|
|
|
|
<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"> </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 => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[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>
|