Files
Friendship-and-chat-uniapp/src/App.vue
T
2026-03-27 09:40:51 +08:00

647 lines
19 KiB
Vue

<script>
import { mapState } from 'pinia';
import { onLaunch, onShow, onError } from '@dcloudio/uni-app';
import { ShoproInit } from './sheep';
import http from './common/request';
import * as msgType from './common/messageType';
import * as enums from './common/enums';
import * as wsApi from './common/wssocket';
import CHAT_ENV from '@/chat-env.js';
import useChatStore from '@/chat-store/chatStore.js';
export default {
/** ↓↓↓ 以下为app启动聊天相关模块初始化,WebSocket链接,加载聊天消息,加载离线消息,加载群组消息,初始化聊天相关的全局状态仓库 */
data() {
return {
isExit: false, // 是否已退出
audioTip: null,
reconnecting: false, // 正在重连标志
privateMessagesBuffer: [],
groupMessagesBuffer: [],
};
},
// #ifndef MP-WEIXIN
computed: {
...mapState(useChatStore, ['chats']),
unreadCount() {
let count = 0;
this.chats?.forEach((chat) => {
if (!chat.isDnd && !chat.delete && chat.targetId != -200) {
count += chat.unreadCount;
}
});
return count;
},
},
watch: {
unreadCount(newCount, oldCount) {
console.log("watch:");
this.refreshUnreadBadge();
},
},
methods: {
refreshUnreadBadge() {
let count = this.unreadCount
if (count <= 0) {
this.chatStore?.chats?.forEach((chat) => {
if (!chat.isDnd && !chat.delete && chat.targetId != -200) {
count += chat.unreadCount;
}
});
}
if (count > 0) {
uni.setTabBarBadge({
index: 2,
text: count + '',
});
} else {
uni.removeTabBarBadge({
index: 2,
complete: () => { },
});
}
},
// 初始化聊天
async init() {
this.reconnecting = false;
this.isExit = false;
try {
// 加载数据
await this.loadStore()
// 初始化websocket
this.initWebSocket();
this.refreshUnreadBadge()
} catch (error) {
console.log("初始化聊天:", error);
this.exit();
}
},
initWebSocket() {
let loginInfo = uni.getStorageSync('loginInfo');
wsApi.connect(CHAT_ENV.WS_URL, loginInfo.accessToken);
wsApi.onConnect(() => {
if (this.reconnecting) {
// 重连成功
this.onReconnectWs();
} else {
// 加载离线消息
this.pullOfflineMessage();
this.configStore.setAppInit(true);
}
});
wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// 异地登录,强制下线
uni.showModal({
content: '您已在其他地方登录,将被强制下线',
showCancel: false,
});
this.exit();
} else if (cmd == 3) {
if (!this.configStore.appInit || this.chatStore.loading) {
// 如果正在拉取离线消息,先存入缓存区,等待消息拉取完成再处理,防止消息乱序
this.privateMessagesBuffer.push(msgInfo);
} else {
// 插入私聊消息
this.handlePrivateMessage(msgInfo);
}
} else if (cmd == 4) {
if (!this.configStore.appInit || this.chatStore.loading) {
// 如果正在拉取离线消息,先存入缓存区,等待消息拉取完成再处理,防止消息乱序
this.privateMessagesBuffer.push(msgInfo);
} else {
// 插入群聊消息
this.handleGroupMessage(msgInfo);
}
} else if (cmd == 5) {
// 系统消息
this.handleSystemMessage(msgInfo);
}
});
wsApi.onClose((res) => {
console.log('ws断开', res);
// 重新连接
if (!this.reconnecting) {
this.reconnectWs();
this.configStore.setAppInit(false);
}
});
},
loadStore() {
return this.userStore.loadUser().then(() => {
const promises = [];
promises.push(this.friendStore.loadFriend());
promises.push(this.groupStore.loadGroup());
promises.push(this.chatStore.loadChat());
// promises.push(this.configStore.loadConfig());
return Promise.all(promises);
});
},
unloadStore() {
this.friendStore.clear();
this.groupStore.clear();
this.chatStore.clear();
this.configStore.clear();
this.userStore.clear();
},
pullOfflineMessage() {
this.chatStore.setLoading(true);
const promises = [];
promises.push(this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId));
promises.push(this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId));
Promise.all(promises)
.then((messages) => {
// 处理离线消息
messages[0].forEach((m) => this.handlePrivateMessage(m));
messages[1].forEach((m) => this.handleGroupMessage(m));
// 处理缓冲区收到的实时消息
this.privateMessagesBuffer.forEach((m) => this.handlePrivateMessage(m));
this.groupMessagesBuffer.forEach((m) => this.handleGroupMessage(m));
// 清空缓冲区
this.privateMessagesBuffer = [];
this.groupMessagesBuffer = [];
// 关闭加载离线标记
this.chatStore.setLoading(false);
// 刷新会话
this.chatStore.refreshChats();
})
.catch((e) => {
console.log(e);
uni.showToast({
title: '拉取离线消息失败',
icon: 'none',
});
this.exit();
});
},
pullPrivateOfflineMessage(minId) {
return this.$http({
url: '/message/private/loadOfflineMessage?minId=' + minId,
method: 'GET',
timeout: 60000,
});
},
pullGroupOfflineMessage(minId) {
return this.$http({
url: '/message/group/loadOfflineMessage?minId=' + minId,
method: 'GET',
timeout: 60000,
});
},
handlePrivateMessage(msg) {
// 标记这条消息是不是自己发的
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
// 好友id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
// 会话信息
let chatInfo = {
type: 'PRIVATE',
targetId: friendId,
};
// 消息已读处理,清空已读数量
if (msg.type == enums.MESSAGE_TYPE.READED) {
this.chatStore.resetUnreadCount(chatInfo);
return;
}
// 消息回执处理,改消息状态为已读
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
this.chatStore.readedMessage({
friendId: msg.sendId,
});
return;
}
// 消息撤回
if (msg.type == enums.MESSAGE_TYPE.RECALL) {
this.chatStore.recallMessage(msg, chatInfo);
return;
}
// 新增好友
if (msg.type == enums.MESSAGE_TYPE.FRIEND_NEW) {
this.friendStore.addFriend(JSON.parse(msg.content));
return;
}
// 删除好友
if (msg.type == enums.MESSAGE_TYPE.FRIEND_DEL) {
this.friendStore.removeFriend(friendId);
return;
}
// 对好友设置免打扰
if (msg.type == enums.MESSAGE_TYPE.FRIEND_DND) {
this.friendStore.setDnd(friendId, JSON.parse(msg.content));
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return;
}
// 消息插入
let friend = this.loadFriendInfo(friendId);
this.insertPrivateMessage(friend, msg);
},
insertPrivateMessage(friend, msg) {
// 单人视频信令
if (msgType.isRtcPrivate(msg.type)) {
// #ifdef MP-WEIXIN
return; // 小程序不支持音视频
// #endif
// 被呼叫,弹出视频页面
let delayTime = 100;
if (
msg.type == enums.MESSAGE_TYPE.RTC_CALL_VOICE ||
msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO
) {
let mode = msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO ? 'video' : 'voice';
let pages = getCurrentPages();
let curPage = pages[pages.length - 1].route;
if (curPage != 'pages/chat/chat-private-video') {
const friendInfo = encodeURIComponent(JSON.stringify(friend));
uni.navigateTo({
url: `/pages/chat/chat-private-video?mode=${mode}&friend=${friendInfo}&isHost=false`,
});
delayTime = 500;
}
}
setTimeout(() => {
uni.$emit('WS_RTC_PRIVATE', msg);
}, delayTime);
return;
}
// 插入消息
if (msgType.isNormal(msg.type) || msgType.isTip(msg.type) || msgType.isAction(msg.type)) {
let chatInfo = {
type: 'PRIVATE',
targetId: friend.id,
showName: friend.nickName,
headImage: friend.headImage,
isDnd: friend.isDnd,
};
// 打开会话
this.chatStore.openChat(chatInfo);
// 插入消息
this.chatStore.insertMessage(msg, chatInfo);
// 播放提示音
if (
!friend.isDnd &&
!this.chatStore.loading &&
!msg.selfSend &&
msgType.isNormal(msg.type) &&
msg.status != enums.MESSAGE_STATUS.READED
) {
this.playAudioTip();
}
}
},
handleGroupMessage(msg) {
// 标记这条消息是不是自己发的
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId,
};
// 消息已读处理
if (msg.type == enums.MESSAGE_TYPE.READED) {
// 我已读对方的消息,清空已读数量
this.chatStore.resetUnreadCount(chatInfo);
return;
}
// 消息回执处理
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
// 更新消息已读人数
let msgInfo = {
id: msg.id,
groupId: msg.groupId,
readedCount: msg.readedCount,
receiptOk: msg.receiptOk,
};
this.chatStore.updateMessage(msgInfo, chatInfo);
return;
}
// 消息撤回
if (msg.type == enums.MESSAGE_TYPE.RECALL) {
this.chatStore.recallMessage(msg, chatInfo);
return;
}
// 新增群
if (msg.type == enums.MESSAGE_TYPE.GROUP_NEW) {
this.groupStore.addGroup(JSON.parse(msg.content));
return;
}
// 删除群
if (msg.type == enums.MESSAGE_TYPE.GROUP_DEL) {
this.groupStore.removeGroup(msg.groupId);
return;
}
// 对群设置免打扰
if (msg.type == enums.MESSAGE_TYPE.GROUP_DND) {
this.groupStore.setDnd(msg.groupId, JSON.parse(msg.content));
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return;
}
// 插入消息
let group = this.loadGroupInfo(msg.groupId);
this.insertGroupMessage(group, msg);
},
handleSystemMessage(msg) {
if (msg.type == enums.MESSAGE_TYPE.USER_BANNED) {
// 用户被封禁
wsApi.close(3099);
uni.showModal({
content: '您的账号已被管理员封禁,原因:' + msg.content,
showCancel: false,
});
this.exit();
}
},
insertGroupMessage(group, msg) {
// 群视频信令
if (msgType.isRtcGroup(msg.type)) {
// #ifdef MP-WEIXIN
return; // 小程序不支持音视频
// #endif
// 被呼叫,弹出视频页面
let delayTime = 100;
if (msg.type == enums.MESSAGE_TYPE.RTC_GROUP_SETUP) {
let pages = getCurrentPages();
let curPage = pages[pages.length - 1].route;
if (curPage != 'pages/chat/chat-group-video') {
const userInfos = encodeURIComponent(msg.content);
const inviterId = msg.sendId;
const groupId = msg.groupId;
uni.navigateTo({
url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=false
&inviterId=${inviterId}&userInfos=${userInfos}`,
});
delayTime = 500;
}
}
// 消息转发到chat-group-video页面进行处理
setTimeout(() => {
uni.$emit('WS_RTC_GROUP', msg);
}, delayTime);
return;
}
// 插入消息
if (msgType.isNormal(msg.type) || msgType.isTip(msg.type) || msgType.isAction(msg.type)) {
let chatInfo = {
type: 'GROUP',
targetId: group.id,
showName: group.showGroupName,
headImage: group.headImageThumb,
isDnd: group.isDnd,
};
// 打开会话
this.chatStore.openChat(chatInfo);
// 插入消息
this.chatStore.insertMessage(msg, chatInfo);
// 播放提示音
if (
!group.isDnd &&
!this.chatStore.loading &&
!msg.selfSend &&
msgType.isNormal(msg.type) &&
msg.status != enums.MESSAGE_STATUS.READED
) {
this.playAudioTip();
}
}
},
loadFriendInfo(id, callback) {
let friend = this.friendStore.findFriend(id);
if (!friend) {
console.log('未知用户:', id);
friend = {
id: id,
showNickName: '未知用户',
headImage: '',
};
}
return friend;
},
loadGroupInfo(id) {
let group = this.groupStore.findGroup(id);
if (!group) {
group = {
id: id,
showGroupName: '未知群聊',
headImageThumb: '',
};
}
return group;
},
exit() {
console.log('exit');
this.isExit = true;
wsApi.close(3099);
uni.removeStorageSync('loginInfo');
uni.reLaunch({
url: '/pages/login/login',
});
this.unloadStore();
},
playAudioTip() {
// 音频播放无法成功
// this.audioTip = uni.createInnerAudioContext();
// this.audioTip.src = "/static/audio/tip.wav";
// this.audioTip.play();
},
// 刷新token
refreshToken(loginInfo) {
return new Promise((resolve, reject) => {
if (!loginInfo || !loginInfo.refreshToken) {
reject();
return;
}
http({
url: '/refreshToken',
method: 'PUT',
header: {
refreshToken: loginInfo.refreshToken,
},
})
.then((newLoginInfo) => {
uni.setStorageSync('loginInfo', newLoginInfo);
resolve();
})
.catch((e) => {
reject(e);
});
});
},
reconnectWs() {
// 已退出则不再重连
if (this.isExit) {
return;
}
// 记录标志
this.reconnecting = true;
// 重新加载一次个人信息,目的是为了保证网络已经正常且token有效
this.userStore
.loadUser()
.then((userInfo) => {
// 重新连接
let loginInfo = uni.getStorageSync('loginInfo');
wsApi.reconnect(CHAT_ENV.WS_URL, loginInfo.accessToken);
})
.catch(() => {
// 5s后重试
setTimeout(() => {
this.reconnectWs();
}, 5000);
});
},
onReconnectWs() {
this.reconnecting = false;
// 重新加载好友和群聊
const promises = [];
promises.push(this.friendStore.loadFriend());
promises.push(this.groupStore.loadGroup());
Promise.all(promises)
.then(() => {
// 加载离线消息
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
this.configStore.setAppInit(true);
})
.catch((e) => {
console.log(e);
this.exit();
});
},
closeSplashscreen(delay) {
// #ifdef APP-PLUS
// 关闭开机动画
setTimeout(() => {
plus.navigator.closeSplashscreen();
}, delay);
// #endif
},
},
// #endif
onLaunch() {
// 加载Shopro底层依赖
ShoproInit();
// #ifndef MP-WEIXIN
this.$mountStore();
this.closeSplashscreen(1000); // 延迟1s,避免用户看到页面跳转
// 登录状态校验
let loginInfo = uni.getStorageSync('loginInfo');
this.refreshToken(loginInfo)
.then(() => {
// 聊天初始化
this.init();
this.closeSplashscreen(0);
})
.catch(() => {
// 跳转到登录页
uni.redirectTo({ url: '/pages/login/login' });
});
// #endif
},
// onError(error) {
// console.log('onError', error);
// },
onShow() {
// #ifdef APP-PLUS
// 获取urlSchemes参数
const args = plus.runtime.arguments;
if (args) {
}
// 获取剪贴板
uni.getClipboardData({
success: (res) => { },
});
// #endif
},
};
</script>
<style lang="scss">
/* @import '@/sheep/scss/index.scss'; */
:root,
page {
font-family: Noto Sans SC, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, SimSun, sans-serif, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
background-color: #222222;
color: #ffffff;
font-size: 14px;
height: 100%;
--wot-color-theme: #d29e52;
--wot-backtop-bg: #d29e52;
--wot-dark-background: #222;
--wot-dark-background2: #222;
--wot-dark-color: #d29e52;
--wot-picker-toolbar-title-color: #fff;
// background-image: url(./static/page-bg.png);
// background-repeat: no-repeat;
// background-size: 100% 100%;
}
@import '@/im.scss';
@import url('./static/icon/iconfont.css');
@import url('./static/icon/self/iconfont.css');
// #ifdef H5
uni-page-head {
display: none; // h5浏览器本身就有标题
}
// #endif
.tab-page {
position: relative;
display: flex;
flex-direction: column;
// #ifdef H5
height: calc(100vh - 50px - $im-nav-bar-height); // h5平台100vh是包含了底部高度,需要减去
top: $im-nav-bar-height;
// #endif
// #ifdef APP-PLUS
height: calc(100vh - var(--status-bar-height) - $im-nav-bar-height ); // app平台还要减去顶部手机状态栏高度
top: calc($im-nav-bar-height + var(--status-bar-height));
// #endif
// #ifdef MP-WEIXIN
height: calc(100vh - $im-nav-bar-height);
top: $im-nav-bar-height;
// #endif
color: $im-text-color;
background: $im-bg-linear;
font-size: $im-font-size;
}
.page {
position: relative;
// #ifdef H5
height: calc(100vh - $im-nav-bar-height); // h5平台100vh是包含了底部高度,需要减去
top: $im-nav-bar-height;
// #endif
// #ifdef APP-PLUS
height: calc(100vh - var(--status-bar-height) - $im-nav-bar-height ); // app平台还要减去顶部手机状态栏高度
top: calc($im-nav-bar-height + var(--status-bar-height));
// #endif
// #ifdef MP-WEIXIN
height: calc(100vh - $im-nav-bar-height);
top: $im-nav-bar-height;
// #endif
color: $im-text-color;
background: $im-bg-linear;
font-size: $im-font-size;
}
</style>