本次关于 uni-app 代码整体重构工作,基于上一期针对 uni-app 官网 demo 从 vue2 迁移 vue3 框架衍生而来,在迁移过程中有明显感知,目前的项目存在的问题为,项目部分代码风格较为不统一,命名不够规范,注释不够清晰、可读性差、以造成如果复用困难重重,本地重构期望能够充分展示 api 在实际项目中的调用方式,尽可能达到示例代码可移植,或能辅助进行即时通讯功能二次开发的能力。
使代码更加可读。
简化或去除冗余代码。
部分组件以及逻辑重命名、重拆分、重合并。
增加全局状态管理。
修改 SDK 引入方式为通过 npm 形式引入
收束 SDK 大部分 API 到统一文件、方便管理调用。
升级 SDK api 至最新的调用方式(监听、发送消息)
增加会话列表接口、消息漫游接口。
SDK 指环信 IM uni-app SDK
Pinia 文档
Uni-app Pinia 文档
pinia 还能通过$reset()方法即可完成对某个 store 的初始化,利用该方法可以非常方便的在切换账号时针对缓存在 stores 中的数据初始化,防止切换后的账号与上一个账号的数据造成冲突。
//Pinia
import * as Pinia from 'pinia';
export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());
return {
app,
Pinia,
};
}
App.vue 改动相对较大,主要为监听的迁移,一部分方法迁移至 stores 中,并且需要重新进行监听的挂载。具体代码可在后续迁移前后比对中看到,或者文尾的看到 github 中看到代码地址。
原有 login 组件登录部分代码比较冗长并且可读性较差因此进行优化。
此改动主要影响为要涉及到将原 setting 组件改为 Me 组件,并将三个原有页面 pages.json 删除并在 home 中引入,并无其他副作用。
以 Conversation 组件举例
主要影响则是这些组件内的逻辑代码会有从结构以及数据源会有较大变化,需要边改造边验证,并且会与 stores、EaseIM 等组件有较大的关系,需要耐心进行调整。
从会话进入到聊天页、从联系人、群组页面进入到聊天页的路由跳转路径全部改为 emChatContainer,并且将会改变 chat 组件使用 targetId(聊天目标 ID)以及 chatType 的方式,因为需要改为通过 inject 接收。
该组件调整难度最大,因为牵扯的组件,以及需要新增的调整的代码较多,需要逐个组件修改并验证,具体代码将在下方局部展示。详情请参看源码地址。
能够在 IM websocket 断开的时候有相应的回调出来,并给到用户相应的提示。
在 addEventHandler 监听中增加 onReconnecting 监听回调,并且在实际触发的时候增加 Toast 提示监听 IM 正在重连中。
PS:onReconnecting 属于实验性回调。
import websdk from 'easemob-websdk/uniApp/Easemob-chat';
import config from './WebIMConfig';
console.group = console.group || {};
console.groupEnd = console.groupEnd || {};
var window = {};
let WebIM = (window.WebIM = uni.WebIM = websdk);
window.WebIM.config = config;
WebIM.conn = new WebIM.connection({
appKey: WebIM.config.appkey,
url: WebIM.config.xmppURL,
apiUrl: WebIM.config.apiURL,
});
export default WebIM;
import EaseSDK from 'easemob-websdk/uniApp/Easemob-chat';
import { EM_APP_KEY, EM_API_URL, EM_WEB_SOCKET_URL } from './config';
let EMClient = (uni.EMClient = {});
EMClient = new EaseSDK.connection({
appKey: EM_APP_KEY,
apiUrl: EM_API_URL,
url: EM_WEB_SOCKET_URL,
});
uni.EMClient = EMClient;
export { EaseSDK, EMClient };
该组件代码过于长,为防止水文嫌疑,因此截取部分代码展示[手动狗头]
<script>
import WebIM from '@/utils/WebIM.js';
import msgStorage from '@/components/chat/msgstorage';
import _chunkArr from './utils/chunkArr';
import msgType from '@/components/chat/msgtype';
import disp from '@/utils/broadcast';
import { onGetSilentConfig } from './components/chat/pushStorage';
let logout = false;
function ack(receiveMsg) {
// 处理未读消息回执
var bodyId = receiveMsg.id; // 需要发送已读回执的消息id
var ackMsg = new WebIM.message('read', WebIM.conn.getUniqueId());
ackMsg.set({
id: bodyId,
to: receiveMsg.from,
});
WebIM.conn.send(ackMsg.body);
}
function onMessageError(err) {
if (err.type === 'error') {
uni.showToast({
title: err.errorText,
});
return false;
}
return true;
}
function getCurrentRoute() {
let pages = getCurrentPages();
if (pages.length > 0) {
let currentPage = pages[pages.length - 1];
return currentPage.route;
}
return '/';
}
// 包含陌生人版本
//该方法用以计算本地存储消息的未读总数。
function calcUnReadSpot(message) {
let myName = uni.getStorageSync('myUsername');
let pushObj = uni.getStorageSync('pushStorageData');
let pushAry = pushObj[myName] || [];
uni.getStorageInfo({
success: function (res) {
let storageKeys = res.keys;
let newChatMsgKeys = [];
let historyChatMsgKeys = [];
storageKeys.forEach((item) => {
if (item.indexOf(myName) > -1 && item.indexOf('rendered_') == -1) {
newChatMsgKeys.push(item);
}
});
let count = newChatMsgKeys.reduce(function (result, curMember, idx) {
let newName = curMember.split(myName)[0];
let chatMsgs;
chatMsgs = uni.getStorageSync(curMember) || [];
//过滤消息来源与当前登录ID一致的消息,不计入总数中。
chatMsgs = chatMsgs.filter((msg) => msg.yourname !== myName);
if (pushAry.includes(newName)) return result;
return result + chatMsgs.length;
}, 0);
getApp().globalData.unReadMessageNum = count;
disp.fire('em.unreadspot', message);
},
});
}
function saveGroups() {
var me = this;
return WebIM.conn.getGroup({
limit: 50,
success: function (res) {
uni.setStorage({
key: 'listGroup',
data: res.data,
});
},
error: function (err) {
console.log(err);
},
});
}
export default {
globalData: {
phoneNumber: '',
unReadMessageNum: 0,
userInfo: null,
userInfoFromServer: null, //用户属性从环信服务器获取
friendUserInfoMap: new Map(), //好友属性
saveFriendList: [],
saveGroupInvitedList: [],
isIPX: false, //是否为iphone X
conn: {
closed: false,
curOpenOpt: {},
open(opt) {
uni.showLoading({
title: '正在初始化客户端..',
mask: true,
});
const actionOpen = () => {
this.curOpenOpt = opt;
WebIM.conn
.open(opt)
.then(() => {
//token获取成功,即可开始请求用户属性。
disp.fire('em.mian.profile.update');
disp.fire('em.mian.friendProfile.update');
})
.catch((err) => {
console.log('>>>>>token获取失败', err);
});
this.closed = false;
};
if (WebIM.conn.isOpened()) {
WebIM.conn.close();
setTimeout(() => {
actionOpen();
}, 300);
} else {
actionOpen();
}
},
reopen() {
if (this.closed) {
//this.open(this.curOpenOpt);
WebIM.conn.open(this.curOpenOpt);
this.closed = false;
}
},
},
onLoginSuccess: function (myName) {
uni.hideLoading();
uni.redirectTo({
url: '../conversation/conversation?myName=' + myName,
});
},
onLaunch() {
var me = this;
var logs = uni.getStorageSync('logs') || [];
logs.unshift(Date.now());
uni.setStorageSync('logs', logs);
disp.on('em.main.ready', function () {
calcUnReadSpot();
});
uni.WebIM.conn.listen({
onOpened(message) {
if (
getCurrentRoute() == 'pages/login/login' ||
getCurrentRoute() == 'pages/login_token/login_token'
) {
me.globalData.onLoginSuccess(
uni.getStorageSync('myUsername').toLowerCase()
);
me.fetchFriendListFromServer();
}
},
onReconnect() {
uni.showToast({
title: '重连中...',
duration: 2000,
});
},
onSocketConnected() {
uni.showToast({
title: 'socket连接成功',
duration: 2000,
});
},
onClosed() {
uni.showToast({
title: '退出登录',
icon: 'none',
duration: 2000,
});
uni.redirectTo({
url: '../login/login',
});
me.globalData.conn.closed = true;
WebIM.conn.close();
},
onTextMessage(message) {
console.log('onTextMessage', message);
if (message) {
if (onMessageError(message)) {
msgStorage.saveReceiveMsg(message, msgType.TEXT);
}
calcUnReadSpot(message);
ack(message);
onGetSilentConfig(message);
}
},
onPictureMessage(message) {
console.log('onPictureMessage', message);
if (message) {
if (onMessageError(message)) {
msgStorage.saveReceiveMsg(message, msgType.IMAGE);
}
calcUnReadSpot(message);
ack(message);
onGetSilentConfig(message);
}
},
});
this.globalData.checkIsIPhoneX();
},
methods: {
async fetchUserInfoWithLoginId() {
const userId = await uni.WebIM.conn.user;
if (userId) {
try {
const { data } = await uni.WebIM.conn.fetchUserInfoById(userId);
this.globalData.userInfoFromServer = Object.assign({}, data[userId]);
} catch (error) {
console.log(error);
uni.showToast({
title: '用户属性获取失败',
icon: 'none',
duration: 2000,
});
}
}
},
async fetchFriendInfoFromServer() {
let friendList = [];
try {
const res = await uni.WebIM.conn.getContacts();
friendList = Object.assign([], res?.data);
if (friendList.length && friendList.length < 99) {
const { data } = await uni.WebIM.conn.fetchUserInfoById(friendList);
this.setFriendUserInfotoMap(data);
} else {
let newArr = _chunkArr(friendList, 99);
for (let i = 0; i < newArr.length; i++) {
const { data } = await uni.WebIM.conn.fetchUserInfoById(newArr[i]);
this.setFriendUserInfotoMap(data);
}
}
} catch (error) {
console.log(error);
uni.showToast({
title: '用户属性获取失败',
icon: 'none',
});
}
},
setFriendUserInfotoMap(data) {
if (Object.keys(data).length) {
for (const key in data) {
if (Object.hasOwnProperty.call(data, key)) {
const values = data[key];
Object.values(values).length &&
this.globalData.friendUserInfoMap.set(key, values);
}
}
}
},
async fetchFriendListFromServer() {
uni.removeStorageSync('member');
try {
const { data } = await WebIM.conn.getContacts();
console.log('>>>>>>App.vue 拉取好友列表');
if (data.length) {
uni.setStorage({
key: 'member',
data: [...data],
});
}
} catch (error) {
console.log('>>>>>好友列表获取失败', error);
}
},
},
};
</script>
<style lang="scss">
@import './app.css';
</style>
可以看到比原有 App.vue 组件有明显的代码简化。
<script>
/* EaseIM */
import '@/EaseIM';
import { emConnectListener, emMountGlobalListener } from '@/EaseIM/listener';
import { emUserInfos, emGroups, emContacts } from '@/EaseIM/imApis';
import { CONNECT_CALLBACK_TYPE } from '@/EaseIM/constant';
import { useLoginStore } from '@/stores/login';
import { useGroupStore } from '@/stores/group';
import { useContactsStore } from '@/stores/contacts';
import { EMClient } from './EaseIM';
export default {
setup() {
const loginStore = useLoginStore();
const groupStore = useGroupStore();
const contactsStore = useContactsStore();
/* 链接所需监听回调 */
//传给监听callback回调
const connectedCallback = (type) => {
console.log('>>>>>连接成功回调', type);
if (type === CONNECT_CALLBACK_TYPE.CONNECT_CALLBACK) {
onConnectedSuccess();
}
if (type === CONNECT_CALLBACK_TYPE.DISCONNECT_CALLBACK) {
onDisconnect();
}
if (type === CONNECT_CALLBACK_TYPE.RECONNECTING_CALLBACK) {
onReconnecting();
}
};
//IM连接成功
const onConnectedSuccess = () => {
const loginUserId = loginStore.loginUserBaseInfos.loginUserId;
if (!loginStore.loginStatus) {
fetchLoginUserNeedData();
}
loginStore.setLoginStatus(true);
uni.hideLoading();
uni.redirectTo({
url: '../home/index?myName=' + loginUserId,
});
};
//IM断开连接
const onDisconnect = () => {
//断开回调触发后,如果业务登录状态为true则说明异常断开需要重新登录
if (!loginStore.loginStatus) {
uni.showToast({
title: '退出登录',
icon: 'none',
duration: 2000,
});
uni.redirectTo({
url: '../login/login',
});
EMClient.close();
} else {
//执行通过token机型重新登录
const loginUserId = uni.getStorageSync('myUsername');
const loginUserToken =
loginUserId && uni.getStorageSync(`EM_${loginUserId}_TOKEN`);
EMClient.open({ user: loginUserId, accessToken: loginUserToken.token });
}
};
//IM重连中
const onReconnecting = () => {
uni.showToast({
title: 'IM 重连中...',
icon: 'none',
});
};
//挂载IM websocket连接成功监听
emConnectListener(connectedCallback);
const { fetchUserInfoWithLoginId, fetchOtherInfoFromServer } =
emUserInfos();
const { fetchJoinedGroupListFromServer } = emGroups();
const { fetchContactsListFromServer } = emContacts();
//获取登录所需基础参数
const fetchLoginUserNeedData = async () => {
//获取好友列表
const friendList = await fetchContactsListFromServer();
await contactsStore.setFriendList(friendList);
//获取群组列表
const joinedGroupList = await fetchJoinedGroupListFromServer();
joinedGroupList.length &&
(await groupStore.setJoinedGroupList(joinedGroupList));
if (friendList.length) {
//获取好友用户属性
const friendProfiles = await fetchOtherInfoFromServer(friendList);
contactsStore.setFriendUserInfotoMap(friendProfiles);
}
//获取当前登录用户好友信息
const profiles = await fetchUserInfoWithLoginId();
await loginStore.setLoginUserProfiles(profiles[EMClient.user]);
};
//挂载全局所需监听回调【好友关系、消息监听、群组监听】
emMountGlobalListener();
},
};
</script>
<style lang="scss">
@import './app.css';
</style>
PS:template 中代码变化不大,为缩减长度暂时省去 template 相关代码
<script setup>
import { reactive, computed } from 'vue';
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
import swipeDelete from '@/components/swipedelete/swipedelete';
import msgtype from '@/components/chat/msgtype';
import dateFormater from '@/utils/dateFormater';
import disp from '@/utils/broadcast';
const WebIM = uni.WebIM;
let isfirstTime = true;
const conversationState = reactive({
// msgtype,
search_btn: true,
search_chats: false,
show_mask: false,
yourname: '',
unReadSpotNum: 0,
unReadNoticeNum: 0,
messageNum: 0,
unReadTotalNotNum: 0,
conversationList: [],
show_clear: false,
member: '',
isIPX: false,
gotop: false,
input_code: '',
groupName: {},
winSize: {},
popButton: ['删除该聊天'],
showPop: false,
currentVal: '',
pushConfigData: [],
defaultAvatar: '/static/images/theme2x.png',
defaultGroupAvatar: '/static/images/groupTheme.png',
});
onLoad(() => {
disp.on('em.subscribe', onChatPageSubscribe);
//监听解散群
disp.on('em.invite.deleteGroup', onChatPageDeleteGroup);
//监听未读消息数
disp.on('em.unreadspot', onChatPageUnreadspot);
//监听未读加群“通知”
disp.on('em.invite.joingroup', onChatPageJoingroup);
//监听好友删除
disp.on('em.contacts.remove', onChatPageRemoveContacts);
//监听好友关系解除
disp.on('em.unsubscribed', onChatPageUnsubscribed);
if (!uni.getStorageSync('listGroup')) {
listGroups();
}
if (!uni.getStorageSync('member')) {
getRoster();
}
readJoinedGroupName();
});
onShow(() => {
uni.hideHomeButton && uni.hideHomeButton();
setTimeout(() => {
getLocalConversationlist();
}, 100);
conversationState.unReadMessageNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
conversationState.messageNum = getApp().globalData.saveFriendList.length;
conversationState.unReadNoticeNum =
getApp().globalData.saveGroupInvitedList.length;
conversationState.unReadTotalNotNum =
getApp().globalData.saveFriendList.length +
getApp().globalData.saveGroupInvitedList.length;
if (getApp().globalData.isIPX) {
conversationState.isIPX = true;
}
});
const showConversationAvatar = computed(() => {
const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
return (item) => {
if (item.chatType === 'singleChat' || item.chatType === 'chat') {
if (
friendUserInfoMap.has(item.username) &&
friendUserInfoMap.get(item.username)?.avatarurl
) {
return friendUserInfoMap.get(item.username).avatarurl;
} else {
return conversationState.defaultAvatar;
}
} else if (
item.chatType.toLowerCase() === 'groupchat' ||
item.chatType === 'chatRoom'
) {
return conversationState.defaultGroupAvatar;
}
};
});
const showConversationName = computed(() => {
const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
return (item) => {
if (item.chatType === 'singleChat' || item.chatType === 'chat') {
if (
friendUserInfoMap.has(item.username) &&
friendUserInfoMap.get(item.username)?.nickname
) {
return friendUserInfoMap.get(item.username).nickname;
} else {
return item.username;
}
} else if (
item.chatType === msgtype.chatType.GROUP_CHAT ||
item.chatType === msgtype.chatType.CHAT_ROOM ||
item.chatType === 'groupchat'
) {
return item.groupName;
}
};
});
const handleTime = computed(() => {
return (item) => {
return dateFormater('MM/DD/HH:mm', item.time);
};
});
const listGroups = () => {
return uni.WebIM.conn.getGroup({
limit: 50,
success: function (res) {
uni.setStorage({
key: 'listGroup',
data: res.data,
});
readJoinedGroupName();
getLocalConversationlist();
},
error: function (err) {
console.log(err);
},
});
};
const getRoster = async () => {
const { data } = await WebIM.conn.getContacts();
if (data.length) {
uni.setStorage({
key: 'member',
data: [...data],
});
conversationState.member = [...data];
//if(!systemReady){
disp.fire('em.main.ready');
//systemReady = true;
//}
getLocalConversationlist();
conversationState.unReadSpotNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
}
console.log('>>>>好友列表获取成功', data);
};
const readJoinedGroupName = () => {
const joinedGroupList = uni.getStorageSync('listGroup');
const groupList = joinedGroupList?.data || joinedGroupList || [];
let groupName = {};
groupList.forEach((item) => {
groupName[item.groupid] = item.groupname;
});
conversationState.groupName = groupName;
};
// 包含陌生人版本
const getLocalConversationlist = () => {
const myName = uni.getStorageSync('myUsername');
uni.getStorageInfo({
success: (res) => {
let storageKeys = res.keys;
let newChatMsgKeys = [];
let historyChatMsgKeys = [];
let len = myName.length;
storageKeys.forEach((item) => {
if (item.slice(-len) == myName && item.indexOf('rendered_') == -1) {
newChatMsgKeys.push(item);
} else if (
item.slice(-len) == myName &&
item.indexOf('rendered_') > -1
) {
historyChatMsgKeys.push(item);
} else if (item === 'INFORM') {
newChatMsgKeys.push(item);
}
});
packageConversation(newChatMsgKeys, historyChatMsgKeys);
},
});
};
//组件会话列表方法
const packageConversation = (newChatMsgKeys, historyChatMsgKeys) => {
const myName = uni.getStorageSync('myUsername');
let conversationList = [];
let lastChatMsg; //最后一条消息
for (let i = historyChatMsgKeys.length; i > 0, i--; ) {
let index = newChatMsgKeys.indexOf(historyChatMsgKeys[i].slice(9));
if (index > -1) {
let newChatMsgs = uni.getStorageSync(newChatMsgKeys[index]) || [];
if (newChatMsgs.length) {
lastChatMsg = newChatMsgs[newChatMsgs.length - 1];
lastChatMsg.unReadCount = newChatMsgs.length;
newChatMsgKeys.splice(index, 1);
} else {
let historyChatMsgs = uni.getStorageSync(historyChatMsgKeys[i]);
if (historyChatMsgs.length) {
lastChatMsg = historyChatMsgs[historyChatMsgs.length - 1];
}
}
} else {
let historyChatMsgs = uni.getStorageSync(historyChatMsgKeys[i]);
if (historyChatMsgs.length) {
lastChatMsg = historyChatMsgs[historyChatMsgs.length - 1];
}
}
if (
lastChatMsg.chatType == msgtype.chatType.GROUP_CHAT ||
lastChatMsg.chatType == msgtype.chatType.CHAT_ROOM ||
lastChatMsg.chatType == 'groupchat'
) {
lastChatMsg.groupName = conversationState.groupName[lastChatMsg.info.to];
}
lastChatMsg &&
lastChatMsg.username != myName &&
conversationList.push(lastChatMsg);
}
for (let i = newChatMsgKeys.length; i > 0, i--; ) {
let newChatMsgs = uni.getStorageSync(newChatMsgKeys[i]) || [];
if (newChatMsgs.length) {
lastChatMsg = newChatMsgs[newChatMsgs.length - 1];
lastChatMsg.unReadCount = newChatMsgs.length;
if (
lastChatMsg.chatType == msgtype.chatType.GROUP_CHAT ||
lastChatMsg.chatType == msgtype.chatType.CHAT_ROOM ||
lastChatMsg.chatType == 'groupchat'
) {
lastChatMsg.groupName =
conversationState.groupName[lastChatMsg.info.to];
}
lastChatMsg.username != myName && conversationList.push(lastChatMsg);
}
}
conversationList.sort((a, b) => {
return b.time - a.time;
});
console.log('>>>>>>conversationList', conversationList);
conversationState.conversationList = conversationList;
};
const openSearch = () => {
conversationState.search_btn = false;
conversationState.search_chats = true;
conversationState.gotop = true;
};
const onSearch = (val) => {
let searchValue = val.detail.value;
var myName = uni.getStorageSync('myUsername');
let serchList = [];
let conversationList = [];
uni.getStorageInfo({
success: function (res) {
let storageKeys = res.keys;
let chatKeys = [];
let len = myName.length;
storageKeys.forEach((item) => {
if (item.slice(-len) == myName) {
chatKeys.push(item);
}
});
chatKeys.forEach((item, index) => {
if (item.indexOf(searchValue) != -1) {
serchList.push(item);
}
});
let lastChatMsg = '';
serchList.forEach((item, index) => {
let chatMsgs = uni.getStorageSync(item) || [];
if (chatMsgs.length) {
lastChatMsg = chatMsgs[chatMsgs.length - 1];
conversationList.push(lastChatMsg);
}
});
conversationState.conversationList = conversationList;
},
});
};
const cancel = () => {
getLocalConversationlist();
conversationState.search_btn = true;
conversationState.search_chats = false;
conversationState.unReadSpotNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
conversationState.gotop = false;
};
const clearInput = () => {
conversationState.input_code = '';
conversationState.show_clear = false;
};
const onInput = (e) => {
let inputValue = e.detail.value;
if (inputValue) {
conversationState.show_clear = true;
} else {
conversationState.show_clear = false;
}
};
const tab_contacts = () => {
uni.redirectTo({
url: '../main/main?myName=' + uni.getStorageSync('myUsername'),
});
};
const close_mask = () => {
conversationState.search_btn = true;
conversationState.search_chats = false;
conversationState.show_mask = false;
};
const tab_setting = () => {
uni.redirectTo({
url: '../setting/setting',
});
};
const tab_notification = () => {
uni.redirectTo({
url: '../notification/notification',
});
};
const into_chatRoom = (event) => {
let detail = JSON.parse(event.currentTarget.dataset.item);
if (
detail.chatType == msgtype.chatType.GROUP_CHAT ||
detail.chatType == msgtype.chatType.CHAT_ROOM ||
detail.groupName
) {
into_groupChatRoom(detail);
} else {
into_singleChatRoom(detail);
}
};
// 单聊
const into_singleChatRoom = (detail) => {
var myName = uni.getStorageSync('myUsername');
var nameList = {
myName: myName,
your: detail.username,
};
const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
if (
friendUserInfoMap.has(nameList.your) &&
friendUserInfoMap.get(nameList.your)?.nickname
) {
nameList.yourNickName = friendUserInfoMap.get(nameList.your).nickname;
}
uni.navigateTo({
url:
'../singleChatEntry/singleChatEntry?username=' + JSON.stringify(nameList),
});
};
// 群聊 和 聊天室 (两个概念)
const into_groupChatRoom = (detail) => {
var myName = uni.getStorageSync('myUsername');
var nameList = {
myName: myName,
your: detail.groupName,
groupId: detail.info.to,
};
uni.navigateTo({
url:
'../groupChatEntry/groupChatEntry?username=' + JSON.stringify(nameList),
});
};
const into_inform = () => {
uni.redirectTo({
url: '../notification/notification',
});
};
const removeAndRefresh = (event) => {
let removeId = event.currentTarget.dataset.item.info.from;
let ary = getApp().globalData.saveFriendList;
let idx;
if (ary.length > 0) {
ary.forEach((v, k) => {
if (v.from == removeId) {
idx = k;
}
});
getApp().globalData.saveFriendList.splice(idx, 1);
}
uni.removeStorageSync('INFORM');
};
const del_chat = (event) => {
let detail = event.currentTarget.dataset.item;
let nameList = {};
// 删除当前选中群组聊天列表
if (detail.chatType == 'groupchat' || detail.chatType == 'chatRoom') {
nameList = {
your: detail.info.to,
};
//删除当前选中通知列表
} else if (detail.chatType === 'INFORM') {
nameList = {
your: 'INFORM',
};
}
//删除当前选中好友聊天列表
else {
nameList = {
your: detail.username,
};
}
var myName = uni.getStorageSync('myUsername');
var currentPage = getCurrentPages();
uni.showModal({
title: '确认删除?',
confirmText: '删除',
success: function (res) {
if (res.confirm) {
uni.removeStorageSync(nameList.your + myName);
uni.removeStorageSync('rendered_' + nameList.your + myName);
nameList.your === 'INFORM' && removeAndRefresh(event);
// if (Object.keys(currentPage[0]).length>0) {
// currentPage[0].onShow();
// }
disp.fire('em.chat.session.remove');
getLocalConversationlist();
}
},
fail: function (err) {
console.log('删除列表', err);
},
});
};
const removeLocalStorage = (yourname) => {
var myName = uni.getStorageSync('myUsername');
uni.removeStorageSync(yourname + myName);
uni.removeStorageSync('rendered_' + yourname + myName);
};
/* 获取窗口尺寸 */
const getWindowSize = () => {
uni.getSystemInfo({
success: (res) => {
conversationState.winSize = {
witdh: res.windowWidth,
height: res.windowHeight,
};
},
});
};
const hidePop = () => {
conversationState.showPop = false;
};
const pickerMenuChange = () => {
del_chat(conversationState.currentVal);
};
/* disp event callback function */
const onChatPageSubscribe = () => {
getLocalConversationlist();
conversationState.messageNum = getApp().globalData.saveFriendList.length;
conversationState.unReadTotalNotNum =
getApp().globalData.saveFriendList.length +
getApp().globalData.saveGroupInvitedList.length;
};
const onChatPageDeleteGroup = (infos) => {
listGroups();
getRoster();
getLocalConversationlist();
conversationState.messageNum = getApp().globalData.saveFriendList.length;
//如果会话存在则执行删除会话
removeLocalStorage(infos.gid);
};
const onChatPageUnreadspot = (message) => {
getLocalConversationlist();
let currentLoginUser = WebIM.conn.context.userId;
let id =
message && message.chatType === 'groupchat' ? message?.to : message?.from;
let pushObj = uni.getStorageSync('pushStorageData');
let pushAry = pushObj[currentLoginUser] || [];
conversationState.pushConfigData = pushAry;
// if (message && pushValue.includes(id)) return
conversationState.unReadSpotNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
};
const onChatPageJoingroup = () => {
conversationState.unReadMessageNum =
getApp().globalData.saveGroupInvitedList.length;
conversationState.unReadTotalNotNum =
getApp().globalData.saveFriendList.length +
getApp().globalData.saveGroupInvitedList.length;
getLocalConversationlist();
};
const onChatPageRemoveContacts = () => {
getLocalConversationlist();
getRoster();
};
const onChatPageUnsubscribed = (message) => {
uni.showToast({
title: `与${message.from}好友关系解除`,
icon: 'none',
});
};
onUnload(() => {
//页面卸载同步取消onload中的订阅,防止重复订阅事件。
disp.off('em.subscribe', conversationState.onChatPageSubscribe);
disp.off('em.invite.deleteGroup', conversationState.onChatPageDeleteGroup);
disp.off('em.unreadspot', conversationState.onChatPageUnreadspot);
disp.off('em.invite.joingroup', conversationState.onChatPageJoingroup);
disp.off('em.contacts.remove', conversationState.onChatPageRemoveContacts);
disp.off('em.unsubscribed', conversationState.onChatPageUnsubscribed);
});
</script>
<style>
@import './conversation.css';
</style>
<script setup>
import { reactive, computed, watch, watchEffect } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import swipeDelete from '@/components/swipedelete/swipedelete';
import { emConversation } from '@/EaseIM/imApis';
import { CHAT_TYPE, MESSAGE_TYPE } from '@/EaseIM/constant';
import { useConversationStore } from '@/stores/conversation';
import { useContactsStore } from '@/stores/contacts';
import { useGroupStore } from '@/stores/group';
import dateFormater from '@/utils/dateFormater';
/* store */
import { useInformStore } from '@/stores/inform';
const conversationState = reactive({
search_btn: true,
search_chats: false,
search_keyword: '',
show_mask: false,
yourname: '',
unReadSpotNum: 0,
unReadNoticeNum: 0,
messageNum: 0,
unReadTotalNotNum: 0,
conversationList: [], //搜索后返回的会话数据,
show_clear: false,
member: '',
isIPX: false,
gotop: false,
groupName: {},
winSize: {},
popButton: ['删除该聊天'],
showPop: false,
currentVal: '',
pushConfigData: [],
defaultAvatar: '/static/images/theme2x.png',
defaultGroupAvatar: '/static/images/groupTheme.png',
});
//群组名称
const groupStore = useGroupStore();
const getGroupName = (groupid) => {
const joinedGroupList = groupStore.joinedGroupList;
let groupName = '';
if (joinedGroupList.length) {
joinedGroupList.forEach((item) => {
if (item.groupid === groupid) {
console.log(item.groupname);
return (groupName = item.groupname);
}
});
return groupName;
} else {
return groupid;
}
};
/* 系统通知 */
const informStore = useInformStore();
//最近一条系统通知
const lastInformData = computed(() => {
return (
informStore.getAllInformsList[informStore.getAllInformsList.length - 1] ||
null
);
});
//未处理系统通知总数
const unReadNoticeNum = computed(() => {
return informStore.getAllInformsList.filter((inform) => !inform.isHandled)
.length;
});
/* 会话列表 */
const conversationStore = useConversationStore();
const {
fetchConversationFromServer,
removeConversationFromServer,
sendChannelAck,
} = emConversation();
const fetchConversationList = async () => {
const res = await fetchConversationFromServer();
if (res?.data?.channel_infos) {
conversationStore.setConversationList(
Object.assign([], res.data.channel_infos)
);
}
};
//会话列表数据
const conversationList = computed(() => {
return conversationStore.sortedConversationList;
});
watchEffect(() => {
console.log('>>>>>执行更新会话列表数据');
conversationState.conversationList = Object.assign(
[],
conversationList.value
);
});
//会话列表name&头像展示处理
const contactsStore = useContactsStore();
//好友属性
const friendUserInfoMap = computed(() => {
return contactsStore.friendUserInfoMap;
});
//会话列表头像
const showConversationAvatar = computed(() => {
return (item) => {
switch (item.chatType) {
case CHAT_TYPE.SINGLE_CHAT:
const friendInfo = friendUserInfoMap.value.get(item.channel_id) || {};
return friendInfo.avatarurl ?? conversationState.defaultAvatar;
case CHAT_TYPE.GROUP_CHAT:
return conversationState.defaultGroupAvatar;
default:
return null;
}
};
});
//会话列表名称
const showConversationName = computed(() => {
return (item) => {
switch (item.chatType) {
case CHAT_TYPE.SINGLE_CHAT:
const friendInfo = friendUserInfoMap.value.get(item.channel_id);
return friendInfo?.nickname || item.channel_id;
case CHAT_TYPE.GROUP_CHAT:
return getGroupName(item.channel_id);
default:
return null;
}
};
});
//时间展示
const handleTime = computed(() => {
return (item) => {
return dateFormater('MM/DD/HH:mm', item.time);
};
});
//删除会话
const deleteConversation = async (eventItem) => {
const { channel_id, chatType } = eventItem;
try {
const res = await uni.showModal({
title: '确认删除?',
confirmText: '删除',
});
if (res.confirm) {
await removeConversationFromServer(channel_id, chatType);
conversationStore.deleteConversation(channel_id);
}
} catch (error) {
uni.showToast({
title: '删除失败',
icon: 'none',
duration: 2000,
});
console.log('删除失败', error);
}
};
/* 搜索会话相关逻辑 */
//开启搜索模式
const openSearch = () => {
conversationState.search_btn = false;
conversationState.search_chats = true;
conversationState.gotop = true;
};
//执行搜索方法
const actionSearch = () => {
const keyWord = conversationState.search_keyword;
let resConversationList = [];
if (keyWord) {
resConversationList = conversationStore.conversationList.filter((item) => {
if (item.chatType === CHAT_TYPE.SINGLE_CHAT || item.chatType === 'chat') {
if (
friendUserInfoMap.value.has(item.channel_id) &&
friendUserInfoMap.value.get(item.channel_id)?.nickname
) {
return (
item.lastMessage.msg?.indexOf(keyWord) > -1 ||
item.channel_id?.indexOf(keyWord) > -1 ||
friendUserInfoMap.value
.get(item.channel_id)
.nickname?.indexOf(keyWord) > -1
);
} else {
return (
item.lastMessage.msg?.indexOf(keyWord) > -1 ||
item.channel_id?.indexOf(keyWord) > -1
);
}
}
if (
item.chatType === CHAT_TYPE.GROUP_CHAT ||
item.chatType === 'groupchat'
) {
return (
item.channel_id.indexOf(keyWord) > -1 ||
getGroupName(item.channel_id).indexOf(keyWord) > -1 ||
item.lastMessage.msg.indexOf(keyWord) > -1
);
}
});
}
console.log('>>>>>执行搜索', resConversationList);
conversationState.conversationList = resConversationList;
};
//取消搜索
const cancelSearch = () => {
conversationState.search_btn = true;
conversationState.search_chats = false;
conversationState.gotop = false;
conversationState.conversationList = conversationList.value;
};
//清空搜索框
const clearSearchInput = () => {
conversationState.search_keyword = '';
conversationState.show_clear = false;
};
//输入框事件触发
const onInput = (e) => {
let inputValue = e.detail.value;
if (inputValue) {
conversationState.show_clear = true;
} else {
cancelSearch();
}
};
const close_mask = () => {
conversationState.search_btn = true;
conversationState.search_chats = false;
conversationState.show_mask = false;
};
/* 获取窗口尺寸 */
const getWindowSize = () => {
uni.getSystemInfo({
success: (res) => {
conversationState.winSize = {
witdh: res.windowWidth,
height: res.windowHeight,
};
},
});
};
const hidePop = () => {
conversationState.showPop = false;
};
const entryInform = () => {
uni.navigateTo({
url: '../notification/notification',
});
};
const entryemChat = (params) => {
console.log('params', params);
//发送channelack 清除服务端该会话未读数,并且清除本地未读红点
sendChannelAck(params.channel_id, params.chatType);
conversationStore.clearConversationUnReadNum(params.channel_id);
uni.navigateTo({
url: `../emChatContainer/index?targetId=${params.channel_id}&chatType=${params.chatType}`,
});
};
onLoad(() => {
if (!conversationList.value.length) {
fetchConversationList();
}
});
onShow(() => {
uni.hideHomeButton && uni.hideHomeButton();
});
</script>
<style>
@import './conversation.css';
</style>
<template>
<view :class="isIPX ? 'chatRoom_tab_X' : 'chatRoom_tab'">
<view class="tableBar" @click="changeTab('conversation')">
<view
v-if="unReadSpotNum > 0 || unReadSpotNum == '99+'"
:class="
'em-unread-spot ' +
(unReadSpotNum == '99+' ? 'em-unread-spot-litleFont' : '')
"
>{{ unReadSpotNum + unReadTotalNotNum }}</view
>
<image
:class="unReadSpotNum > 0 || unReadSpotNum == '99+' ? 'haveSpot' : ''"
:src="
tabType === 'conversation'
? highlightConversationImg
: conversationImg
"
></image>
<text :class="tabType === 'conversation' && 'activeText'">会话</text>
</view>
<view class="tableBar" @click="changeTab('contacts')">
<image
:src="tabType === 'contacts' ? highlightContactsImg : contactsImg"
></image>
<text :class="tabType === 'contacts' && 'activeText'">联系人</text>
</view>
<view class="tableBar" @click="changeTab('me')">
<image :src="tabType === 'me' ? highlightSettingImg : settingImg"></image>
<text :class="tabType === 'me' && 'activeText'">我的</text>
</view>
</view>
</template>
<script setup>
import { ref, toRefs } from 'vue';
/* images */
const conversationImg = '/static/images/session2x.png';
const highlightConversationImg = '/static/images/sessionhighlight2x.png';
const contactsImg = '/static/images/comtacts2x.png';
const highlightContactsImg = '/static/images/comtactshighlight2x.png';
const settingImg = '/static/images/setting2x.png';
const highlightSettingImg = '/static/images/settinghighlight2x.png';
/* props */
const props = defineProps({
tabType: {
type: String,
default: 'conversation',
required: true,
},
});
/* emits */
const emits = defineEmits(['switchHomeComponent']);
const { tabType } = toRefs(props);
const isIPX = ref(false);
const unReadSpotNum = ref(0);
const unReadTotalNotNum = ref(0);
const changeTab = (type) => {
emits('switchHomeComponent', type);
};
</script>
<style scoped>
@import './index.css';
</style>
没有使用 Vue 中的动态组件(component)实现是因为 uni-app 打包到某些平台不支持。
<template>
<view>
<template v-if="isActiveComps === 'conversation'">
<Conversation />
</template>
<template v-if="isActiveComps === 'contacts'">
<Contacts />
</template>
<template v-if="isActiveComps === 'me'">
<Me />
</template>
<Tabbar
:tab-type="isActiveComps"
@switchHomeComponent="switchHomeComponent"
/>
</view>
</template>
<script setup>
import { ref, watchEffect } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
/* components */
import Tabbar from '@/layout/tabbar';
import Conversation from '@/pages/conversation/conversation.vue';
import Contacts from '@/pages/contacts/contacts.vue';
import Me from '@/pages/me/me.vue';
const isActiveComps = ref('conversation');
const switchHomeComponent = (type) => {
isActiveComps.value = type;
};
/* 设置当前标题 */
const titleMap = {
conversation: '会话列表',
contacts: '联系人',
me: '我的',
};
watchEffect(() => {
uni.setNavigationBarTitle({
title: titleMap[isActiveComps.value],
});
});
onLoad((options) => {
//通过路由传参的形式可指定该页面展示某个指定组件
if (options.page) {
switchHomeComponent(options.page);
}
});
</script>
<template>
<div>
<em-chat />
</div>
</template>
<script setup>
import { toRefs, reactive, provide, readonly, computed } from 'vue';
import EmChat from '@/components/emChat';
import { onNavigationBarButtonTap } from '@dcloudio/uni-app';
import { useContactsStore } from '@/stores/contacts';
import { useGroupStore } from '@/stores/group';
import { CHAT_TYPE } from '@/EaseIM/constant';
const props = defineProps({
targetId: {
type: String,
value: '',
required: true,
},
chatType: {
type: String,
value: '',
required: true,
},
});
const { targetId, chatType } = toRefs(reactive(props));
console.log(targetId, chatType);
provide('targetId', readonly(targetId));
provide('chatType', readonly(chatType));
/* 处理NavigationBarTitle展示 */
//群组名称
const groupStore = useGroupStore();
const getGroupName = (groupid) => {
const joinedGroupList = groupStore.joinedGroupList;
let groupName = '';
if (joinedGroupList.length) {
joinedGroupList.forEach((item) => {
if (item.groupid === groupid) {
console.log(item.groupname);
return (groupName = item.groupname);
}
});
return groupName;
} else {
return groupid;
}
};
const contactsStore = useContactsStore();
//好友属性
const friendUserInfoMap = computed(() => {
return contactsStore.friendUserInfoMap;
});
//会话列表名称
const getTheIdName = (chatType, targetId) => {
switch (chatType) {
case CHAT_TYPE.SINGLE_CHAT:
const friendInfo = friendUserInfoMap.value.get(targetId);
return friendInfo?.nickname || targetId;
case CHAT_TYPE.GROUP_CHAT:
return getGroupName(targetId);
default:
return null;
}
};
uni.setNavigationBarTitle({
title: getTheIdName(chatType.value, targetId.value),
});
onNavigationBarButtonTap(() => {
uni.navigateTo({
url: `/pages/moreMenu/moreMenu?username=${targetId.value}&type=${chatType.value}`,
});
});
</script>
<style scoped>
@import './index.css';
</style>
export const EM_API_URL = 'https://a1.easemob.com';
export const EM_WEB_SOCKET_URL = 'wss://im-api-wechat.easemob.com/websocket';
export const EM_APP_KEY = 'easemob#easeim';
export const CHAT_TYPE = {
SINGLE_CHAT: 'singleChat',
GROUP_CHAT: 'groupChat',
};
export const HANDLER_EVENT_NAME = {
CONNECT_EVENT: 'connectEvent',
MESSAGES_EVENT: 'messagesEvent',
CONTACTS_EVENT: 'contactsEvent',
GROUP_EVENT: 'groupEvent',
};
export const CONNECT_CALLBACK_TYPE = {
CONNECT_CALLBACK: 'connected',
DISCONNECT_CALLBACK: 'disconnected',
RECONNECTING_CALLBACK: 'reconnecting',
};
export const MESSAGE_TYPE = {
IMAGE: 'img',
TEXT: 'txt',
LOCATION: 'location',
VIDEO: 'video',
AUDIO: 'audio',
EMOJI: 'emoji',
FILE: 'file',
CUSTOM: 'custom',
};
import { EMClient } from '../index';
const emContacts = () => {
const fetchContactsListFromServer = () => {
return new Promise((resolve, reject) => {
EMClient.getContacts()
.then((res) => {
const { data } = res;
resolve(data);
})
.catch((error) => {
reject(error);
});
});
};
const removeContactFromServer = (contactId) => {
if (contactId) {
EMClient.deleteContact(contactId);
}
};
const addContact = (contactId, applyMsg) => {
if (contactId) {
EMClient.addContact(contactId, applyMsg);
}
};
const acceptContactInvite = (contactId) => {
if (contactId) {
EMClient.acceptContactInvite(contactId);
}
};
const declineContactInvite = (contactId) => {
if (contactId) {
EMClient.declineContactInvite(contactId);
}
};
return {
fetchContactsListFromServer,
removeContactFromServer,
acceptContactInvite,
declineContactInvite,
addContact,
};
};
export default emContacts;
import { EMClient } from '../index';
import { CONNECT_CALLBACK_TYPE, HANDLER_EVENT_NAME } from '../constant';
export const emConnectListener = (callback, listenerEventName) => {
console.log('>>>>连接监听已挂载');
const connectListenFunc = {
onConnected: () => {
console.log('connected...');
callback && callback(CONNECT_CALLBACK_TYPE.CONNECT_CALLBACK);
},
onDisconnected: () => {
callback && callback(CONNECT_CALLBACK_TYPE.DISCONNECT_CALLBACK);
console.log('disconnected...');
},
onReconnecting: () => {
callback && callback(CONNECT_CALLBACK_TYPE.RECONNECTING_CALLBACK);
},
};
EMClient.removeEventHandler(
listenerEventName || HANDLER_EVENT_NAME.CONNECT_EVENT
);
EMClient.addEventHandler(
listenerEventName || HANDLER_EVENT_NAME.CONNECT_EVENT,
connectListenFunc
);
};
/* 用以获取消息存储格式时的key */
const getEMKey = (loginId, fromId, toId, chatType) => {
let key = '';
if (chatType === 'singleChat') {
if (loginId === fromId) {
key = toId;
} else {
key = fromId;
}
} else if (chatType === 'groupChat') {
key = toId;
}
return key;
};
export default getEMKey;
import EaseSDK from 'easemob-websdk/uniApp/Easemob-chat';
import { EM_APP_KEY, EM_API_URL, EM_WEB_SOCKET_URL } from './config';
let EMClient = (uni.EMClient = {});
EMClient = new EaseSDK.connection({
appKey: EM_APP_KEY,
apiUrl: EM_API_URL,
url: EM_WEB_SOCKET_URL,
});
uni.EMClient = EMClient;
export { EaseSDK, EMClient };
<template>
<!-- <chat-suit-emoji id="chat-suit-emoji" bind:newEmojiStr="emojiAction"></chat-suit-emoji> -->
<form class="text-input">
<view :class="mainState.isIPX ? 'f-row-x' : 'f-row'">
<!-- 发送语音 -->
<view>
<image
class="icon-mic"
src="/static/images/voice.png"
@tap="openRecordModal"
></image>
</view>
<!-- 输入框 -->
<textarea
class="f news"
type="text"
cursor-spacing="65"
confirm-type="done"
v-model.trim="mainState.inputMessage"
@confirm="sendMessage"
@input="bindMessage"
@tap="focus"
@focus="focus"
@blur="blur"
:confirm-hold="mainState.isIPX ? true : false"
auto-height
:show-confirm-bar="false"
maxlength="300"
/>
<view>
<image
class="icon-mic"
src="/static/images/Emoji.png"
@tap="openEmoji"
></image>
</view>
<view v-show="!mainState.inputMessage" @tap="openFunModal">
<image class="icon-mic" src="/static/images/ad.png"></image>
</view>
<button
class="send-btn-style"
hover-class="hover"
@tap="sendMessage"
v-show="mainState.inputMessage"
>
发送
</button>
</view>
</form>
</template>
<script setup>
import { reactive, toRefs } from 'vue';
import msgType from '@/components/chat/msgtype';
import msgStorage from '@/components/chat/msgstorage';
import disp from '@/utils/broadcast';
const WebIM = uni.WebIM;
/* props */
const props = defineProps({
chatParams: {
type: Object,
default: () => ({}),
},
chatType: {
type: String,
default: msgType.chatType.SINGLE_CHAT,
},
});
const { chatParams, chatType } = toRefs(props);
/* emits */
const $emits = defineEmits([
'inputFocused',
'inputBlured',
'closeFunModal',
'closeFunModal',
'openEmoji',
'openRecordModal',
'openFunModal',
]);
const mainState = reactive({
inputMessage: '',
// render input 的值
userMessage: '', // input 的实时值
isIPX: false,
});
mainState.isIPX = getApp().globalData.isIPX;
const focus = () => {
$emits('inputFocused', null, {
bubbles: true,
});
};
const blur = () => {
$emits('inputBlured', null, {
bubbles: true,
});
};
const isGroupChat = () => {
return chatType.value == msgType.chatType.CHAT_ROOM;
};
const getSendToParam = () => {
console.log('chatParmas', chatParams);
return isGroupChat() ? chatParams.value.groupId : chatParams.value.your;
};
const bindMessage = (e) => {
mainState.userMessage = e.detail.value;
};
const emojiAction = (emoji) => {
let str;
let msglen = mainState.userMessage.length - 1;
if (emoji && emoji != '[del]') {
str = mainState.userMessage + emoji;
} else if (emoji == '[del]') {
let start = mainState.userMessage.lastIndexOf('[');
let end = mainState.userMessage.lastIndexOf(']');
let len = end - start;
if (end != -1 && end == msglen && len >= 3 && len <= 4) {
str = mainState.userMessage.slice(0, start);
} else {
str = mainState.userMessage.slice(0, msglen);
}
}
mainState.userMessage = str;
mainState.inputMessage = str;
};
const sendMessage = () => {
if (mainState.userMessage.match(/^\s*$/)) return;
let id = WebIM.conn.getUniqueId();
let msg = new WebIM.message(msgType.TEXT, id);
msg.set({
msg: mainState.userMessage,
from: WebIM.conn.user,
to: getSendToParam(),
// roomType: false,
chatType: isGroupChat()
? msgType.chatType.GROUP_CHAT
: msgType.chatType.SINGLE_CHAT,
success(id, serverMsgId) {
console.log('成功了');
// 关闭表情弹窗
$emits.cancelEmoji && $emits.cancelEmoji();
$emits.closeFunModal && $emits.closeFunModal();
disp.fire('em.chat.sendSuccess', id, mainState.userMessage);
},
fail(id, serverMsgId) {
console.log('失败了');
},
});
WebIM.conn.send(msg.body);
let obj = {
msg: msg,
type: msgType.TEXT,
};
saveSendMsg(obj);
mainState.userMessage = '';
mainState.inputMessage = '';
uni.hideKeyboard();
};
const saveSendMsg = (evt) => {
msgStorage.saveMsg(evt.msg, evt.type);
};
const openEmoji = () => {
$emits('openEmoji');
};
const openRecordModal = () => {
$emits('openRecordModal');
};
const openFunModal = () => {
$emits('openFunModal');
};
defineExpose({
emojiAction,
});
</script>
<style>
@import './main.css';
</style>
<template>
<form class="text-input">
<view class="f-row">
<!-- 发送语音 -->
<view @click="emits('toggleRecordModal')">
<image class="icon-mic" src="/static/images/voice.png"></image>
</view>
<!-- 输入框 -->
<textarea
class="f news"
type="text"
cursor-spacing="65"
confirm-type="send"
v-model.trim="inputContent"
@focus="inputFocus"
@confirm="sendTextMessage"
:confirm-hold="true"
auto-height
:show-confirm-bar="false"
maxlength="300"
/>
<view @click="emits('openEmojiModal')">
<image class="icon-mic" src="/static/images/Emoji.png"></image>
</view>
<view v-show="!inputContent" @click="emits('openFunModal')">
<image class="icon-mic" src="/static/images/ad.png"></image>
</view>
<button
class="send-btn-style"
hover-class="hover"
@tap="sendTextMessage"
v-show="inputContent"
>
发送
</button>
</view>
</form>
</template>
<script setup>
import { ref, inject } from 'vue';
import { emMessages } from '@/EaseIM/imApis';
/* emits */
const emits = defineEmits([
'toggleRecordModal',
'openEmojiModal',
'openFunModal',
'closeAllModal',
]);
const inputContent = ref('');
//删除输入内容中的emojiMapStr
const delEmojiMapString = () => {
if (!inputContent.value) return;
let newInputContent = '';
let inputContentlength = inputContent.value.length - 1;
let start = inputContent.value.lastIndexOf('[');
let end = inputContent.value.lastIndexOf(']');
let len = end - start;
if (end != -1 && end == inputContentlength && len >= 3 && len <= 4) {
newInputContent = inputContent.value.slice(0, start);
} else {
newInputContent = inputContent.value.slice(0, inputContentlength);
}
inputContent.value = newInputContent;
};
//发送文本消息
const { sendDisplayMessages } = emMessages();
const injectTargetId = inject('targetId');
const injeactChatType = inject('chatType');
const sendTextMessage = async () => {
const params = {
// 消息类型。
type: 'txt',
// 消息内容。
msg: inputContent.value,
// 消息接收方:单聊为对方用户 ID,群聊和聊天室分别为群组 ID 和聊天室 ID。
to: injectTargetId.value,
// 会话类型:单聊、群聊和聊天室分别为 `singleChat`、`groupChat` 和 `chatRoom`。
chatType: injeactChatType.value,
};
try {
const res = await sendDisplayMessages({ ...params });
emits('closeAllModal');
console.log('>>>>>文本消息发送成功', res);
} catch (error) {
console.log('>>>>>文本消息发送失败', error);
uni.showToast({
title: '消息发送失败',
icon: 'none',
});
} finally {
inputContent.value = '';
uni.hideKeyboard();
}
};
const inputFocus = () => {
console.log('>>>>输入框聚焦');
emits('closeAllModal');
};
defineExpose({
inputContent,
delEmojiMapString,
});
</script>
<style>
@import './index.css';
</style>
<template>
<view
scroll-y="true"
:class="
msglistState.view + ' wrap ' + (msglistState.isIPX ? 'scroll_view_X' : '')
"
@tap="onTap"
upper-threshold="-50"
:scroll-into-view="msglistState.toView"
>
<view>
<!-- 弹出举报入口 -->
<uni-popup ref="alertReport">
<button @click="showSelectReportType">举报</button>
<button @click="cannelReport">取消</button>
</uni-popup>
<!-- 展示举报选项 -->
<uni-popup ref="selectReportType">
<button
v-for="(item, index) in msglistState.typeList"
:key="index"
@click="pickReportType(item)"
>
{{ item.text }}
</button>
<button type="warn" @click="hideSelectReportType">取消</button>
</uni-popup>
<!-- 填写举报原因 -->
<uni-popup ref="inputReportReason" type="dialog">
<uni-popup-dialog
mode="input"
title="举报原因"
placeholder="请输入举报原因"
@confirm="reportMsg"
@cancel="msglistState.reason = ''"
>
<uni-easyinput
type="textarea"
v-model="msglistState.reason"
placeholder="请填写举报内容"
:maxlength="300"
></uni-easyinput>
</uni-popup-dialog>
</uni-popup>
</view>
<view class="tips"
>本应用仅用于环信产品功能开发测试,请勿用于非法用途。任何涉及转账、汇款、裸聊、网恋、网购退款、投资理财等统统都是诈骗,请勿相信!</view
>
<view
@longtap="actionAleartReportMsg(item)"
class="message"
v-for="item in msglistState.chatMsg"
:key="item.mid"
:id="item.mid"
>
<!-- <view class="time">
<text class="time-text">{{ item.time }}</text>
</view>-->
<view class="main" :class="item.style">
<view class="user">
<!-- yourname:就是消息的 from -->
<text v-if="!item.style" class="user-text">{{
showMessageListNickname(item.yourname) + ' ' + handleTime(item)
}}</text>
</view>
<image class="avatar" :src="showMessageListAvatar(item)" />
<view class="msg">
<image
class="err"
:class="item.style == 'self' && item.isFail ? 'show' : 'hide'"
src="/static/images/msgerr.png"
/>
<image
v-if="item.style == 'self'"
src="/static/images/poprightarrow2x.png"
class="msg_poprightarrow"
/>
<image
v-if="item.style == ''"
src="/static/images/popleftarrow2x.png"
class="msg_popleftarrow"
/>
<view
v-if="
item.msg.type == msgtype.IMAGE || item.msg.type == msgtype.VIDEO
"
>
<image
v-if="item.msg.type == msgtype.IMAGE"
class="avatar"
:src="item.msg.data"
style="width: 90px; height: 120px; margin: 2px auto"
mode="aspectFit"
@tap="previewImage"
:data-url="item.msg.data"
/>
<video
v-if="item.msg.type == msgtype.VIDEO"
:src="item.msg.data"
controls
style="width: 300rpx"
/>
</view>
<audio-msg
v-if="item.msg.type == msgtype.AUDIO"
:msg="item"
></audio-msg>
<file-msg v-if="item.msg.type == msgtype.FILE" :msg="item"></file-msg>
<view
v-else-if="
item.msg.type == msgtype.TEXT || item.msg.type == msgtype.EMOJI
"
>
<view
class="template"
v-for="(d_item, d_index) in item.msg.data"
:key="d_index"
>
<text
:data-msg="item"
v-if="d_item.type == msgtype.TEXT"
class="msg-text"
style="float: left"
selectable="true"
>{{ d_item.data }}</text
>
<image
v-if="d_item.type == msgtype.EMOJI"
class="avatar"
:src="'/static/images/faces/' + d_item.data"
style="
width: 25px;
height: 25px;
margin: 0 0 2px 0;
float: left;
"
/>
</view>
</view>
<!-- 个人名片 -->
<view
v-else-if="
item.msg.type == msgtype.CUSTOM && item.customEvent === 'userCard'
"
@click="to_profile_page(item.msg.data)"
>
<view class="usercard_mian">
<image
:src="
item.msg.data.avatarurl ||
item.msg.data.avatar ||
defaultAvatar
"
/>
<text class="name">{{
item.msg.data.nickname || item.msg.data.uid
}}</text>
</view>
<!-- <u-divider :use-slot="false" /> -->
<text>[个人名片]</text>
</view>
</view>
</view>
</view>
</view>
<!-- <view style="height: 1px;"></view> -->
</template>
<script setup>
import { reactive, ref, computed, onMounted, onUnmounted } from 'vue';
import msgStorage from '../msgstorage';
// let msgStorage = require("../msgstorage");
import disp from '@/utils/broadcast';
import dateFormater from '@/utils/dateFormater';
// let disp = require('../../../utils/broadcast');
import msgtype from '@/components/chat/msgtype';
import audioMsg from './type/audio/audio';
import fileMsg from './type/file';
let LIST_STATUS = {
SHORT: 'scroll_view_change',
NORMAL: 'scroll_view',
};
let page = 0;
let Index = 0;
let curMsgMid = '';
let isFail = false;
const WebIM = uni.WebIM;
/* props */
const props = defineProps({
chatParams: {
type: Object,
default: () => ({}),
required: true,
},
});
const { chatParams } = props;
console.log('msglist', chatParams);
/* emits */
const $emit = defineEmits(['msglistTap']);
const msglistState = reactive({
view: LIST_STATUS.NORMAL,
toView: '',
chatMsg: [],
__visibility__: false,
isIPX: false,
title: '消息举报',
list: [
{
text: '举报',
},
],
rptMsgId: '', // 举报消息id
rptType: '', // 举报类型
reason: '',
typeList: [
{
text: '涉政',
},
{
text: '涉黄',
},
{
text: '广告',
},
{
text: '辱骂',
},
{
text: '暴恐',
},
{
text: '违禁',
},
],
defaultAvatar: '/static/images/theme2x.png',
defaultGroupAvatar: '/static/images/groupTheme.png',
usernameObj: null,
});
//做初始参数设置
msglistState.__visibility__ = true;
page = 0;
Index = 0;
onUnmounted(() => {
msglistState.__visibility__ = false;
msgStorage.off('newChatMsg', dispMsg);
});
onMounted(() => {
if (getApp().globalData.isIPX) {
msglistState.isIPX = true;
}
//根据原有uni demo 处理似乎支付宝小程序有参数传递问题,因此针对该平台单独取传递的参数
if (uni.getSystemInfoSync().uniPlatform === 'mp-alipay') {
msglistState.usernameObj = Object.assign({}, uni.username);
} else {
msglistState.usernameObj = Object.assign({}, chatParams);
}
const usernameObj = msglistState.usernameObj;
console.log('usernameObj', usernameObj);
let myUsername = uni.getStorageSync('myUsername');
let sessionKey = usernameObj.groupId
? usernameObj.groupId + myUsername
: usernameObj.your + myUsername;
let chatMsg = uni.getStorageSync(sessionKey) || [];
renderMsg(null, null, chatMsg, sessionKey);
uni.setStorageSync(sessionKey, null);
disp.on('em.error.sendMsgErr', function (err) {
// curMsgMid = err.data.mid;
isFail = true;
// return;
console.log('发送失败了');
return;
let msgList = me.chatMsg;
msgList.map((item) => {
if (
item.mid.substring(item.mid.length - 10) ==
curMsgMid.substring(curMsgMid.length - 10)
) {
item.msg.data[0].isFail = true;
item.isFail = true;
me.setData({
chatMsg: msgList,
});
}
});
uni.setStorageSync('rendered_' + sessionKey, msgList);
});
msgStorage.on('newChatMsg', dispMsg);
});
/* computed */
//消息列表头像展示
const showMessageListAvatar = computed(() => {
const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
const myUserInfos = getApp().globalData.userInfoFromServer;
return (item) => {
if (!item.style) {
if (
friendUserInfoMap.has(item.username) &&
friendUserInfoMap.get(item.username)?.avatarurl
) {
return friendUserInfoMap.get(item.username).avatarurl;
} else {
return msglistState.defaultAvatar;
}
} else {
if (myUserInfos?.avatarurl) {
return myUserInfos.avatarurl;
} else {
return msglistState.defaultAvatar;
}
}
};
});
//消息列表昵称显示
const showMessageListNickname = computed(() => {
const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
return (hxId) => {
if (friendUserInfoMap.has(hxId) && friendUserInfoMap.get(hxId)?.nickname) {
return friendUserInfoMap.get(hxId).nickname;
} else {
return hxId;
}
};
});
//处理时间显示
const handleTime = computed(() => {
return (item) => {
return dateFormater('MM/DD/HH:mm', item.time);
};
});
const normalScroll = () => {
msglistState.view = LIST_STATUS.NORMAL;
};
//TODO 待优化
//此处用到了发布订阅默认去订阅,msgstorage 文件中 发布的newChatMsg 事件从而取到了存储后的消息list
let curChatMsgList = null;
const dispMsg = (renderableMsg, type, curChatMsg, sesskey) => {
const usernameObj = msglistState.usernameObj;
let myUsername = uni.getStorageSync('myUsername');
let sessionKey = usernameObj.groupId
? usernameObj.groupId + myUsername
: usernameObj.your + myUsername;
curChatMsgList = curChatMsg;
if (!msglistState.__visibility__) return; // 判断是否属于当前会话
if (usernameObj.groupId) {
// 群消息的 to 是 id,from 是 name
if (
renderableMsg.info.from == usernameObj.groupId ||
renderableMsg.info.to == usernameObj.groupId
) {
if (sesskey == sessionKey) {
renderMsg(renderableMsg, type, curChatMsg, sessionKey, 'newMsg');
}
}
} else if (
renderableMsg.info.from == usernameObj.your ||
renderableMsg.info.to == usernameObj.your
) {
if (sesskey == sessionKey) {
renderMsg(renderableMsg, type, curChatMsg, sessionKey, 'newMsg');
}
}
};
//消息渲染方法
const renderMsg = (renderableMsg, type, curChatMsg, sessionKey, isnew) => {
console.log('curChatMsg, sessionKey, isnew', curChatMsg, sessionKey, isnew);
let historyChatMsgs = uni.getStorageSync('rendered_' + sessionKey) || [];
historyChatMsgs = historyChatMsgs.concat(curChatMsg);
if (!historyChatMsgs.length) return;
if (isnew == 'newMsg') {
msglistState.chatMsg = msglistState.chatMsg.concat(curChatMsg);
msglistState.toView = historyChatMsgs[historyChatMsgs.length - 1].mid;
} else {
msglistState.chatMsg = historyChatMsgs.slice(-10);
msglistState.toView = historyChatMsgs[historyChatMsgs.length - 1].mid;
}
uni.setStorageSync('rendered_' + sessionKey, historyChatMsgs);
let chatMsg = uni.getStorageSync(sessionKey) || [];
chatMsg.map(function (item, index) {
curChatMsg.map(function (item2, index2) {
if (item2.mid == item.mid) {
chatMsg.splice(index, 1);
}
});
});
uni.setStorageSync(sessionKey, chatMsg);
Index = historyChatMsgs.slice(-10).length;
// setTimeout 兼容支付宝小程序
setTimeout(() => {
uni.pageScrollTo({
scrollTop: 5000,
duration: 100,
fail: (e) => {
//console.log('滚失败了', e)
},
});
}, 100);
if (isFail) {
renderFail(sessionKey);
}
};
const renderFail = (sessionKey) => {
let msgList = msglistState.chatMsg;
msgList.map((item) => {
if (
item.mid.substring(item.mid.length - 10) ==
curMsgMid.substring(curMsgMid.length - 10)
) {
item.msg.data[0].isFail = true;
item.isFail = true;
msglistState.chatMsg = msgList;
}
});
if (curChatMsgList[0].mid == curMsgMid) {
curChatMsgList[0].msg.data[0].isShow = false;
curChatMsgList[0].isShow = false;
}
uni.setStorageSync('rendered_' + sessionKey, msgList);
isFail = false;
};
const onTap = () => {
$emit('msglistTap', null, {
bubbles: true,
});
};
const shortScroll = () => {
msglistState.view = LIST_STATUS.SHORT;
};
const previewImage = (event) => {
var url = event.target.dataset.url;
uni.previewImage({
urls: [url], // 需要预览的图片 http 链接列表
});
};
const getHistoryMsg = () => {
let usernameObj = msglistState.usernameObj;
let myUsername = uni.getStorageSync('myUsername');
let sessionKey = usernameObj.groupId
? usernameObj.groupId + myUsername
: usernameObj.your + myUsername;
let historyChatMsgs = uni.getStorageSync('rendered_' + sessionKey) || [];
if (Index < historyChatMsgs.length) {
let timesMsgList = historyChatMsgs.slice(-Index - 10, -Index);
msglistState.chatMsg = timesMsgList.concat(msglistState.chatMsg);
msglistState.toView = timesMsgList[timesMsgList.length - 1].mid;
Index += timesMsgList.length;
if (timesMsgList.length == 10) {
page++;
}
uni.stopPullDownRefresh();
}
};
const to_profile_page = (userInfo) => {
if (userInfo) {
uni.navigateTo({
url: `../profile/profile?otherProfile=${JSON.stringify(userInfo)}`,
});
}
};
/* 举报消息 */
//弹出举报
const alertReport = ref(null);
const actionAleartReportMsg = (item) => {
if (item.style !== 'self') {
alertReport.value.open('bottom');
msglistState.showRpt = true;
msglistState.rptMsgId = item.mid;
}
};
//取消举报
const cannelReport = () => {
alertReport.value.close();
};
//选择举报类型
const selectReportType = ref(null);
//展示举报类型面板
const showSelectReportType = () => {
alertReport.value.close();
selectReportType.value.open('bottom');
};
const pickReportType = (item) => {
msglistState.rptType = item.text;
hideSelectReportType();
actionAleartReportReason(item);
};
const hideSelectReportType = () => {
selectReportType.value.close();
};
//填写举报原因
const inputReportReason = ref(null);
const actionAleartReportReason = (item) => {
console.log('>>>>>>输入举报内容', item);
inputReportReason.value.open();
};
const reportMsg = () => {
if (msglistState.reason === '') {
uni.showToast({ title: '请填写举报原因', icon: 'none' });
return;
}
WebIM.conn
.reportMessage({
reportType: msglistState.rptType, // 举报类型
reportReason: msglistState.reason, // 举报原因。
messageId: msglistState.rptMsgId, // 上报消息id
})
.then(() => {
uni.showToast({ title: '举报成功', icon: 'none' });
})
.catch((e) => {
console.log('>>>>举报失败', e);
uni.showToast({ title: '举报失败', icon: 'none' });
})
.finally(() => {
msglistState.reason = '';
msglistState.rptType = '';
msglistState.rptMsgId = '';
});
};
defineExpose({
normalScroll,
getHistoryMsg,
shortScroll,
});
</script>
<style>
@import './msglist.css';
</style>
<template>
<view
scroll-y="true"
:class="
msglistState.view + ' wrap ' + (msglistState.isIPX ? 'scroll_view_X' : '')
"
upper-threshold="-50"
:scroll-into-view="msglistState.toView"
>
<view>
<!-- 弹出举报入口 -->
<uni-popup ref="alertReport">
<button @click="showSelectReportType">举报</button>
<button @click="cannelReport">取消</button>
</uni-popup>
<!-- 展示举报选项 -->
<uni-popup ref="selectReportType">
<button
v-for="(item, index) in msglistState.typeList"
:key="index"
@click="pickReportType(item)"
>
{{ item.text }}
</button>
<button type="warn" @click="hideSelectReportType">取消</button>
</uni-popup>
<!-- 填写举报原因 -->
<uni-popup ref="inputReportReason" type="dialog">
<uni-popup-dialog
mode="input"
title="举报原因"
placeholder="请输入举报原因"
@confirm="reportMsg"
@cancel="msglistState.reason = ''"
>
<uni-easyinput
type="textarea"
v-model="msglistState.reason"
placeholder="请填写举报内容"
:maxlength="300"
></uni-easyinput>
</uni-popup-dialog>
</uni-popup>
</view>
<view class="tips"
>本应用仅用于环信产品功能开发测试,请勿用于非法用途。任何涉及转账、汇款、裸聊、网恋、网购退款、投资理财等统统都是诈骗,请勿相信!</view
>
<view
@longtap="actionAleartReportMsg(msgBody)"
class="message"
v-for="(msgBody, index) in messageList"
:key="msgBody.id + index + ''"
:id="msgBody.id"
>
<!-- 消息体 -->
<view class="main" :class="isSelf(msgBody) ? 'self' : ''">
<view class="user">
<!-- yourname:就是消息的 from -->
<text v-if="!isSelf(msgBody)" class="user-text">{{
showMessageListNickname(msgBody.from) + ' ' + handleTime(msgBody)
}}</text>
</view>
<image class="avatar" :src="showMessageListAvatar(msgBody)" />
<view class="msg">
<image
v-if="isSelf(msgBody)"
src="/static/images/poprightarrow2x.png"
class="msg_poprightarrow"
/>
<image
v-if="!isSelf(msgBody)"
src="/static/images/popleftarrow2x.png"
class="msg_popleftarrow"
/>
<!-- 文本类型消息 -->
<view v-if="msgBody.type === MESSAGE_TYPE.TEXT">
<view
class="template"
v-for="(d_item, d_index) in parseMsgEmoji(msgBody.msg)"
:key="d_index"
>
<text
:data-msg="msgBody"
v-if="d_item.type == MESSAGE_TYPE.TEXT"
class="msg-text"
style="float: left"
selectable="true"
>{{ d_item.data }}</text
>
<image
v-if="d_item.type == MESSAGE_TYPE.EMOJI"
class="avatar"
:src="'/static/images/faces/' + d_item.data"
style="
width: 25px;
height: 25px;
margin: 0 0 2px 0;
float: left;
"
/>
</view>
</view>
<!-- 文件类型消息 -->
<file-msg
v-if="msgBody.type === MESSAGE_TYPE.FILE"
:msg="msgBody"
></file-msg>
<!-- 语音片段类型消息 -->
<audio-msg
v-if="msgBody.type === MESSAGE_TYPE.AUDIO"
:msg="msgBody"
></audio-msg>
<!-- 图片以及视频类型消息 -->
<view
v-if="
msgBody.type == MESSAGE_TYPE.IMAGE ||
msgBody.type == MESSAGE_TYPE.VIDEO
"
>
<image
v-if="msgBody.type == MESSAGE_TYPE.IMAGE"
class="avatar"
:src="msgBody.url"
style="width: 90px; height: 120px; margin: 2px auto"
mode="aspectFit"
@tap="previewImage(msgBody.url)"
/>
<video
v-if="msgBody.type == MESSAGE_TYPE.VIDEO"
:src="msgBody.url"
controls
style="width: 300rpx"
/>
</view>
<!-- 自定义类型消息 -->
<view
v-if="
msgBody.type == MESSAGE_TYPE.CUSTOM &&
msgBody.customEvent === 'userCard'
"
@click="entryProfilePage(msgBody.customExts)"
>
<view class="usercard_mian">
<image
:src="
msgBody.customExts.avatarurl ||
msgBody.customExts.avatar ||
msglistState.defaultAvatar
"
/>
<text class="name">{{
msgBody.customExts.nickname || msgBody.customExts.uid
}}</text>
</view>
<!-- <u-divider :use-slot="false" /> -->
<text>[个人名片]</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
computed,
watch,
onMounted,
inject,
nextTick,
} from 'vue';
import { onPullDownRefresh, onNavigationBarButtonTap } from '@dcloudio/uni-app';
/* EaseIM */
import parseEmoji from '@/EaseIM/utils/paseEmoji';
import { CHAT_TYPE, MESSAGE_TYPE } from '@/EaseIM/constant';
/* stores */
import { useLoginStore } from '@/stores/login';
import { useMessageStore } from '@/stores/message';
import { useContactsStore } from '@/stores/contacts';
/* utils */
import dateFormater from '@/utils/dateFormater';
/* im apis */
import { emMessages } from '@/EaseIM/imApis';
/* components */
import FileMsg from './type/file';
import AudioMsg from './type/audio/audio';
const msglistState = reactive({
isIPX: false,
toView: 0,
//漫游当前游标
view: 'wrap',
title: '消息举报',
list: [
{
text: '举报',
},
],
rptMsgId: '', // 举报消息id
rptType: '', // 举报类型
reason: '',
typeList: [
{
text: '涉政',
},
{
text: '涉黄',
},
{
text: '广告',
},
{
text: '辱骂',
},
{
text: '暴恐',
},
{
text: '违禁',
},
],
defaultAvatar: '/static/images/theme2x.png',
defaultGroupAvatar: '/static/images/groupTheme.png',
});
const injectTargetId = inject('targetId');
const injectChatType = inject('chatType');
/* 消息相关逻辑处理 */
const { reportMessages, fetchHistoryMessagesFromServer } = emMessages();
//该用户当前的聊天记录
const messageStore = useMessageStore();
const messageList = computed(() => {
return (
messageStore.messageCollection[injectTargetId.value] ||
getMoreHistoryMessages() ||
[]
);
});
//获取更多历史消息
const getMoreHistoryMessages = async () => {
const sourceMessage =
messageStore.messageCollection[injectTargetId.value] || [];
const cursorMsgId = (sourceMessage.length && sourceMessage[0]?.id) || -1;
const params = {
targetId: injectTargetId.value,
chatType: injectChatType.value,
cursor: cursorMsgId,
};
try {
let res = await fetchHistoryMessagesFromServer(params);
if (res.messages.length) {
messageStore.fetchHistoryPushToMsgCollection(
injectTargetId.value,
res.messages.reverse()
);
} else {
uni.showToast({ title: '暂无更多历史记录', icon: 'none' });
}
uni.stopPullDownRefresh();
} catch (error) {
uni.stopPullDownRefresh();
uni.showToast('历史消息获取失败...');
console.log('>>>>>返回失败', error);
}
};
onMounted(() => {
nextTick(() => {
uni.pageScrollTo({
scrollTop: 100000,
duration: 50,
});
});
});
//监听消息内容改变,滚动列表
watch(
messageList,
() => {
nextTick(() => {
uni.pageScrollTo({
scrollTop: 100000,
duration: 100,
});
});
},
{
deep: true,
}
);
//消息列表头像展示
const loginStore = useLoginStore();
const contactsStore = useContactsStore();
//登录用户属性
const myUserInfos = computed(() => {
return loginStore.loginUserProfiles;
});
//好友属性
const friendUserInfoMap = computed(() => {
return contactsStore.friendUserInfoMap;
});
//判消息来源是否为自己
const isSelf = computed(() => {
return (item) => {
return item.from === loginStore.loginUserBaseInfos.loginUserId;
};
});
const showMessageListAvatar = computed(() => {
const friendMap = friendUserInfoMap.value;
return (item) => {
if (item.from !== loginStore.loginUserBaseInfos.loginUserId) {
return friendMap.get(item.from)?.avatarurl || msglistState.defaultAvatar;
} else {
return myUserInfos.value?.avatarurl || msglistState.defaultAvatar;
}
};
});
//消息列表昵称显示
const showMessageListNickname = computed(() => {
const friendMap = friendUserInfoMap.value;
return (hxId) => {
return friendMap.get(hxId)?.nickname || hxId;
};
});
//处理时间显示
const handleTime = computed(() => {
return (item) => {
return dateFormater('MM/DD/HH:mm', item.time);
};
});
//解析表情图片
const parseMsgEmoji = computed(() => {
return (content) => {
return parseEmoji(content);
};
});
//预览图片方法
const previewImage = (url) => {
uni.previewImage({
urls: [url], // 需要预览的图片 http 链接列表
});
};
//点击查看个人名片
const entryProfilePage = (userInfo) => {
if (userInfo) {
uni.navigateTo({
url: `../profile/profile?otherProfile=${JSON.stringify(userInfo)}`,
});
}
};
/* 举报消息 */
//弹出举报
const alertReport = ref(null);
const actionAleartReportMsg = (item) => {
if (item.style !== 'self') {
alertReport.value.open('bottom');
msglistState.showRpt = true;
msglistState.rptMsgId = item.id;
}
};
//取消举报
const cannelReport = () => {
alertReport.value.close();
};
//选择举报类型
const selectReportType = ref(null);
//展示举报类型面板
const showSelectReportType = () => {
alertReport.value.close();
selectReportType.value.open('bottom');
};
const pickReportType = (item) => {
msglistState.rptType = item.text;
hideSelectReportType();
actionAleartReportReason(item);
};
const hideSelectReportType = () => {
selectReportType.value.close();
};
//填写举报原因
const inputReportReason = ref(null);
const actionAleartReportReason = (item) => {
inputReportReason.value.open();
};
const reportMsg = async () => {
if (msglistState.reason === '') {
uni.showToast({ title: '请填写举报原因', icon: 'none' });
return;
}
const reportParams = {
reportType: msglistState.rptType,
reportReason: msglistState.reason,
messageId: msglistState.rptMsgId,
};
try {
await reportMessages({ ...reportParams });
uni.showToast({ title: '举报成功', icon: 'none' });
} catch (error) {
console.log('>>>>举报失败', error);
uni.showToast({ title: '举报失败', icon: 'none' });
} finally {
msglistState.reason = '';
msglistState.rptType = '';
msglistState.rptMsgId = '';
}
};
onPullDownRefresh(() => {
getMoreHistoryMessages();
console.log('>>>>>开始了下拉页面');
});
</script>
<style scoped>
@import './index.css';
</style>
还有更多重构代码篇幅有限不便一一展示,感兴趣请至片尾点击 github 地址查看。
问题简述:该问题在 H5 以及 app 中运行均正常展示,但测试发现运行至微信小程序中,会话列表、联系人、我的页面三个页面样式无法加载,效果如下图:
排查解决:发现这三个组件由原来页面级跳转改为了动态切换组件,但是在 pages.json 中仍然配置有该三大组件的路由映射地址,导致打包运行至微信小程序中时,样式出现丢失未能加载。去掉 pages.json 中仍存在的路由映射地址即可恢复正常。
问题简述:打包至微信小程序时点击 emoji 发送,发送框无法展示 emoji 映射的静态资源图片,效果如下图:
问题描述:打包至微信小程序时发现发送图片功能异常,导致消息发送失败。
排查解决:经排查发现微信小程序微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,需使用 uni.chooseMedia 代替。
因此经过处理,判断如果是微信小程序平台,字节平台,京东平台使用 uni.chooseMedia 去进行文件的选取。
当然还需要注意 uni.chooseMedia 与 uni.chooseImage 返回的字段不一致,因此在后续发送时也需要针对性的进行处理。
问题描述:运行至原生端,在点击附件类消息发送时,例如发送语音、发送图片、拍照发送图片等功能提示 XXX 模块未加载。
排查解决:在 HB 中进行云打包之前,请记得 manifest.json / App 模块配置中勾选如下模块。
非常高兴能够对 webim-uniapp-demo 重构,这个事情是我一直都想要做的事情,因为原有的项目代码已经不能很好的帮助想要拿此项目作为参考或者复用的的开发者完成高效的 IM 功能开发。
在重构过程中受益匪浅。因为在这个过程中,我通过对整个 demo 的代码重新整理和改写,不仅使自己对 SDK 的集成有了更深入的认识,更让我意识到 IM 相关功能相比于传统业务项目来说具有更多的灵活性。这种灵活性可能是因为 IM 功能通常需要处理实时性和互动性方面的需求,而这些特点也让开发者有更多的空间去创造新的功能和体验。此外,在这个过程中,我还了解到一些 SDK 的最佳实践,对 Vue3 中的组合式 API 使用也让我感受到了 Vue3 语法的灵活性,im 监听以及 api 的拆分以及仿 hook 的使用方式,有助于后续的扩展维护。
任何项目的经验都是宝贵的,但愿帮助我将来在其他的项目中更好地开发出稳定、可靠和高效的应用程序。
如果你有使用到环信 uni-app-demo,如果改写后的代码能够对你有所帮助,那么这件事情真是泰裤辣!
环信 uni-app 文档地址
重构前 uni-app-demo-Vue2 版本源码地址
重构前 uni-app-demo-Vue3 版本源码地址
重构后 uni-app-demo 源码地址
环信 uni-app Demo升级改造计划——Vue2迁移到Vue3(一)
最后多说一句,如果觉得有帮助请点赞支持一下!本 demo 还有三期计划(增加音视频功能),敬请期待!